@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/CHANGELOG.md +18 -0
- package/lib/InsertResults.js +2 -1
- package/lib/SQLService.js +64 -52
- package/lib/common/DatabaseService.js +84 -130
- package/lib/common/generic-pool.js +34 -0
- package/lib/common/session-context.js +32 -0
- package/lib/cql-functions.js +14 -1
- package/lib/cqn2sql.js +215 -171
- package/lib/cqn4sql.js +118 -56
- package/lib/deep-queries.js +4 -3
- package/lib/fill-in-keys.js +5 -4
- package/lib/infer/index.js +41 -19
- package/lib/infer/join-tree.js +2 -2
- package/package.json +2 -5
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(
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
197
|
-
sql = `SELECT`
|
|
199
|
+
let sql = `SELECT`
|
|
198
200
|
if (distinct) sql += ` DISTINCT`
|
|
199
|
-
if (!_empty(
|
|
200
|
-
if (!_empty(
|
|
201
|
-
if (!_empty(
|
|
202
|
-
if (!_empty(
|
|
203
|
-
if (!_empty(
|
|
204
|
-
if (!_empty(
|
|
205
|
-
if (one)
|
|
206
|
-
|
|
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(
|
|
221
|
-
|
|
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 =
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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 <
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
418
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
560
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
sql =
|
|
606
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
659
|
-
|
|
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
|
-
|
|
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
|
-
|
|
692
|
-
case '
|
|
693
|
-
|
|
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 (
|
|
702
|
-
else
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
let
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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
|
-
|
|
836
|
-
|
|
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
|
|
859
|
-
|
|
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
|