@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,1612 @@
1
+ const cds = require('@sap/cds')
2
+ const cds_infer = require('./infer')
3
+ const cqn4sql = require('./cqn4sql')
4
+ const { resolveTable } = require('./utils')
5
+
6
+ const _simple_queries = cds.env.features.sql_simple_queries
7
+ const _strict_booleans = _simple_queries < 2
8
+ // REVISIT: make string the default in next major
9
+ const _count_as_string = cds.env.features.count_as_string
10
+ const _count = _count_as_string ? { func: 'count', cast: { type: 'cds.String' } } : { func: 'count' }
11
+
12
+ const { Readable } = require('stream')
13
+
14
+ const DEBUG = cds.log('sql|sqlite')
15
+
16
+ class CQN2SQLRenderer {
17
+ /**
18
+ * Creates a new CQN2SQL instance for processing a query
19
+ * @constructor
20
+ * @param {import('@sap/cds/apis/services').ContextProperties} context the cds.context of the request
21
+ */
22
+ constructor(srv) {
23
+ this.srv = srv
24
+ this.context = srv?.context || cds.context // Using srv.context is required due to stakeholders doing unmanaged txs without cds.context being set
25
+ this.class = new.target // for IntelliSense
26
+ this.class._init() // is a noop for subsequent calls
27
+ this.model = srv?.model
28
+ // Overwrite smart quoting
29
+ if (cds.env.sql.names === 'quoted') {
30
+ this.class.prototype.name = (name, query) => {
31
+ const e = name.id || name
32
+ const entity = query?._target || this.model?.definitions[e]
33
+ return (!entity?.['@cds.persistence.skip'] && entity?.['@cds.persistence.name']) || e
34
+ }
35
+ this.class.prototype.quote = (s) => `"${String(s).replace(/"/g, '""')}"`
36
+ }
37
+ }
38
+
39
+ BINARY_TYPES = {
40
+ 'cds.Binary': 1,
41
+ 'cds.LargeBinary': 1,
42
+ 'cds.hana.BINARY': 1,
43
+ }
44
+
45
+ static _add_mixins(aspect, mixins) {
46
+ const fqn = this.name + aspect
47
+ const types = cds.builtin.types
48
+ for (let each in mixins) {
49
+ const def = types[each]
50
+ if (!def) continue
51
+ const value = mixins[each]
52
+ if (value?.get) Object.defineProperty(def, fqn, { get: value.get })
53
+ else Object.defineProperty(def, fqn, { value })
54
+ }
55
+ return fqn
56
+ }
57
+
58
+ /**
59
+ * Initializes the class one first creation to link types to data converters
60
+ */
61
+ static _init() {
62
+ this._localized = this._add_mixins(':localized', this.localized)
63
+ this._convertInput = this._add_mixins(':convertInput', this.InputConverters)
64
+ this._convertOutput = this._add_mixins(':convertOutput', this.OutputConverters)
65
+ this._sqlType = this._add_mixins(':sqlType', this.TypeMap)
66
+ // Have all-uppercase all-lowercase, and capitalized keywords to speed up lookups
67
+ for (let each in this.ReservedWords) {
68
+ // ORDER
69
+ this.ReservedWords[each[0] + each.slice(1).toLowerCase()] = 1 // Order
70
+ this.ReservedWords[each.toLowerCase()] = 1 // order
71
+ }
72
+ this._init = () => { } // makes this a noop for subsequent calls
73
+ }
74
+
75
+ /**
76
+ * Renders incoming query into SQL and generates binding values
77
+ * @param {import('./infer/cqn').Query} q CQN query to be rendered
78
+ * @param {unknown[]|undefined} vars Values to be used for params
79
+ * @returns {CQN2SQLRenderer|unknown}
80
+ */
81
+ render(q, vars) {
82
+ const kind = q.kind || Object.keys(q)[0] // SELECT, INSERT, ...
83
+ if (q._with) this._with = q._with
84
+ /**
85
+ * @type {string} the rendered SQL string
86
+ */
87
+ this.sql = '' // to have it as first property for debugging
88
+ /** @type {unknown[]} */
89
+ this.values = [] // prepare values, filled in by subroutines
90
+ this[kind]((this.cqn = q)) // actual sql rendering happens here
91
+ if (this._with?.length) {
92
+ this.render_with()
93
+ }
94
+ if (vars?.length && !this.values?.length) this.values = vars
95
+ if (vars && Object.keys(vars).length && !this.values?.length) this.values = vars
96
+ const sanitize_values = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
97
+
98
+ if (DEBUG._debug) {
99
+ let values = sanitize_values && (this.entries || this.values?.length > 0) ? ['***'] : this.entries || this.values || []
100
+ if (values && !Array.isArray(values)) {
101
+ values = [values]
102
+ }
103
+ DEBUG.debug(this.sql, values)
104
+ }
105
+
106
+ return this
107
+ }
108
+
109
+ render_with() {
110
+ const sql = this.sql
111
+ let recursive = false
112
+ const values = this.values
113
+ const prefix = this._with.map(q => {
114
+ const values = this.values = []
115
+ let sql
116
+ if ('SELECT' in q) sql = `${this.quote(q.as)} AS (${this.SELECT(q)})`
117
+ else if ('SET' in q) {
118
+ recursive = true
119
+ const { SET } = q
120
+ sql = `${this.quote(q.as)}(${SET.args[0].SELECT.columns?.map(c => this.quote(this.column_name(c))) || ''}) AS (${this.SELECT(SET.args[0])} ${SET.op?.toUpperCase() || 'UNION'} ${SET.all ? 'ALL' : ''} ${this.SELECT(SET.args[1])}${SET.orderBy ? ` ORDER BY ${this.orderBy(SET.orderBy)}` : ''})`
121
+ }
122
+ return { sql, values }
123
+ })
124
+ this.sql = `WITH${recursive ? ' RECURSIVE' : ''} ${prefix.map(p => p.sql)} ${sql}`
125
+ this.values = [...prefix.map(p => p.values).flat(), ...values]
126
+ }
127
+
128
+ /**
129
+ * Links the incoming query with the current service model
130
+ * @param {import('./infer/cqn').Query} q
131
+ * @returns {import('./infer/cqn').Query}
132
+ */
133
+ infer(q) {
134
+ return q._target instanceof cds.entity ? q : cds_infer(q)
135
+ }
136
+
137
+ cqn4sql(q) {
138
+ return cqn4sql(q, this.model)
139
+ }
140
+
141
+ // CREATE Statements ------------------------------------------------
142
+
143
+ /**
144
+ * Renders a CREATE query into generic SQL
145
+ * @param {import('./infer/cqn').CREATE} q
146
+ */
147
+ CREATE(q) {
148
+ let { _target: target } = q
149
+ let query = target?.query || q.CREATE.as
150
+ if (!target || target._unresolved) {
151
+ const entity = q.CREATE.entity
152
+ target = typeof entity === 'string' ? { name: entity } : q.CREATE.entity
153
+ }
154
+
155
+ const name = this.name(target.name, q)
156
+ // Don't allow place holders inside views
157
+ delete this.values
158
+ this.sql =
159
+ !query || target['@cds.persistence.table']
160
+ ? `CREATE TABLE ${this.quote(name)} ( ${this.CREATE_elements(target.elements)} )`
161
+ : `CREATE VIEW ${this.quote(name)} AS ${this.SELECT(this.cqn4sql(query))}`
162
+ this.values = []
163
+ return
164
+ }
165
+
166
+ /**
167
+ * Renders a column clause for the given elements
168
+ * @param {import('./infer/cqn').elements} elements
169
+ * @returns {string} SQL
170
+ */
171
+ CREATE_elements(elements) {
172
+ let sql = ''
173
+ let keys = ''
174
+ for (let e in elements) {
175
+ const definition = elements[e]
176
+ if (definition.isAssociation) continue
177
+ if (definition.key) keys = `${keys}, ${this.quote(definition.name)}`
178
+ const s = this.CREATE_element(definition)
179
+ if (s) sql += `, ${s}`
180
+ }
181
+ return `${sql.slice(2)}${keys && `, PRIMARY KEY(${keys.slice(2)})`}`
182
+ }
183
+
184
+ /**
185
+ * Renders a column definition for the given element
186
+ * @param {import('./infer/cqn').element} element
187
+ * @returns {string} SQL
188
+ */
189
+ CREATE_element(element) {
190
+ const type = this.type4(element)
191
+ if (type) return this.quote(element.name) + ' ' + type
192
+ }
193
+
194
+ /**
195
+ * Renders the SQL type definition for the given element
196
+ * @param {import('./infer/cqn').element} element
197
+ * @returns {string}
198
+ */
199
+ type4(element) {
200
+ if (!element._type) element = cds.builtin.types[element.type] || element
201
+ const fn = element[this.class._sqlType]
202
+ return (
203
+ fn?.(element) || element._type?.replace('cds.', '').toUpperCase() || cds.error`Unsupported type: ${element.type}`
204
+ )
205
+ }
206
+
207
+ /** @callback converter */
208
+
209
+ /** @type {Object<string,import('@sap/cds/apis/csn').Definition>} */
210
+ static TypeMap = {
211
+ // Utilizing cds.linked inheritance
212
+ UUID: () => `NVARCHAR(36)`,
213
+ String: e => `NVARCHAR(${e.length || 5000})`,
214
+ Binary: e => `VARBINARY(${e.length || 5000})`,
215
+ UInt8: () => 'TINYINT',
216
+ Int16: () => 'SMALLINT',
217
+ Int32: () => 'INT',
218
+ Int64: () => 'BIGINT',
219
+ Integer: () => 'INT',
220
+ Integer64: () => 'BIGINT',
221
+ LargeString: () => 'NCLOB',
222
+ LargeBinary: () => 'BLOB',
223
+ Association: () => false,
224
+ Composition: () => false,
225
+ array: () => 'NCLOB',
226
+ Map: () => 'NCLOB',
227
+ // HANA types
228
+ 'cds.hana.TINYINT': () => 'TINYINT',
229
+ 'cds.hana.REAL': () => 'REAL',
230
+ 'cds.hana.CHAR': e => `CHAR(${e.length || 1})`,
231
+ 'cds.hana.ST_POINT': () => 'ST_POINT',
232
+ 'cds.hana.ST_GEOMETRY': () => 'ST_GEOMETRY',
233
+ }
234
+
235
+ // DROP Statements ------------------------------------------------
236
+
237
+ /**
238
+ * Renders a DROP query into generic SQL
239
+ * @param {import('./infer/cqn').DROP} q
240
+ */
241
+ DROP(q) {
242
+ const { _target: target } = q
243
+ const isView = target?.query || target?.projection || q.DROP.view
244
+ const name = target?.name || q.DROP.table?.ref?.[0] || q.DROP.view?.ref?.[0]
245
+ return (this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} IF EXISTS ${this.quote(this.name(name, q))}`)
246
+ }
247
+
248
+ // SELECT Statements ------------------------------------------------
249
+
250
+ /**
251
+ * Renders a SELECT statement into generic SQL
252
+ * @param {import('./infer/cqn').SELECT} q
253
+ */
254
+ SELECT(q) {
255
+ let { from, expand, where, groupBy, having, orderBy, limit, one, distinct, localized, forUpdate, forShareLock, recurse } =
256
+ q.SELECT
257
+
258
+ if (from?.join && !q.SELECT.columns) {
259
+ throw new Error('CQN query using joins must specify the selected columns.')
260
+ }
261
+
262
+ // REVISIT: When selecting from an entity that is not in the model the from.where are not normalized (as cqn4sql is skipped)
263
+ if (!where && from?.ref?.length === 1 && from.ref[0]?.where) where = from.ref[0]?.where
264
+
265
+ let sql = `SELECT`
266
+ if (distinct) sql += ` DISTINCT`
267
+ if (recurse) sql += this.SELECT_recurse(q)
268
+ else {
269
+ sql += ` ${this.SELECT_columns(q)}`
270
+ if (!_empty(from)) sql += ` FROM ${this.from(from, q)}`
271
+ else sql += this.from_dummy()
272
+ }
273
+ if (!recurse && !_empty(where)) sql += ` WHERE ${this.where(where)}`
274
+ if (!recurse && !_empty(groupBy)) sql += ` GROUP BY ${this.groupBy(groupBy)}`
275
+ if (!recurse && !_empty(having)) sql += ` HAVING ${this.having(having)}`
276
+ if (!recurse && !_empty(orderBy)) sql += ` ORDER BY ${this.orderBy(orderBy, localized)}`
277
+ if (one) limit = Object.assign({}, limit, { rows: { val: 1 } })
278
+ if (limit) sql += ` LIMIT ${this.limit(limit)}`
279
+ if (forUpdate) sql += ` ${this.forUpdate(forUpdate)}`
280
+ else if (forShareLock) sql += ` ${this.forShareLock(forShareLock)}`
281
+ // Expand cannot work without an inferred query
282
+ if (expand) {
283
+ if ('elements' in q) sql = this.SELECT_expand(q, sql)
284
+ else cds.error`Query was not inferred and includes expand. For which the metadata is missing.`
285
+ }
286
+ return (this.sql = sql)
287
+ }
288
+
289
+ SELECT_recurse(q) {
290
+ let { from, columns, where, orderBy, recurse, _internal } = q.SELECT
291
+
292
+ const _target = q._target
293
+
294
+ if (_target && where) {
295
+ const keys = []
296
+ for (const _key in _target.keys) {
297
+ const k = _target.keys[_key]
298
+ if (!k.virtual && !k.isAssociation && !k.value) {
299
+ keys.push({ ref: [_key] })
300
+ }
301
+ }
302
+
303
+ // `where` needs to be wrapped to also support `where == ['exists', { SELECT }]` which is not allowed in `START WHERE`
304
+ const clone = q.clone()
305
+ clone.SELECT.columns = keys
306
+ clone.SELECT.recurse = undefined
307
+ clone.SELECT.limit = undefined
308
+ clone.SELECT.expand = undefined // omits JSON
309
+ where = [{ list: keys }, 'in', clone]
310
+ }
311
+
312
+ const requiredComputedColumns = { PARENT_ID: true, NODE_ID: true }
313
+ if (!_internal) requiredComputedColumns.RANK = true
314
+ const addComputedColumn = (name) => {
315
+ if (requiredComputedColumns[name]) return
316
+ requiredComputedColumns[name] = true
317
+ }
318
+
319
+ // The hierarchy functions will output the following columns. Which might clash with the entity columns
320
+ const reservedColumnNames = {
321
+ PARENT_ID: 1, NODE_ID: 1,
322
+ HIERARCHY_RANK: 1, HIERARCHY_DISTANCE: 1, HIERARCHY_LEVEL: 1, HIERARCHY_TREE_SIZE: 1
323
+ }
324
+ const availableComputedColumns = {
325
+ // Input computed columns
326
+ PARENT_ID: false,
327
+ NODE_ID: false,
328
+
329
+ // Output computed columns
330
+ RANK: { xpr: [{ ref: ['HIERARCHY_RANK'] }, '-', { val: 1, param: false }], as: 'RANK' },
331
+ Distance: { func: where?.length ? 'min' : 'max', args: [{ ref: ['HIERARCHY_DISTANCE'] }], as: 'Distance' },
332
+ DistanceFromRoot: { xpr: [{ ref: ['HIERARCHY_LEVEL'] }, '-', { val: 1, param: false }], as: 'DistanceFromRoot' },
333
+ DrillState: false,
334
+ LimitedDescendantCount: { xpr: [{ ref: ['HIERARCHY_TREE_SIZE'] }, '-', { val: 1, param: false }], as: 'LimitedDescendantCount' },
335
+ LimitedRank: { xpr: [{ func: 'row_number', args: [] }, 'OVER', { xpr: ['ORDER', 'BY', { ref: ['HIERARCHY_RANK'] }, 'ASC'] }, '-', { val: 1, param: false }], as: 'LimitedRank' }
336
+ }
337
+
338
+ const columnsFiltered = columns
339
+ .filter(x => {
340
+ if (x.element?.isAssociation) return false
341
+ const name = this.column_name(x)
342
+ if (name === '$$RN$$') return false
343
+ // REVISIT: ensure that the selected column is one of the hierarchy computed columns by unifying their common definition
344
+ if (x.element?.['@Core.Computed'] && name in availableComputedColumns) {
345
+ addComputedColumn(name)
346
+ return false
347
+ }
348
+ return true
349
+ })
350
+ const columnsOut = []
351
+ const columnsIn = []
352
+ const target = q._target || q.target
353
+ for (const name in target.elements) {
354
+ const ref = { ref: [name] }
355
+ const element = target.elements[name]
356
+ if (element.virtual || element.isAssociation) continue
357
+ if (name in availableComputedColumns) continue
358
+ if (name.toUpperCase() in reservedColumnNames) ref.as = `$$${name}$$`
359
+ // This only supports calculated elements within the scope of the own entity
360
+ if ('value' in element) {
361
+ const requested = columnsFiltered.find(c => this.column_name(c) === element.name)
362
+ if (requested) columnsIn.push(requested)
363
+ else continue
364
+ }
365
+ else columnsIn.push(ref)
366
+ const foreignkey4 = element._foreignKey4
367
+ if (
368
+ from.args ||
369
+ columnsFiltered.find(c => this.column_name(c) === name) ||
370
+ // foreignkey needs to be included when the association is expanded
371
+ (foreignkey4 && q.SELECT.columns.some(c => c.element?.isAssociation && c.element.name === foreignkey4))
372
+ ) {
373
+ columnsOut.push(ref.as ? { ref: [ref.as], as: name } : ref)
374
+ }
375
+ }
376
+
377
+ const nodeKeys = []
378
+ const parentKeys = []
379
+ const association = target.elements[recurse.ref[0]]
380
+ association._foreignKeys.forEach(fk => {
381
+ nodeKeys.push(fk.childElement.name)
382
+ parentKeys.push(fk.parentElement.name)
383
+ })
384
+
385
+ columnsIn.push(
386
+ nodeKeys.length === 1
387
+ ? { ref: nodeKeys, as: 'NODE_ID' }
388
+ : { func: 'HIERARCHY_COMPOSITE_ID', args: nodeKeys.map(n => ({ ref: [n] })), as: 'NODE_ID' },
389
+ parentKeys.length === 1
390
+ ? { ref: parentKeys, as: 'PARENT_ID' }
391
+ : { func: 'HIERARCHY_COMPOSITE_ID', args: parentKeys.map(n => ({ ref: [n] })), as: 'PARENT_ID' },
392
+ )
393
+
394
+ if (orderBy) {
395
+ orderBy = orderBy.filter(o => o.ref).map(r => {
396
+ let col = r.ref.at(-1)
397
+ if (col.toUpperCase() in reservedColumnNames) col = `$$${col}$$`
398
+ if (!columnsIn.find(c => this.column_name(c) === col)) {
399
+ columnsIn.push({ ref: [col] })
400
+ }
401
+ return { ...r, ref: [col] }
402
+ })
403
+ }
404
+
405
+ // In the case of join operations make sure to compute the hierarchy from the source table only
406
+ const stableFrom = getStableFrom(from)
407
+ const alias = stableFrom.as
408
+ const source = () => {
409
+ return ({
410
+ func: 'HIERARCHY',
411
+ args: [{ xpr: ['SOURCE', { SELECT: { columns: columnsIn, from: stableFrom } }, ...(orderBy ? ['SIBLING', 'ORDER', 'BY', `${this.orderBy(orderBy)}`] : [])] }],
412
+ as: alias
413
+ })
414
+ }
415
+
416
+ const expandedByNr = { list: [] } // DistanceTo(...,null)
417
+ const expandedByOne = { list: [] } // DistanceTo(...,1)
418
+ const expandedByZero = { list: [] } // not DistanceTo(...,null)
419
+ let expandedFilter = []
420
+ // If a root where exists it should always be DistanceFromRoot otherwise when a recurse.where exists with only DistanceTo() calls
421
+ let distanceType = 'DistanceFromRoot'
422
+ let distanceVal
423
+
424
+ if (recurse.where) {
425
+ distanceType = where?.length ? 'DistanceFromRoot' : 'Distance'
426
+ if (recurse.where[0] === 'and') recurse.where = recurse.where.slice(1)
427
+ expandedFilter = [...recurse.where]
428
+ collectDistanceTo(expandedFilter)
429
+ }
430
+
431
+ const direction = where?.length ? 'ANCESTORS' : 'DESCENDANTS'
432
+ // Ensure that the distance value is being computed
433
+ if (distanceType) addComputedColumn(distanceType)
434
+
435
+ let distanceClause = []
436
+ if (distanceType === 'Distance') {
437
+ const isOne = expandedByOne.list.length
438
+ distanceClause = ['DISTANCE', ...(
439
+ isOne
440
+ ? [{ val: 1 }]
441
+ : ['FROM', { val: 1 }]
442
+ )]
443
+ where = [{ ref: ['NODE_ID'] }, 'IN', isOne ? expandedByOne : expandedByNr]
444
+ expandedFilter = []
445
+ }
446
+
447
+ availableComputedColumns.DrillState = {
448
+ xpr: [ // When the node doesn't have children make it a leaf
449
+ 'CASE', 'WHEN', { ref: ['HIERARCHY_TREE_SIZE'] }, '=', { val: 1, param: false }, 'THEN', { val: 'leaf', param: false },
450
+ ...(where?.length // When there is a where filter the final node will always be a leaf
451
+ ? ['WHEN', { func: where?.length ? 'min' : 'max', args: [{ ref: ['HIERARCHY_DISTANCE'] }] }, '=', { val: 0, param: false }, 'THEN', { val: 'leaf', param: false }]
452
+ : []
453
+ ), // When having expanded by 0 level nodes make sure they are collapsed
454
+ ...(expandedByZero.list.length
455
+ ? ['WHEN', { ref: ['NODE_ID'] }, 'IN', expandedByZero, 'THEN', { val: 'collapsed', param: false }]
456
+ : []
457
+ ), // When having expanded by null or one nodes compute them as expanded
458
+ ...(expandedByNr.list.length || expandedByOne.list.length
459
+ ? ['WHEN', { ref: ['NODE_ID'] }, 'IN', { list: [...expandedByNr.list, ...expandedByOne.list] }, 'THEN', { val: 'expanded', param: false }]
460
+ : []
461
+ ), // When having expanded by one level node make its children collapsed
462
+ ...(expandedByOne.list.length
463
+ ? ['WHEN', { ref: ['PARENT_ID'] }, 'IN', expandedByOne, 'THEN', { val: 'collapsed', param: false }]
464
+ : []
465
+ ), // When using DistanceFromRoot compute all entries within the levels as expanded
466
+ ...(distanceType === 'DistanceFromRoot' && distanceVal
467
+ ? [
468
+ 'WHEN', { ref: ['HIERARCHY_LEVEL'] }, '<>', { val: distanceVal.val + 1 },
469
+ 'THEN', { val: 'expanded', param: false },
470
+ ]
471
+ : []
472
+ ), // Default to expanded when default filter behavior is truthy
473
+ 'ELSE', { val: (recurse.where && !expandedByZero.list.length) && distanceType ? 'collapsed' : 'expanded', param: false },
474
+ 'END',
475
+ ],
476
+ as: 'DrillState'
477
+ }
478
+
479
+ for (const name in requiredComputedColumns) {
480
+ const def = availableComputedColumns[name]
481
+ if (def) columnsOut.push(def)
482
+ }
483
+ if (_internal) columnsOut.push({ ref: ['NODE_ID'] })
484
+
485
+ const graph = distanceType === 'DistanceFromRoot' && !where
486
+ ? { SELECT: { columns: columnsOut, from: source(), where: expandedFilter } }
487
+ : {
488
+ SELECT: {
489
+ columns: columnsOut,
490
+ from: {
491
+ func: `HIERARCHY_${direction}`,
492
+ args: [{
493
+ xpr: [
494
+ 'SOURCE', source(), 'AS', this.quote(alias),
495
+ 'START', 'WHERE', {
496
+ xpr: where // Requires special where logic before being put into the args
497
+ ? from.args
498
+ ? [{ ref: ['NODE_ID'] }, 'IN', { SELECT: { columns: [columnsIn.find(c => c.as === 'NODE_ID')], from, where: where } }]
499
+ : this.is_comparator?.({ xpr: where }) ?? true ? where : [...where, '=', { val: true, param: false }]
500
+ : [{ ref: ['PARENT_ID'] }, '=', { val: null }]
501
+ },
502
+ ...distanceClause
503
+ ]
504
+ }]
505
+ },
506
+ where: expandedFilter.length ? expandedFilter : undefined,
507
+ orderBy: [{ ref: ['HIERARCHY_RANK'], sort: 'asc' }],
508
+ groupBy: [{ ref: ['NODE_ID'] }, { ref: ['PARENT_ID'] }, { ref: ['HIERARCHY_RANK'] }, { ref: ['HIERARCHY_LEVEL'] }, { ref: ['HIERARCHY_TREE_SIZE'] }, ...columnsOut.filter(c => c.ref)],
509
+ }
510
+ }
511
+
512
+ const columnsQuery = cds.ql(q).clone()
513
+ columnsQuery.SELECT.columns = columns.map(x => {
514
+ if (x.element && 'value' in x.element) return { element: x.element, ref: [this.column_name(x)] }
515
+ return x
516
+ })
517
+ const recurseColumns = this.SELECT_columns(columnsQuery)
518
+ // Only apply result join if the columns contain a references which doesn't start with the source alias
519
+ if (from.args && columns.find(c => c.ref?.[0] === alias)) {
520
+ graph.as = alias
521
+ return ` ${recurseColumns} FROM ${this.from(setStableFrom(from, graph))}`
522
+ }
523
+
524
+ return ` ${recurseColumns} FROM (${this.SELECT(graph)})${alias ? ` AS ${this.quote(alias)}` : ''} `
525
+
526
+ function collectDistanceTo(where, innot = false) {
527
+ for (let i = 0; i < where.length; i++) {
528
+ const c = where[i]
529
+ if (c === 'not') {
530
+ distanceType = 'DistanceFromRoot'
531
+ innot = true
532
+ }
533
+ else if (c.func === 'DistanceTo') {
534
+ const expr = c.args[0]
535
+ // { func: 'HIERARCHY_COMPOSITE_ID', args: nodeKeys.map(n => ({ val: cur[n] })) }
536
+ const to = c.args[1].val
537
+ const list = to === 1
538
+ ? expandedByOne
539
+ : innot
540
+ ? expandedByZero
541
+ : expandedByNr
542
+
543
+ if (!list._where) {
544
+ list._where = []
545
+ where.splice(i, 1,
546
+ ...(to === 1
547
+ ? [{ ref: ['PARENT_ID'] }, 'IN', list]
548
+ : [{ ref: ['NODE_ID'] }, 'IN', {
549
+ SELECT: {
550
+ _internal: true,
551
+ columns: [{ ref: ['NODE_ID'], element: { '@Core.Computed': true } }],
552
+ from: q.SELECT.from,
553
+ recurse: {
554
+ ref: recurse.ref,
555
+ where: list._where,
556
+ },
557
+ },
558
+ target,
559
+ }])
560
+ )
561
+ i += 2
562
+ } else {
563
+ // Remove current entry from where
564
+ if (where[i - 1] === 'not') {
565
+ where.splice(i - 2, 3)
566
+ i -= 3
567
+ } else {
568
+ where.splice(i - 1, 2)
569
+ i -= 2
570
+ }
571
+ }
572
+ list.list.push(expr)
573
+ list._where.push(c)
574
+ }
575
+ else if (c.ref?.[0] === 'DistanceFromRoot') {
576
+ distanceType = 'DistanceFromRoot'
577
+ where[i] = { ref: ['HIERARCHY_LEVEL'] }
578
+ i += 2
579
+ distanceVal = where[i]
580
+ where[i] = { val: where[i].val + 1 }
581
+ }
582
+ }
583
+ }
584
+
585
+ function getStableFrom(from) {
586
+ if (from.args) return getStableFrom(from.args[0])
587
+ return from
588
+ }
589
+
590
+ function setStableFrom(from, src) {
591
+ if (from.args) {
592
+ const ret = { ...from }
593
+ ret.args = [...ret.args]
594
+ ret.args[0] = setStableFrom(ret.args[0], src)
595
+ return ret
596
+ }
597
+ return src
598
+ }
599
+ }
600
+
601
+ /**
602
+ * Renders a column clause into generic SQL
603
+ * @param {import('./infer/cqn').SELECT} param0
604
+ * @returns {string} SQL
605
+ */
606
+ SELECT_columns(q) {
607
+ const ret = []
608
+ const arr = q.SELECT.columns ?? ['*']
609
+ for (const x of arr) {
610
+ if (x.SELECT?.count) arr.push(this.SELECT_count(x))
611
+ ret.push(this.column_expr(x, q))
612
+ }
613
+ return ret
614
+ }
615
+
616
+ /**
617
+ * Renders a JSON select around the provided SQL statement
618
+ * @param {import('./infer/cqn').SELECT} param0
619
+ * @param {string} sql
620
+ * @returns {string} SQL
621
+ */
622
+ SELECT_expand(q, sql) {
623
+ if (!('elements' in q)) return sql
624
+
625
+ const SELECT = q.SELECT
626
+ if (!SELECT.columns) return sql
627
+
628
+ const isRoot = SELECT.expand === 'root'
629
+ const isSimple = _simple_queries &&
630
+ isRoot && // Simple queries are only allowed to have a root
631
+ !ObjectKeys(q.elements).some(e =>
632
+ _strict_booleans && q.elements[e].type === 'cds.Boolean' || // REVISIT: Booleans require json for sqlite
633
+ q.elements[e].isAssociation || // Indicates columns contains an expand
634
+ q.elements[e].$assocExpand || // REVISIT: sometimes associations are structs
635
+ q.elements[e].items // Array types require to be inlined with a json result
636
+ )
637
+
638
+ let cols = SELECT.columns.map(isSimple
639
+ ? x => {
640
+ const name = this.column_name(x)
641
+ const escaped = `${name.replace(/"/g, '""')}`
642
+ return `${this.output_converter4(x.element, this.quote(name))} AS "${escaped}"`
643
+ }
644
+ : x => {
645
+ const name = this.column_name(x)
646
+ return `${this.string(`$.${JSON.stringify(name)}`)},${this.output_converter4(x.element, this.quote(name))}`
647
+ }).flat()
648
+
649
+ if (isSimple) return `SELECT ${cols} FROM (${sql})`
650
+
651
+ // Prevent SQLite from hitting function argument limit of 100
652
+ let obj = "'{}'"
653
+ for (let i = 0; i < cols.length; i += 48) {
654
+ obj = `jsonb_insert(${obj},${cols.slice(i, i + 48)})`
655
+ }
656
+ return `SELECT ${isRoot || SELECT.one ? obj.replace('jsonb', 'json') : `jsonb_group_array(${obj})`} as _json_ FROM (${sql})`
657
+ }
658
+
659
+ SELECT_count(q) {
660
+ const countQuery = cds.ql.clone(q, {
661
+ columns: [_count],
662
+ one: 0, limit: 0, orderBy: 0, expand: 0, count: 0
663
+ })
664
+ countQuery.as = q.as + '@odata.count'
665
+ countQuery.elements = undefined
666
+ countQuery.element = cds.builtin.types.Int64
667
+ return countQuery
668
+ }
669
+
670
+ /**
671
+ * Renders a SELECT column expression into generic SQL
672
+ * @param {import('./infer/cqn').col} x
673
+ * @returns {string} SQL
674
+ */
675
+ column_expr(x, q) {
676
+ if (x === '*') return '*'
677
+
678
+ let sql = x.param !== true && typeof x.val === 'number' ? this.expr({ param: false, __proto__: x }) : this.expr(x)
679
+ let alias = this.column_alias4(x, q)
680
+ if (alias) sql += ' as ' + this.quote(alias)
681
+ return sql
682
+ }
683
+
684
+ /**
685
+ * Extracts the column alias from a SELECT column expression
686
+ * @param {import('./infer/cqn').col} x
687
+ * @returns {string}
688
+ */
689
+ column_alias4(x) {
690
+ return typeof x.as === 'string' ? x.as : x.func || x.val
691
+ }
692
+
693
+ /**
694
+ * Renders a FROM clause into generic SQL
695
+ * @param {import('./infer/cqn').source} from
696
+ * @returns {string} SQL
697
+ */
698
+ from(from, q) {
699
+ const { ref, as } = from
700
+ const _aliased = as ? s => s + ` as ${this.quote(as)}` : s => s
701
+ if (ref) {
702
+ let z = ref[0]
703
+ if (z.args) {
704
+ return _aliased(`${this.quote(this.name(z, q))}${this.from_args(z.args)}`)
705
+ }
706
+ return _aliased(this.quote(this.name(z, q)))
707
+ }
708
+ if (from.SELECT) return _aliased(`(${this.SELECT(from)})`)
709
+ if (from.join) return `${this.from(from.args[0])} ${from.join} JOIN ${this.from(from.args[1])}${from.on ? ` ON ${this.where(from.on)}` : ''}`
710
+ if (from.func) return _aliased(this.func(from))
711
+ }
712
+
713
+ /**
714
+ * Renders a FROM clause into generic SQL
715
+ * @param {import('./infer/cqn').source} from
716
+ * @returns {string} SQL
717
+ */
718
+ with(query) {
719
+ this._with ??= []
720
+ this._with.push(query)
721
+ return { ref: [query.as] }
722
+ }
723
+
724
+ /**
725
+ * Renders a FROM clause for when the query does not have a target
726
+ * @returns {string} SQL
727
+ */
728
+ from_dummy() {
729
+ return ''
730
+ }
731
+
732
+ /**
733
+ * Renders a FROM clause into generic SQL
734
+ * @param {import('./infer/cqn').ref['ref'][0]['args']} args
735
+ * @returns {string} SQL
736
+ */
737
+ from_args(args) {
738
+ args
739
+ cds.error`Parameterized views are not supported by ${this.constructor.name}`
740
+ }
741
+
742
+ /**
743
+ * Renders a WHERE clause into generic SQL
744
+ * @param {import('./infer/cqn').predicate} xpr
745
+ * @returns {string} SQL
746
+ */
747
+ where(xpr) {
748
+ return this.xpr({ xpr })
749
+ }
750
+
751
+ /**
752
+ * Renders a transformed where clause that maps the query target view to the source table
753
+ * @param {import('./infer/cqn').source} alias
754
+ * @param {import('./infer/cqn').predicate} where
755
+ * @param {import('./infer/cqn').query} q
756
+ * @returns SQL
757
+ */
758
+ where_resolved(alias, where, q) {
759
+ const transitions = this.srv.resolve.transitions(q)
760
+ if (transitions.target === transitions.queryTarget) return this.where(where)
761
+
762
+ // view and table column refs to be matched
763
+ const viewCols = []
764
+ const tableCols = []
765
+
766
+ // Only match key columns when possible
767
+ const elements = q._target.keys || q._target.elements
768
+ for (const c in elements) {
769
+ if (
770
+ c in elements
771
+ && transitions.mapping.has(c)
772
+ && this.physical_column(elements, c)
773
+ ) {
774
+ viewCols.push({ ref: [c] })
775
+ tableCols.push(transitions.mapping.get(c))
776
+ }
777
+ }
778
+ return tableCols.length > 0
779
+ ? this.where([{ list: tableCols }, 'in', SELECT.from(q._target).alias(alias).columns(viewCols).where(where)])
780
+ : this.where(where)
781
+ }
782
+
783
+ /**
784
+ * Renders a HAVING clause into generic SQL
785
+ * @param {import('./infer/cqn').predicate} xpr
786
+ * @returns {string} SQL
787
+ */
788
+ having(xpr) {
789
+ return this.xpr({ xpr })
790
+ }
791
+
792
+ /**
793
+ * Renders a groupBy clause into generic SQL
794
+ * @param {import('./infer/cqn').expr[]} clause
795
+ * @returns {string[] | string} SQL
796
+ */
797
+ groupBy(clause) {
798
+ return clause.map(c => this.expr(c))
799
+ }
800
+
801
+ /**
802
+ * Renders an orderBy clause into generic SQL
803
+ * @param {import('./infer/cqn').ordering_term[]} orderBy
804
+ * @param {boolean | undefined} localized
805
+ * @returns {string[] | string} SQL
806
+ */
807
+ orderBy(orderBy, localized) {
808
+ return orderBy.map(c => {
809
+ const o = (localized && this.context.locale)
810
+ ? this.expr(c) +
811
+ (c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
812
+ (c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
813
+ : this.expr(c) + (c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
814
+ if (c.nulls) return o + ' NULLS ' + (c.nulls.toLowerCase() === 'first' ? 'FIRST' : 'LAST')
815
+ return o
816
+ })
817
+ }
818
+
819
+ /**
820
+ * Renders an limit clause into generic SQL
821
+ * @param {import('./infer/cqn').limit} param0
822
+ * @returns {string} SQL
823
+ * @throws {Error} When no rows are defined
824
+ */
825
+ limit({ rows, offset }) {
826
+ if (!rows) throw new Error('Rows parameter is missing in SELECT.limit(rows, offset)')
827
+ return !offset ? this.val(rows) : `${this.val(rows)} OFFSET ${this.val(offset)}`
828
+ }
829
+
830
+ /**
831
+ * Renders an forUpdate clause into generic SQL
832
+ * @param {import('./infer/cqn').SELECT["SELECT"]["forUpdate"]} update
833
+ * @returns {string} SQL
834
+ */
835
+ forUpdate(update) {
836
+ const { wait, of, ignoreLocked } = update
837
+ let sql = 'FOR UPDATE'
838
+ if (!_empty(of)) sql += ` OF ${of.map(x => this.expr(x)).join(', ')}`
839
+ if (ignoreLocked) sql += ' IGNORE LOCKED'
840
+ if (typeof wait === 'number') sql += ` WAIT ${wait}`
841
+ return sql
842
+ }
843
+
844
+ /**
845
+ * Renders an forShareLock clause into generic SQL
846
+ * @param {import('./infer/cqn').SELECT["SELECT"]["forShareLock"]} update
847
+ * @returns {string} SQL
848
+ */
849
+ forShareLock(lock) {
850
+ const { wait, of } = lock
851
+ let sql = 'FOR SHARE LOCK'
852
+ if (!_empty(of)) sql += ` OF ${of.map(x => this.expr(x)).join(', ')}`
853
+ if (typeof wait === 'number') sql += ` WAIT ${wait}`
854
+ return sql
855
+ }
856
+
857
+ // INSERT Statements ------------------------------------------------
858
+
859
+ /**
860
+ * Renders an INSERT query into generic SQL
861
+ * @param {import('./infer/cqn').INSERT} q
862
+ * @returns {string} SQL
863
+ */
864
+ INSERT(q) {
865
+ const { INSERT } = q
866
+ return INSERT.entries
867
+ ? this.INSERT_entries(q)
868
+ : INSERT.rows
869
+ ? this.INSERT_rows(q)
870
+ : INSERT.values
871
+ ? this.INSERT_values(q)
872
+ : INSERT.from || INSERT.as
873
+ ? this.INSERT_select(q)
874
+ : cds.error`Missing .entries, .rows, or .values in ${q}`
875
+ }
876
+
877
+ /**
878
+ * Renders an INSERT query with entries property
879
+ * @param {import('./infer/cqn').INSERT} q
880
+ * @returns {string} SQL
881
+ */
882
+ INSERT_entries(q) {
883
+ const { INSERT } = q
884
+ const elements = q.elements || q._target?.elements
885
+ if (!elements && !INSERT.entries?.length) {
886
+ return // REVISIT: mtx sends an insert statement without entries and no reference entity
887
+ }
888
+ const transitions = this.srv.resolve.transitions(q)
889
+ const columns = elements
890
+ ? ObjectKeys(elements).filter(c => this.physical_column(elements, c)
891
+ && (c = transitions.mapping.get(c)?.ref?.[0] || c)
892
+ && c in transitions.target.elements
893
+ && this.physical_column(transitions.target.elements, c)
894
+ )
895
+ : ObjectKeys(INSERT.entries[0])
896
+
897
+ /** @type {string[]} */
898
+ this.columns = columns
899
+
900
+ const alias = INSERT.into.as
901
+ const entity = q._target ? this.table_name(q) : INSERT.into.ref[0]
902
+ if (!elements) {
903
+ this.entries = INSERT.entries.map(e => columns.map(c => e[c]))
904
+ const param = this.param.bind(this, { ref: ['?'] })
905
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))}) VALUES (${columns.map(param)})`)
906
+ }
907
+
908
+ // Include this.values for placeholders
909
+ /** @type {unknown[][]} */
910
+ this.entries = []
911
+ if (INSERT.entries[0] instanceof Readable && !INSERT.entries[0].readableObjectMode) {
912
+ INSERT.entries[0].type = 'json'
913
+ this.entries = [[...this.values, INSERT.entries[0]]]
914
+ } else {
915
+ const entries = INSERT.entries[0]?.[Symbol.iterator] || INSERT.entries[0]?.[Symbol.asyncIterator] || INSERT.entries[0] instanceof Readable ? INSERT.entries[0] : INSERT.entries
916
+ const stream = Readable.from(this.INSERT_entries_stream(entries), { objectMode: false })
917
+ stream.type = 'json'
918
+ stream._raw = entries
919
+ this.entries = [[...this.values, stream]]
920
+ }
921
+
922
+ const extractions = this._managed = this.managed(columns.map(c => ({ name: c })), elements)
923
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c))
924
+ }) SELECT ${extractions.slice(0, columns.length).map(c => c.insert)} FROM json_each(?)`)
925
+ }
926
+
927
+ async *INSERT_entries_stream(entries, binaryEncoding = 'base64') {
928
+ const elements = this.cqn._target?.elements || {}
929
+ const bufferLimit = 65536 // 1 << 16
930
+ let buffer = '['
931
+
932
+ let sep = ''
933
+ for await (const row of entries) {
934
+ buffer += `${sep}{`
935
+ if (!sep) sep = ','
936
+
937
+ let sepsub = ''
938
+ for (const key in row) {
939
+ let val = row[key]
940
+ if (val === undefined) continue
941
+ const keyJSON = `${sepsub}${JSON.stringify(key)}:`
942
+ if (!sepsub) sepsub = ','
943
+
944
+ if (val instanceof Readable) {
945
+ buffer += `${keyJSON}"`
946
+
947
+ // TODO: double check that it works
948
+ val.setEncoding(binaryEncoding)
949
+ for await (const chunk of val) {
950
+ buffer += chunk
951
+ if (buffer.length > bufferLimit) {
952
+ yield buffer
953
+ buffer = ''
954
+ }
955
+ }
956
+
957
+ buffer += '"'
958
+ } else {
959
+ if (val != null && elements[key]?.type in this.BINARY_TYPES) {
960
+ val = Buffer.from(val, 'base64').toString(binaryEncoding)
961
+ }
962
+ buffer += `${keyJSON}${JSON.stringify(val)}`
963
+ }
964
+ }
965
+ buffer += '}'
966
+ if (buffer.length > bufferLimit) {
967
+ yield buffer
968
+ buffer = ''
969
+ }
970
+ }
971
+
972
+ buffer += ']'
973
+ yield buffer
974
+ }
975
+
976
+ async *INSERT_rows_stream(entries, binaryEncoding = 'base64') {
977
+ const elements = this.cqn._target?.elements || {}
978
+ const bufferLimit = 65536 // 1 << 16
979
+ let buffer = '['
980
+
981
+ let sep = ''
982
+ for (const row of entries) {
983
+ buffer += `${sep}[`
984
+ if (!sep) sep = ','
985
+
986
+ let sepsub = ''
987
+ for (let key = 0; key < row.length; key++) {
988
+ let val = row[key]
989
+ if (val instanceof Readable) {
990
+ buffer += `${sepsub}"`
991
+
992
+ // TODO: double check that it works
993
+ val.setEncoding(binaryEncoding)
994
+ for await (const chunk of val) {
995
+ buffer += chunk
996
+ if (buffer.length > bufferLimit) {
997
+ yield buffer
998
+ buffer = ''
999
+ }
1000
+ }
1001
+
1002
+ buffer += '"'
1003
+ } else {
1004
+ if (val != null && elements[this.columns[key]]?.type in this.BINARY_TYPES) {
1005
+ val = Buffer.from(val, 'base64').toString(binaryEncoding)
1006
+ }
1007
+ buffer += `${sepsub}${val == null ? 'null' : JSON.stringify(val)}`
1008
+ }
1009
+
1010
+ if (!sepsub) sepsub = ','
1011
+ }
1012
+ buffer += ']'
1013
+ if (buffer.length > bufferLimit) {
1014
+ yield buffer
1015
+ buffer = ''
1016
+ }
1017
+ }
1018
+
1019
+ buffer += ']'
1020
+ yield buffer
1021
+ }
1022
+
1023
+ /**
1024
+ * Renders an INSERT query with rows property
1025
+ * @param {import('./infer/cqn').INSERT} q
1026
+ * @returns {string} SQL
1027
+ */
1028
+ INSERT_rows(q) {
1029
+ const { INSERT } = q
1030
+ const entity = q._target ? this.table_name(q) : INSERT.into.ref[0]
1031
+ const alias = INSERT.into.as
1032
+ const elements = q.elements || q._target?.elements
1033
+ const columns = this.columns = INSERT.columns || cds.error`Cannot insert rows without columns or elements`
1034
+
1035
+ if (!elements) {
1036
+ this.entries = INSERT.rows
1037
+ const param = this.param.bind(this, { ref: ['?'] })
1038
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))}) VALUES (${columns.map(param)})`)
1039
+ }
1040
+
1041
+ if (INSERT.rows[0] instanceof Readable) {
1042
+ INSERT.rows[0].type = 'json'
1043
+ this.entries = [[...this.values, INSERT.rows[0]]]
1044
+ } else {
1045
+ const stream = Readable.from(this.INSERT_rows_stream(INSERT.rows), { objectMode: false })
1046
+ stream.type = 'json'
1047
+ stream._raw = INSERT.rows
1048
+ this.entries = [[...this.values, stream]]
1049
+ }
1050
+
1051
+ const extraction = (this._managed = this.managed(columns.map(c => ({ name: c })), elements))
1052
+ .slice(0, columns.length)
1053
+ .map(c => c.converter(c.extract))
1054
+
1055
+ const transitions = this.srv.resolve.transitions(q)
1056
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c))
1057
+ }) SELECT ${extraction} FROM json_each(?)`)
1058
+ }
1059
+
1060
+ /**
1061
+ * Renders an INSERT query with values property
1062
+ * @param {import('./infer/cqn').INSERT} q
1063
+ * @returns {string} SQL
1064
+ */
1065
+ INSERT_values(q) {
1066
+ let { columns, values } = q.INSERT
1067
+ return this.render({ __proto__: q, INSERT: { __proto__: q.INSERT, columns, rows: [values] } })
1068
+ }
1069
+
1070
+ /**
1071
+ * Renders an INSERT query from SELECT query
1072
+ * @param {import('./infer/cqn').INSERT} q
1073
+ * @returns {string} SQL
1074
+ */
1075
+ INSERT_select(q) {
1076
+ const { INSERT } = q
1077
+ const entity = q._target ? this.table_name(q) : INSERT.into.ref[0]
1078
+ const alias = INSERT.into.as
1079
+ const src = this.cqn4sql(INSERT.from)
1080
+ const elements = q.elements || q._target?.elements || {}
1081
+ const transitions = this.srv.resolve.transitions(q)
1082
+ let columns = (this.columns = (INSERT.columns || src.SELECT.columns?.map(c => this.column_name(c)) || ObjectKeys(src.elements) || ObjectKeys(elements))
1083
+ .filter(c => this.physical_column(elements, c)
1084
+ && (c = transitions.mapping.get(c)?.ref?.[0] || c)
1085
+ && c in transitions.target.elements
1086
+ && this.physical_column(transitions.target.elements, c)
1087
+ ))
1088
+
1089
+ const extractions = this._managed = this.managed(columns.map(c => ({ name: c, sql: `NEW.${this.quote(c)}` })), elements)
1090
+ const sql = extractions.length > columns.length
1091
+ ? `SELECT ${extractions.map(c => `${c.insert} AS ${this.quote(c.name)}`)} FROM (${this.SELECT(src)}) AS NEW`
1092
+ : this.SELECT(src)
1093
+ if (extractions.length > columns.length) columns = this.columns = extractions.map(c => c.name)
1094
+ this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c))}) ${sql}`
1095
+ this.entries = [this.values]
1096
+ return this.sql
1097
+ }
1098
+
1099
+ /**
1100
+ * Wraps the provided SQL expression for output processing
1101
+ * @param {import('./infer/cqn').element} element
1102
+ * @param {string} expr
1103
+ * @returns {string} SQL
1104
+ */
1105
+ output_converter4(element, expr) {
1106
+ const fn = element?.[this.class._convertOutput]
1107
+ return fn?.(expr, element) || expr
1108
+ }
1109
+
1110
+ /** @type {import('./converters').Converters} */
1111
+ static InputConverters = {} // subclasses to override
1112
+
1113
+ /** @type {import('./converters').Converters} */
1114
+ static OutputConverters = {} // subclasses to override
1115
+
1116
+ static localized = { String: { get() { return this['@cds.collate'] !== false } }, UUID: false }
1117
+
1118
+ // UPSERT Statements ------------------------------------------------
1119
+
1120
+ /**
1121
+ * Renders an UPSERT query into generic SQL
1122
+ * @param {import('./infer/cqn').UPDATE} q
1123
+ * @returns {string} SQL
1124
+ */
1125
+ UPSERT(q) {
1126
+ const { UPSERT } = q
1127
+
1128
+ let sql = this.INSERT({ __proto__: q, INSERT: UPSERT })
1129
+ if (!q._target?.keys) return sql
1130
+ const keys = []
1131
+ for (const k of ObjectKeys(q._target?.keys)) {
1132
+ const element = q._target.keys[k]
1133
+ if (element.isAssociation || element.virtual) continue
1134
+ keys.push(k)
1135
+ }
1136
+
1137
+ const elements = q._target?.elements || {}
1138
+ // temporal data
1139
+ for (const k of ObjectKeys(elements)) {
1140
+ if (elements[k]['@cds.valid.from']) keys.push(k)
1141
+ }
1142
+
1143
+ const keyCompare = keys
1144
+ .map(k => `NEW.${this.quote(k)}=OLD.${this.quote(k)}`)
1145
+ .join(' AND ')
1146
+
1147
+ let columns = this.columns // this.columns is computed as part of this.INSERT
1148
+ const entity = q._target ? this.table_name(q) : this.name(UPSERT.into.ref[0], q)
1149
+ if (UPSERT.entries || UPSERT.rows || UPSERT.values) {
1150
+ const managed = this._managed.slice(0, columns.length)
1151
+
1152
+ const extractkeys = managed
1153
+ .filter(c => keys.includes(c.name))
1154
+ .map(c => `${c.onInsert || c.sql} as ${this.quote(c.name)}`)
1155
+
1156
+ sql = `SELECT ${managed.map(c => c.upsert
1157
+ .replace(/value->/g, '"$$$$value$$$$"->')
1158
+ .replace(/json_type\(value,/g, 'json_type("$$$$value$$$$",'))
1159
+ } FROM (SELECT value as "$$value$$", ${extractkeys} from json_each(?)) as NEW LEFT JOIN ${this.quote(entity)} AS OLD ON ${keyCompare}`
1160
+ } else {
1161
+ const extractions = this._managed
1162
+ if (this.values) this.values = [] // Clear previously computed values
1163
+ const src = this.cqn4sql(UPSERT.from || UPSERT.as)
1164
+ const aliasedQuery = cds.ql.SELECT
1165
+ .columns(src.SELECT.columns
1166
+ .map((c, i) => ({ ref: [this.column_name(c)], as: this.columns[i] }))
1167
+ )
1168
+ .from(src)
1169
+ sql = `SELECT ${extractions.map(c => `${c.upsert}`)} FROM (${this.SELECT(aliasedQuery)}) AS NEW LEFT JOIN ${this.quote(entity)} AS OLD ON ${keyCompare}`
1170
+ if (extractions.length > columns.length) columns = this.columns = extractions.map(c => c.name)
1171
+ this.entries = [this.values]
1172
+ }
1173
+
1174
+ const updateColumns = columns.filter(c => {
1175
+ if (keys.includes(c)) return false //> keys go into ON CONFLICT clause
1176
+ let e = elements[c]
1177
+ if (!e) return true //> pass through to native SQL columns not in CDS model
1178
+ if (e.virtual) return true //> skip virtual elements
1179
+ if (e.value) return true //> skip calculated elements
1180
+ // if (e.isAssociation) return true //> this breaks a a test in @sap/cds -> need to follow up how to correctly handle deep upserts
1181
+ else return true
1182
+ }).map(c => `${this.quote(c)} = excluded.${this.quote(c)}`)
1183
+
1184
+ const transitions = this.srv.resolve.transitions(q)
1185
+ return (this.sql = `INSERT INTO ${this.quote(entity)} (${columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c))}) ${sql
1186
+ } WHERE TRUE ON CONFLICT(${keys.map(c => this.quote(c))}) DO ${updateColumns.length ? `UPDATE SET ${updateColumns}` : 'NOTHING'}`)
1187
+ }
1188
+
1189
+ // UPDATE Statements ------------------------------------------------
1190
+
1191
+ /**
1192
+ * Renders an UPDATE query into generic SQL
1193
+ * @param {import('./infer/cqn').UPDATE} q
1194
+ * @returns {string} SQL
1195
+ */
1196
+ UPDATE(q) {
1197
+ const { entity, with: _with, data, where } = q.UPDATE
1198
+ const transitions = this.srv.resolve.transitions(q)
1199
+ const elements = q._target?.elements
1200
+ let sql = `UPDATE ${this.quote(this.table_name(q))}`
1201
+ if (entity.as) sql += ` AS ${this.quote(entity.as)}`
1202
+
1203
+ const _add = (data, sql4) => {
1204
+ for (let col in data) {
1205
+ const c = transitions.mapping.get(col)?.ref?.[0] || col
1206
+ const columnExistsInDatabase = elements
1207
+ && this.physical_column(elements, col)
1208
+ && c in transitions.target.elements
1209
+ && this.physical_column(transitions.target.elements, c)
1210
+ if (!elements || columnExistsInDatabase) {
1211
+ columns.push({ name: c, sql: sql4(data[col], col) })
1212
+ }
1213
+ }
1214
+ }
1215
+
1216
+ let columns = []
1217
+ if (data) _add(data, val => this.val({ val }))
1218
+ if (_with) _add(_with, x => this.expr(x))
1219
+
1220
+ const extraction = this.managed(columns, elements)
1221
+ .filter((c, i) => {
1222
+ if (transitions.mapping.get(c.name)?.ref?.length > 1) return false
1223
+ return columns[i] || c.onUpdate
1224
+ }).map((c, i) => `${this.quote(transitions.mapping.get(c.name)?.ref?.[0] || c.name)}=${!columns[i] ? c.onUpdate : c.sql}`)
1225
+
1226
+ sql += ` SET ${extraction}`
1227
+ if (where) sql += ` WHERE ${this.where_resolved(entity.as, where, q)}`
1228
+ return (this.sql = sql)
1229
+ }
1230
+
1231
+ // DELETE Statements ------------------------------------------------
1232
+
1233
+ /**
1234
+ * Renders a DELETE query into generic SQL
1235
+ * @param {import('./infer/cqn').DELETE} param0
1236
+ * @returns {string} SQL
1237
+ */
1238
+ DELETE(q) {
1239
+ const { DELETE: { where, from } } = q
1240
+ let sql = `DELETE FROM ${this.quote(this.table_name(q))}`
1241
+ if (from.as) sql += ` AS ${this.quote(from.as)}`
1242
+ if (where) sql += ` WHERE ${this.where(where)}`
1243
+ return (this.sql = sql)
1244
+ }
1245
+
1246
+ // Expression Clauses ---------------------------------------------
1247
+
1248
+ /**
1249
+ * Renders an expression object into generic SQL
1250
+ * @param {import('./infer/cqn').expr} x
1251
+ * @returns {string} SQL
1252
+ * @throws {Error} When an unknown un supported expression is provided
1253
+ */
1254
+ expr(x) {
1255
+ const wrap = x.cast ? sql => `cast(${sql} as ${this.type4(x.cast)})` : sql => sql
1256
+ if (typeof x === 'string') throw cds.error`Unsupported expr: ${x}`
1257
+ if (x.param) return wrap(this.param(x))
1258
+ if ('ref' in x) return wrap(this.ref(x))
1259
+ if ('val' in x) return wrap(this.val(x))
1260
+ if ('func' in x) return wrap(this.func(x))
1261
+ if ('xpr' in x) return wrap(this.xpr(x))
1262
+ if ('list' in x) return wrap(this.list(x))
1263
+ if ('SELECT' in x) return wrap(`(${this.SELECT(x)})`)
1264
+ else throw cds.error`Unsupported expr: ${x}`
1265
+ }
1266
+
1267
+ /**
1268
+ * Renders an list of expression objects into generic SQL
1269
+ * @param {import('./infer/cqn').xpr} param0
1270
+ * @returns {string} SQL
1271
+ */
1272
+ xpr({ xpr }) {
1273
+ return xpr
1274
+ .map((x, i) => {
1275
+ if (x in { LIKE: 1, like: 1 } && is_regexp(xpr[i + 1]?.val)) return this.operator('regexp')
1276
+ if (typeof x === 'string') return this.operator(x, i, xpr)
1277
+ if (x.xpr && !x.func) return `(${this.xpr(x)})`
1278
+ else return this.expr(x)
1279
+ })
1280
+ .join(' ')
1281
+ }
1282
+
1283
+ /**
1284
+ * Renders an operation into generic SQL
1285
+ * @param {string} x The current operator string
1286
+ * @param {Number} i Current index of the operator inside the xpr
1287
+ * @param {import('./infer/cqn').predicate[]} xpr The parent xpr in which the operator is used
1288
+ * @returns {string} The correct operator string
1289
+ */
1290
+ operator(x, i, xpr) {
1291
+
1292
+ // Translate = to IS NULL for rhs operand being NULL literal
1293
+ if (x === '=') return xpr[i + 1]?.val === null
1294
+ ? _inline_null(xpr[i + 1]) || 'is'
1295
+ : '='
1296
+
1297
+ // Translate == to IS NULL for rhs operand being NULL literal, otherwise ...
1298
+ // Translate == to IS NOT DISTINCT FROM, unless both operands cannot be NULL
1299
+ if (x === '==') return xpr[i + 1]?.val === null
1300
+ ? _inline_null(xpr[i + 1]) || 'is'
1301
+ : _not_null(i - 1) && _not_null(i + 1)
1302
+ ? '='
1303
+ : this.is_not_distinct_from_
1304
+
1305
+ // Translate != to IS NOT NULL for rhs operand being NULL literal, otherwise...
1306
+ // Translate != to IS DISTINCT FROM, unless both operands cannot be NULL
1307
+ if (x === '!=') return xpr[i + 1]?.val === null
1308
+ ? _inline_null(xpr[i + 1]) || 'is not'
1309
+ : _not_null(i - 1) && _not_null(i + 1)
1310
+ ? '<>'
1311
+ : this.is_distinct_from_
1312
+
1313
+ else return x
1314
+
1315
+ function _inline_null(n) {
1316
+ n.param = false
1317
+ }
1318
+
1319
+ /** Checks if the operand at xpr[i+-1] can be NULL. @returns true if not */
1320
+ function _not_null(i) {
1321
+ const operand = xpr[i]
1322
+ if (!operand) return false
1323
+ if (operand.val != null) return true // non-null values are not null
1324
+ let element = operand.element
1325
+ if (!element) return false
1326
+ if (element.key) return true // primary keys usually should not be null
1327
+ if (element.notNull) return true // not null elements cannot be null
1328
+ }
1329
+ }
1330
+
1331
+ get is_distinct_from_() { return 'is distinct from' }
1332
+ get is_not_distinct_from_() { return 'is not distinct from' }
1333
+
1334
+ /**
1335
+ * Renders an argument place holder into the SQL for prepared statements
1336
+ * @param {import('./infer/cqn').ref} param0
1337
+ * @returns {string} SQL
1338
+ * @throws {Error} When an unsupported ref definition is provided
1339
+ */
1340
+ param({ ref }) {
1341
+ if (ref.length > 1) throw cds.error`Unsupported nested ref parameter: ${ref}`
1342
+ return ref[0] === '?' ? '?' : `:${ref}`
1343
+ }
1344
+
1345
+ /**
1346
+ * Renders a ref into generic SQL
1347
+ * @param {import('./infer/cqn').ref} param0
1348
+ * @returns {string} SQL
1349
+ */
1350
+ ref({ ref }) {
1351
+ switch (ref[0]) {
1352
+ case '$now': return this.func({ func: 'session_context', args: [{ val: '$now', param: false }] }) // REVISIT: why do we need param: false here?
1353
+ case '$user': return this.func({ func: 'session_context', args: [{ val: '$user.' + ref[1] || 'id', param: false }] }) // REVISIT: same here?
1354
+ default: return ref.map(r => this.quote(r)).join('.')
1355
+ }
1356
+ }
1357
+
1358
+ /**
1359
+ * Renders a value into the correct SQL syntax or a placeholder for a prepared statement
1360
+ * @param {import('./infer/cqn').val} param0
1361
+ * @returns {string} SQL
1362
+ */
1363
+ val({ val, param }) {
1364
+ switch (typeof val) {
1365
+ case 'function': throw new Error('Function values not supported.')
1366
+ case 'undefined': val = null
1367
+ break
1368
+ case 'boolean': return `${val}`
1369
+ case 'object':
1370
+ if (val !== null) {
1371
+ if (val instanceof Date) val = val.toJSON() // returns null if invalid
1372
+ else if (val instanceof Readable); // go on with default below
1373
+ else if (Buffer.isBuffer(val)); // go on with default below
1374
+ else if (is_regexp(val)) val = val.source
1375
+ else val = JSON.stringify(val)
1376
+ }
1377
+ }
1378
+ if (!this.values || param === false) {
1379
+ switch (typeof val) {
1380
+ case 'string': return this.string(val)
1381
+ case 'object': return 'NULL'
1382
+ default:
1383
+ return `${val}`
1384
+ }
1385
+ }
1386
+ this.values.push(val)
1387
+ return '?'
1388
+ }
1389
+
1390
+ static Functions = require('./cql-functions')
1391
+ /**
1392
+ * Renders a function call into mapped SQL definitions from the Functions definition
1393
+ * @param {import('./infer/cqn').func} param0
1394
+ * @returns {string} SQL
1395
+ */
1396
+ func({ func, args, xpr }) {
1397
+ const wrap = e => (e === '*' ? e : { __proto__: e, toString: (x = e) => this.expr(x) })
1398
+ args = args || []
1399
+ if (Array.isArray(args)) {
1400
+ args = args.map(wrap)
1401
+ } else if (typeof args === 'object') {
1402
+ const org = args
1403
+ const wrapped = {
1404
+ toString: () => {
1405
+ const ret = []
1406
+ for (const prop in org) {
1407
+ ret.push(`${this.quote(prop)} => ${wrapped[prop]}`)
1408
+ }
1409
+ return ret.join(',')
1410
+ }
1411
+ }
1412
+ for (const prop in args) {
1413
+ wrapped[prop] = wrap(args[prop])
1414
+ }
1415
+ args = wrapped
1416
+ } else {
1417
+ cds.error`Invalid arguments provided for function '${func}' (${args})`
1418
+ }
1419
+ const fn = this.class.Functions[func]?.apply(this, Array.isArray(args) ? args : [args]) || `${func}(${args})`
1420
+ if (xpr) return `${fn} ${this.xpr({ xpr })}`
1421
+ return fn
1422
+ }
1423
+
1424
+ /**
1425
+ * Renders a list into generic SQL
1426
+ * @param {import('./infer/cqn').list} param0
1427
+ * @returns {string} SQL
1428
+ */
1429
+ list({ list }) {
1430
+ return `(${list.map(e => this.expr(e))})`
1431
+ }
1432
+
1433
+ /**
1434
+ * Renders a javascript string into a SQL string literal
1435
+ * @param {string} s
1436
+ * @returns {string} SQL
1437
+ */
1438
+ string(s) {
1439
+ return `'${s.replace(/'/g, "''")}'`
1440
+ }
1441
+
1442
+ /**
1443
+ * Calculates the effect column name
1444
+ * @param {import('./infer/cqn').col} col
1445
+ * @returns {string} explicit/implicit column alias
1446
+ */
1447
+ column_name(col) {
1448
+ if (col === '*')
1449
+ // REVISIT: When could this ever happen? I think this is only about that irrealistic test whech uses column_name to implement SELECT_columns. We should eliminate column_name as its only used and designed for use in SELECT_expand, isn't it?
1450
+ cds.error`Query was not inferred and includes '*' in the columns. For which there is no column name available.`
1451
+ return (typeof col.as === 'string' && col.as) || ('val' in col && col.val + '') || col.func || col.ref.at(-1)
1452
+ }
1453
+
1454
+ /**
1455
+ * Calculates the Database table name of the query target
1456
+ * @param {import('./infer/cqn').Query} query
1457
+ * @returns {string} Database table name
1458
+ */
1459
+ table_name(q) {
1460
+ const table = resolveTable(q._target)
1461
+ return this.name(table.name, { _target: table })
1462
+ }
1463
+
1464
+ /**
1465
+ * Calculates the Database name of the given name
1466
+ * @param {string|import('./infer/cqn').ref} name
1467
+ * @param {import('./infer/cqn').Query} query
1468
+ * @returns {string} Database name
1469
+ */
1470
+ name(name) {
1471
+ return (name.id || name).replace(/\./g, '_')
1472
+ }
1473
+
1474
+ /** @type {unknown} */
1475
+ static ReservedWords = {}
1476
+ /**
1477
+ * Ensures that the given identifier is properly quoted when required by the database
1478
+ * @param {string} s
1479
+ * @returns {string} SQL
1480
+ */
1481
+ quote(s) {
1482
+ if (typeof s !== 'string') return '"' + s + '"'
1483
+ if (s.includes('"')) return '"' + s.replace(/"/g, '""') + '"'
1484
+ if (s in this.class.ReservedWords || !/^[A-Za-z_][A-Za-z_$0-9]*$/.test(s)) return '"' + s + '"'
1485
+ return s
1486
+ }
1487
+
1488
+ /**
1489
+ * Converts the columns array into an array of SQL expressions that extract the correct value from inserted JSON data
1490
+ * @param {object[]} columns
1491
+ * @param {import('./infer/cqn').elements} elements
1492
+ * @param {Boolean} isUpdate
1493
+ * @returns {string[]} Array of SQL expressions for processing input JSON data
1494
+ */
1495
+ managed(columns, elements) {
1496
+ const cdsOnInsert = '@cds.on.insert'
1497
+ const cdsOnUpdate = '@cds.on.update'
1498
+
1499
+ const { _convertInput } = this.class
1500
+ // Ensure that missing managed columns are added
1501
+ const requiredColumns = !elements
1502
+ ? []
1503
+ : ObjectKeys(elements)
1504
+ .filter(e => {
1505
+ const element = elements[e]
1506
+ // Actual mandatory check
1507
+ if (!(element.default || element[cdsOnInsert] || element[cdsOnUpdate])) return false
1508
+ // Physical column check
1509
+ if (!element || element.virtual || element.isAssociation) return false
1510
+ // Existence check
1511
+ if (columns.find(c => c.name === e)) return false
1512
+ return true
1513
+ })
1514
+ .map(name => ({ name, sql: 'NULL' }))
1515
+
1516
+ const keys = ObjectKeys(elements).filter(e => elements[e].key && !elements[e].isAssociation)
1517
+ const keyZero = keys[0] && this.quote(keys[0])
1518
+
1519
+ return [...columns, ...requiredColumns].map(({ name, sql }) => {
1520
+ const element = elements?.[name] || {}
1521
+
1522
+ const converter = a => element[_convertInput]?.(a, element) || a
1523
+ let extract
1524
+ if (!sql) {
1525
+ ({ sql, extract } = this.managed_extract(name, element, converter))
1526
+ } else {
1527
+ extract = sql = converter(sql)
1528
+ }
1529
+ // if (sql[0] !== '$') sql = converter(sql, element)
1530
+
1531
+ let onInsert = this.managed_session_context(element[cdsOnInsert]?.['='])
1532
+ || this.managed_session_context(element.default?.ref?.[0])
1533
+ || (element.default && { __proto__: element.default, param: false })
1534
+ let onUpdate = this.managed_session_context(element[cdsOnUpdate]?.['='])
1535
+
1536
+ if (onInsert) onInsert = this.expr(onInsert)
1537
+ if (onUpdate) onUpdate = this.expr(onUpdate)
1538
+
1539
+ const qname = this.quote(name)
1540
+
1541
+ const insert = onInsert ? this.managed_default(name, converter(onInsert), sql) : sql
1542
+ const update = onUpdate ? this.managed_default(name, converter(onUpdate), sql) : sql
1543
+ const upsert = keyZero && (
1544
+ // upsert requires the keys to be provided for the existance join (default values optional)
1545
+ element.key
1546
+ // If both insert and update have the same managed definition exclude the old value check
1547
+ || (onInsert && onUpdate && insert === update)
1548
+ ? `${insert} as ${qname}`
1549
+ : `CASE WHEN OLD.${keyZero} IS NULL THEN ${
1550
+ // If key of old is null execute insert
1551
+ insert
1552
+ } ELSE ${
1553
+ // Else execute managed update or keep old if no new data if provided
1554
+ onUpdate ? update : this.managed_default(name, `OLD.${qname}`, update)
1555
+ } END as ${qname}`
1556
+ )
1557
+
1558
+ return {
1559
+ name, // Element name
1560
+ sql, // Reference SQL
1561
+ extract, // Source SQL
1562
+ converter, // Converter logic
1563
+ // action specific full logic
1564
+ insert, update, upsert,
1565
+ // action specific isolated logic
1566
+ onInsert, onUpdate
1567
+ }
1568
+ })
1569
+ }
1570
+
1571
+ physical_column(elements, c) {
1572
+ return elements[c] && !elements[c].virtual && !elements[c].value && !elements[c].isAssociation
1573
+ }
1574
+
1575
+ managed_extract(name, element, converter) {
1576
+ const { UPSERT, INSERT } = this.cqn
1577
+ const extract = !(INSERT?.entries || UPSERT?.entries) && (INSERT?.rows || UPSERT?.rows)
1578
+ ? `value->>${this.string(`$[${this.columns.indexOf(name)}]`)}`
1579
+ : `value->>${this.string(`$.${JSON.stringify(name)}`)}`
1580
+ const sql = converter?.(extract) || extract
1581
+ return { extract, sql }
1582
+ }
1583
+
1584
+ managed_session_context(src) {
1585
+ const val = _managed[src]
1586
+ return val && { func: 'session_context', args: [{ val, param: false }] }
1587
+ }
1588
+
1589
+ managed_default(name, managed, src) {
1590
+ return `(CASE WHEN json_type(value,${this.managed_extract(name).extract.slice(8)}) IS NULL THEN ${managed} ELSE ${src} END)`
1591
+ }
1592
+ }
1593
+
1594
+ Readable.prototype[require('node:util').inspect.custom] = Readable.prototype.toJSON = function () { return this._raw || `[object ${this.constructor.name}]` }
1595
+
1596
+ const ObjectKeys = o => (o && [...ObjectKeys(o.__proto__), ...Object.keys(o)]) || []
1597
+ const _managed = {
1598
+ '$user.id': '$user.id',
1599
+ $user: '$user.id',
1600
+ $now: '$now',
1601
+ }
1602
+
1603
+ const is_regexp = x => x?.constructor?.name === 'RegExp' // NOTE: x instanceof RegExp doesn't work in repl
1604
+ const _empty = a => !a || a.length === 0
1605
+
1606
+ /**
1607
+ * @param {import('@sap/cds/apis/cqn').Query} q
1608
+ * @param {import('@sap/cds/apis/csn').CSN} m
1609
+ */
1610
+ module.exports = (q, m) => new CQN2SQLRenderer({ model: m }).render(cqn4sql(q, m))
1611
+ module.exports.class = CQN2SQLRenderer
1612
+ module.exports.classDefinition = CQN2SQLRenderer // class is a reserved typescript word