@cap-js/db-service 1.1.0 → 1.2.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 CHANGED
@@ -2,6 +2,8 @@ const cds = require('@sap/cds/lib')
2
2
  const cds_infer = require('./infer')
3
3
  const cqn4sql = require('./cqn4sql')
4
4
 
5
+ const { Readable } = require('stream')
6
+
5
7
  const DEBUG = (() => {
6
8
  let DEBUG = cds.debug('sql-json')
7
9
  if (DEBUG) return DEBUG
@@ -19,35 +21,37 @@ class CQN2SQLRenderer {
19
21
  * @constructor
20
22
  * @param {import('@sap/cds/apis/services').ContextProperties} context the cds.context of the request
21
23
  */
22
- constructor(context) {
23
- /**
24
- * @type {import('@sap/cds/apis/services').ContextProperties}
25
- */
26
- this.context = cds.context || context
27
- // REVISIT: find a way to make CQN2SQLRenderer work in SQLService as well
28
- /** @type {CQN2SQLRenderer|unknown} */
24
+ constructor(srv) {
25
+ this.context = srv?.context || cds.context // Using srv.context is required due to stakeholders doing unmanaged txs without cds.context being set
29
26
  this.class = new.target // for IntelliSense
30
27
  this.class._init() // is a noop for subsequent calls
31
28
  }
32
29
 
30
+ static _add_mixins (aspect, mixins) {
31
+ const fqn = this.name + aspect
32
+ const types = cds.builtin.types
33
+ for (let each in mixins) {
34
+ const def = types[each]
35
+ if (!def) continue
36
+ Object.defineProperty(def, fqn, { value: mixins[each] })
37
+ }
38
+ return fqn
39
+ }
40
+
33
41
  /**
34
42
  * Initializes the class one first creation to link types to data converters
35
43
  */
36
44
  static _init() {
37
- const _add_mixins = (aspect, mixins) => {
38
- const fqn = this.name + aspect
39
- const types = cds.builtin.types
40
- for (let each in mixins) {
41
- const def = types[each]
42
- if (!def) continue
43
- Object.defineProperty(def, fqn, { value: mixins[each] })
44
- }
45
- return fqn
45
+ this._localized = this._add_mixins(':localized', this.localized)
46
+ this._convertInput = this._add_mixins(':convertInput', this.InputConverters)
47
+ this._convertOutput = this._add_mixins(':convertOutput', this.OutputConverters)
48
+ this._sqlType = this._add_mixins(':sqlType', this.TypeMap)
49
+ // Have all-uppercase all-lowercase, and capitalized keywords to speed up lookups
50
+ for (let each in this.ReservedWords) {
51
+ // ORDER
52
+ this.ReservedWords[each[0] + each.slice(1).toLowerCase()] = 1 // Order
53
+ this.ReservedWords[each.toLowerCase()] = 1 // order
46
54
  }
47
- this._localized = _add_mixins(':localized', this.localized)
48
- this._convertInput = _add_mixins(':convertInput', this.InputConverters)
49
- this._convertOutput = _add_mixins(':convertOutput', this.OutputConverters)
50
- this._sqlType = _add_mixins(':sqlType', this.TypeMap)
51
55
  this._init = () => {} // makes this a noop for subsequent calls
52
56
  }
53
57
 
@@ -189,23 +193,22 @@ class CQN2SQLRenderer {
189
193
  */
190
194
  SELECT(q) {
191
195
  let { from, expand, where, groupBy, having, orderBy, limit, one, distinct, localized } = q.SELECT
192
- if (!expand) expand = q.SELECT.expand = has_expands(q) || has_arrays(q)
193
196
  // REVISIT: When selecting from an entity that is not in the model the from.where are not normalized (as cqn4sql is skipped)
194
197
  if (!where && from?.ref?.length === 1 && from.ref[0]?.where) where = from.ref[0]?.where
195
198
  let columns = this.SELECT_columns(q)
196
- let x,
197
- sql = `SELECT`
199
+ let sql = `SELECT`
198
200
  if (distinct) sql += ` DISTINCT`
199
- if (!_empty((x = columns))) sql += ` ${x}`
200
- if (!_empty((x = from))) sql += ` FROM ${this.from(x)}`
201
- if (!_empty((x = where))) sql += ` WHERE ${this.where(x)}`
202
- if (!_empty((x = groupBy))) sql += ` GROUP BY ${this.groupBy(x)}`
203
- if (!_empty((x = having))) sql += ` HAVING ${this.having(x)}`
204
- if (!_empty((x = orderBy))) sql += ` ORDER BY ${this.orderBy(x, localized)}`
205
- if (one) sql += ` LIMIT ${this.limit({ rows: { val: 1 } })}`
206
- else if ((x = limit)) sql += ` LIMIT ${this.limit(x)}`
201
+ if (!_empty(columns)) sql += ` ${columns}`
202
+ if (!_empty(from)) sql += ` FROM ${this.from(from)}`
203
+ if (!_empty(where)) sql += ` WHERE ${this.where(where)}`
204
+ if (!_empty(groupBy)) sql += ` GROUP BY ${this.groupBy(groupBy)}`
205
+ if (!_empty(having)) sql += ` HAVING ${this.having(having)}`
206
+ if (!_empty(orderBy)) sql += ` ORDER BY ${this.orderBy(orderBy, localized)}`
207
+ if (one) limit = Object.assign({}, limit, { rows: { val: 1 } })
208
+ if (limit) sql += ` LIMIT ${this.limit(limit)}`
207
209
  // Expand cannot work without an inferred query
208
210
  if (expand) {
211
+ // REVISIT: Why don't we handle that as an error in SELECT_expand?
209
212
  if (!q.elements) cds.error`Query was not inferred and includes expand. For which the metadata is missing.`
210
213
  sql = this.SELECT_expand(q, sql)
211
214
  }
@@ -217,13 +220,8 @@ class CQN2SQLRenderer {
217
220
  * @param {import('./infer/cqn').SELECT} param0
218
221
  * @returns {string} SQL
219
222
  */
220
- SELECT_columns({ SELECT }) {
221
- // REVISIT: We don't have to run x.as through this.column_name(), do we?
222
- if (!SELECT.columns) return '*'
223
- return SELECT.columns.map(x => {
224
- if (x === '*') return x
225
- return this.column_expr(x) + (typeof x.as === 'string' ? ' as ' + this.quote(x.as) : '')
226
- })
223
+ SELECT_columns(q) {
224
+ return (q.SELECT.columns ?? ['*']).map(x => this.column_expr(x, q))
227
225
  }
228
226
 
229
227
  /**
@@ -234,27 +232,21 @@ class CQN2SQLRenderer {
234
232
  */
235
233
  SELECT_expand({ SELECT, elements }, sql) {
236
234
  if (!SELECT.columns) return sql
237
- if (!elements) return sql
238
- let cols = !SELECT.columns
239
- ? ['*']
240
- : SELECT.columns.map(x => {
241
- const name = this.column_name(x)
242
- // REVISIT: can be removed when alias handling is resolved properly
243
- const d = elements[name] || elements[name.substring(1, name.length - 1)]
244
- let col = `'$."${name}"',${this.output_converter4(d, this.quote(name))}`
245
-
246
- if (x.SELECT?.count) {
247
- // Return both the sub select and the count for @odata.count
248
- const qc = cds.ql.clone(x, { columns: [{ func: 'count' }], one: 1, limit: 0, orderBy: 0 })
249
- col += `, '$."${name}@odata.count"',${this.expr(qc)}`
250
- }
251
- return col
252
- })
235
+ if (!elements) return sql // REVISIT: Above we say this is an error condition, but here we say it's ok?
236
+ let cols = SELECT.columns.map(x => {
237
+ const name = this.column_name(x)
238
+ let col = `'$."${name}"',${this.output_converter4(x.element, this.quote(name))}`
239
+ if (x.SELECT?.count) {
240
+ // Return both the sub select and the count for @odata.count
241
+ const qc = cds.ql.clone(x, { columns: [{ func: 'count' }], one: 1, limit: 0, orderBy: 0 })
242
+ col += `, '$."${name}@odata.count"',${this.expr(qc)}`
243
+ }
244
+ return col
245
+ })
253
246
 
254
247
  // Prevent SQLite from hitting function argument limit of 100
255
- let colsLength = cols.length
256
248
  let obj = "'{}'"
257
- for (let i = 0; i < colsLength; i += 48) {
249
+ for (let i = 0; i < cols.length; i += 48) {
258
250
  obj = `json_insert(${obj},${cols.slice(i, i + 48)})`
259
251
  }
260
252
  return `SELECT ${SELECT.one || SELECT.expand === 'root' ? obj : `json_group_array(${obj})`} as _json_ FROM (${sql})`
@@ -265,34 +257,41 @@ class CQN2SQLRenderer {
265
257
  * @param {import('./infer/cqn').col} x
266
258
  * @returns {string} SQL
267
259
  */
268
- column_expr(x) {
269
- if (x.func && !x.as) x.as = x.func
260
+ column_expr(x, q) {
261
+ if (x === '*') return '*'
262
+ ///////////////////////////////////////////////////////////////////////////////////////
263
+ // REVISIT: that should move out of here!
270
264
  if (x?.element?.['@cds.extension']) {
271
- x.as = x.as || x.element.name
272
- return `extensions__->${this.string('$."' + x.element.name + '"')}`
265
+ return `extensions__->${this.string('$."' + x.element.name + '"')} as ${x.as || x.element.name}`
273
266
  }
267
+ ///////////////////////////////////////////////////////////////////////////////////////
274
268
  let sql = this.expr(x)
269
+ let alias = this.column_alias4(x, q)
270
+ if (alias) sql += ' as ' + this.quote(alias)
275
271
  return sql
276
272
  }
277
273
 
274
+ /**
275
+ * Extracts the column alias from a SELECT column expression
276
+ * @param {import('./infer/cqn').col} x
277
+ * @returns {string}
278
+ */
279
+ column_alias4(x) {
280
+ return typeof x.as === 'string' ? x.as : x.func
281
+ }
282
+
278
283
  /**
279
284
  * Renders a FROM clause into generic SQL
280
285
  * @param {import('./infer/cqn').source} from
281
286
  * @returns {string} SQL
282
287
  */
283
288
  from(from) {
284
- const { ref, as } = from,
285
- _aliased = as ? s => s + ` as ${this.quote(as)}` : s => s
289
+ const { ref, as } = from
290
+ const _aliased = as ? s => s + ` as ${this.quote(as)}` : s => s
286
291
  if (ref) return _aliased(this.quote(this.name(ref[0])))
287
292
  if (from.SELECT) return _aliased(`(${this.SELECT(from)})`)
288
- if (from.join) {
289
- const {
290
- join,
291
- args: [left, right],
292
- on,
293
- } = from
294
- return `${this.from(left)} ${join} JOIN ${this.from(right)} ON ${this.xpr({ xpr: on })}`
295
- }
293
+ if (from.join)
294
+ return `${this.from(from.args[0])} ${from.join} JOIN ${this.from(from.args[1])} ON ${this.where(from.on)}`
296
295
  }
297
296
 
298
297
  /**
@@ -414,8 +413,10 @@ class CQN2SQLRenderer {
414
413
  .filter(a => a)
415
414
  .map(c => c.sql)
416
415
 
417
- this.entries = [[JSON.stringify(INSERT.entries)]]
418
- return (this.sql = `INSERT INTO ${entity}${alias ? ' as ' + this.quote(alias) : ''} (${
416
+ // Include this.values for placeholders
417
+ /** @type {unknown[][]} */
418
+ this.entries = [[...this.values, JSON.stringify(INSERT.entries)]]
419
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${
419
420
  this.columns
420
421
  }) SELECT ${extraction} FROM json_each(?)`)
421
422
  }
@@ -433,14 +434,11 @@ class CQN2SQLRenderer {
433
434
  if (!INSERT.columns && !elements) {
434
435
  throw cds.error`Cannot insert rows without columns or elements`
435
436
  }
436
- let columns = INSERT.columns || (elements && ObjectKeys(elements))
437
- if (elements) {
438
- columns = columns.filter(c => c in elements && !elements[c].virtual && !elements[c].isAssociation)
439
- }
437
+ let columns = INSERT.columns || (elements && ObjectKeys(elements).filter(c => !elements[c].virtual && !elements[c].isAssociation))
440
438
  this.columns = columns.map(c => this.quote(c))
441
439
 
442
440
  const inputConverterKey = this.class._convertInput
443
- const extraction = columns.map((c, i) => {
441
+ const extraction = columns.map((c,i) => {
444
442
  const element = elements?.[c] || {}
445
443
  const extract = `value->>'$[${i}]'`
446
444
  const converter = element[inputConverterKey] || (e => e)
@@ -448,7 +446,7 @@ class CQN2SQLRenderer {
448
446
  })
449
447
 
450
448
  this.entries = [[JSON.stringify(INSERT.rows)]]
451
- return (this.sql = `INSERT INTO ${entity}${alias ? ' as ' + this.quote(alias) : ''} (${
449
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${
452
450
  this.columns
453
451
  }) SELECT ${extraction} FROM json_each(?)`)
454
452
  }
@@ -536,30 +534,26 @@ class CQN2SQLRenderer {
536
534
  * @returns {string} SQL
537
535
  */
538
536
  UPDATE(q) {
539
- const {
540
- UPDATE: { entity, with: _with, data, where },
541
- } = q,
542
- elements = q.target?.elements
537
+ const { entity, with: _with, data, where } = q.UPDATE
538
+ const elements = q.target?.elements
543
539
  let sql = `UPDATE ${this.name(entity.ref?.[0] || entity)}`
544
540
  if (entity.as) sql += ` AS ${entity.as}`
541
+
545
542
  let columns = []
546
- if (data)
547
- for (let c in data)
543
+ if (data) _add (data, val => this.val({val}))
544
+ if (_with) _add (_with, x => this.expr(x))
545
+ function _add (data, sql4) {
546
+ for (let c in data) {
548
547
  if (!elements || (c in elements && !elements[c].virtual)) {
549
- columns.push({ name: c, sql: this.val({ val: data[c] }) })
550
- }
551
- if (_with)
552
- for (let c in _with)
553
- if (!elements || (c in elements && !elements[c].virtual)) {
554
- columns.push({ name: c, sql: this.expr(_with[c]) })
548
+ columns.push({ name: c, sql: sql4(data[c]) })
555
549
  }
550
+ }
551
+ }
556
552
 
557
553
  columns = columns.map(c => {
558
- if (q.elements?.[c.name]?.['@cds.extension']) {
559
- return {
560
- name: 'extensions__',
561
- sql: `json_set(extensions__,${this.string('$."' + c.name + '"')},${c.sql})`,
562
- }
554
+ if (q.elements?.[c.name]?.['@cds.extension']) return {
555
+ name: 'extensions__',
556
+ sql: `json_set(extensions__,${this.string('$."' + c.name + '"')},${c.sql})`,
563
557
  }
564
558
  return c
565
559
  })
@@ -592,24 +586,65 @@ class CQN2SQLRenderer {
592
586
  * @returns {string} SQL
593
587
  */
594
588
  STREAM(q) {
595
- let { from, into, where, column, data } = q.STREAM
596
- let x, sql
597
- // reading stream
598
- if (from) {
599
- sql = `SELECT`
600
- if (!_empty((x = column))) sql += ` ${this.quote(x)}`
601
- if (!_empty((x = from))) sql += ` FROM ${this.from(x)}`
589
+ const { STREAM } = q
590
+ return STREAM.from
591
+ ? this.STREAM_from(q)
592
+ : STREAM.into
593
+ ? this.STREAM_into(q)
594
+ : cds.error`Missing .form or .into in ${q}`
595
+ }
596
+
597
+ /**
598
+ * Renders a STREAM.into query into generic SQL
599
+ * @param {import('./infer/cqn').STREAM} q
600
+ * @returns {string} SQL
601
+ */
602
+ STREAM_into(q) {
603
+ const { into, column, where, data } = q.STREAM
604
+
605
+ let sql
606
+ if (!_empty(column)) {
607
+ data.type = 'binary'
608
+ const update = UPDATE(into)
609
+ .with({ [column]: data })
610
+ .where(where)
611
+ Object.defineProperty(update, 'target', { value: q.target })
612
+ sql = this.UPDATE(update)
602
613
  } else {
603
- // writing stream
604
- const entity = this.name(q.target?.name || into.ref[0])
605
- sql = `UPDATE ${this.quote(entity)}${into.as ? ` AS ${into.as}` : ``} SET ${this.quote(column)}=?`
606
- this.entries = [data]
614
+ data.type = 'json'
615
+ // REVISIT: decide whether dataset streams should behave like INSERT or UPSERT
616
+ sql = this.UPSERT(UPSERT([{}]).into(into).forSQL())
617
+ this.values = [data]
607
618
  }
608
- if (!_empty((x = where))) sql += ` WHERE ${this.where(x)}`
609
- if (from) sql += ` LIMIT ${this.limit({ rows: { val: 1 } })}`
619
+
610
620
  return (this.sql = sql)
611
621
  }
612
622
 
623
+ /**
624
+ * Renders a STREAM.from query into generic SQL
625
+ * @param {import('./infer/cqn').STREAM} q
626
+ * @returns {string} SQL
627
+ */
628
+ STREAM_from(q) {
629
+ const { column, from, where, columns } = q.STREAM
630
+
631
+ const select = cds.ql
632
+ .SELECT(column ? [column] : columns)
633
+ .where(where)
634
+ .limit(column ? 1 : undefined)
635
+
636
+ // SELECT.from() does not accept joins
637
+ select.SELECT.from = from
638
+
639
+ if (column) {
640
+ this.one = true
641
+ } else {
642
+ select.SELECT.expand = 'root'
643
+ this.one = !!from.SELECT?.one
644
+ }
645
+ return this.SELECT(select.forSQL())
646
+ }
647
+
613
648
  // Expression Clauses ---------------------------------------------
614
649
 
615
650
  /**
@@ -655,11 +690,35 @@ class CQN2SQLRenderer {
655
690
  * @returns {string} The correct operator string
656
691
  */
657
692
  operator(x, i, xpr) {
658
- if (x === '=' && xpr[i + 1]?.val === null) return 'is'
659
- if (x === '!=') return 'is not'
693
+
694
+ // Translate = to IS NULL for rhs operand being NULL literal
695
+ if (x === '=') return xpr[i+1]?.val === null ? 'is' : '='
696
+
697
+ // Translate == to IS NOT NULL for rhs operand being NULL literal, otherwise ...
698
+ // Translate == to IS NOT DISTINCT FROM, unless both operands cannot be NULL
699
+ if (x === '==') return xpr[i+1]?.val === null ? 'is' : _not_null(i-1) && _not_null(i+1) ? '=' : this.is_not_distinct_from_
700
+
701
+ // Translate != to IS NULL for rhs operand being NULL literal, otherwise...
702
+ // Translate != to IS DISTINCT FROM, unless both operands cannot be NULL
703
+ if (x === '!=') return xpr[i+1]?.val === null ? 'is not' : _not_null(i-1) && _not_null(i+1) ? '<>' : this.is_distinct_from_
704
+
660
705
  else return x
706
+
707
+ /** Checks if the operand at xpr[i+-1] can be NULL. @returns true if not */
708
+ function _not_null(i) {
709
+ const operand = xpr[i]
710
+ if (!operand) return false
711
+ if (operand.val != null) return true // non-null values are not null
712
+ let element = operand.element
713
+ if (!element) return false
714
+ if (element.key) return true // primary keys usually should not be null
715
+ if (element.notNull) return true // not null elements cannot be null
716
+ }
661
717
  }
662
718
 
719
+ get is_distinct_from_() { return 'is distinct from' }
720
+ get is_not_distinct_from_() { return 'is not distinct from' }
721
+
663
722
  /**
664
723
  * Renders an argument place holder into the SQL for prepared statements
665
724
  * @param {import('./infer/cqn').ref} param0
@@ -677,7 +736,12 @@ class CQN2SQLRenderer {
677
736
  * @returns {string} SQL
678
737
  */
679
738
  ref({ ref }) {
680
- return ref.map(r => this.quote(r)).join('.')
739
+ switch (ref[0]) {
740
+ case '$now': return this.func({ func: 'session_context', args: [{ val: '$now' }]})
741
+ case '$user':
742
+ case '$user.id': return this.func({ func: 'session_context', args: [{ val: '$user.id' }]})
743
+ default: return ref.map(r => this.quote(r)).join('.')
744
+ }
681
745
  }
682
746
 
683
747
  /**
@@ -687,22 +751,21 @@ class CQN2SQLRenderer {
687
751
  */
688
752
  val({ val }) {
689
753
  switch (typeof val) {
690
- case 'function':
691
- throw new Error('Function values not supported.')
692
- case 'undefined':
693
- return 'NULL'
694
- case 'boolean':
695
- return val
696
- case 'number':
697
- return val // REVISIT for HANA
754
+ case 'function': throw new Error('Function values not supported.')
755
+ case 'undefined': return 'NULL'
756
+ case 'boolean': return `${val}`
757
+ case 'number': return `${val}` // REVISIT for HANA
698
758
  case 'object':
699
759
  if (val === null) return 'NULL'
700
760
  if (val instanceof Date) return `'${val.toISOString()}'`
701
- if (Buffer.isBuffer(val)) val = val.toString('base64')
702
- else val = this.regex(val) || this.json(val)
761
+ if (val instanceof Readable) ; // go on with default below
762
+ else if (Buffer.isBuffer(val)) val = val.toString('base64')
763
+ else if (is_regexp(val)) val = val.source
764
+ else val = JSON.stringify(val)
765
+ case 'string': // eslint-disable-line no-fallthrough
703
766
  }
704
767
  if (!this.values) return this.string(val)
705
- this.values.push(val)
768
+ else this.values.push(val)
706
769
  return '?'
707
770
  }
708
771
 
@@ -727,25 +790,7 @@ class CQN2SQLRenderer {
727
790
  }
728
791
 
729
792
  /**
730
- * Renders a Regular Expression into its string representation
731
- * @param {RegExp} o
732
- * @returns {string} SQL
733
- */
734
- regex(o) {
735
- if (is_regexp(o)) return o.source
736
- }
737
-
738
- /**
739
- * Renders the object as a JSON string in generic SQL
740
- * @param {object} o
741
- * @returns {string} SQL
742
- */
743
- json(o) {
744
- return this.string(JSON.stringify(o))
745
- }
746
-
747
- /**
748
- * Renders a javascript string into a generic SQL string
793
+ * Renders a javascript string into a SQL string literal
749
794
  * @param {string} s
750
795
  * @returns {string} SQL
751
796
  */
@@ -760,8 +805,9 @@ class CQN2SQLRenderer {
760
805
  */
761
806
  column_name(col) {
762
807
  if (col === '*')
808
+ // 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?
763
809
  cds.error`Query was not inferred and includes '*' in the columns. For which there is no column name available.`
764
- return (typeof col.as === 'string' && col.as) || ('val' in col && col.val + '') || col.ref[col.ref.length - 1]
810
+ return (typeof col.as === 'string' && col.as) || ('val' in col && col.val + '') || col.func || col.ref.at(-1)
765
811
  }
766
812
 
767
813
  /**
@@ -783,7 +829,8 @@ class CQN2SQLRenderer {
783
829
  quote(s) {
784
830
  if (typeof s !== 'string') return '"' + s + '"'
785
831
  if (s.includes('"')) return '"' + s.replace(/"/g, '""') + '"'
786
- if (s.toUpperCase() in this.class.ReservedWords || /^\d|[$' ?@./\\]/.test(s)) return '"' + s + '"'
832
+ // Column names like "Order" clash with "ORDER" keyword so toUpperCase is required
833
+ if (s in this.class.ReservedWords || /^\d|[$' ?@./\\]/.test(s)) return '"' + s + '"'
787
834
  return s
788
835
  }
789
836
 
@@ -796,46 +843,39 @@ class CQN2SQLRenderer {
796
843
  */
797
844
  managed(columns, elements, isUpdate = false) {
798
845
  const annotation = isUpdate ? '@cds.on.update' : '@cds.on.insert'
799
- const inputConverterKey = this.class._convertInput
846
+ const { _convertInput } = this.class
800
847
  // Ensure that missing managed columns are added
801
848
  const requiredColumns = !elements
802
849
  ? []
803
850
  : Object.keys(elements)
804
851
  .filter(
805
852
  e =>
806
- (elements[e]?.[annotation] || (!isUpdate && elements[e]?.default && !elements[e].virtual)) &&
853
+ (elements[e]?.[annotation] || (!isUpdate && elements[e]?.default && !elements[e].virtual && !elements[e].isAssociation)) &&
807
854
  !columns.find(c => c.name === e),
808
855
  )
809
856
  .map(name => ({ name, sql: 'NULL' }))
810
857
 
811
858
  return [...columns, ...requiredColumns].map(({ name, sql }) => {
812
- const element = elements?.[name] || {}
813
- let extract = sql ?? `value->>'$."${name}"'`
814
- const converter = element[inputConverterKey] || (e => e)
815
- let managed = element[annotation]?.['=']
816
- switch (managed) {
817
- case '$user.id':
818
- case '$user':
819
- managed = this.string(this.context.user.id)
820
- break
821
- case '$now':
822
- managed = this.string(this.context.timestamp.toISOString())
823
- break
824
- default:
825
- managed = undefined
826
- }
827
- if (!isUpdate) {
859
+ let element = elements?.[name] || {}
860
+ if (!sql) sql = `value->>'$."${name}"'`
861
+
862
+ let converter = element[_convertInput]
863
+ if (converter && sql[0] !== '$') sql = converter(sql, element)
864
+
865
+ let val = _managed[element[annotation]?.['=']]
866
+ if (val) sql = `coalesce(${sql}, ${this.func({ func: 'session_context', args: [{ val }] })})`
867
+
868
+ else if (!isUpdate && element.default) {
828
869
  const d = element.default
829
- if (d && (d.val !== undefined || d.ref?.[0] === '$now')) {
830
- extract = `(CASE WHEN json_type(value,'$."${name}"') IS NULL THEN ${this.defaultValue(
831
- d.val,
832
- )} ELSE ${extract} END)`
870
+ if (d.val !== undefined || d.ref?.[0] === '$now') {
871
+ // REVISIT: d.ref is not used afterwards
872
+ sql = `(CASE WHEN json_type(value,'$."${name}"') IS NULL THEN ${
873
+ this.defaultValue(d.val) // REVISIT: this.defaultValue is a strange function
874
+ } ELSE ${sql} END)`
833
875
  }
834
876
  }
835
- return {
836
- name,
837
- sql: converter(managed === undefined ? extract : `coalesce(${extract}, ${managed})`, element),
838
- }
877
+
878
+ return { name, sql }
839
879
  })
840
880
  }
841
881
 
@@ -844,6 +884,7 @@ class CQN2SQLRenderer {
844
884
  * @param {string} defaultValue
845
885
  * @returns {string}
846
886
  */
887
+ // REVISIT: This is a strange method, also overridden inconsistently in postgres
847
888
  defaultValue(defaultValue = this.context.timestamp.toISOString()) {
848
889
  return typeof defaultValue === 'string' ? this.string(defaultValue) : defaultValue
849
890
  }
@@ -855,8 +896,11 @@ Buffer.prototype.toJSON = function () {
855
896
  }
856
897
 
857
898
  const ObjectKeys = o => (o && [...ObjectKeys(o.__proto__), ...Object.keys(o)]) || []
858
- const has_expands = q => q.SELECT.columns?.some(c => c.SELECT?.expand)
859
- const has_arrays = q => q.elements && Object.values(q.elements).some(e => e.items)
899
+ const _managed = {
900
+ '$user.id': '$user.id',
901
+ $user: '$user.id',
902
+ $now: '$now',
903
+ }
860
904
 
861
905
  const is_regexp = x => x?.constructor?.name === 'RegExp' // NOTE: x instanceof RegExp doesn't work in repl
862
906
  const _empty = a => !a || a.length === 0