@cap-js/db-service 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cqn2sql.js ADDED
@@ -0,0 +1,605 @@
1
+ const cds = require('@sap/cds/lib')
2
+ const cds_infer = require('./infer')
3
+ const cqn4sql = require('./cqn4sql')
4
+
5
+ const DEBUG = (() => {
6
+ let DEBUG = cds.debug('sql-json')
7
+ if (DEBUG) return DEBUG
8
+ else DEBUG = cds.debug('sql|sqlite')
9
+ if (DEBUG) {
10
+ return DEBUG
11
+ // (sql, ...more) => DEBUG (sql.replace(/(?:SELECT[\n\r\s]+(json_group_array\()?[\n\r\s]*json_insert\((\n|\r|.)*?\)[\n\r\s]*\)?[\n\r\s]+as[\n\r\s]+_json_[\n\r\s]+FROM[\n\r\s]*\(|\)[\n\r\s]*(\)[\n\r\s]+AS )|\)$)/gim,(a,b,c,d) => d || ''), ...more)
12
+ // FIXME: looses closing ) on INSERT queries
13
+ }
14
+ })()
15
+
16
+ class CQN2SQLRenderer {
17
+ constructor(context) {
18
+ this.context = cds.context || context
19
+ this.class = new.target // for IntelliSense
20
+ this.class._init() // is a noop for subsequent calls
21
+ }
22
+ static _init() {
23
+ const _add_mixins = (aspect, mixins) => {
24
+ const fqn = this.name + aspect
25
+ const types = cds.builtin.types
26
+ for (let each in mixins) {
27
+ const def = types[each]
28
+ if (!def) continue
29
+ Object.defineProperty(def, fqn, { value: mixins[each] })
30
+ }
31
+ return fqn
32
+ }
33
+ this._localized = _add_mixins(':localized', this.localized)
34
+ this._convertInput = _add_mixins(':convertInput', this.InputConverters)
35
+ this._convertOutput = _add_mixins(':convertOutput', this.OutputConverters)
36
+ this._sqlType = _add_mixins(':sqlType', this.TypeMap)
37
+ this._init = () => {} // makes this a noop for subsequent calls
38
+ }
39
+
40
+ render(q, vars) {
41
+ const cmd = q.cmd || Object.keys(q)[0] // SELECT, INSERT, ...
42
+ this.sql = '' // to have it as first property for debugging
43
+ this.values = [] // prepare values, filled in by subroutines
44
+ this[cmd]((this.cqn = q)) // actual sql rendering happens here
45
+ if (vars?.length && !this.values.length) this.values = vars
46
+ const sanitize_values = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
47
+ DEBUG?.(
48
+ this.sql,
49
+ sanitize_values && (this.entries || this.values?.length > 0) ? ['***'] : this.entries || this.values,
50
+ )
51
+ return this
52
+ }
53
+
54
+ infer(q) {
55
+ return q.target ? q : cds_infer(q)
56
+ }
57
+
58
+ // CREATE Statements ------------------------------------------------
59
+
60
+ CREATE(q) {
61
+ const { target } = q,
62
+ { query } = target
63
+ const name = this.name(target.name)
64
+ // Don't allow place holders inside views
65
+ delete this.values
66
+ this.sql =
67
+ !query || target['@cds.persistence.table']
68
+ ? `CREATE TABLE ${name} ( ${this.CREATE_elements(target.elements)} )`
69
+ : `CREATE VIEW ${name} AS ${this.SELECT(cqn4sql(query))}`
70
+ this.values = []
71
+ return
72
+ }
73
+
74
+ CREATE_elements(elements) {
75
+ let sql = ''
76
+ for (let e in elements) {
77
+ const definition = elements[e]
78
+ if (definition.isAssociation) continue
79
+ const s = this.CREATE_element(definition)
80
+ if (s) sql += `${s}, `
81
+ }
82
+ return sql.slice(0, -2)
83
+ }
84
+
85
+ CREATE_element(element) {
86
+ const type = this.type4(element)
87
+ if (type) return this.quote(element.name) + ' ' + type
88
+ }
89
+
90
+ type4(element) {
91
+ if (!element._type) element = cds.builtin.types[element.type] || element
92
+ const fn = element[this.class._sqlType]
93
+ return (
94
+ fn?.(element) || element._type?.replace('cds.', '').toUpperCase() || cds.error`Unsupported type: ${element.type}`
95
+ )
96
+ }
97
+
98
+ static TypeMap = {
99
+ // Utilizing cds.linked inheritance
100
+ String: e => `NVARCHAR(${e.length || 5000})`,
101
+ Binary: e => `VARBINARY(${e.length || 5000})`,
102
+ Int64: () => 'BIGINT',
103
+ Int32: () => 'INTEGER',
104
+ Int16: () => 'SMALLINT',
105
+ UInt8: () => 'SMALLINT',
106
+ Integer64: () => 'BIGINT',
107
+ LargeString: () => 'NCLOB',
108
+ LargeBinary: () => 'BLOB',
109
+ Association: () => false,
110
+ Composition: () => false,
111
+ array: () => 'NCLOB',
112
+ // HANA types
113
+ /* Disabled as these types are linked to normal cds types
114
+ 'cds.hana.TINYINT': () => 'REAL',
115
+ 'cds.hana.REAL': () => 'REAL',
116
+ 'cds.hana.CHAR': e => `CHAR(${e.length || 1})`,
117
+ 'cds.hana.ST_POINT': () => 'ST_POINT',
118
+ 'cds.hana.ST_GEOMETRY': () => 'ST_GEO',*/
119
+ }
120
+
121
+ // DROP Statements ------------------------------------------------
122
+
123
+ DROP(q) {
124
+ const { target } = q
125
+ const isView = target.query || target.projection
126
+ return (this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} IF EXISTS ${this.name(target.name)}`)
127
+ }
128
+
129
+ // SELECT Statements ------------------------------------------------
130
+
131
+ SELECT(q) {
132
+ let { from, expand, where, groupBy, having, orderBy, limit, one, distinct, localized } = q.SELECT
133
+ if (!expand) expand = q.SELECT.expand = has_expands(q) || has_arrays(q)
134
+ // REVISIT: When selecting from an entity that is not in the model the from.where are not normalized (as cqn4sql is skipped)
135
+ if (!where && from?.ref?.length === 1 && from.ref[0]?.where) where = from.ref[0]?.where
136
+ let columns = this.SELECT_columns(q)
137
+ let x,
138
+ sql = `SELECT`
139
+ if (distinct) sql += ` DISTINCT`
140
+ if (!_empty((x = columns))) sql += ` ${x}`
141
+ if (!_empty((x = from))) sql += ` FROM ${this.from(x)}`
142
+ if (!_empty((x = where))) sql += ` WHERE ${this.where(x)}`
143
+ if (!_empty((x = groupBy))) sql += ` GROUP BY ${this.groupBy(x)}`
144
+ if (!_empty((x = having))) sql += ` HAVING ${this.having(x)}`
145
+ if (!_empty((x = orderBy))) sql += ` ORDER BY ${this.orderBy(x, localized)}`
146
+ if (one) sql += ` LIMIT ${this.limit({ rows: { val: 1 } })}`
147
+ else if ((x = limit)) sql += ` LIMIT ${this.limit(x)}`
148
+ if (expand) sql = this.SELECT_expand(q, sql)
149
+ return (this.sql = sql)
150
+ }
151
+
152
+ SELECT_columns({ SELECT }) {
153
+ // REVISIT: We don't have to run x.as through this.column_name(), do we?
154
+ if (!SELECT.columns) return '*'
155
+ return SELECT.columns.map(x => this.column_expr(x) + (typeof x.as === 'string' ? ' as ' + this.quote(x.as) : ''))
156
+ }
157
+
158
+ SELECT_expand({ SELECT, elements }, sql) {
159
+ if (!SELECT.columns) return sql
160
+ if (!elements) return sql
161
+ let cols = !SELECT.columns
162
+ ? ['*']
163
+ : SELECT.columns.map(x => {
164
+ const name = this.column_name(x)
165
+ // REVISIT: can be removed when alias handling is resolved properly
166
+ const d = elements[name] || elements[name.substring(1, name.length - 1)]
167
+ let col = `'$."${name}"',${this.output_converter4(d, this.quote(name))}`
168
+
169
+ if (x.SELECT?.count) {
170
+ // Return both the sub select and the count for @odata.count
171
+ const qc = cds.ql.clone(x, { columns: [{ func: 'count' }], one: 1, limit: 0, orderBy: 0 })
172
+ col += `, '$."${name}@odata.count"',${this.expr(qc)}`
173
+ }
174
+ return col
175
+ })
176
+
177
+ // Prevent SQLite from hitting function argument limit of 100
178
+ let colsLength = cols.length
179
+ let obj = "'{}'"
180
+ for (let i = 0; i < colsLength; i += 48) {
181
+ obj = `json_insert(${obj},${cols.slice(i, i + 48)})`
182
+ }
183
+ return `SELECT ${SELECT.one || SELECT.expand === 'root' ? obj : `json_group_array(${obj})`} as _json_ FROM (${sql})`
184
+ }
185
+
186
+ column_expr(x) {
187
+ if (x.func && !x.as) x.as = x.func
188
+ if (x?.element?.['@cds.extension']) {
189
+ x.as = x.as || x.element.name
190
+ return `extensions__->${this.string('$."' + x.element.name + '"')}`
191
+ }
192
+ let sql = this.expr(x)
193
+ return sql
194
+ }
195
+
196
+ from(from) {
197
+ const { ref, as } = from,
198
+ _aliased = as ? s => s + ` as ${this.quote(as)}` : s => s
199
+ if (ref) return _aliased(this.quote(this.name(ref[0])))
200
+ if (from.SELECT) return _aliased(`(${this.SELECT(from)})`)
201
+ if (from.join) {
202
+ const {
203
+ join,
204
+ args: [left, right],
205
+ on,
206
+ } = from
207
+ return `${this.from(left)} ${join} JOIN ${this.from(right)} ON ${this.xpr({ xpr: on })}`
208
+ }
209
+ }
210
+
211
+ where(xpr) {
212
+ return this.xpr({ xpr })
213
+ }
214
+
215
+ having(xpr) {
216
+ return this.xpr({ xpr })
217
+ }
218
+
219
+ groupBy(clause) {
220
+ return clause.map(c => this.expr(c))
221
+ }
222
+
223
+ orderBy(orderBy, localized) {
224
+ return orderBy.map(
225
+ localized
226
+ ? c =>
227
+ this.expr(c) +
228
+ (c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
229
+ (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
230
+ : c => this.expr(c) + (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC'),
231
+ )
232
+ }
233
+
234
+ limit({ rows, offset }) {
235
+ if (!rows) throw new Error('Rows parameter is missing in SELECT.limit(rows, offset)')
236
+ return !offset ? rows.val : `${rows.val} OFFSET ${offset.val}`
237
+ }
238
+
239
+ // INSERT Statements ------------------------------------------------
240
+
241
+ INSERT(q) {
242
+ const { INSERT } = q
243
+ return INSERT.entries
244
+ ? this.INSERT_entries(q)
245
+ : INSERT.rows
246
+ ? this.INSERT_rows(q)
247
+ : INSERT.values
248
+ ? this.INSERT_values(q)
249
+ : INSERT.as
250
+ ? this.INSERT_select(q)
251
+ : cds.error`Missing .entries, .rows, or .values in ${q}`
252
+ }
253
+
254
+ INSERT_entries(q) {
255
+ const { INSERT } = q
256
+ const entity = this.name(q.target?.name || INSERT.into.ref[0])
257
+ const alias = INSERT.into.as
258
+ const elements = q.elements || q.target?.elements
259
+ if (!elements && !INSERT.entries?.length) {
260
+ return // REVISIT: mtx sends an insert statement without entries and no reference entity
261
+ }
262
+ const columns = elements
263
+ ? ObjectKeys(elements).filter(c => c in elements && !elements[c].virtual && !elements[c].isAssociation)
264
+ : ObjectKeys(INSERT.entries[0])
265
+ this.columns = columns.filter(elements ? c => !elements[c]?.['@cds.extension'] : () => true).map(c => this.quote(c))
266
+
267
+ const extractions = this.managed(
268
+ columns.map(c => ({ name: c })),
269
+ elements,
270
+ !!q.UPSERT,
271
+ )
272
+ const extraction = extractions
273
+ .map(c => {
274
+ const element = elements?.[c.name]
275
+ if (element?.['@cds.extension']) {
276
+ return false
277
+ }
278
+ if (c.name === 'extensions__') {
279
+ const merges = extractions.filter(c => elements?.[c.name]?.['@cds.extension'])
280
+ if (merges.length) {
281
+ c.sql = `json_set(ifnull(${c.sql},'{}'),${merges.map(
282
+ c => this.string('$."' + c.name + '"') + ',' + c.sql,
283
+ )})`
284
+ }
285
+ }
286
+ return c
287
+ })
288
+ .filter(a => a)
289
+ .map(c => c.sql)
290
+
291
+ this.entries = [[JSON.stringify(INSERT.entries)]]
292
+ return (this.sql = `INSERT INTO ${entity}${alias ? ' as ' + this.quote(alias) : ''} (${
293
+ this.columns
294
+ }) SELECT ${extraction} FROM json_each(?)`)
295
+ }
296
+
297
+ INSERT_rows(q) {
298
+ const { INSERT } = q
299
+ const entity = this.name(q.target?.name || INSERT.into.ref[0])
300
+ const alias = INSERT.into.as
301
+ const elements = q.elements || q.target?.elements
302
+ if (!INSERT.columns && !elements) {
303
+ throw cds.error`Cannot insert rows without columns or elements`
304
+ }
305
+ let columns = INSERT.columns || (elements && ObjectKeys(elements))
306
+ if (elements) {
307
+ columns = columns.filter(c => c in elements && !elements[c].virtual && !elements[c].isAssociation)
308
+ }
309
+ this.columns = columns.map(c => this.quote(c))
310
+
311
+ const inputConverterKey = this.class._convertInput
312
+ const extraction = columns.map((c, i) => {
313
+ const element = elements?.[c] || {}
314
+ const extract = `value->>'$[${i}]'`
315
+ const converter = element[inputConverterKey] || (e => e)
316
+ return converter(extract, element)
317
+ })
318
+
319
+ this.entries = [[JSON.stringify(INSERT.rows)]]
320
+ return (this.sql = `INSERT INTO ${entity}${alias ? ' as ' + this.quote(alias) : ''} (${
321
+ this.columns
322
+ }) SELECT ${extraction} FROM json_each(?)`)
323
+ }
324
+
325
+ INSERT_values(q) {
326
+ let { columns, values } = q.INSERT
327
+ return this.INSERT_rows({ __proto__: q, INSERT: { __proto__: q.INSERT, columns, rows: [values] } })
328
+ }
329
+
330
+ INSERT_select(q) {
331
+ const { INSERT } = q
332
+ const entity = this.name(q.target.name)
333
+ const alias = INSERT.into.as
334
+ const elements = q.elements || q.target?.elements || {}
335
+ const columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
336
+ c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
337
+ ))
338
+ this.sql = `INSERT INTO ${entity}${alias ? ' as ' + this.quote(alias) : ''} (${columns}) ${this.SELECT(
339
+ cqn4sql(INSERT.as),
340
+ )}`
341
+ this.entries = [this.values]
342
+ return this.sql
343
+ }
344
+
345
+ output_converter4(element, expr) {
346
+ const fn = element?.[this.class._convertOutput]
347
+ return fn?.(expr, element) || expr
348
+ }
349
+
350
+ static InputConverters = {} // subclasses to override
351
+
352
+ static OutputConverters = {} // subclasses to override
353
+
354
+ static localized = { String: true, UUID: false }
355
+
356
+ // UPSERT Statements ------------------------------------------------
357
+
358
+ UPSERT(q) {
359
+ let { UPSERT } = q,
360
+ sql = this.INSERT({ __proto__: q, INSERT: UPSERT })
361
+ let keys = q.target?.keys
362
+ if (!keys) return (this.sql = sql) // REVISIT: We should converge q.target and q._target
363
+ keys = Object.keys(keys).filter(k => !keys[k].isAssociation)
364
+
365
+ let updateColumns = q.UPSERT.entries ? Object.keys(q.UPSERT.entries[0]) : this.columns
366
+ updateColumns = updateColumns
367
+ .filter(c => !keys.includes(c))
368
+ .map(c => `${this.quote(c)} = excluded.${this.quote(c)}`)
369
+
370
+ keys = keys.map(k => this.quote(k))
371
+ const conflict = updateColumns.length
372
+ ? `ON CONFLICT(${keys}) DO UPDATE SET ` + updateColumns
373
+ : `ON CONFLICT(${keys}) DO NOTHING`
374
+ return (this.sql = `${sql} WHERE true ${conflict}`)
375
+ }
376
+
377
+ // UPDATE Statements ------------------------------------------------
378
+
379
+ UPDATE(q) {
380
+ const {
381
+ UPDATE: { entity, with: _with, data, where },
382
+ } = q,
383
+ elements = q.target?.elements
384
+ let sql = `UPDATE ${this.name(entity.ref?.[0] || entity)}`
385
+ if (entity.as) sql += ` AS ${entity.as}`
386
+ let columns = []
387
+ if (data)
388
+ for (let c in data)
389
+ if (!elements || (c in elements && !elements[c].virtual)) {
390
+ columns.push({ name: c, sql: this.val({ val: data[c] }) })
391
+ }
392
+ if (_with)
393
+ for (let c in _with)
394
+ if (!elements || (c in elements && !elements[c].virtual)) {
395
+ columns.push({ name: c, sql: this.expr(_with[c]) })
396
+ }
397
+
398
+ columns = columns.map(c => {
399
+ if (q.elements?.[c.name]?.['@cds.extension']) {
400
+ return {
401
+ name: 'extensions__',
402
+ sql: `json_set(extensions__,${this.string('$."' + c.name + '"')},${c.sql})`,
403
+ }
404
+ }
405
+ return c
406
+ })
407
+
408
+ const extraction = this.managed(columns, elements, true).map(c => `${this.quote(c.name)}=${c.sql}`)
409
+
410
+ sql += ` SET ${extraction}`
411
+ if (where) sql += ` WHERE ${this.where(where)}`
412
+ return (this.sql = sql)
413
+ }
414
+
415
+ // DELETE Statements ------------------------------------------------
416
+
417
+ DELETE({ DELETE: { from, where } }) {
418
+ let sql = `DELETE FROM ${this.from(from)}`
419
+ if (where) sql += ` WHERE ${this.where(where)}`
420
+ return (this.sql = sql)
421
+ }
422
+
423
+ // STREAM Statement -------------------------------------------------
424
+
425
+ STREAM(q) {
426
+ let { from, into, where, column, data } = q.STREAM
427
+ let x, sql
428
+ // reading stream
429
+ if (from) {
430
+ sql = `SELECT`
431
+ if (!_empty((x = column))) sql += ` ${this.quote(x)}`
432
+ if (!_empty((x = from))) sql += ` FROM ${this.from(x)}`
433
+ } else {
434
+ // writing stream
435
+ const entity = this.name(q.target?.name || into.ref[0])
436
+ sql = `UPDATE ${this.quote(entity)}${into.as ? ` AS ${into.as}` : ``} SET ${this.quote(column)}=?`
437
+ this.entries = [data]
438
+ }
439
+ if (!_empty((x = where))) sql += ` WHERE ${this.where(x)}`
440
+ if (from) sql += ` LIMIT ${this.limit({ rows: { val: 1 } })}`
441
+ return (this.sql = sql)
442
+ }
443
+
444
+ // Expression Clauses ---------------------------------------------
445
+
446
+ expr(x) {
447
+ const wrap = x.cast ? sql => `cast(${sql} as ${this.type4(x.cast)})` : sql => sql
448
+ if (typeof x === 'string') throw cds.error`Unsupported expr: ${x}`
449
+ if ('param' in x) return wrap(this.param(x))
450
+ if ('ref' in x) return wrap(this.ref(x))
451
+ if ('val' in x) return wrap(this.val(x))
452
+ if ('xpr' in x) return wrap(this.xpr(x))
453
+ if ('func' in x) return wrap(this.func(x))
454
+ if ('list' in x) return wrap(this.list(x))
455
+ if ('SELECT' in x) return wrap(`(${this.SELECT(x)})`)
456
+ else throw cds.error`Unsupported expr: ${x}`
457
+ }
458
+
459
+ xpr({ xpr }) {
460
+ return xpr
461
+ .map((x, i) => {
462
+ if (x in { LIKE: 1, like: 1 } && is_regexp(xpr[i + 1]?.val)) return this.operator('regexp')
463
+ if (typeof x === 'string') return this.operator(x, i, xpr)
464
+ if (x.xpr) return `(${this.xpr(x)})`
465
+ else return this.expr(x)
466
+ })
467
+ .join(' ')
468
+ }
469
+
470
+ operator(x, i, xpr) {
471
+ if (x === '=' && xpr[i + 1]?.val === null) return 'is'
472
+ if (x === '!=') return 'is not'
473
+ else return x
474
+ }
475
+
476
+ param({ ref }) {
477
+ if (ref.length > 1) throw cds.error`Unsupported nested ref parameter: ${ref}`
478
+ return ref[0] === '?' ? '?' : `:${ref}`
479
+ }
480
+
481
+ ref({ ref }) {
482
+ return ref.map(r => this.quote(r)).join('.')
483
+ }
484
+
485
+ val({ val }) {
486
+ switch (typeof val) {
487
+ case 'function':
488
+ throw new Error('Function values not supported.')
489
+ case 'undefined':
490
+ return 'NULL'
491
+ case 'boolean':
492
+ return val
493
+ case 'number':
494
+ return val // REVISIT for HANA
495
+ case 'object':
496
+ if (val === null) return 'NULL'
497
+ if (val instanceof Date) return `'${val.toISOString()}'`
498
+ if (Buffer.isBuffer(val)) val = val.toString('base64')
499
+ else val = this.regex(val) || this.json(val)
500
+ }
501
+ if (!this.values) return this.string(val)
502
+ this.values.push(val)
503
+ return '?'
504
+ }
505
+
506
+ static Functions = require('./cql-functions')
507
+ func({ func, args }) {
508
+ args = (args || []).map(e => (e === '*' ? e : { __proto__: e, toString: (x = e) => this.expr(x) }))
509
+ return this.class.Functions[func]?.apply(this.class.Functions, args) || `${func}(${args})`
510
+ }
511
+
512
+ list({ list }) {
513
+ return `(${list.map(e => this.expr(e))})`
514
+ }
515
+
516
+ regex(o) {
517
+ if (is_regexp(o)) return o.source
518
+ }
519
+
520
+ json(o) {
521
+ return JSON.stringify(o)
522
+ }
523
+
524
+ string(s) {
525
+ return `'${s.replace(/'/g, "''")}'`
526
+ }
527
+
528
+ column_name(col) {
529
+ return (typeof col.as === 'string' && col.as) || ('val' in col && col.val + '') || col.ref[col.ref.length - 1]
530
+ }
531
+
532
+ name(name) {
533
+ return (name.id || name).replace(/\./g, '_')
534
+ }
535
+
536
+ static ReservedWords = {}
537
+ quote(s) {
538
+ if (typeof s !== 'string') return '"' + s + '"'
539
+ if (s.includes('"')) return '"' + s.replace(/"/g, '""') + '"'
540
+ if (s.toUpperCase() in this.class.ReservedWords || /^\d|[$' @./\\]/.test(s)) return '"' + s + '"'
541
+ return s
542
+ }
543
+
544
+ managed(columns, elements, isUpdate = false) {
545
+ const annotation = isUpdate ? '@cds.on.update' : '@cds.on.insert'
546
+ const inputConverterKey = this.class._convertInput
547
+ // Ensure that missing managed columns are added
548
+ const requiredColumns = !elements
549
+ ? []
550
+ : Object.keys(elements)
551
+ .filter(
552
+ e =>
553
+ (elements[e]?.[annotation] || (!isUpdate && elements[e]?.default && !elements[e].virtual)) &&
554
+ !columns.find(c => c.name === e),
555
+ )
556
+ .map(name => ({ name, sql: 'NULL' }))
557
+
558
+ return [...columns, ...requiredColumns].map(({ name, sql }) => {
559
+ const element = elements?.[name] || {}
560
+ let extract = sql ?? `value->>'$."${name}"'`
561
+ const converter = element[inputConverterKey] || (e => e)
562
+ let managed = element[annotation]?.['=']
563
+ switch (managed) {
564
+ case '$user.id':
565
+ case '$user':
566
+ managed = this.string(this.context.user.id)
567
+ break
568
+ case '$now':
569
+ managed = this.string(this.context.timestamp.toISOString())
570
+ break
571
+ default:
572
+ managed = undefined
573
+ }
574
+ if (!isUpdate) {
575
+ const d = element.default
576
+ if (d && (d.val !== undefined || d.ref?.[0] === '$now')) {
577
+ extract = `(CASE WHEN json_type(value,'$."${name}"') IS NULL THEN ${this.defaultValue(
578
+ d.val,
579
+ )} ELSE ${extract} END)`
580
+ }
581
+ }
582
+ return {
583
+ name,
584
+ sql: converter(managed === undefined ? extract : `coalesce(${extract}, ${managed})`, element),
585
+ }
586
+ })
587
+ }
588
+
589
+ defaultValue(defaultValue = this.context.timestamp.toISOString()) {
590
+ return typeof defaultValue === 'string' ? this.string(defaultValue) : defaultValue
591
+ }
592
+ }
593
+
594
+ // REVISIT: Workaround for JSON.stringify to work with buffers
595
+ Buffer.prototype.toJSON = function () {
596
+ return this.toString('base64')
597
+ }
598
+
599
+ const ObjectKeys = o => (o && [...ObjectKeys(o.__proto__), ...Object.keys(o)]) || []
600
+ const has_expands = q => q.SELECT.columns?.some(c => c.SELECT?.expand)
601
+ const has_arrays = q => q.elements && Object.values(q.elements).some(e => e.items)
602
+
603
+ const is_regexp = x => x?.constructor?.name === 'RegExp' // NOTE: x instanceof RegExp doesn't work in repl
604
+ const _empty = a => !a || a.length === 0
605
+ module.exports = Object.assign((q, m) => new CQN2SQLRenderer().render(cqn4sql(q, m), m), { class: CQN2SQLRenderer })