@cap-js/db-service 1.1.0 → 1.2.1
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 +24 -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 +218 -173
- package/lib/cqn4sql.js +132 -59
- 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,28 +232,23 @@ 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
|
+
return [col, `'${name}@odata.count',${this.expr(qc)}`]
|
|
243
|
+
}
|
|
244
|
+
return col
|
|
245
|
+
}).flat()
|
|
253
246
|
|
|
254
247
|
// Prevent SQLite from hitting function argument limit of 100
|
|
255
|
-
let
|
|
256
|
-
let
|
|
257
|
-
|
|
258
|
-
obj = `
|
|
248
|
+
let obj = ''
|
|
249
|
+
for (let i = 0; i < cols.length; i += 50) {
|
|
250
|
+
const n = `json_object(${cols.slice(i, i + 50)})`
|
|
251
|
+
obj = obj ? `json_patch(${obj},${n})` : n
|
|
259
252
|
}
|
|
260
253
|
return `SELECT ${SELECT.one || SELECT.expand === 'root' ? obj : `json_group_array(${obj})`} as _json_ FROM (${sql})`
|
|
261
254
|
}
|
|
@@ -265,34 +258,41 @@ class CQN2SQLRenderer {
|
|
|
265
258
|
* @param {import('./infer/cqn').col} x
|
|
266
259
|
* @returns {string} SQL
|
|
267
260
|
*/
|
|
268
|
-
column_expr(x) {
|
|
269
|
-
if (x
|
|
261
|
+
column_expr(x, q) {
|
|
262
|
+
if (x === '*') return '*'
|
|
263
|
+
///////////////////////////////////////////////////////////////////////////////////////
|
|
264
|
+
// REVISIT: that should move out of here!
|
|
270
265
|
if (x?.element?.['@cds.extension']) {
|
|
271
|
-
x.as
|
|
272
|
-
return `extensions__->${this.string('$."' + x.element.name + '"')}`
|
|
266
|
+
return `extensions__->${this.string('$."' + x.element.name + '"')} as ${x.as || x.element.name}`
|
|
273
267
|
}
|
|
268
|
+
///////////////////////////////////////////////////////////////////////////////////////
|
|
274
269
|
let sql = this.expr(x)
|
|
270
|
+
let alias = this.column_alias4(x, q)
|
|
271
|
+
if (alias) sql += ' as ' + this.quote(alias)
|
|
275
272
|
return sql
|
|
276
273
|
}
|
|
277
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Extracts the column alias from a SELECT column expression
|
|
277
|
+
* @param {import('./infer/cqn').col} x
|
|
278
|
+
* @returns {string}
|
|
279
|
+
*/
|
|
280
|
+
column_alias4(x) {
|
|
281
|
+
return typeof x.as === 'string' ? x.as : x.func
|
|
282
|
+
}
|
|
283
|
+
|
|
278
284
|
/**
|
|
279
285
|
* Renders a FROM clause into generic SQL
|
|
280
286
|
* @param {import('./infer/cqn').source} from
|
|
281
287
|
* @returns {string} SQL
|
|
282
288
|
*/
|
|
283
289
|
from(from) {
|
|
284
|
-
const { ref, as } = from
|
|
285
|
-
|
|
290
|
+
const { ref, as } = from
|
|
291
|
+
const _aliased = as ? s => s + ` as ${this.quote(as)}` : s => s
|
|
286
292
|
if (ref) return _aliased(this.quote(this.name(ref[0])))
|
|
287
293
|
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
|
-
}
|
|
294
|
+
if (from.join)
|
|
295
|
+
return `${this.from(from.args[0])} ${from.join} JOIN ${this.from(from.args[1])} ON ${this.where(from.on)}`
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
/**
|
|
@@ -414,8 +414,10 @@ class CQN2SQLRenderer {
|
|
|
414
414
|
.filter(a => a)
|
|
415
415
|
.map(c => c.sql)
|
|
416
416
|
|
|
417
|
-
this.
|
|
418
|
-
|
|
417
|
+
// Include this.values for placeholders
|
|
418
|
+
/** @type {unknown[][]} */
|
|
419
|
+
this.entries = [[...this.values, JSON.stringify(INSERT.entries)]]
|
|
420
|
+
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${
|
|
419
421
|
this.columns
|
|
420
422
|
}) SELECT ${extraction} FROM json_each(?)`)
|
|
421
423
|
}
|
|
@@ -433,14 +435,11 @@ class CQN2SQLRenderer {
|
|
|
433
435
|
if (!INSERT.columns && !elements) {
|
|
434
436
|
throw cds.error`Cannot insert rows without columns or elements`
|
|
435
437
|
}
|
|
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
|
-
}
|
|
438
|
+
let columns = INSERT.columns || (elements && ObjectKeys(elements).filter(c => !elements[c].virtual && !elements[c].isAssociation))
|
|
440
439
|
this.columns = columns.map(c => this.quote(c))
|
|
441
440
|
|
|
442
441
|
const inputConverterKey = this.class._convertInput
|
|
443
|
-
const extraction = columns.map((c,
|
|
442
|
+
const extraction = columns.map((c,i) => {
|
|
444
443
|
const element = elements?.[c] || {}
|
|
445
444
|
const extract = `value->>'$[${i}]'`
|
|
446
445
|
const converter = element[inputConverterKey] || (e => e)
|
|
@@ -448,7 +447,7 @@ class CQN2SQLRenderer {
|
|
|
448
447
|
})
|
|
449
448
|
|
|
450
449
|
this.entries = [[JSON.stringify(INSERT.rows)]]
|
|
451
|
-
return (this.sql = `INSERT INTO ${entity}${alias ? ' as ' + this.quote(alias) : ''} (${
|
|
450
|
+
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${
|
|
452
451
|
this.columns
|
|
453
452
|
}) SELECT ${extraction} FROM json_each(?)`)
|
|
454
453
|
}
|
|
@@ -536,30 +535,26 @@ class CQN2SQLRenderer {
|
|
|
536
535
|
* @returns {string} SQL
|
|
537
536
|
*/
|
|
538
537
|
UPDATE(q) {
|
|
539
|
-
const {
|
|
540
|
-
|
|
541
|
-
} = q,
|
|
542
|
-
elements = q.target?.elements
|
|
538
|
+
const { entity, with: _with, data, where } = q.UPDATE
|
|
539
|
+
const elements = q.target?.elements
|
|
543
540
|
let sql = `UPDATE ${this.name(entity.ref?.[0] || entity)}`
|
|
544
541
|
if (entity.as) sql += ` AS ${entity.as}`
|
|
542
|
+
|
|
545
543
|
let columns = []
|
|
546
|
-
if (data)
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
if (_with)
|
|
552
|
-
for (let c in _with)
|
|
544
|
+
if (data) _add (data, val => this.val({val}))
|
|
545
|
+
if (_with) _add (_with, x => this.expr(x))
|
|
546
|
+
function _add (data, sql4) {
|
|
547
|
+
for (let c in data) {
|
|
553
548
|
if (!elements || (c in elements && !elements[c].virtual)) {
|
|
554
|
-
columns.push({ name: c, sql:
|
|
549
|
+
columns.push({ name: c, sql: sql4(data[c]) })
|
|
555
550
|
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
556
553
|
|
|
557
554
|
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
|
-
}
|
|
555
|
+
if (q.elements?.[c.name]?.['@cds.extension']) return {
|
|
556
|
+
name: 'extensions__',
|
|
557
|
+
sql: `json_set(extensions__,${this.string('$."' + c.name + '"')},${c.sql})`,
|
|
563
558
|
}
|
|
564
559
|
return c
|
|
565
560
|
})
|
|
@@ -592,24 +587,65 @@ class CQN2SQLRenderer {
|
|
|
592
587
|
* @returns {string} SQL
|
|
593
588
|
*/
|
|
594
589
|
STREAM(q) {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
590
|
+
const { STREAM } = q
|
|
591
|
+
return STREAM.from
|
|
592
|
+
? this.STREAM_from(q)
|
|
593
|
+
: STREAM.into
|
|
594
|
+
? this.STREAM_into(q)
|
|
595
|
+
: cds.error`Missing .form or .into in ${q}`
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Renders a STREAM.into query into generic SQL
|
|
600
|
+
* @param {import('./infer/cqn').STREAM} q
|
|
601
|
+
* @returns {string} SQL
|
|
602
|
+
*/
|
|
603
|
+
STREAM_into(q) {
|
|
604
|
+
const { into, column, where, data } = q.STREAM
|
|
605
|
+
|
|
606
|
+
let sql
|
|
607
|
+
if (!_empty(column)) {
|
|
608
|
+
data.type = 'binary'
|
|
609
|
+
const update = UPDATE(into)
|
|
610
|
+
.with({ [column]: data })
|
|
611
|
+
.where(where)
|
|
612
|
+
Object.defineProperty(update, 'target', { value: q.target })
|
|
613
|
+
sql = this.UPDATE(update)
|
|
602
614
|
} else {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
sql =
|
|
606
|
-
this.
|
|
615
|
+
data.type = 'json'
|
|
616
|
+
// REVISIT: decide whether dataset streams should behave like INSERT or UPSERT
|
|
617
|
+
sql = this.UPSERT(UPSERT([{}]).into(into).forSQL())
|
|
618
|
+
this.values = [data]
|
|
607
619
|
}
|
|
608
|
-
|
|
609
|
-
if (from) sql += ` LIMIT ${this.limit({ rows: { val: 1 } })}`
|
|
620
|
+
|
|
610
621
|
return (this.sql = sql)
|
|
611
622
|
}
|
|
612
623
|
|
|
624
|
+
/**
|
|
625
|
+
* Renders a STREAM.from query into generic SQL
|
|
626
|
+
* @param {import('./infer/cqn').STREAM} q
|
|
627
|
+
* @returns {string} SQL
|
|
628
|
+
*/
|
|
629
|
+
STREAM_from(q) {
|
|
630
|
+
const { column, from, where, columns } = q.STREAM
|
|
631
|
+
|
|
632
|
+
const select = cds.ql
|
|
633
|
+
.SELECT(column ? [column] : columns)
|
|
634
|
+
.where(where)
|
|
635
|
+
.limit(column ? 1 : undefined)
|
|
636
|
+
|
|
637
|
+
// SELECT.from() does not accept joins
|
|
638
|
+
select.SELECT.from = from
|
|
639
|
+
|
|
640
|
+
if (column) {
|
|
641
|
+
this.one = true
|
|
642
|
+
} else {
|
|
643
|
+
select.SELECT.expand = 'root'
|
|
644
|
+
this.one = !!from.SELECT?.one
|
|
645
|
+
}
|
|
646
|
+
return this.SELECT(select.forSQL())
|
|
647
|
+
}
|
|
648
|
+
|
|
613
649
|
// Expression Clauses ---------------------------------------------
|
|
614
650
|
|
|
615
651
|
/**
|
|
@@ -655,11 +691,35 @@ class CQN2SQLRenderer {
|
|
|
655
691
|
* @returns {string} The correct operator string
|
|
656
692
|
*/
|
|
657
693
|
operator(x, i, xpr) {
|
|
658
|
-
|
|
659
|
-
|
|
694
|
+
|
|
695
|
+
// Translate = to IS NULL for rhs operand being NULL literal
|
|
696
|
+
if (x === '=') return xpr[i+1]?.val === null ? 'is' : '='
|
|
697
|
+
|
|
698
|
+
// Translate == to IS NOT NULL for rhs operand being NULL literal, otherwise ...
|
|
699
|
+
// Translate == to IS NOT DISTINCT FROM, unless both operands cannot be NULL
|
|
700
|
+
if (x === '==') return xpr[i+1]?.val === null ? 'is' : _not_null(i-1) && _not_null(i+1) ? '=' : this.is_not_distinct_from_
|
|
701
|
+
|
|
702
|
+
// Translate != to IS NULL for rhs operand being NULL literal, otherwise...
|
|
703
|
+
// Translate != to IS DISTINCT FROM, unless both operands cannot be NULL
|
|
704
|
+
if (x === '!=') return xpr[i+1]?.val === null ? 'is not' : _not_null(i-1) && _not_null(i+1) ? '<>' : this.is_distinct_from_
|
|
705
|
+
|
|
660
706
|
else return x
|
|
707
|
+
|
|
708
|
+
/** Checks if the operand at xpr[i+-1] can be NULL. @returns true if not */
|
|
709
|
+
function _not_null(i) {
|
|
710
|
+
const operand = xpr[i]
|
|
711
|
+
if (!operand) return false
|
|
712
|
+
if (operand.val != null) return true // non-null values are not null
|
|
713
|
+
let element = operand.element
|
|
714
|
+
if (!element) return false
|
|
715
|
+
if (element.key) return true // primary keys usually should not be null
|
|
716
|
+
if (element.notNull) return true // not null elements cannot be null
|
|
717
|
+
}
|
|
661
718
|
}
|
|
662
719
|
|
|
720
|
+
get is_distinct_from_() { return 'is distinct from' }
|
|
721
|
+
get is_not_distinct_from_() { return 'is not distinct from' }
|
|
722
|
+
|
|
663
723
|
/**
|
|
664
724
|
* Renders an argument place holder into the SQL for prepared statements
|
|
665
725
|
* @param {import('./infer/cqn').ref} param0
|
|
@@ -677,7 +737,12 @@ class CQN2SQLRenderer {
|
|
|
677
737
|
* @returns {string} SQL
|
|
678
738
|
*/
|
|
679
739
|
ref({ ref }) {
|
|
680
|
-
|
|
740
|
+
switch (ref[0]) {
|
|
741
|
+
case '$now': return this.func({ func: 'session_context', args: [{ val: '$now' }]})
|
|
742
|
+
case '$user':
|
|
743
|
+
case '$user.id': return this.func({ func: 'session_context', args: [{ val: '$user.id' }]})
|
|
744
|
+
default: return ref.map(r => this.quote(r)).join('.')
|
|
745
|
+
}
|
|
681
746
|
}
|
|
682
747
|
|
|
683
748
|
/**
|
|
@@ -687,22 +752,21 @@ class CQN2SQLRenderer {
|
|
|
687
752
|
*/
|
|
688
753
|
val({ val }) {
|
|
689
754
|
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
|
|
755
|
+
case 'function': throw new Error('Function values not supported.')
|
|
756
|
+
case 'undefined': return 'NULL'
|
|
757
|
+
case 'boolean': return `${val}`
|
|
758
|
+
case 'number': return `${val}` // REVISIT for HANA
|
|
698
759
|
case 'object':
|
|
699
760
|
if (val === null) return 'NULL'
|
|
700
761
|
if (val instanceof Date) return `'${val.toISOString()}'`
|
|
701
|
-
if (
|
|
702
|
-
else
|
|
762
|
+
if (val instanceof Readable) ; // go on with default below
|
|
763
|
+
else if (Buffer.isBuffer(val)) val = val.toString('base64')
|
|
764
|
+
else if (is_regexp(val)) val = val.source
|
|
765
|
+
else val = JSON.stringify(val)
|
|
766
|
+
case 'string': // eslint-disable-line no-fallthrough
|
|
703
767
|
}
|
|
704
768
|
if (!this.values) return this.string(val)
|
|
705
|
-
this.values.push(val)
|
|
769
|
+
else this.values.push(val)
|
|
706
770
|
return '?'
|
|
707
771
|
}
|
|
708
772
|
|
|
@@ -727,25 +791,7 @@ class CQN2SQLRenderer {
|
|
|
727
791
|
}
|
|
728
792
|
|
|
729
793
|
/**
|
|
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
|
|
794
|
+
* Renders a javascript string into a SQL string literal
|
|
749
795
|
* @param {string} s
|
|
750
796
|
* @returns {string} SQL
|
|
751
797
|
*/
|
|
@@ -760,8 +806,9 @@ class CQN2SQLRenderer {
|
|
|
760
806
|
*/
|
|
761
807
|
column_name(col) {
|
|
762
808
|
if (col === '*')
|
|
809
|
+
// 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
810
|
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.
|
|
811
|
+
return (typeof col.as === 'string' && col.as) || ('val' in col && col.val + '') || col.func || col.ref.at(-1)
|
|
765
812
|
}
|
|
766
813
|
|
|
767
814
|
/**
|
|
@@ -783,7 +830,8 @@ class CQN2SQLRenderer {
|
|
|
783
830
|
quote(s) {
|
|
784
831
|
if (typeof s !== 'string') return '"' + s + '"'
|
|
785
832
|
if (s.includes('"')) return '"' + s.replace(/"/g, '""') + '"'
|
|
786
|
-
|
|
833
|
+
// Column names like "Order" clash with "ORDER" keyword so toUpperCase is required
|
|
834
|
+
if (s in this.class.ReservedWords || /^\d|[$' ?@./\\]/.test(s)) return '"' + s + '"'
|
|
787
835
|
return s
|
|
788
836
|
}
|
|
789
837
|
|
|
@@ -796,46 +844,39 @@ class CQN2SQLRenderer {
|
|
|
796
844
|
*/
|
|
797
845
|
managed(columns, elements, isUpdate = false) {
|
|
798
846
|
const annotation = isUpdate ? '@cds.on.update' : '@cds.on.insert'
|
|
799
|
-
const
|
|
847
|
+
const { _convertInput } = this.class
|
|
800
848
|
// Ensure that missing managed columns are added
|
|
801
849
|
const requiredColumns = !elements
|
|
802
850
|
? []
|
|
803
851
|
: Object.keys(elements)
|
|
804
852
|
.filter(
|
|
805
853
|
e =>
|
|
806
|
-
(elements[e]?.[annotation] || (!isUpdate && elements[e]?.default && !elements[e].virtual)) &&
|
|
854
|
+
(elements[e]?.[annotation] || (!isUpdate && elements[e]?.default && !elements[e].virtual && !elements[e].isAssociation)) &&
|
|
807
855
|
!columns.find(c => c.name === e),
|
|
808
856
|
)
|
|
809
857
|
.map(name => ({ name, sql: 'NULL' }))
|
|
810
858
|
|
|
811
859
|
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) {
|
|
860
|
+
let element = elements?.[name] || {}
|
|
861
|
+
if (!sql) sql = `value->>'$."${name}"'`
|
|
862
|
+
|
|
863
|
+
let converter = element[_convertInput]
|
|
864
|
+
if (converter && sql[0] !== '$') sql = converter(sql, element)
|
|
865
|
+
|
|
866
|
+
let val = _managed[element[annotation]?.['=']]
|
|
867
|
+
if (val) sql = `coalesce(${sql}, ${this.func({ func: 'session_context', args: [{ val }] })})`
|
|
868
|
+
|
|
869
|
+
else if (!isUpdate && element.default) {
|
|
828
870
|
const d = element.default
|
|
829
|
-
if (d
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
871
|
+
if (d.val !== undefined || d.ref?.[0] === '$now') {
|
|
872
|
+
// REVISIT: d.ref is not used afterwards
|
|
873
|
+
sql = `(CASE WHEN json_type(value,'$."${name}"') IS NULL THEN ${
|
|
874
|
+
this.defaultValue(d.val) // REVISIT: this.defaultValue is a strange function
|
|
875
|
+
} ELSE ${sql} END)`
|
|
833
876
|
}
|
|
834
877
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
sql: converter(managed === undefined ? extract : `coalesce(${extract}, ${managed})`, element),
|
|
838
|
-
}
|
|
878
|
+
|
|
879
|
+
return { name, sql }
|
|
839
880
|
})
|
|
840
881
|
}
|
|
841
882
|
|
|
@@ -844,6 +885,7 @@ class CQN2SQLRenderer {
|
|
|
844
885
|
* @param {string} defaultValue
|
|
845
886
|
* @returns {string}
|
|
846
887
|
*/
|
|
888
|
+
// REVISIT: This is a strange method, also overridden inconsistently in postgres
|
|
847
889
|
defaultValue(defaultValue = this.context.timestamp.toISOString()) {
|
|
848
890
|
return typeof defaultValue === 'string' ? this.string(defaultValue) : defaultValue
|
|
849
891
|
}
|
|
@@ -855,8 +897,11 @@ Buffer.prototype.toJSON = function () {
|
|
|
855
897
|
}
|
|
856
898
|
|
|
857
899
|
const ObjectKeys = o => (o && [...ObjectKeys(o.__proto__), ...Object.keys(o)]) || []
|
|
858
|
-
const
|
|
859
|
-
|
|
900
|
+
const _managed = {
|
|
901
|
+
'$user.id': '$user.id',
|
|
902
|
+
$user: '$user.id',
|
|
903
|
+
$now: '$now',
|
|
904
|
+
}
|
|
860
905
|
|
|
861
906
|
const is_regexp = x => x?.constructor?.name === 'RegExp' // NOTE: x instanceof RegExp doesn't work in repl
|
|
862
907
|
const _empty = a => !a || a.length === 0
|