@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/CHANGELOG.md +41 -0
- package/lib/SQLService.js +131 -82
- package/lib/common/DatabaseService.js +6 -14
- package/lib/common/session-context.js +14 -0
- package/lib/cqn2sql.js +203 -137
- package/lib/cqn4sql.js +109 -125
- package/lib/fill-in-keys.js +6 -1
- package/lib/infer/cqn.d.ts +0 -3
- package/lib/infer/index.js +36 -23
- package/lib/infer/join-tree.js +5 -6
- package/package.json +2 -2
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
|
|
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
|
|
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
|
-
|
|
250
|
-
let obj = ''
|
|
255
|
+
const isRoot = SELECT.expand === 'root'
|
|
251
256
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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 = [
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
this.
|
|
460
|
-
|
|
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
|
-
|
|
521
|
-
|
|
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
|
|
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
|
-
|
|
529
|
-
|
|
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
|
|
556
|
-
if (_with) _add
|
|
557
|
-
function _add
|
|
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: `
|
|
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 (
|
|
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 === '=')
|
|
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':
|
|
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)
|
|
773
|
-
if (val instanceof Readable)
|
|
774
|
-
else if (Buffer.isBuffer(val))
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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
|
-
|
|
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
|
|