@cap-js/db-service 1.5.0 → 1.6.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,12 @@ const cds = require('@sap/cds/lib')
2
2
  const cds_infer = require('./infer')
3
3
  const cqn4sql = require('./cqn4sql')
4
4
 
5
+ const BINARY_TYPES = {
6
+ 'cds.Binary': 1,
7
+ 'cds.LargeBinary': 1,
8
+ 'cds.hana.BINARY': 1,
9
+ }
10
+
5
11
  const { Readable } = require('stream')
6
12
 
7
13
  const DEBUG = (() => {
@@ -27,7 +33,7 @@ class CQN2SQLRenderer {
27
33
  this.class._init() // is a noop for subsequent calls
28
34
  }
29
35
 
30
- static _add_mixins (aspect, mixins) {
36
+ static _add_mixins(aspect, mixins) {
31
37
  const fqn = this.name + aspect
32
38
  const types = cds.builtin.types
33
39
  for (let each in mixins) {
@@ -52,7 +58,7 @@ class CQN2SQLRenderer {
52
58
  this.ReservedWords[each[0] + each.slice(1).toLowerCase()] = 1 // Order
53
59
  this.ReservedWords[each.toLowerCase()] = 1 // order
54
60
  }
55
- this._init = () => {} // makes this a noop for subsequent calls
61
+ this._init = () => { } // makes this a noop for subsequent calls
56
62
  }
57
63
 
58
64
  /**
@@ -208,7 +214,7 @@ class CQN2SQLRenderer {
208
214
  if (limit) sql += ` LIMIT ${this.limit(limit)}`
209
215
  // Expand cannot work without an inferred query
210
216
  if (expand) {
211
- if ('elements' in q) sql = this.SELECT_expand (q,sql)
217
+ if ('elements' in q) sql = this.SELECT_expand(q, sql)
212
218
  else cds.error`Query was not inferred and includes expand. For which the metadata is missing.`
213
219
  }
214
220
  return (this.sql = sql)
@@ -237,7 +243,7 @@ class CQN2SQLRenderer {
237
243
 
238
244
  let cols = SELECT.columns.map(x => {
239
245
  const name = this.column_name(x)
240
- let col = `'${name}',${this.output_converter4(x.element, this.quote(name))}`
246
+ let col = `'$."${name}"',${this.output_converter4(x.element, this.quote(name))}`
241
247
  if (x.SELECT?.count) {
242
248
  // Return both the sub select and the count for @odata.count
243
249
  const qc = cds.ql.clone(x, { columns: [{ func: 'count' }], one: 1, limit: 0, orderBy: 0 })
@@ -246,21 +252,14 @@ class CQN2SQLRenderer {
246
252
  return col
247
253
  }).flat()
248
254
 
249
- // Prevent SQLite from hitting function argument limit of 100
250
- let obj = ''
255
+ const isRoot = SELECT.expand === 'root'
251
256
 
252
- if(cols.length < 50) obj = `json_object(${cols.slice(0, 50)})`
253
- else {
254
- const chunks = []
255
- for (let i = 0; i < cols.length; i += 50) {
256
- chunks.push(`json_object(${cols.slice(i, i + 50)})`)
257
- }
258
- // REVISIT: json_merge is a user defined function, bad performance!
259
- obj = `json_merge(${chunks})`
257
+ // Prevent SQLite from hitting function argument limit of 100
258
+ let obj = "'{}'"
259
+ for (let i = 0; i < cols.length; i += 48) {
260
+ obj = `jsonb_insert(${obj},${cols.slice(i, i + 48)})`
260
261
  }
261
-
262
-
263
- return `SELECT ${SELECT.one || SELECT.expand === 'root' ? obj : `json_group_array(${obj.includes('json_merge') ? `json_insert(${obj})` : obj})`} as _json_ FROM (${sql})`
262
+ return `SELECT ${isRoot || SELECT.one ? obj.replace('jsonb', 'json') : `jsonb_group_array(${obj})`} as _json_ FROM (${sql})`
264
263
  }
265
264
 
266
265
  /**
@@ -342,9 +341,9 @@ class CQN2SQLRenderer {
342
341
  return orderBy.map(
343
342
  localized
344
343
  ? c =>
345
- this.expr(c) +
346
- (c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
347
- (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
344
+ this.expr(c) +
345
+ (c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
346
+ (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
348
347
  : c => this.expr(c) + (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC'),
349
348
  )
350
349
  }
@@ -372,12 +371,12 @@ class CQN2SQLRenderer {
372
371
  return INSERT.entries
373
372
  ? this.INSERT_entries(q)
374
373
  : INSERT.rows
375
- ? this.INSERT_rows(q)
376
- : INSERT.values
377
- ? this.INSERT_values(q)
378
- : INSERT.as
379
- ? this.INSERT_select(q)
380
- : cds.error`Missing .entries, .rows, or .values in ${q}`
374
+ ? this.INSERT_rows(q)
375
+ : INSERT.values
376
+ ? this.INSERT_values(q)
377
+ : INSERT.as
378
+ ? this.INSERT_select(q)
379
+ : cds.error`Missing .entries, .rows, or .values in ${q}`
381
380
  }
382
381
 
383
382
  /**
@@ -394,12 +393,18 @@ class CQN2SQLRenderer {
394
393
  return // REVISIT: mtx sends an insert statement without entries and no reference entity
395
394
  }
396
395
  const columns = elements
397
- ? ObjectKeys(elements).filter(c => c in elements && !elements[c].virtual && !elements[c].isAssociation)
396
+ ? ObjectKeys(elements).filter(c => c in elements && !elements[c].virtual && !elements[c].value && !elements[c].isAssociation)
398
397
  : ObjectKeys(INSERT.entries[0])
399
398
 
400
399
  /** @type {string[]} */
401
400
  this.columns = columns.filter(elements ? c => !elements[c]?.['@cds.extension'] : () => true).map(c => this.quote(c))
402
401
 
402
+ if (!elements) {
403
+ this.entries = INSERT.entries.map(e => columns.map(c => e[c]))
404
+ const param = this.param.bind(this, { ref: ['?'] })
405
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns}) VALUES (${columns.map(param)})`)
406
+ }
407
+
403
408
  const extractions = this.managed(
404
409
  columns.map(c => ({ name: c })),
405
410
  elements,
@@ -426,10 +431,119 @@ class CQN2SQLRenderer {
426
431
 
427
432
  // Include this.values for placeholders
428
433
  /** @type {unknown[][]} */
429
- this.entries = [[...this.values, JSON.stringify(INSERT.entries)]]
430
- return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${
431
- this.columns
432
- }) SELECT ${extraction} FROM json_each(?)`)
434
+ this.entries = []
435
+ if (INSERT.entries[0] instanceof Readable) {
436
+ INSERT.entries[0].type = 'json'
437
+ this.entries = [[...this.values, INSERT.entries[0]]]
438
+ } else {
439
+ const stream = Readable.from(this.INSERT_entries_stream(INSERT.entries), { objectMode: false })
440
+ stream.type = 'json'
441
+ this.entries = [[...this.values, stream]]
442
+ }
443
+
444
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns
445
+ }) SELECT ${extraction} FROM json_each(?)`)
446
+ }
447
+
448
+ async *INSERT_entries_stream(entries, binaryEncoding = 'base64') {
449
+ const elements = this.cqn.target?.elements || {}
450
+ const transformBase64 = binaryEncoding === 'base64'
451
+ ? a => a
452
+ : a => a != null ? Buffer.from(a, 'base64').toString(binaryEncoding) : a
453
+ const bufferLimit = 65536 // 1 << 16
454
+ let buffer = '['
455
+
456
+ let sep = ''
457
+ for (const row of entries) {
458
+ buffer += `${sep}{`
459
+ if (!sep) sep = ','
460
+
461
+ let sepsub = ''
462
+ for (const key in row) {
463
+ const keyJSON = `${sepsub}${JSON.stringify(key)}:`
464
+ if (!sepsub) sepsub = ','
465
+
466
+ let val = row[key]
467
+ if (val instanceof Readable) {
468
+ buffer += `${keyJSON}"`
469
+
470
+ // TODO: double check that it works
471
+ val.setEncoding(binaryEncoding)
472
+ for await (const chunk of val) {
473
+ buffer += chunk
474
+ if (buffer.length > bufferLimit) {
475
+ yield buffer
476
+ buffer = ''
477
+ }
478
+ }
479
+
480
+ buffer += '"'
481
+ } else {
482
+ if (elements[key]?.type in BINARY_TYPES) {
483
+ val = transformBase64(val)
484
+ }
485
+ buffer += `${keyJSON}${val === undefined ? 'null' : JSON.stringify(val)}`
486
+ }
487
+ }
488
+ buffer += '}'
489
+ if (buffer.length > bufferLimit) {
490
+ yield buffer
491
+ buffer = ''
492
+ }
493
+ }
494
+
495
+ buffer += ']'
496
+ yield buffer
497
+ }
498
+
499
+ async *INSERT_rows_stream(entries, binaryEncoding = 'base64') {
500
+ const elements = this.cqn.target?.elements || {}
501
+ const transformBase64 = binaryEncoding === 'base64'
502
+ ? a => a
503
+ : a => a != null ? Buffer.from(a, 'base64').toString(binaryEncoding) : a
504
+ const bufferLimit = 65536 // 1 << 16
505
+ let buffer = '['
506
+
507
+ let sep = ''
508
+ for (const row of entries) {
509
+ buffer += `${sep}[`
510
+ if (!sep) sep = ','
511
+
512
+ let sepsub = ''
513
+ for (let key = 0; key < row.length; key++) {
514
+ let val = row[key]
515
+ if (val instanceof Readable) {
516
+ buffer += `${sepsub}"`
517
+
518
+ // TODO: double check that it works
519
+ val.setEncoding(binaryEncoding)
520
+ for await (const chunk of val) {
521
+ buffer += chunk
522
+ if (buffer.length > bufferLimit) {
523
+ yield buffer
524
+ buffer = ''
525
+ }
526
+ }
527
+
528
+ buffer += '"'
529
+ } else {
530
+ if (elements[this.columns[key]]?.type in BINARY_TYPES) {
531
+ val = transformBase64(val)
532
+ }
533
+ buffer += `${sepsub}${val === undefined ? 'null' : JSON.stringify(val)}`
534
+ }
535
+
536
+ if (!sepsub) sepsub = ','
537
+ }
538
+ buffer += ']'
539
+ if (buffer.length > bufferLimit) {
540
+ yield buffer
541
+ buffer = ''
542
+ }
543
+ }
544
+
545
+ buffer += ']'
546
+ yield buffer
433
547
  }
434
548
 
435
549
  /**
@@ -443,21 +557,35 @@ class CQN2SQLRenderer {
443
557
  const alias = INSERT.into.as
444
558
  const elements = q.elements || q.target?.elements
445
559
  const columns = INSERT.columns
446
- || cds.error`Cannot insert rows without columns or elements`
560
+ || cds.error`Cannot insert rows without columns or elements`
447
561
 
448
562
  const inputConverter = this.class._convertInput
449
- const extraction = columns.map((c,i) => {
563
+ const extraction = columns.map((c, i) => {
450
564
  const extract = `value->>'$[${i}]'`
451
565
  const element = elements?.[c]
452
566
  const converter = element?.[inputConverter]
453
- return converter?.(extract,element) || extract
567
+ return converter?.(extract, element) || extract
454
568
  })
455
569
 
456
570
  this.columns = columns.map(c => this.quote(c))
457
- this.entries = [[JSON.stringify(INSERT.rows)]]
458
- return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${
459
- this.columns
460
- }) SELECT ${extraction} FROM json_each(?)`)
571
+
572
+ if (!elements) {
573
+ this.entries = INSERT.rows
574
+ const param = this.param.bind(this, { ref: ['?'] })
575
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns}) VALUES (${columns.map(param)})`)
576
+ }
577
+
578
+ if (INSERT.rows[0] instanceof Readable) {
579
+ INSERT.rows[0].type = 'json'
580
+ this.entries = [[...this.values, INSERT.rows[0]]]
581
+ } else {
582
+ const stream = Readable.from(this.INSERT_rows_stream(INSERT.rows), { objectMode: false })
583
+ stream.type = 'json'
584
+ this.entries = [[...this.values, stream]]
585
+ }
586
+
587
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns
588
+ }) SELECT ${extraction} FROM json_each(?)`)
461
589
  }
462
590
 
463
591
  /**
@@ -517,16 +645,23 @@ class CQN2SQLRenderer {
517
645
  * @returns {string} SQL
518
646
  */
519
647
  UPSERT(q) {
520
- let { UPSERT } = q,
521
- sql = this.INSERT({ __proto__: q, INSERT: UPSERT })
648
+ const { UPSERT } = q
649
+ const elements = q.target?.elements || {}
650
+ let sql = this.INSERT({ __proto__: q, INSERT: UPSERT })
522
651
  let keys = q.target?.keys
523
- if (!keys) return (this.sql = sql) // REVISIT: We should converge q.target and q._target
652
+ if (!keys) return this.sql = sql
524
653
  keys = Object.keys(keys).filter(k => !keys[k].isAssociation)
525
654
 
526
655
  let updateColumns = q.UPSERT.entries ? Object.keys(q.UPSERT.entries[0]) : this.columns
527
- updateColumns = updateColumns
528
- .filter(c => !keys.includes(c))
529
- .map(c => `${this.quote(c)} = excluded.${this.quote(c)}`)
656
+ updateColumns = updateColumns.filter(c => {
657
+ if (keys.includes(c)) return false //> keys go into ON CONFLICT clause
658
+ let e = elements[c]
659
+ if (!e) return true //> pass through to native SQL columns not in CDS model
660
+ if (e.virtual) return true //> skip virtual elements
661
+ if (e.value) return true //> skip calculated elements
662
+ // if (e.isAssociation) return true //> this breaks a a test in @sap/cds -> need to follow up how to correctly handle deep upserts
663
+ else return true
664
+ }).map(c => `${this.quote(c)} = excluded.${this.quote(c)}`)
530
665
 
531
666
  // temporal data
532
667
  keys.push(...Object.values(q.target.elements).filter(e => e['@cds.valid.from']).map(e => e.name))
@@ -552,9 +687,9 @@ class CQN2SQLRenderer {
552
687
  if (entity.as) sql += ` AS ${entity.as}`
553
688
 
554
689
  let columns = []
555
- if (data) _add (data, val => this.val({val}))
556
- if (_with) _add (_with, x => this.expr(x))
557
- function _add (data, sql4) {
690
+ if (data) _add(data, val => this.val({ val }))
691
+ if (_with) _add(_with, x => this.expr(x))
692
+ function _add(data, sql4) {
558
693
  for (let c in data) {
559
694
  if (!elements || (c in elements && !elements[c].virtual)) {
560
695
  columns.push({ name: c, sql: sql4(data[c]) })
@@ -565,7 +700,7 @@ class CQN2SQLRenderer {
565
700
  columns = columns.map(c => {
566
701
  if (q.elements?.[c.name]?.['@cds.extension']) return {
567
702
  name: 'extensions__',
568
- sql: `json_set(extensions__,${this.string('$."' + c.name + '"')},${c.sql})`,
703
+ sql: `jsonb_set(extensions__,${this.string('$."' + c.name + '"')},${c.sql})`,
569
704
  }
570
705
  return c
571
706
  })
@@ -590,73 +725,6 @@ class CQN2SQLRenderer {
590
725
  return (this.sql = sql)
591
726
  }
592
727
 
593
- // STREAM Statement -------------------------------------------------
594
-
595
- /**
596
- * Renders a STREAM query into generic SQL
597
- * @param {import('./infer/cqn').STREAM} q
598
- * @returns {string} SQL
599
- */
600
- STREAM(q) {
601
- const { STREAM } = q
602
- return STREAM.from
603
- ? this.STREAM_from(q)
604
- : STREAM.into
605
- ? this.STREAM_into(q)
606
- : cds.error`Missing .form or .into in ${q}`
607
- }
608
-
609
- /**
610
- * Renders a STREAM.into query into generic SQL
611
- * @param {import('./infer/cqn').STREAM} q
612
- * @returns {string} SQL
613
- */
614
- STREAM_into(q) {
615
- const { into, column, where, data } = q.STREAM
616
-
617
- let sql
618
- if (!_empty(column)) {
619
- data.type = 'binary'
620
- const update = UPDATE(into)
621
- .with({ [column]: data })
622
- .where(where)
623
- Object.defineProperty(update, 'target', { value: q.target })
624
- sql = this.UPDATE(update)
625
- } else {
626
- data.type = 'json'
627
- // REVISIT: decide whether dataset streams should behave like INSERT or UPSERT
628
- sql = this.UPSERT(UPSERT([{}]).into(into).forSQL())
629
- this.values = [data]
630
- }
631
-
632
- return (this.sql = sql)
633
- }
634
-
635
- /**
636
- * Renders a STREAM.from query into generic SQL
637
- * @param {import('./infer/cqn').STREAM} q
638
- * @returns {string} SQL
639
- */
640
- STREAM_from(q) {
641
- const { column, from, where, columns } = q.STREAM
642
-
643
- const select = cds.ql
644
- .SELECT(column ? [column] : columns)
645
- .where(where)
646
- .limit(column ? 1 : undefined)
647
-
648
- // SELECT.from() does not accept joins
649
- select.SELECT.from = from
650
-
651
- if (column) {
652
- this.one = true
653
- } else {
654
- select.SELECT.expand = 'root'
655
- this.one = !!from.SELECT?.one
656
- }
657
- return this.SELECT(select.forSQL())
658
- }
659
-
660
728
  // Expression Clauses ---------------------------------------------
661
729
 
662
730
  /**
@@ -668,7 +736,7 @@ class CQN2SQLRenderer {
668
736
  expr(x) {
669
737
  const wrap = x.cast ? sql => `cast(${sql} as ${this.type4(x.cast)})` : sql => sql
670
738
  if (typeof x === 'string') throw cds.error`Unsupported expr: ${x}`
671
- if ('param' in x) return wrap(this.param(x))
739
+ if (x.param) return wrap(this.param(x))
672
740
  if ('ref' in x) return wrap(this.ref(x))
673
741
  if ('val' in x) return wrap(this.val(x))
674
742
  if ('xpr' in x) return wrap(this.xpr(x))
@@ -704,15 +772,15 @@ class CQN2SQLRenderer {
704
772
  operator(x, i, xpr) {
705
773
 
706
774
  // Translate = to IS NULL for rhs operand being NULL literal
707
- if (x === '=') return xpr[i+1]?.val === null ? 'is' : '='
775
+ if (x === '=') return xpr[i + 1]?.val === null ? 'is' : '='
708
776
 
709
777
  // Translate == to IS NOT NULL for rhs operand being NULL literal, otherwise ...
710
778
  // Translate == to IS NOT DISTINCT FROM, unless both operands cannot be NULL
711
- if (x === '==') return xpr[i+1]?.val === null ? 'is' : _not_null(i-1) && _not_null(i+1) ? '=' : this.is_not_distinct_from_
779
+ if (x === '==') return xpr[i + 1]?.val === null ? 'is' : _not_null(i - 1) && _not_null(i + 1) ? '=' : this.is_not_distinct_from_
712
780
 
713
781
  // Translate != to IS NULL for rhs operand being NULL literal, otherwise...
714
782
  // Translate != to IS DISTINCT FROM, unless both operands cannot be NULL
715
- if (x === '!=') return xpr[i+1]?.val === null ? 'is not' : _not_null(i-1) && _not_null(i+1) ? '<>' : this.is_distinct_from_
783
+ if (x === '!=') return xpr[i + 1]?.val === null ? 'is not' : _not_null(i - 1) && _not_null(i + 1) ? '<>' : this.is_distinct_from_
716
784
 
717
785
  else return x
718
786
 
@@ -749,9 +817,8 @@ class CQN2SQLRenderer {
749
817
  */
750
818
  ref({ ref }) {
751
819
  switch (ref[0]) {
752
- case '$now': return this.func({ func: 'session_context', args: [{ val: '$now' }]})
753
- case '$user':
754
- case '$user.id': return this.func({ func: 'session_context', args: [{ val: '$user.id' }]})
820
+ case '$now': return this.func({ func: 'session_context', args: [{ val: '$now', param: false }] }) // REVISIT: why do we need param: false here?
821
+ case '$user': return this.func({ func: 'session_context', args: [{ val: '$user.'+ref[1]||'id', param: false }] }) // REVISIT: same here?
755
822
  default: return ref.map(r => this.quote(r)).join('.')
756
823
  }
757
824
  }
@@ -761,7 +828,7 @@ class CQN2SQLRenderer {
761
828
  * @param {import('./infer/cqn').val} param0
762
829
  * @returns {string} SQL
763
830
  */
764
- val({ val }) {
831
+ val({ val, param }) {
765
832
  switch (typeof val) {
766
833
  case 'function': throw new Error('Function values not supported.')
767
834
  case 'undefined': return 'NULL'
@@ -769,14 +836,14 @@ class CQN2SQLRenderer {
769
836
  case 'number': return `${val}` // REVISIT for HANA
770
837
  case 'object':
771
838
  if (val === null) return 'NULL'
772
- if (val instanceof Date) return `'${val.toISOString()}'`
773
- if (val instanceof Readable) ; // go on with default below
774
- else if (Buffer.isBuffer(val)) val = val.toString('base64')
839
+ if (val instanceof Date) val = val.toJSON() // returns null if invalid
840
+ else if (val instanceof Readable); // go on with default below
841
+ else if (Buffer.isBuffer(val)); // go on with default below
775
842
  else if (is_regexp(val)) val = val.source
776
843
  else val = JSON.stringify(val)
777
844
  case 'string': // eslint-disable-line no-fallthrough
778
845
  }
779
- if (!this.values) return this.string(val)
846
+ if (!this.values || param === false) return this.string(val)
780
847
  else this.values.push(val)
781
848
  return '?'
782
849
  }
@@ -860,12 +927,12 @@ class CQN2SQLRenderer {
860
927
  const requiredColumns = !elements
861
928
  ? []
862
929
  : Object.keys(elements)
863
- .filter(
864
- e =>
865
- (elements[e]?.[annotation] || (!isUpdate && elements[e]?.default && !elements[e].virtual && !elements[e].isAssociation)) &&
866
- !columns.find(c => c.name === e),
867
- )
868
- .map(name => ({ name, sql: 'NULL' }))
930
+ .filter(
931
+ e =>
932
+ (elements[e]?.[annotation] || (!isUpdate && elements[e]?.default && !elements[e].virtual && !elements[e].isAssociation)) &&
933
+ !columns.find(c => c.name === e),
934
+ )
935
+ .map(name => ({ name, sql: 'NULL' }))
869
936
 
870
937
  return [...columns, ...requiredColumns].map(({ name, sql }) => {
871
938
  let element = elements?.[name] || {}
@@ -875,14 +942,13 @@ class CQN2SQLRenderer {
875
942
  if (converter && sql[0] !== '$') sql = converter(sql, element)
876
943
 
877
944
  let val = _managed[element[annotation]?.['=']]
878
- if (val) sql = `coalesce(${sql}, ${this.func({ func: 'session_context', args: [{ val }] })})`
945
+ if (val) sql = `coalesce(${sql}, ${this.func({ func: 'session_context', args: [{ val, param: false }] })})`
879
946
  else if (!isUpdate && element.default) {
880
947
  const d = element.default
881
948
  if (d.val !== undefined || d.ref?.[0] === '$now') {
882
949
  // REVISIT: d.ref is not used afterwards
883
- sql = `(CASE WHEN json_type(value,'$."${name}"') IS NULL THEN ${
884
- this.defaultValue(d.val) // REVISIT: this.defaultValue is a strange function
885
- } ELSE ${sql} END)`
950
+ sql = `(CASE WHEN json_type(value,'$."${name}"') IS NULL THEN ${this.defaultValue(d.val) // REVISIT: this.defaultValue is a strange function
951
+ } ELSE ${sql} END)`
886
952
  }
887
953
  }
888
954