@sap/cds-dk 10.0.3 → 10.0.5

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/bin/debug.js +6 -2
  3. package/bin/up.js +1 -1
  4. package/bin/watch.js +1 -0
  5. package/lib/build/plugins/helm/files/templates/_deployment_helpers.tpl +2 -0
  6. package/lib/build/plugins/nodejs/index.js +6 -3
  7. package/lib/build/util.js +1 -1
  8. package/lib/init/template/ams/files/containerize.yaml.hbs +1 -0
  9. package/lib/init/template/approuter/files/values.yaml.hbs +9 -1
  10. package/lib/init/template/data/as-csv.js +4 -2
  11. package/lib/init/template/data/as-json.js +4 -1
  12. package/lib/init/template/github-actions/files/workflows/cf.yaml.hbs +6 -6
  13. package/lib/init/template/ias/files/values.yaml.hbs +4 -4
  14. package/lib/init/template/portal/files/values.yaml.hbs +12 -0
  15. package/lib/init/template/sample/files/app/browse/fiori-service.cds +2 -0
  16. package/lib/init/template/typer/files/xsconfig.json +1 -1
  17. package/lib/init/template/typescript/files/package.json +1 -1
  18. package/lib/lint/eslintHelp.json +1 -1
  19. package/lib/watch/esm-resolve-registration.js +27 -0
  20. package/lib/watch/vite-watch.js +1 -1
  21. package/node_modules/@cap-js/db-service/CHANGELOG.md +768 -0
  22. package/node_modules/@cap-js/db-service/LICENSE +201 -0
  23. package/node_modules/@cap-js/db-service/README.md +26 -0
  24. package/node_modules/@cap-js/db-service/index.js +19 -0
  25. package/node_modules/@cap-js/db-service/lib/InsertResults.js +122 -0
  26. package/node_modules/@cap-js/db-service/lib/SQLService.js +581 -0
  27. package/node_modules/@cap-js/db-service/lib/common/DatabaseService.js +179 -0
  28. package/node_modules/@cap-js/db-service/lib/common/factory.d.ts +5 -0
  29. package/node_modules/@cap-js/db-service/lib/common/generic-pool.js +310 -0
  30. package/node_modules/@cap-js/db-service/lib/common/session-context.js +46 -0
  31. package/node_modules/@cap-js/db-service/lib/converters.d.ts +24 -0
  32. package/node_modules/@cap-js/db-service/lib/cql-functions.js +464 -0
  33. package/node_modules/@cap-js/db-service/lib/cqn2pql.js +116 -0
  34. package/node_modules/@cap-js/db-service/lib/cqn2sql.js +1612 -0
  35. package/node_modules/@cap-js/db-service/lib/cqn4sql.js +2895 -0
  36. package/node_modules/@cap-js/db-service/lib/deep-queries.js +290 -0
  37. package/node_modules/@cap-js/db-service/lib/fill-in-keys.js +75 -0
  38. package/node_modules/@cap-js/db-service/lib/infer/cqn.d.ts +42 -0
  39. package/node_modules/@cap-js/db-service/lib/infer/index.js +1290 -0
  40. package/node_modules/@cap-js/db-service/lib/infer/join-tree.js +278 -0
  41. package/node_modules/@cap-js/db-service/lib/infer/pseudos.js +24 -0
  42. package/node_modules/@cap-js/db-service/lib/search.js +220 -0
  43. package/node_modules/@cap-js/db-service/lib/utils.js +177 -0
  44. package/node_modules/@cap-js/db-service/package.json +36 -0
  45. package/node_modules/@cap-js/sqlite/CHANGELOG.md +404 -0
  46. package/node_modules/@cap-js/sqlite/LICENSE +201 -0
  47. package/node_modules/@cap-js/sqlite/README.md +34 -0
  48. package/node_modules/@cap-js/sqlite/cds-plugin.js +0 -0
  49. package/node_modules/@cap-js/sqlite/index.js +2 -0
  50. package/node_modules/@cap-js/sqlite/lib/SQLiteService.js +324 -0
  51. package/node_modules/@cap-js/sqlite/lib/cql-functions.js +160 -0
  52. package/node_modules/@cap-js/sqlite/lib/node-sqlite.js +36 -0
  53. package/node_modules/@cap-js/sqlite/lib/session.json +3 -0
  54. package/node_modules/@cap-js/sqlite/lib/sql.js.js +99 -0
  55. package/node_modules/@cap-js/sqlite/package.json +68 -0
  56. package/node_modules/@sap/cds-compiler/CHANGELOG.md +14 -0
  57. package/node_modules/@sap/cds-compiler/bin/cdsse.js +1 -1
  58. package/node_modules/@sap/cds-compiler/bin/cdsv2m.js +1 -1
  59. package/node_modules/@sap/cds-compiler/doc/IncompatibleChanges_v7.md +20 -13
  60. package/node_modules/@sap/cds-compiler/lib/base/builtins.js +4 -1
  61. package/node_modules/@sap/cds-compiler/lib/base/message-registry.js +15 -2
  62. package/node_modules/@sap/cds-compiler/lib/checks/defaultValues.js +0 -1
  63. package/node_modules/@sap/cds-compiler/lib/compiler/resolve.js +6 -5
  64. package/node_modules/@sap/cds-compiler/lib/compiler/tweak-assocs.js +27 -6
  65. package/node_modules/@sap/cds-compiler/lib/edm/annotations/edmJson.js +5 -0
  66. package/node_modules/@sap/cds-compiler/lib/json/from-csn.js +3 -3
  67. package/node_modules/@sap/cds-compiler/lib/json/to-csn.js +5 -4
  68. package/node_modules/@sap/cds-compiler/lib/parsers/AstBuildingParser.js +14 -3
  69. package/node_modules/@sap/cds-compiler/lib/tool-lib/enrichCsn.js +1 -1
  70. package/node_modules/@sap/cds-compiler/lib/transform/db/cdsPersistence.js +3 -2
  71. package/node_modules/@sap/cds-compiler/lib/transform/db/views.js +2 -2
  72. package/node_modules/@sap/cds-compiler/lib/transform/transformUtils.js +1 -2
  73. package/node_modules/@sap/cds-compiler/package.json +1 -1
  74. package/node_modules/iconv-lite/encodings/sbcs-data.js +2 -0
  75. package/node_modules/iconv-lite/encodings/utf32.js +10 -3
  76. package/node_modules/iconv-lite/package.json +2 -2
  77. package/node_modules/iconv-lite/types/encodings.d.ts +2 -0
  78. package/node_modules/ws/lib/receiver.js +15 -32
  79. package/node_modules/ws/lib/websocket-server.js +4 -4
  80. package/node_modules/ws/lib/websocket.js +4 -4
  81. package/node_modules/ws/package.json +5 -1
  82. package/package.json +5 -3
@@ -0,0 +1,581 @@
1
+ const cds = require('@sap/cds')
2
+ const DEBUG = cds.log('sql|db')
3
+ const { Readable, Transform } = require('stream')
4
+ const { pipeline } = require('stream/promises')
5
+ const DatabaseService = require('./common/DatabaseService')
6
+ const cqn4sql = require('./cqn4sql')
7
+ const { resolveTable } = require('./utils')
8
+
9
+ // REVISIT: make string the default in next major
10
+ const _count_as_string = cds.env.features.count_as_string
11
+ const _count = _count_as_string ? { func: 'count', cast: { type: 'cds.String' } } : { func: 'count' }
12
+
13
+ const BINARY_TYPES = {
14
+ 'cds.Binary': 1,
15
+ 'cds.hana.BINARY': 1
16
+ }
17
+
18
+ /**
19
+ * Checks if parameter is an object that at least contains one property.
20
+ *
21
+ * @param {*} obj
22
+ * @returns Boolean
23
+ */
24
+ const _hasProps = (obj) => {
25
+ if (!obj) return false
26
+ for (const p in obj) {
27
+ return true
28
+ }
29
+ return false
30
+ }
31
+
32
+ /** @typedef {import('@sap/cds/apis/services').Request} Request */
33
+
34
+ /**
35
+ * @callback Handler
36
+ * @param {Request} req
37
+ * @param {(err? : Error) => {Promise<unknown>}} next
38
+ * @returns {Promise<unknown>}
39
+ */
40
+
41
+ class SQLService extends DatabaseService {
42
+ init() {
43
+ this.on(['INSERT', 'UPSERT', 'UPDATE'], require('./fill-in-keys')) // REVISIT should be replaced by correct input processing eventually
44
+ this.on(['INSERT', 'UPSERT', 'UPDATE'], require('./deep-queries').onDeep)
45
+ if (cds.env.features.db_strict) {
46
+ this.before(['INSERT', 'UPSERT', 'UPDATE'], ({ query }) => {
47
+ const elements = query._target?.elements
48
+ if (!elements) return
49
+ const kind = query.kind || Object.keys(query)[0]
50
+ const operation = query[kind]
51
+ if (!operation.columns && !operation.entries && !operation.data) return
52
+ const columns =
53
+ operation.columns ||
54
+ Object.keys(
55
+ operation.data ||
56
+ operation.entries?.reduce((acc, obj) => {
57
+ return Object.assign(acc, obj)
58
+ }, {}),
59
+ )
60
+ const invalidColumns = columns.filter(c => !(c in elements))
61
+
62
+ if (invalidColumns.length > 0) {
63
+ cds.error(`STRICT MODE: Trying to ${kind} non existent columns (${invalidColumns})`)
64
+ }
65
+ })
66
+ }
67
+ this.on(['SELECT'], this.onSELECT)
68
+ this.on(['INSERT'], this.onINSERT)
69
+ this.on(['UPSERT'], this.onUPSERT)
70
+ this.on(['UPDATE'], this.onUPDATE)
71
+ this.on(['DELETE'], this.onDELETE)
72
+ this.on(['CREATE ENTITY', 'DROP ENTITY'], this.onSIMPLE)
73
+ this.on(['BEGIN', 'COMMIT', 'ROLLBACK'], this.onEVENT)
74
+ this.on(['*'], this.onPlainSQL)
75
+ return super.init()
76
+ }
77
+
78
+ _changeToStreams(columns, rows, one) {
79
+ if (!rows || !columns) return
80
+ if (!Array.isArray(rows)) rows = [rows]
81
+ if (!rows.length || !Object.keys(rows[0]).length) return
82
+
83
+ let changes = false
84
+ for (let col of columns) {
85
+ const name = col.as || col.ref?.[col.ref.length - 1] || (typeof col === 'string' && col)
86
+ if (col.element?.isAssociation) {
87
+ if (one) this._changeToStreams(col.SELECT.columns, rows[0][name], false)
88
+ else
89
+ changes = rows.some(row => !this._changeToStreams(col.SELECT.columns, row[name], false))
90
+ } else if (col.element?.type === 'cds.LargeBinary') {
91
+ changes = true
92
+ if (one) rows[0][name] = this._stream(rows[0][name])
93
+ else
94
+ rows.forEach(row => {
95
+ row[name] = this._stream(row[name])
96
+ })
97
+ } else if (col.element?.type in BINARY_TYPES) {
98
+ changes = true
99
+ if (one) rows[0][name] = this._buffer(rows[0][name])
100
+ else
101
+ rows.forEach(row => {
102
+ row[name] = this._buffer(row[name])
103
+ })
104
+ }
105
+ }
106
+ return changes
107
+ }
108
+
109
+ _stream(val) {
110
+ if (val === null) return null
111
+ if (val instanceof Readable) return val
112
+ // Buffer.from only applies encoding when the input is a string
113
+ let raw = typeof val === 'string' ? Buffer.from(val.toString(), 'base64') : val
114
+ return new Readable({
115
+ read(size) {
116
+ if (raw.length === 0) return this.push(null)
117
+ const chunk = raw.slice(0, size)
118
+ raw = raw.slice(size)
119
+ this.push(chunk)
120
+ },
121
+ })
122
+ }
123
+
124
+ _buffer(val) {
125
+ if (val === null) return null
126
+ return Buffer.from(val, 'base64')
127
+ }
128
+
129
+ /**
130
+ * Handler for SELECT
131
+ * @type {Handler}
132
+ */
133
+ async onSELECT({ query, data, iterator, objectMode }) {
134
+ // REVISIT: for custom joins, infer is called twice, which is bad
135
+ // --> make cds.infer properly work with custom joins and remove this
136
+ if (!(query._target instanceof cds.entity)) {
137
+ try { this.infer(query) } catch { /**/ }
138
+ }
139
+ if (!query._target?._unresolved) { // REVISIT: use query._target instead
140
+ // Will return multiple rows with objects inside
141
+ query.SELECT.expand = 'root'
142
+ }
143
+
144
+ const { sql, values, cqn } = this.cqn2sql(query, data)
145
+ const expand = query.SELECT.expand
146
+ delete query.SELECT.expand
147
+ const isOne = cqn.SELECT.one || query.SELECT.from?.ref?.[0].cardinality?.max === 1
148
+
149
+ let ps = await this.prepare(sql)
150
+ let rows = iterator ? await ps.stream(values, isOne, objectMode) : await ps.all(values)
151
+ try {
152
+ if (rows.length)
153
+ if (expand) rows = rows.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))
154
+
155
+ if (!iterator) {
156
+ this._changeToStreams(cqn.SELECT.columns, rows, query.SELECT.one)
157
+ } else if (objectMode) {
158
+ const converter = (row) => this._changeToStreams(cqn.SELECT.columns, row, true)
159
+ const changeToStreams = new Transform({
160
+ objectMode: true,
161
+ transform(row, enc, cb) {
162
+ converter(row)
163
+ cb(null, row)
164
+ }
165
+ })
166
+ pipeline(rows, changeToStreams)
167
+ rows = changeToStreams
168
+ }
169
+
170
+ if (cqn.SELECT.count) {
171
+ // REVISIT: the runtime always expects that the count is preserved with .map, required for renaming in mocks
172
+ return SQLService._arrayWithCount(rows, await this.count(query, rows))
173
+ }
174
+
175
+ return !iterator && isOne ? rows[0] : rows
176
+ } catch (err) {
177
+ // Ensure that iterators receive pre stream errors
178
+ if (iterator) rows.emit('error', err)
179
+ throw err
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Handler for INSERT
185
+ * @type {Handler}
186
+ */
187
+ async onINSERT({ query, data }) {
188
+ const { sql, entries, cqn } = this.cqn2sql(query, data)
189
+ if (!sql) return // Do nothing when there is nothing to be done // REVISIT: fix within mtxs
190
+ const ps = await this.prepare(sql)
191
+ const results = entries ? await Promise.all(entries.map(e => ps.run(e))) : await ps.run()
192
+ return new this.class.InsertResults(cqn, results)
193
+ }
194
+
195
+ /**
196
+ * Handler for UPSERT
197
+ * @type {Handler}
198
+ */
199
+ async onUPSERT({ query, data }) {
200
+ const { sql, entries } = this.cqn2sql(query, data)
201
+ if (!sql) return // Do nothing when there is nothing to be done // REVISIT: When does this happen?
202
+ let ps = await this.prepare(sql)
203
+ let results = entries ? await Promise.all(entries.map(e => ps.run(e))) : await ps.run()
204
+ let changes = results.reduce?.((total,r) => total + r.changes, 0) ?? results.changes
205
+ return this._return_affected(changes)
206
+ }
207
+
208
+ /**
209
+ * Handler for UPDATE
210
+ * @type {Handler}
211
+ */
212
+ async onUPDATE(req) {
213
+ // noop if not a touch for @cds.on.update
214
+ if (
215
+ !_hasProps(req.query.UPDATE.data) &&
216
+ !_hasProps(req.query.UPDATE.with) &&
217
+ !Object.values(req.target?.elements || {}).some(e => e['@cds.on.update'])
218
+ )
219
+ return this._return_affected(0)
220
+ return this.onSIMPLE(req)
221
+ }
222
+
223
+ /**
224
+ * Handler for CREATE, DROP, UPDATE, DELETE, with simple CQN
225
+ * @type {Handler}
226
+ */
227
+ async onSIMPLE({ query, data }) {
228
+ const { sql, values } = this.cqn2sql(query, data)
229
+ let ps = await this.prepare(sql)
230
+ let { changes } = await ps.run(values)
231
+ return this._return_affected(changes)
232
+ }
233
+
234
+ get onDELETE() {
235
+ // REVISIT: It's not yet 100 % clear under which circumstances we can rely on db constraints
236
+ return (super.onDELETE = /* cds.env.features.assert_integrity === 'db' ? this.onSIMPLE : */ deep_delete)
237
+ async function deep_delete(/** @type {Request} */ req) {
238
+ const resolve = this.resolve
239
+ const transitions = resolve.transitions(req.query)
240
+ if (transitions.target !== transitions.queryTarget) {
241
+ const keys = []
242
+ const transitionsTarget = transitions.queryTarget.keys || transitions.queryTarget.elements
243
+ for (const key in transitionsTarget) {
244
+ const exists = e => e && !e.virtual && !e.value && !e.isAssociation
245
+ if (exists(transitionsTarget[key])) keys.push(key)
246
+ }
247
+ const matchedKeys = keys.filter(key => transitions.mapping.has(key)).map(k => ({ ref: [k] }))
248
+ const query = DELETE.from({
249
+ ref: [
250
+ {
251
+ id: transitions.target.name,
252
+ where: [
253
+ { list: matchedKeys.map(k => transitions.mapping.get(k.ref[0])) },
254
+ 'in',
255
+ SELECT.from(req.query.DELETE.from).columns(matchedKeys).where(req.query.DELETE.where),
256
+ ],
257
+ },
258
+ ],
259
+ })
260
+ return this.onDELETE({ query, target: transitions.target })
261
+ }
262
+ const table = resolveTable(req.target)
263
+ const { compositions } = table
264
+ if (compositions) {
265
+ // Transform CQL`DELETE from Foo[p1] WHERE p2` into CQL`DELETE from Foo[p1 and p2]`
266
+ let { from, where } = req.query.DELETE
267
+ if (typeof from === 'string') from = { ref: [from] }
268
+ if (where) {
269
+ let last = from.ref.at(-1)
270
+ if (last.where) [last, where] = [last.id, [{ xpr: last.where }, 'and', { xpr: where }]]
271
+ from = { ref: [...from.ref.slice(0, -1), { id: last, where }] }
272
+ }
273
+ // Process child compositions depth-first
274
+ let { depth = 0, visited = [] } = req
275
+ visited.push(req.target.name)
276
+ await Promise.all(
277
+ Object.values(compositions).map(c => {
278
+ if (c._target['@cds.persistence.skip'] === true) return
279
+ if (c._target === req.target) {
280
+ // the Genre.children case
281
+ if (++depth > (c['@depth'] || 3)) return
282
+ } else if (visited.includes(c._target.name))
283
+ throw new Error(
284
+ `Transitive circular composition detected: \n\n` +
285
+ ` ${visited.join(' > ')} > ${c._target.name} \n\n` +
286
+ `These are not supported by deep delete.`,
287
+ )
288
+ // Prepare and run deep query, à la CQL`DELETE from Foo[pred]:comp1.comp2...`
289
+ const query = DELETE.from({ ref: [...from.ref, c.name] })
290
+ query._target = c._target
291
+ return this.onDELETE({ query, depth, visited: [...visited], target: c._target })
292
+ }),
293
+ )
294
+ }
295
+ return this.onSIMPLE(req)
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Handler for BEGIN, COMMIT, ROLLBACK, which don't have any CQN
301
+ * @type {Handler}
302
+ */
303
+ async onEVENT({ event }) {
304
+ if (DEBUG._debug) DEBUG.debug(event) // in the other cases above DEBUG happens in cqn2sql
305
+ return await this.exec(event)
306
+ }
307
+
308
+ /**
309
+ * Handler for SQL statements which don't have any CQN
310
+ * @type {Handler}
311
+ */
312
+ async onPlainSQL({ query, data }, next) {
313
+ if (typeof query === 'string') {
314
+ if (DEBUG._debug) DEBUG.debug(query, data)
315
+ const ps = await this.prepare(query)
316
+ const exec = this.hasResults(query) ? d => ps.all(d) : d => ps.run(d)
317
+ if (Array.isArray(data) && Array.isArray(data[0])) return await Promise.all(data.map(exec))
318
+ else return exec(data)
319
+ } else return next()
320
+ }
321
+
322
+ /**
323
+ * Override in subclasses to detect more statements to be called with ps.all()
324
+ * @param {string} sql
325
+ */
326
+ hasResults(sql) {
327
+ return /^\s*(SELECT|WITH|CALL|PRAGMA table_info)/i.test(sql)
328
+ }
329
+
330
+ /**
331
+ * Derives and executes a query to fill in `$count` for given query
332
+ * @param {import('@sap/cds/apis/cqn').SELECT} query - SELECT CQN
333
+ * @param {unknown[]} ret - Results of the original query
334
+ * @returns {Promise<number|string>}
335
+ */
336
+ async count(query, ret) {
337
+ if (ret?.length) {
338
+ const { one, limit: _ } = query.SELECT,
339
+ n = ret.length
340
+ const [max, offset = 0] = one ? [1] : _ ? [_.rows?.val, _.offset?.val] : []
341
+ if (max === undefined || (n < max && (n || !offset))) return _count_as_string ? `${n + offset}` : n + offset
342
+ }
343
+
344
+ // Keep original query columns when potentially used insde conditions
345
+ const { having, groupBy } = query.SELECT
346
+ let columns = []
347
+ if (having?.length || groupBy?.length) {
348
+ columns = query.SELECT.columns.filter(c => !c.expand)
349
+ }
350
+ if (columns.length === 0) columns.push({ val: 1 })
351
+ const cq = SELECT.one([_count]).from(
352
+ cds.ql.clone(query, {
353
+ columns,
354
+ localized: false,
355
+ expand: false,
356
+ limit: undefined,
357
+ orderBy: undefined,
358
+ }),
359
+ )
360
+ const { count } = await this.onSELECT({ query: cq })
361
+ return count
362
+ }
363
+
364
+ /**
365
+ * Streaming API variant of .run().
366
+ * @param {import('@sap/cds/apis/cqn').SELECT} query - SELECT CQN
367
+ * @param {function} callback - Function to be invoked for each row
368
+ */
369
+ foreach(query, callback) {
370
+ return query.foreach(callback)
371
+ }
372
+
373
+ /**
374
+ * Helper class for results of INSERTs.
375
+ * Subclasses may override this.
376
+ */
377
+ static InsertResults = require('./InsertResults')
378
+
379
+ /**
380
+ * Helper class implementing {@link SQLService#cqn2sql}.
381
+ * Subclasses commonly override this.
382
+ */
383
+ static CQN2SQL = require('./cqn2sql').class
384
+
385
+ // REVISIT: There must be a better way!
386
+ // preserves $count for .map calls on array
387
+ static _arrayWithCount = function (a, count) {
388
+ const _map = a.map
389
+
390
+ const map = function (..._) {
391
+ return SQLService._arrayWithCount(_map.call(a, ..._), count)
392
+ }
393
+ return Object.defineProperties(a, {
394
+ $count: { value: count, enumerable: false, configurable: true, writable: true },
395
+ map: { value: map, enumerable: false, configurable: true, writable: true },
396
+ })
397
+ }
398
+
399
+ constructor(...args) {
400
+ super(...args)
401
+ this.class = new.target // for IntelliSense
402
+ }
403
+
404
+ /**
405
+ * @param {import('@sap/cds/apis/cqn').Query} query
406
+ * @returns {typeof SQLService.CQN2SQL}
407
+ */
408
+ cqn2sql(query, values) {
409
+ const cqn2sql = new this.class.CQN2SQL(this)
410
+ const q = this.cqn4sql(query)
411
+ const sql = cqn2sql.render(q, values)
412
+ return sql
413
+ }
414
+
415
+ /**
416
+ * @param {import('@sap/cds/apis/cqn').Query} q
417
+ * @returns {import('./infer/cqn').Query}
418
+ */
419
+ cqn4sql(q, useTechnicalAlias = true) {
420
+ if (
421
+ !cds.env.features.db_strict &&
422
+ !q.SELECT?.from?.join &&
423
+ !q.SELECT?.from?.SELECT &&
424
+ !this.model?.definitions[_target_name4(q)]
425
+ ) return q
426
+ else return cqn4sql(q, this.model, useTechnicalAlias)
427
+ }
428
+
429
+ /**
430
+ * Returns a Promise which resolves to a prepared statement object with
431
+ * `{run,get,all}` signature as specified in {@link PreparedStatement}.
432
+ * @abstract
433
+ * @param {string} sql The SQL String to be prepared
434
+ * @returns {PreparedStatement}
435
+ */
436
+ async prepare(sql) {
437
+ sql
438
+ throw '2b overridden by subclass'
439
+ }
440
+
441
+ /**
442
+ * Used to execute simple SQL statement like BEGIN, COMMIT, ROLLBACK
443
+ * @param {string} sql
444
+ * @returns {Promise<unknown>} The result of the query
445
+ */
446
+ async exec(sql) {
447
+ sql
448
+ throw '2b overridden by subclass'
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Interface of prepared statement objects as returned by {@link SQLService#prepare}
454
+ * @class
455
+ * @interface
456
+ */
457
+ class PreparedStatement {
458
+ /**
459
+ * Executes a prepared DML query, i.e., INSERT, UPDATE, DELETE, CREATE, DROP
460
+ * @abstract
461
+ * @param {unknown|unknown[]} binding_params
462
+ */
463
+ async run(binding_params) {
464
+ binding_params
465
+ return 0
466
+ }
467
+ /**
468
+ * Executes a prepared SELECT query and returns a single/first row only
469
+ * @abstract
470
+ * @param {unknown|unknown[]} binding_params
471
+ * @returns {Promise<unknown>}
472
+ */
473
+ async get(binding_params) {
474
+ binding_params
475
+ return {}
476
+ }
477
+ /**
478
+ * Executes a prepared SELECT query and returns an array of all rows
479
+ * @abstract
480
+ * @param {unknown|unknown[]} binding_params
481
+ * @returns {Promise<unknown[]>}
482
+ */
483
+ async all(binding_params) {
484
+ binding_params
485
+ return [{}]
486
+ }
487
+ /**
488
+ * Executes a prepared SELECT query and returns a stream of the result
489
+ * @abstract
490
+ * @param {unknown|unknown[]} binding_params
491
+ * @returns {ReadableStream<string|Buffer>} A stream of the result
492
+ */
493
+ async stream(binding_params) {
494
+ binding_params
495
+ }
496
+ }
497
+ SQLService.prototype.PreparedStatement = PreparedStatement
498
+
499
+ /** @param {import('@sap/cds').ql.Query} q */
500
+ const _target_name4 = q => {
501
+ const target = q._subject
502
+ || q.SELECT?.from
503
+ || q.INSERT?.into
504
+ || q.UPSERT?.into
505
+ || q.UPDATE?.entity
506
+ || q.DELETE?.from
507
+ || q.CREATE?.entity
508
+ || q.DROP?.entity
509
+ if (target?.SET?.op === 'union') throw new cds.error('UNION-based queries are not supported')
510
+ if (!target?.ref) return target
511
+ const [first] = target.ref
512
+ return first.id || first
513
+ }
514
+
515
+
516
+ // Add support for cqn2pql if debug logging for pql is enabled, or if running in the REPL.
517
+ const DEBUG_PQL = cds.log('pql')
518
+ if (DEBUG_PQL._debug || cds.repl) {
519
+
520
+ // Add helper method to convert CQN to PQL, used below...
521
+ SQLService.prototype.cqn2pql = function cqn2pql(query, values) {
522
+ const CQN2PQL = cqn2pql.renderer ??= require('./cqn2pql')
523
+ return new CQN2PQL(this).render(query, values)
524
+ }
525
+
526
+ // Add support for logging generated PQL if debug logging for pql is enabled.
527
+ if (DEBUG_PQL._debug) {
528
+ const $super = SQLService.prototype.cqn2sql
529
+ SQLService.prototype.cqn2sql = function (query, values) {
530
+ const q2 = this.cqn4sql(query, false) // FIXME: calling cqn4sql twice per query is utterly expensive, isn't it ?!?
531
+ const pql = this.cqn2pql(q2, values)
532
+ DEBUG_PQL.debug(pql.sql, pql.values ?? '')
533
+ return $super.call(this, query, values)
534
+ }
535
+ }
536
+
537
+ // If running in the REPL, extend cds.ql.Query with helpers to inspect queries.
538
+ if (cds.repl) {
539
+
540
+ cds.extend(cds.ql.Query).with(
541
+ class {
542
+ forSQL() {
543
+ const cqn = db.srv.cqn4sql(this)
544
+ return this.flat(cqn)
545
+ }
546
+ forSql() { return this.forSQL() }
547
+ toSQL() {
548
+ if (this.SELECT) this.SELECT.expand = 'root' // Enforces using json functions always for top-level SELECTS
549
+ const { sql, values } = db.srv.cqn2sql(this)
550
+ return { sql, values } // skipping .cqn property
551
+ }
552
+ toSql() {
553
+ const { sql } = this.toSQL()
554
+ return sql
555
+ }
556
+ toPQL() {
557
+ const { sql, values } = db.srv.cqn2pql(this)
558
+ return { sql, values } // skipping .cqn property
559
+ }
560
+ toPql() {
561
+ const { sql } = this.toPQL()
562
+ return sql
563
+ }
564
+ }
565
+ )
566
+
567
+ /**
568
+ * Dummy SQL service used in extensions to cds.ql above,
569
+ * if no real SQL service is available yet through cds.db.
570
+ */
571
+ class db extends SQLService {
572
+ /** @returns {SQLService} */
573
+ static get srv() { return cds.db || (this.singleton ??= new this) }
574
+ get factory() { return null }
575
+ get model() { return cds.model }
576
+ }
577
+ }
578
+ }
579
+
580
+ Object.assign(SQLService, { _target_name4 })
581
+ module.exports = SQLService