@cap-js/sqlite 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,223 @@
1
+ const cds = require('../../../cds'), DEBUG = cds.debug('sql|db')
2
+ const DatabaseService = require('../DatabaseService')
3
+ const cqn4sql = require('./cqn4sql')
4
+ const { target_name4 } = require('./utils')
5
+ const { PassThrough, pipeline } = require('stream')
6
+
7
+
8
+ class SQLService extends DatabaseService {
9
+
10
+ init() {
11
+ this.on([ 'INSERT', 'UPSERT', 'UPDATE', 'DELETE' ], require('./workarounds').input) // REVISIT should be replaced by correct input processing eventually
12
+ this.on([ 'INSERT', 'UPSERT', 'UPDATE', 'DELETE' ], require('./deep').onDeep)
13
+ this.on([ 'SELECT' ], this.onSELECT)
14
+ this.on([ 'INSERT', 'UPSERT' ], this.onINSERT)
15
+ this.on([ 'UPDATE' ], this.onUPDATE)
16
+ this.on([ 'DELETE', 'CREATE ENTITY', 'DROP ENTITY' ], this.onSIMPLE)
17
+ this.on([ 'BEGIN', 'COMMIT', 'ROLLBACK' ], this.onEVENT)
18
+ this.on([ '*' ], this.onPlainSQL)
19
+ return super.init()
20
+ }
21
+
22
+ /** Handler for SELECT */
23
+ async onSELECT ({ query, data }) {
24
+ // REVISIT: disable this for queries like (SELECT 1)
25
+ // Will return multiple rows with objects inside
26
+ // REVISIT: streaming: if we need custom app and db handlers with app stream and cds.stream
27
+ if (query._streaming) return this.onStream(query) // TODO: implemented on HANA
28
+ query.SELECT.expand = 'root'
29
+ const { sql, values, cqn } = this.cqn2sql (query, data)
30
+ let ps = await this.prepare(sql)
31
+ let rows = await ps.all(values)
32
+ if (rows.length)
33
+ if (cqn.SELECT.expand) rows = rows.map(r => typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r)
34
+ if (cqn.SELECT.count) rows.$count = await this.count(query, rows)
35
+ return cqn.SELECT.one || query.SELECT.from.ref?.[0].cardinality?.max === 1 ? rows[0] || null : rows
36
+ }
37
+
38
+ /** Handler for INSERT & UPSERT, which support bulk queries */
39
+ async onINSERT ({ query, data }) {
40
+ const { sql, entries, cqn } = this.cqn2sql (query, data)
41
+ if(!sql) return // Do nothing when there is nothing to be done
42
+ const ps = await this.prepare(sql)
43
+ const results = entries ? await Promise.all(entries.map(e => ps.run(e))) : await ps.run()
44
+ return new this.class.InsertResults (cqn, results)
45
+ }
46
+
47
+ /** Handler for UPDATE */
48
+ async onUPDATE (req) {
49
+ return this.onSIMPLE(req)
50
+ }
51
+
52
+ /** Handler for CREATE, DROP, UPDATE, DELETE, with simple CQN */
53
+ async onSIMPLE ({ query, data }) {
54
+ const { sql, values } = this.cqn2sql (query, data)
55
+ let ps = await this.prepare(sql)
56
+ return (await ps.run(values)).changes
57
+ }
58
+
59
+ /** Handler for BEGIN, COMMIT, ROLLBACK, which don't have any CQN */
60
+ async onEVENT ({ event }) {
61
+ DEBUG?.(event) // in the other cases above DEBUG happens in cqn2sql
62
+ return await this.exec(event)
63
+ }
64
+
65
+ /** Handler for SQL statements which don't have any CQN */
66
+ async onPlainSQL ({ query, data }, next) {
67
+ if (typeof query === 'string') {
68
+ DEBUG?.(query)
69
+ const ps = await this.prepare(query)
70
+ const exec = this.hasResults(query) ? d => ps.all(d) : d => ps.run(d)
71
+ if (Array.isArray(data) && typeof data[0] === 'object')
72
+ return await Promise.all(data.map(exec))
73
+ else return exec(data)
74
+ }
75
+ else return next()
76
+ }
77
+
78
+ /** Override in subclasses to detect more statements to be called with ps.all() */
79
+ hasResults(sql) {
80
+ return /^(SELECT|WITH|CALL|PRAGMA table_info)/.test(sql)
81
+ }
82
+
83
+
84
+ /** Derives and executes a query to fill in `$count` for given query */
85
+ async count (query, ret) {
86
+ if (ret) {
87
+ const { one, limit:_ } = query.SELECT, n = ret.length
88
+ const [ max, offset=0 ] = one ? [1] : _ ? [ _.rows?.val, _.offset?.val ] : []
89
+ if (max === undefined || n < max && (n || !offset)) return n + offset
90
+ }
91
+ const cq = cds.ql.clone (query, {
92
+ columns: [{func:'count'}],
93
+ localized: false,
94
+ expand: false,
95
+ limit: 0,
96
+ orderBy: 0
97
+ })
98
+ const { sql, values } = this.cqn2sql(cq)
99
+ const ps = await this.prepare(sql)
100
+ const { count } = await ps.get(values)
101
+ return count
102
+ }
103
+
104
+ /**
105
+ * Streaming
106
+ * Returns either a readable stream for sync calls or a readable stream promise for async calls
107
+ */
108
+ stream(q) {
109
+ return typeof q === 'object'
110
+ // aynchronous API: cds.stream(query)
111
+ ? this.run(Object.assign(q, { _streaming: true }))
112
+ // synchronous API: cds.stream('column').from(entity).where(...)
113
+ : new StreamCQN(q, this)
114
+ }
115
+
116
+
117
+ static InsertResults = require('./InsertResults')
118
+
119
+ /**
120
+ * Helper class implementing {@link SQLService#cqn2sql}.
121
+ * Subclasses commonly override this.
122
+ */
123
+ static CQN2SQL = require('./cqn2sql').class
124
+ constructor() {
125
+ super(...arguments)
126
+ this.class = new.target // for IntelliSense
127
+ }
128
+ cqn2sql(q,values) {
129
+ const cqn = this.cqn4sql(q)
130
+ return (new this.class.CQN2SQL) .render (cqn, values)
131
+ }
132
+ cqn4sql(q) {
133
+ if (!this.model?.definitions[target_name4(q)]) return _unquirked(q)
134
+ return cqn4sql (q, this.model)
135
+ }
136
+
137
+ /**
138
+ * Returns a Promise which resolves to a prepared statement object with
139
+ * `{run,get,all}` signature as specified in {@link PreparedStatement}.
140
+ * @returns {PreparedStatement}
141
+ */
142
+ // eslint-disable-next-line no-unused-vars
143
+ async prepare (sql) { throw '2b overridden by subclass' }
144
+
145
+ /**
146
+ * Used to execute simple SQL statement like BEGIN, COMMIT, ROLLBACK
147
+ */
148
+ // eslint-disable-next-line no-unused-vars
149
+ async exec (sql) { throw '2b overridden by subclass' }
150
+
151
+ }
152
+
153
+
154
+ /** Interface of prepared statement objects as returned by {@link SQLService#prepare} */
155
+ class PreparedStatement { // eslint-disable-line no-unused-vars
156
+ /**
157
+ * Executes a prepared DML query, i.e., INSERT, UPDATE, DELETE, CREATE, DROP
158
+ * @param {[]|{}} binding_params
159
+ */
160
+ async run (binding_params) {} // eslint-disable-line no-unused-vars
161
+ /**
162
+ * Executes a prepared SELECT query and returns a single/first row only
163
+ * @param {[]|{}} binding_params
164
+ */
165
+ async get (binding_params) { return {} } // eslint-disable-line no-unused-vars
166
+ /**
167
+ * Executes a prepared SELECT query and returns an array of all rows
168
+ * @param {[]|{}} binding_params
169
+ */
170
+ async all (binding_params) { return [{}] } // eslint-disable-line no-unused-vars
171
+ }
172
+
173
+ /**
174
+ * Class that builds and runs stream CQN
175
+ */
176
+ class StreamCQN {
177
+ constructor (column, srv) {
178
+ this.column = column
179
+ this.srv = srv
180
+ this.result = new PassThrough()
181
+ }
182
+ /** synchronous streaming API: returns readable stream or class instance for chaining */
183
+ from (...args) {
184
+ this.sq = SELECT.from(...args)
185
+ this.sq._streaming = true
186
+ if (this.column) this.sq.columns([this.column])
187
+ const ref = this.sq.SELECT.from.ref
188
+ if (!ref?.[ref.length - 1].where) return this
189
+ this._runStream()
190
+ return this.result
191
+ }
192
+ /** synchronous streaming API: returns readable stream */
193
+ where (...args) {
194
+ this.sq.where(...args)
195
+ this._runStream()
196
+ return this.result
197
+ }
198
+
199
+ async _runStream() {
200
+ try {
201
+ const stream = await this.srv.run(this.sq)
202
+ // In case of streaming error while streaming from stream to this.result
203
+ // the error is emitted to both streams. After this the output stream this.result is destroyed.
204
+ // No explicit closing of this.result is needed.
205
+ // In (theoretical) case if for some error this.result is not destroyed the code like below can be used
206
+ // as callback: err => err && this.result.push(null)
207
+ stream ? pipeline(stream, this.result, () => {}) : this.result.push(null)
208
+ }
209
+ catch (err) { this.result.emit('error', err); this.result.push(null) }
210
+ }
211
+ }
212
+
213
+ const _unquirked = q => {
214
+ if (typeof q.INSERT?.into === 'string') q.INSERT.into = {ref:[q.INSERT.into]}
215
+ if (typeof q.UPSERT?.into === 'string') q.UPSERT.into = {ref:[q.UPSERT.into]}
216
+ if (typeof q.UPDATE?.entity === 'string') q.UPDATE.entity = {ref:[q.UPDATE.entity]}
217
+ if (typeof q.DELETE?.from === 'string') q.DELETE.from = {ref:[q.DELETE.from]}
218
+ if (typeof q.CREATE?.entity === 'string') q.CREATE.entity = {ref:[q.CREATE.entity]}
219
+ if (typeof q.DROP?.entity === 'string') q.DROP.entity = {ref:[q.DROP.entity]}
220
+ return q
221
+ }
222
+
223
+ module.exports = SQLService
@@ -0,0 +1,17 @@
1
+ module.exports.copy = function (obj) {
2
+ const walk = function (par, prop) {
3
+ const val = prop ? par[prop] : par
4
+
5
+ // If value is native return
6
+ if (typeof val !== 'object' || val == null || val instanceof RegExp || val instanceof Date || val instanceof Buffer)
7
+ return val
8
+
9
+ const ret = Array.isArray(val) ? [] : {}
10
+ Object.keys(val).forEach(k => {
11
+ ret[k] = walk(val, k)
12
+ })
13
+ return ret
14
+ }
15
+
16
+ return walk(obj)
17
+ }