@cap-js/db-service 2.9.0 → 2.10.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 +16 -0
- package/lib/InsertResults.js +6 -3
- package/lib/SQLService.js +83 -42
- package/lib/cql-functions.js +1 -1
- package/lib/cqn2pql.js +116 -0
- package/lib/cqn2sql.js +7 -6
- package/lib/cqn4sql.js +281 -153
- package/lib/infer/index.js +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@
|
|
|
4
4
|
- The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
5
5
|
- This project adheres to [Semantic Versioning](http://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [2.10.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.9.0...db-service-v2.10.0) (2026-04-22)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* `cds.features.count_as_string` ([#1556](https://github.com/cap-js/cds-dbs/issues/1556)) ([00e0e60](https://github.com/cap-js/cds-dbs/commit/00e0e60d68edf0d42c1fce2fae3bb1286aca131e))
|
|
13
|
+
* **cqn4sql:** support for enums ([#1527](https://github.com/cap-js/cds-dbs/issues/1527)) ([27c4279](https://github.com/cap-js/cds-dbs/commit/27c4279c495fce8344c785e4489e3116d1a52c55))
|
|
14
|
+
* pql ([#1532](https://github.com/cap-js/cds-dbs/issues/1532)) ([943f76a](https://github.com/cap-js/cds-dbs/commit/943f76a3e4405eb91f0f4b929590212500c49c30))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
* `$self` reference to func column in `having` ([#1539](https://github.com/cap-js/cds-dbs/issues/1539)) ([9eac576](https://github.com/cap-js/cds-dbs/commit/9eac5762fc4d254a1bc54bded1dd6a492299f576)), closes [#1528](https://github.com/cap-js/cds-dbs/issues/1528)
|
|
20
|
+
* foreign key not included in wildcard select from subquery ([#1540](https://github.com/cap-js/cds-dbs/issues/1540)) ([0fde4ed](https://github.com/cap-js/cds-dbs/commit/0fde4eda21a389c68982f348e9e7c3680c00dcb3)), closes [#1127](https://github.com/cap-js/cds-dbs/issues/1127)
|
|
21
|
+
* sqlite generated key is named lastInsertRowid ([#1501](https://github.com/cap-js/cds-dbs/issues/1501)) ([a4d3437](https://github.com/cap-js/cds-dbs/commit/a4d34378297c8afdb13abb7e664165012c36eb8f))
|
|
22
|
+
|
|
7
23
|
## [2.9.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.8.2...db-service-v2.9.0) (2026-03-09)
|
|
8
24
|
|
|
9
25
|
|
package/lib/InsertResults.js
CHANGED
|
@@ -69,9 +69,12 @@ module.exports = class InsertResult {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// If no generated keys in entries/rows/values we might have database-generated keys
|
|
72
|
-
const rows = this.results.slice(0, this.affectedRows) // only up to # of root entries
|
|
73
72
|
return (super[iterator] = function* () {
|
|
74
|
-
for (const
|
|
73
|
+
for (const row of this.results) {
|
|
74
|
+
const affectedRows = this.affectedRows4(row) - 1
|
|
75
|
+
const lastInsertRowid = this.insertedRowId4(row)
|
|
76
|
+
for (let i = lastInsertRowid - affectedRows; i<=lastInsertRowid;i++) yield { [k1]: i }
|
|
77
|
+
}
|
|
75
78
|
})
|
|
76
79
|
}
|
|
77
80
|
|
|
@@ -99,7 +102,7 @@ module.exports = class InsertResult {
|
|
|
99
102
|
* @returns {number}
|
|
100
103
|
*/
|
|
101
104
|
insertedRowId4(result) {
|
|
102
|
-
return result.
|
|
105
|
+
return result.lastInsertRowid
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
/**
|
package/lib/SQLService.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
const cds = require('@sap/cds')
|
|
2
|
-
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
|
+
const DEBUG = cds.log('sql|db')
|
|
3
3
|
const { Readable, Transform } = require('stream')
|
|
4
4
|
const { pipeline } = require('stream/promises')
|
|
5
5
|
const DatabaseService = require('./common/DatabaseService')
|
|
6
6
|
const cqn4sql = require('./cqn4sql')
|
|
7
7
|
const { resolveTable } = require('./utils')
|
|
8
8
|
|
|
9
|
+
// REVISIT: make string the default in next major
|
|
10
|
+
const _count_as_string = cds.env.features.count_as_string
|
|
11
|
+
const _count = _count_as_string ? { func: 'count', cast: { type: 'cds.String' } } : { func: 'count' }
|
|
12
|
+
|
|
9
13
|
const BINARY_TYPES = {
|
|
10
14
|
'cds.Binary': 1,
|
|
11
15
|
'cds.hana.BINARY': 1
|
|
@@ -17,7 +21,7 @@ const BINARY_TYPES = {
|
|
|
17
21
|
* @param {*} obj
|
|
18
22
|
* @returns Boolean
|
|
19
23
|
*/
|
|
20
|
-
const _hasProps = (obj) => {
|
|
24
|
+
const _hasProps = (obj) => {
|
|
21
25
|
if (!obj) return false
|
|
22
26
|
for (const p in obj) {
|
|
23
27
|
return true
|
|
@@ -74,7 +78,7 @@ class SQLService extends DatabaseService {
|
|
|
74
78
|
_changeToStreams(columns, rows, one) {
|
|
75
79
|
if (!rows || !columns) return
|
|
76
80
|
if (!Array.isArray(rows)) rows = [rows]
|
|
77
|
-
if (!rows.length || !Object.keys(rows[0]).length) return
|
|
81
|
+
if (!rows.length || !Object.keys(rows[0]).length) return
|
|
78
82
|
|
|
79
83
|
let changes = false
|
|
80
84
|
for (let col of columns) {
|
|
@@ -149,7 +153,7 @@ class SQLService extends DatabaseService {
|
|
|
149
153
|
if (expand) rows = rows.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))
|
|
150
154
|
|
|
151
155
|
if (!iterator) {
|
|
152
|
-
|
|
156
|
+
this._changeToStreams(cqn.SELECT.columns, rows, query.SELECT.one)
|
|
153
157
|
} else if (objectMode) {
|
|
154
158
|
const converter = (row) => this._changeToStreams(cqn.SELECT.columns, row, true)
|
|
155
159
|
const changeToStreams = new Transform({
|
|
@@ -198,7 +202,7 @@ class SQLService extends DatabaseService {
|
|
|
198
202
|
const ps = await this.prepare(sql)
|
|
199
203
|
const results = entries ? await Promise.all(entries.map(e => ps.run(e))) : await ps.run()
|
|
200
204
|
// REVISIT: results isn't an array, when no entries -> how could that work? when do we have no entries?
|
|
201
|
-
return results.reduce((total, affectedRows) =>
|
|
205
|
+
return results.reduce((total, affectedRows) => total + affectedRows.changes, 0)
|
|
202
206
|
}
|
|
203
207
|
|
|
204
208
|
/**
|
|
@@ -296,7 +300,7 @@ class SQLService extends DatabaseService {
|
|
|
296
300
|
* @type {Handler}
|
|
297
301
|
*/
|
|
298
302
|
async onEVENT({ event }) {
|
|
299
|
-
DEBUG
|
|
303
|
+
if(DEBUG._debug) DEBUG.debug(event) // in the other cases above DEBUG happens in cqn2sql
|
|
300
304
|
return await this.exec(event)
|
|
301
305
|
}
|
|
302
306
|
|
|
@@ -306,7 +310,7 @@ class SQLService extends DatabaseService {
|
|
|
306
310
|
*/
|
|
307
311
|
async onPlainSQL({ query, data }, next) {
|
|
308
312
|
if (typeof query === 'string') {
|
|
309
|
-
DEBUG
|
|
313
|
+
if(DEBUG._debug) DEBUG.debug(query, data)
|
|
310
314
|
const ps = await this.prepare(query)
|
|
311
315
|
const exec = this.hasResults(query) ? d => ps.all(d) : d => ps.run(d)
|
|
312
316
|
if (Array.isArray(data) && Array.isArray(data[0])) return await Promise.all(data.map(exec))
|
|
@@ -326,24 +330,24 @@ class SQLService extends DatabaseService {
|
|
|
326
330
|
* Derives and executes a query to fill in `$count` for given query
|
|
327
331
|
* @param {import('@sap/cds/apis/cqn').SELECT} query - SELECT CQN
|
|
328
332
|
* @param {unknown[]} ret - Results of the original query
|
|
329
|
-
* @returns {Promise<number>}
|
|
333
|
+
* @returns {Promise<number|string>}
|
|
330
334
|
*/
|
|
331
335
|
async count(query, ret) {
|
|
332
336
|
if (ret?.length) {
|
|
333
337
|
const { one, limit: _ } = query.SELECT,
|
|
334
338
|
n = ret.length
|
|
335
339
|
const [max, offset = 0] = one ? [1] : _ ? [_.rows?.val, _.offset?.val] : []
|
|
336
|
-
if (max === undefined || (n < max && (n || !offset))) return n + offset
|
|
340
|
+
if (max === undefined || (n < max && (n || !offset))) return _count_as_string ? `${n + offset}` : n + offset
|
|
337
341
|
}
|
|
338
342
|
|
|
339
343
|
// Keep original query columns when potentially used insde conditions
|
|
340
344
|
const { having, groupBy } = query.SELECT
|
|
341
345
|
let columns = []
|
|
342
|
-
if(
|
|
346
|
+
if (having?.length || groupBy?.length) {
|
|
343
347
|
columns = query.SELECT.columns.filter(c => !c.expand)
|
|
344
348
|
}
|
|
345
349
|
if (columns.length === 0) columns.push({ val: 1 })
|
|
346
|
-
const cq = SELECT.one([
|
|
350
|
+
const cq = SELECT.one([_count]).from(
|
|
347
351
|
cds.ql.clone(query, {
|
|
348
352
|
columns,
|
|
349
353
|
localized: false,
|
|
@@ -361,7 +365,7 @@ class SQLService extends DatabaseService {
|
|
|
361
365
|
* @param {import('@sap/cds/apis/cqn').SELECT} query - SELECT CQN
|
|
362
366
|
* @param {function} callback - Function to be invoked for each row
|
|
363
367
|
*/
|
|
364
|
-
foreach
|
|
368
|
+
foreach(query, callback) {
|
|
365
369
|
return query.foreach(callback)
|
|
366
370
|
}
|
|
367
371
|
|
|
@@ -391,36 +395,34 @@ class SQLService extends DatabaseService {
|
|
|
391
395
|
})
|
|
392
396
|
}
|
|
393
397
|
|
|
394
|
-
/** @param {unknown[]} args */
|
|
395
398
|
constructor(...args) {
|
|
396
399
|
super(...args)
|
|
397
|
-
/** @type {unknown} */
|
|
398
400
|
this.class = new.target // for IntelliSense
|
|
399
401
|
}
|
|
400
402
|
|
|
401
403
|
/**
|
|
402
404
|
* @param {import('@sap/cds/apis/cqn').Query} query
|
|
403
|
-
* @param {unknown} values
|
|
404
405
|
* @returns {typeof SQLService.CQN2SQL}
|
|
405
406
|
*/
|
|
406
407
|
cqn2sql(query, values) {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
408
|
+
const cqn2sql = new this.class.CQN2SQL(this)
|
|
409
|
+
const q = this.cqn4sql(query)
|
|
410
|
+
const sql = cqn2sql.render(q, values)
|
|
411
|
+
return sql
|
|
410
412
|
}
|
|
411
413
|
|
|
412
414
|
/**
|
|
413
415
|
* @param {import('@sap/cds/apis/cqn').Query} q
|
|
414
416
|
* @returns {import('./infer/cqn').Query}
|
|
415
417
|
*/
|
|
416
|
-
cqn4sql(q) {
|
|
418
|
+
cqn4sql(q, useTechnicalAlias=true) {
|
|
417
419
|
if (
|
|
418
420
|
!cds.env.features.db_strict &&
|
|
419
421
|
!q.SELECT?.from?.join &&
|
|
420
422
|
!q.SELECT?.from?.SELECT &&
|
|
421
423
|
!this.model?.definitions[_target_name4(q)]
|
|
422
424
|
) return q
|
|
423
|
-
else return cqn4sql(q, this.model)
|
|
425
|
+
else return cqn4sql(q, this.model, useTechnicalAlias)
|
|
424
426
|
}
|
|
425
427
|
|
|
426
428
|
/**
|
|
@@ -509,31 +511,70 @@ const _target_name4 = q => {
|
|
|
509
511
|
return first.id || first
|
|
510
512
|
}
|
|
511
513
|
|
|
512
|
-
const sqls = new (class extends SQLService {
|
|
513
|
-
get factory() {
|
|
514
|
-
return null
|
|
515
|
-
}
|
|
516
514
|
|
|
517
|
-
|
|
518
|
-
|
|
515
|
+
// Add support for cqn2pql if debug logging for pql is enabled, or if running in the REPL.
|
|
516
|
+
const DEBUG_PQL = cds.log('pql')
|
|
517
|
+
if (DEBUG_PQL._debug || cds.repl) {
|
|
518
|
+
|
|
519
|
+
// Add helper method to convert CQN to PQL, used below...
|
|
520
|
+
SQLService.prototype.cqn2pql = function cqn2pql (query, values) {
|
|
521
|
+
const CQN2PQL = cqn2pql.renderer ??= require('./cqn2pql')
|
|
522
|
+
return new CQN2PQL(this).render(query, values)
|
|
519
523
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
let { sql, values } = (cds.db || sqls).cqn2sql(this)
|
|
530
|
-
return { sql, values } // skipping .cqn property
|
|
524
|
+
|
|
525
|
+
// Add support for logging generated PQL if debug logging for pql is enabled.
|
|
526
|
+
if (DEBUG_PQL._debug) {
|
|
527
|
+
const $super = SQLService.prototype.cqn2sql
|
|
528
|
+
SQLService.prototype.cqn2sql = function (query, values) {
|
|
529
|
+
const q2 = this.cqn4sql(query, false) // FIXME: calling cqn4sql twice per query is utterly expensive, isn't it ?!?
|
|
530
|
+
const pql = this.cqn2pql(q2, values)
|
|
531
|
+
DEBUG_PQL.debug(pql.sql, pql.values ?? '')
|
|
532
|
+
return $super.call(this, query, values)
|
|
531
533
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// If running in the REPL, extend cds.ql.Query with helpers to inspect queries.
|
|
537
|
+
if (cds.repl) {
|
|
538
|
+
|
|
539
|
+
cds.extend(cds.ql.Query).with(
|
|
540
|
+
class {
|
|
541
|
+
forSQL() {
|
|
542
|
+
const cqn = db.srv.cqn4sql(this)
|
|
543
|
+
return this.flat(cqn)
|
|
544
|
+
}
|
|
545
|
+
forSql() { return this.forSQL() }
|
|
546
|
+
toSQL() {
|
|
547
|
+
if (this.SELECT) this.SELECT.expand = 'root' // Enforces using json functions always for top-level SELECTS
|
|
548
|
+
const { sql, values } = db.srv.cqn2sql(this)
|
|
549
|
+
return { sql, values } // skipping .cqn property
|
|
550
|
+
}
|
|
551
|
+
toSql() {
|
|
552
|
+
const { sql } = this.toSQL()
|
|
553
|
+
return sql
|
|
554
|
+
}
|
|
555
|
+
toPQL() {
|
|
556
|
+
const { sql, values } = db.srv.cqn2pql(this)
|
|
557
|
+
return { sql, values } // skipping .cqn property
|
|
558
|
+
}
|
|
559
|
+
toPql() {
|
|
560
|
+
const { sql } = this.toPQL()
|
|
561
|
+
return sql
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Dummy SQL service used in extensions to cds.ql above,
|
|
568
|
+
* if no real SQL service is available yet through cds.db.
|
|
569
|
+
*/
|
|
570
|
+
class db extends SQLService {
|
|
571
|
+
/** @returns {SQLService} */
|
|
572
|
+
static get srv() { return cds.db || (this.singleton ??= new this) }
|
|
573
|
+
get factory() { return null }
|
|
574
|
+
get model() { return cds.model }
|
|
534
575
|
}
|
|
535
|
-
}
|
|
536
|
-
|
|
576
|
+
}
|
|
577
|
+
}
|
|
537
578
|
|
|
538
579
|
Object.assign(SQLService, { _target_name4 })
|
|
539
580
|
module.exports = SQLService
|
package/lib/cql-functions.js
CHANGED
|
@@ -293,7 +293,7 @@ SELECT
|
|
|
293
293
|
(SELECT MAX(HIERARCHY_RANK) + 1 FROM ${ranked})
|
|
294
294
|
) - Source.HIERARCHY_RANK AS HIERARCHY_TREE_SIZE
|
|
295
295
|
FROM ${ranked} AS Source`)
|
|
296
|
-
Hierarchy.as =
|
|
296
|
+
Hierarchy.as = `H${uniqueCounter}`
|
|
297
297
|
Hierarchy.SELECT.columns = [...Hierarchy.SELECT.columns, ...passThroughColumns]
|
|
298
298
|
Hierarchy = this.expr(this.with(Hierarchy))
|
|
299
299
|
|
package/lib/cqn2pql.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
|
+
|
|
3
|
+
const CQN2SQL = require('./cqn2sql.js').class
|
|
4
|
+
|
|
5
|
+
class CQN2PQLRenderer extends CQN2SQL {
|
|
6
|
+
|
|
7
|
+
SELECT(q) {
|
|
8
|
+
this.values = undefined // inline all values
|
|
9
|
+
return (this.sql = super.SELECT(q)
|
|
10
|
+
.replaceAll('\n FROM', '\nFROM')
|
|
11
|
+
.replaceAll(/([^ ]) (FROM|WHERE|GROUP BY|HAVING|ORDER BY|LIMIT) /g, (a, b, c) => `${b}\n${c} `)
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
SELECT_columns(q) {
|
|
16
|
+
return super.SELECT_columns(q).map((c, i) => `${(i % 5 === 0) ? '\n ' : ' '}${c}${/ as /i.test(c) ? '\n' : ''}`).join(',')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
column_expr(x, q) {
|
|
20
|
+
// omit alias when target is a single source
|
|
21
|
+
if (q.SELECT.from.ref && x?.ref) x.ref = x.ref.slice(-1)
|
|
22
|
+
return super.column_expr(x, q)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
SELECT_expand(q, sql) { return sql }
|
|
26
|
+
|
|
27
|
+
INSERT_entries(q) {
|
|
28
|
+
super.INSERT_entries(q)
|
|
29
|
+
this.sql = this.sql
|
|
30
|
+
.replaceAll(/AS (.*?)([, ])(?=[^\n])/ig, (a, b, c) => `AS ${b}${c}\n${c === ',' ? ' ' : ''}`)
|
|
31
|
+
.replaceAll(/ *= */ig, ' = ')
|
|
32
|
+
.replaceAll('value AS "$$value$$"', 'value')
|
|
33
|
+
.replaceAll(' WHERE ', '\nWHERE ')
|
|
34
|
+
.replaceAll(' SELECT ', '\nSELECT')
|
|
35
|
+
.replaceAll('(SELECT ', '(SELECT\n ')
|
|
36
|
+
.replaceAll('))', ')\n)')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
INSERT_rows(q) {
|
|
40
|
+
super.INSERT_rows(q)
|
|
41
|
+
this.sql = this.sql.replaceAll('SELECT', '\nSELECT')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
UPSERT(q) {
|
|
45
|
+
super.UPSERT(q)
|
|
46
|
+
this.sql = this.sql
|
|
47
|
+
.replaceAll('INSERT', 'UPSERT')
|
|
48
|
+
.replaceAll(/AS (.*?)([, ])(?=[^\n])/ig, (a, b, c) => `AS ${b}${c}\n${c === ',' ? ' ' : ''}`)
|
|
49
|
+
.replaceAll(/ *= */ig, ' = ')
|
|
50
|
+
.replaceAll('value AS "$$value$$"', 'value')
|
|
51
|
+
.replaceAll(' WHERE ', '\nWHERE ')
|
|
52
|
+
.replaceAll(' SELECT ', '\nSELECT')
|
|
53
|
+
.replaceAll('(SELECT ', '(SELECT\n ')
|
|
54
|
+
.replaceAll('))', ')\n)')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
expr(x) {
|
|
58
|
+
const wrap = x.cast ? sql => `cast(${sql} as ${this.type4(x.cast)})` : sql => sql
|
|
59
|
+
if (typeof x === 'string') throw cds.error`Unsupported expr: ${x}`
|
|
60
|
+
if (x.param) return wrap(this.param(x))
|
|
61
|
+
if ('ref' in x) return wrap(this.ref(x))
|
|
62
|
+
if ('val' in x) return wrap(this.val(x))
|
|
63
|
+
if ('func' in x) return wrap(this.func(x))
|
|
64
|
+
if ('xpr' in x) return wrap(this.xpr(x))
|
|
65
|
+
if ('list' in x) return wrap(this.list(x))
|
|
66
|
+
if ('SELECT' in x) return wrap(`(\n ${this.SELECT(x).replaceAll('\n', '\n ')}\n )`)
|
|
67
|
+
else throw cds.error`Unsupported expr: ${x}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
quote(s) { return s }
|
|
71
|
+
|
|
72
|
+
managed(columns, elements) {
|
|
73
|
+
const keys = ObjectKeys(elements).filter(e => elements[e].key && !elements[e].isAssociation)
|
|
74
|
+
const keyZero = keys[0]
|
|
75
|
+
|
|
76
|
+
const ret = super.managed(columns, elements)
|
|
77
|
+
|
|
78
|
+
ret.forEach(c => {
|
|
79
|
+
const { name, insert, update, onInsert, onUpdate } = c
|
|
80
|
+
const element = elements?.[name]
|
|
81
|
+
c.upsert = keyZero && (
|
|
82
|
+
// upsert requires the keys to be provided for the existance join (default values optional)
|
|
83
|
+
element?.key
|
|
84
|
+
// If both insert and update have the same managed definition exclude the old value check
|
|
85
|
+
|| (onInsert && onUpdate && insert === update)
|
|
86
|
+
? `${insert} as ${name}`
|
|
87
|
+
: `!OLD.${keyZero} ? ${
|
|
88
|
+
// If key of old is null execute insert
|
|
89
|
+
insert
|
|
90
|
+
} : ${
|
|
91
|
+
// Else execute managed update or keep old if no new data if provided
|
|
92
|
+
onUpdate ? update : `(${this.managed_default(name, `OLD.${name}`, update)})`
|
|
93
|
+
} as ${name}`
|
|
94
|
+
)
|
|
95
|
+
if (c.upsert) c.upsert = '\n ' + c.upsert
|
|
96
|
+
})
|
|
97
|
+
return ret
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
managed_default(name, managed, src) {
|
|
101
|
+
return `!${src} ? ${managed} : ${src}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
managed_extract(name) {
|
|
105
|
+
const { UPSERT, INSERT } = this.cqn
|
|
106
|
+
const extract = !(INSERT?.entries || UPSERT?.entries) && (INSERT?.rows || UPSERT?.rows)
|
|
107
|
+
? `value[${this.columns.indexOf(name)}]`
|
|
108
|
+
: `value[${JSON.stringify(name)}]`
|
|
109
|
+
const sql = extract
|
|
110
|
+
return { extract, sql }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const ObjectKeys = o => (o && [...ObjectKeys(o.__proto__), ...Object.keys(o)]) || []
|
|
115
|
+
|
|
116
|
+
module.exports = CQN2PQLRenderer
|
package/lib/cqn2sql.js
CHANGED
|
@@ -5,12 +5,13 @@ const { resolveTable } = require('./utils')
|
|
|
5
5
|
|
|
6
6
|
const _simple_queries = cds.env.features.sql_simple_queries
|
|
7
7
|
const _strict_booleans = _simple_queries < 2
|
|
8
|
+
// REVISIT: make string the default in next major
|
|
9
|
+
const _count_as_string = cds.env.features.count_as_string
|
|
10
|
+
const _count = _count_as_string ? { func: 'count', cast: { type: 'cds.String' } } : { func: 'count' }
|
|
8
11
|
|
|
9
12
|
const { Readable } = require('stream')
|
|
10
13
|
|
|
11
|
-
const DEBUG = cds.
|
|
12
|
-
const LOG_SQL = cds.log('sql')
|
|
13
|
-
const LOG_SQLITE = cds.log('sqlite')
|
|
14
|
+
const DEBUG = cds.log('sql|sqlite')
|
|
14
15
|
|
|
15
16
|
class CQN2SQLRenderer {
|
|
16
17
|
/**
|
|
@@ -94,12 +95,12 @@ class CQN2SQLRenderer {
|
|
|
94
95
|
if (vars && Object.keys(vars).length && !this.values?.length) this.values = vars
|
|
95
96
|
const sanitize_values = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
|
|
96
97
|
|
|
97
|
-
if (DEBUG
|
|
98
|
+
if (DEBUG._debug) {
|
|
98
99
|
let values = sanitize_values && (this.entries || this.values?.length > 0) ? ['***'] : this.entries || this.values || []
|
|
99
100
|
if (values && !Array.isArray(values)) {
|
|
100
101
|
values = [values]
|
|
101
102
|
}
|
|
102
|
-
DEBUG(this.sql, values)
|
|
103
|
+
DEBUG.debug(this.sql, values)
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
return this
|
|
@@ -657,7 +658,7 @@ class CQN2SQLRenderer {
|
|
|
657
658
|
|
|
658
659
|
SELECT_count(q) {
|
|
659
660
|
const countQuery = cds.ql.clone(q, {
|
|
660
|
-
columns: [
|
|
661
|
+
columns: [_count],
|
|
661
662
|
one: 0, limit: 0, orderBy: 0, expand: 0, count: 0
|
|
662
663
|
})
|
|
663
664
|
countQuery.as = q.as + '@odata.count'
|
package/lib/cqn4sql.js
CHANGED
|
@@ -9,7 +9,7 @@ const {
|
|
|
9
9
|
prettyPrintRef,
|
|
10
10
|
isCalculatedOnRead,
|
|
11
11
|
isCalculatedElement,
|
|
12
|
-
getImplicitAlias,
|
|
12
|
+
getImplicitAlias: _getImplicitAlias,
|
|
13
13
|
defineProperty,
|
|
14
14
|
getModelUtils,
|
|
15
15
|
hasOwnSkip,
|
|
@@ -54,7 +54,8 @@ const { pseudos } = require('./infer/pseudos')
|
|
|
54
54
|
* @param {object} model
|
|
55
55
|
* @returns {object} transformedQuery the transformed query
|
|
56
56
|
*/
|
|
57
|
-
function cqn4sql(originalQuery, model) {
|
|
57
|
+
function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
|
|
58
|
+
const getImplicitAlias = str => _getImplicitAlias(str, useTechnicalAlias)
|
|
58
59
|
let inferred = typeof originalQuery === 'string' ? cds.parse.cql(originalQuery) : cds.ql.clone(originalQuery)
|
|
59
60
|
const hasCustomJoins =
|
|
60
61
|
originalQuery.SELECT?.from.args && (!originalQuery.joinTree || originalQuery.joinTree.isInitial)
|
|
@@ -81,7 +82,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
81
82
|
if (inferred.UPDATE?.entity.ref?.at(-1).id) {
|
|
82
83
|
assignQueryModifiers(inferred.UPDATE, inferred.UPDATE.entity.ref.at(-1))
|
|
83
84
|
}
|
|
84
|
-
inferred = infer(inferred, model)
|
|
85
|
+
inferred = infer(inferred, model, useTechnicalAlias)
|
|
85
86
|
const { getLocalizedName, isLocalized, getDefinition } = getModelUtils(model, originalQuery) // TODO: pass model to getModelUtils
|
|
86
87
|
// if the query has custom joins we don't want to transform it
|
|
87
88
|
// TODO: move all the way to the top of this function once cds.infer supports joins as well
|
|
@@ -134,7 +135,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
134
135
|
// match primary keys of the target entity with the subquery
|
|
135
136
|
primaryKey.list.forEach(k => subquery.SELECT.columns.push({ ref: k.ref.slice(1) }))
|
|
136
137
|
|
|
137
|
-
const transformedSubquery = cqn4sql(subquery, model)
|
|
138
|
+
const transformedSubquery = cqn4sql(subquery, model, useTechnicalAlias)
|
|
138
139
|
|
|
139
140
|
// replace where condition of original query with the transformed subquery
|
|
140
141
|
// correlate UPDATE / DELETE query with subquery by primary key matches
|
|
@@ -229,9 +230,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
229
230
|
}
|
|
230
231
|
}
|
|
231
232
|
}
|
|
232
|
-
const inferredDQ = infer(q, model)
|
|
233
|
+
const inferredDQ = infer(q, model, useTechnicalAlias)
|
|
233
234
|
inferredDQ._with = transformedQuery._with
|
|
234
|
-
const transformedDQ = cqn4sql(inferredDQ, model)
|
|
235
|
+
const transformedDQ = cqn4sql(inferredDQ, model, useTechnicalAlias)
|
|
235
236
|
|
|
236
237
|
if (q.SELECT?.from?.args) {
|
|
237
238
|
for (const arg of q.SELECT.from.args) {
|
|
@@ -622,6 +623,11 @@ function cqn4sql(originalQuery, model) {
|
|
|
622
623
|
|
|
623
624
|
function getTransformedColumn(col) {
|
|
624
625
|
let ret
|
|
626
|
+
if (col !== null && typeof col === 'object' && '#' in col) {
|
|
627
|
+
ret = resolveEnumToken(col, [], -1)
|
|
628
|
+
// cast is already resolved inside resolveEnumToken; do not overwrite it here
|
|
629
|
+
return ret
|
|
630
|
+
}
|
|
625
631
|
if (col.func) {
|
|
626
632
|
ret = {
|
|
627
633
|
func: col.func,
|
|
@@ -634,7 +640,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
634
640
|
ret.xpr = getTransformedTokenStream(col.xpr)
|
|
635
641
|
}
|
|
636
642
|
if (ret) {
|
|
637
|
-
if (col.cast) ret.cast = col.cast
|
|
643
|
+
if (col.cast) ret.cast = resolveEnumCastType(col.cast)
|
|
638
644
|
return ret
|
|
639
645
|
}
|
|
640
646
|
return copy(col)
|
|
@@ -726,10 +732,11 @@ function cqn4sql(originalQuery, model) {
|
|
|
726
732
|
},
|
|
727
733
|
})
|
|
728
734
|
} else {
|
|
729
|
-
// target column is `val` or `
|
|
735
|
+
// target column is `val`, `xpr`, or `func` — destructure and throw away the ref with the $self
|
|
730
736
|
// eslint-disable-next-line no-unused-vars
|
|
731
|
-
const { xpr, val, ref, as: _as, ...rest } = referencedColumn
|
|
737
|
+
const { xpr, val, func, args, ref, as: _as, ...rest } = referencedColumn
|
|
732
738
|
if (xpr) rest.xpr = xpr
|
|
739
|
+
else if (func) { rest.func = func; rest.args = args }
|
|
733
740
|
else rest.val = val
|
|
734
741
|
dollarSelfColumn = { ...rest } // reassign dummyColumn without 'ref'
|
|
735
742
|
if (!omitAlias) dollarSelfColumn.as = as
|
|
@@ -836,9 +843,11 @@ function cqn4sql(originalQuery, model) {
|
|
|
836
843
|
return { ...token, xpr: augmentInlineXprRefs(token.xpr, parentCol) }
|
|
837
844
|
}
|
|
838
845
|
if (token.func && token.args) {
|
|
839
|
-
return {
|
|
840
|
-
|
|
841
|
-
|
|
846
|
+
return {
|
|
847
|
+
...token, args: token.args.map(arg =>
|
|
848
|
+
arg.ref ? augmentInlineXprRefs([arg], parentCol)[0] : arg
|
|
849
|
+
)
|
|
850
|
+
}
|
|
842
851
|
}
|
|
843
852
|
return token
|
|
844
853
|
})
|
|
@@ -1347,7 +1356,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1347
1356
|
if (isLocalized(target)) q.SELECT.localized = true
|
|
1348
1357
|
if (q.SELECT.from.ref && !q.SELECT.from.as) assignUniqueSubqueryAlias()
|
|
1349
1358
|
if (cds.env.features.runtime_views) q._with = transformedQuery._with
|
|
1350
|
-
const _q = cqn4sql(q, model)
|
|
1359
|
+
const _q = cqn4sql(q, model, useTechnicalAlias)
|
|
1351
1360
|
if (cds.env.features.runtime_views && _q._with) {
|
|
1352
1361
|
if (!transformedQuery._with) transformedQuery._with = _q._with
|
|
1353
1362
|
delete _q._with
|
|
@@ -1388,8 +1397,8 @@ function cqn4sql(originalQuery, model) {
|
|
|
1388
1397
|
if (!exclude.includes(k)) {
|
|
1389
1398
|
const { index, tableAlias } = inferred.$combinedElements[k][0]
|
|
1390
1399
|
const element = tableAlias.elements[k]
|
|
1391
|
-
// ignore FK for odata csn / ignore blobs from wildcard expansion
|
|
1392
|
-
if (isManagedAssocInFlatMode(element) || element.type === 'cds.LargeBinary') continue
|
|
1400
|
+
// ignore FK for odata csn (but not for subquery sources where FK is not a separate element) / ignore blobs from wildcard expansion
|
|
1401
|
+
if ((!tableAlias.SELECT && isManagedAssocInFlatMode(element)) || element.type === 'cds.LargeBinary') continue
|
|
1393
1402
|
// for wildcard on subquery in from, just reference the elements
|
|
1394
1403
|
if (tableAlias.SELECT && !element.elements && !element.target) {
|
|
1395
1404
|
wildcardColumns.push(index ? { ref: [index, k] } : { ref: [k] })
|
|
@@ -1437,35 +1446,23 @@ function cqn4sql(originalQuery, model) {
|
|
|
1437
1446
|
|
|
1438
1447
|
/**
|
|
1439
1448
|
* Recursively expands a structured element into flat columns, representing all leaf paths.
|
|
1440
|
-
* This function transforms complex structured elements into simple column representations.
|
|
1441
1449
|
*
|
|
1442
|
-
*
|
|
1443
|
-
*
|
|
1444
|
-
* If it's an association, it fetches flat columns for it's foreign keys.
|
|
1445
|
-
* If it's a scalar, it creates a flat column for it.
|
|
1450
|
+
* Structures → flat sub-element columns. Associations → flat foreign key columns.
|
|
1451
|
+
* Scalars → single column reference.
|
|
1446
1452
|
*
|
|
1447
|
-
*
|
|
1448
|
-
*
|
|
1449
|
-
* @param {object} column - The structured element which needs to be expanded.
|
|
1453
|
+
* @param {object} column - The element to expand (may be a ref with $refLinks, or a raw element definition).
|
|
1450
1454
|
* @param {{
|
|
1451
|
-
*
|
|
1452
|
-
*
|
|
1453
|
-
*
|
|
1454
|
-
* }} names -
|
|
1455
|
-
*
|
|
1456
|
-
*
|
|
1457
|
-
*
|
|
1458
|
-
*
|
|
1459
|
-
*
|
|
1460
|
-
* @param {
|
|
1461
|
-
*
|
|
1462
|
-
* `{ struct_foo_leaf1 as bar_foo_leaf1, struct_foo_leaf2 as bar_foo_leaf2 }`.
|
|
1463
|
-
* @param {string} tableAlias - The table alias to prepend to the column name. Optional.
|
|
1464
|
-
* @param {Array} csnPath - An array containing CSN paths. Optional.
|
|
1465
|
-
* @param {Array} exclude - An array of columns to be excluded from the flat structure. Optional.
|
|
1466
|
-
* @param {Array} replace - An array of columns to be replaced in the flat structure. Optional.
|
|
1467
|
-
*
|
|
1468
|
-
* @returns {object[]} Returns an array of flat column(s) for the given element.
|
|
1455
|
+
* baseName?: string,
|
|
1456
|
+
* columnAlias?: string,
|
|
1457
|
+
* tableAlias?: string
|
|
1458
|
+
* }} [names] - Naming context:
|
|
1459
|
+
* - `baseName` — accumulated underscore-joined prefix for the flat column ref (e.g. `'address'` → `'address_street'`).
|
|
1460
|
+
* - `columnAlias` — explicit alias for the output column. Defaults to `column.as` when omitted.
|
|
1461
|
+
* - `tableAlias` — table alias prepended to the column ref.
|
|
1462
|
+
* @param {string[]} [csnPath=[]] - Accumulated CSN element path (used for `_csnPath` metadata on leaf columns).
|
|
1463
|
+
* @param {{ exclude?: Array, replace?: Array }} [excludeAndReplace] - Columns to exclude or replace during wildcard expansion.
|
|
1464
|
+
* @param {boolean} [isWildcard=false] - Whether this expansion originates from a wildcard; filters out LargeBinary.
|
|
1465
|
+
* @returns {object[]} Flat column(s) for the given element.
|
|
1469
1466
|
*/
|
|
1470
1467
|
function getFlatColumnsFor(column, names, csnPath = [], excludeAndReplace, isWildcard = false) {
|
|
1471
1468
|
if (!column) return column
|
|
@@ -1478,28 +1475,13 @@ function cqn4sql(originalQuery, model) {
|
|
|
1478
1475
|
let firstNonJoinRelevantAssoc, stepAfterAssoc
|
|
1479
1476
|
let element = $refLinks ? $refLinks[$refLinks.length - 1].definition : column
|
|
1480
1477
|
if (isWildcard && element.type === 'cds.LargeBinary') return []
|
|
1481
|
-
if (element.on && !element.keys)
|
|
1482
|
-
|
|
1483
|
-
else if (element.virtual === true) return []
|
|
1484
|
-
else if (!isJoinRelevant && flatName) baseName = flatName
|
|
1485
|
-
else if (isJoinRelevant) {
|
|
1486
|
-
const leafAssocIndex = column.$refLinks.findIndex(link => link.definition.isAssociation && link.onlyForeignKeyAccess)
|
|
1487
|
-
firstNonJoinRelevantAssoc = column.$refLinks[leafAssocIndex] || [...column.$refLinks].reverse().find(link => link.definition.isAssociation)
|
|
1488
|
-
stepAfterAssoc = column.$refLinks.at(leafAssocIndex + 1) || column.$refLinks.at(-1)
|
|
1489
|
-
let elements = firstNonJoinRelevantAssoc.definition.elements || firstNonJoinRelevantAssoc.definition.foreignKeys
|
|
1490
|
-
if (elements && stepAfterAssoc.definition.name in elements) {
|
|
1491
|
-
element = firstNonJoinRelevantAssoc.definition
|
|
1492
|
-
baseName = getFullName(firstNonJoinRelevantAssoc.definition)
|
|
1493
|
-
columnAlias = column.as || column.ref.slice(0, -1).map(idOnly).join('_')
|
|
1494
|
-
} else baseName = getFullName(column.$refLinks[column.$refLinks.length - 1].definition)
|
|
1495
|
-
|
|
1496
|
-
if (column.element && !isAssocOrStruct(column.element)) {
|
|
1497
|
-
columnAlias = column.as || leafAssocIndex === -1 ? columnAlias : column.ref.slice(leafAssocIndex - 1).map(idOnly).join('_')
|
|
1498
|
-
const res = { ref: [tableAlias, calculateElementName(column)], as: columnAlias }
|
|
1499
|
-
setElementOnColumns(res, column.element)
|
|
1500
|
-
return [res]
|
|
1501
|
-
}
|
|
1478
|
+
if (element.on && !element.keys) return [] // unmanaged doesn't make it into columns
|
|
1479
|
+
if (element.virtual === true) return []
|
|
1502
1480
|
|
|
1481
|
+
if (!isJoinRelevant && flatName) baseName = flatName
|
|
1482
|
+
else if (isJoinRelevant) {
|
|
1483
|
+
const earlyResult = resolveJoinRelevantNames()
|
|
1484
|
+
if (earlyResult) return earlyResult
|
|
1503
1485
|
} else if (!baseName && structsAreUnfoldedAlready) {
|
|
1504
1486
|
baseName = element.name // name is already fully constructed
|
|
1505
1487
|
} else {
|
|
@@ -1530,108 +1512,41 @@ function cqn4sql(originalQuery, model) {
|
|
|
1530
1512
|
return getFlatColumnsFor(replacedBy, { baseName, columnAlias: replacedBy.as, tableAlias }, csnPath)
|
|
1531
1513
|
}
|
|
1532
1514
|
|
|
1533
|
-
csnPath
|
|
1515
|
+
csnPath = [...csnPath, element.name]
|
|
1534
1516
|
|
|
1535
|
-
if (element.keys)
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
const flattenThisForeignKey =
|
|
1541
|
-
!$refLinks || // the association is passed as element, not as ref --> flatten full foreign key
|
|
1542
|
-
element === $refLinks.at(-1).definition || // the association is the leaf of the ref --> flatten full foreign key
|
|
1543
|
-
keyElement === stepAfterAssoc.definition // the foreign key is the leaf of the ref --> only flatten this specific foreign key
|
|
1544
|
-
if (flattenThisForeignKey) {
|
|
1545
|
-
const fkElement = getElementForRef(k.ref, getDefinition(element.target))
|
|
1546
|
-
let fkBaseName
|
|
1547
|
-
if (!firstNonJoinRelevantAssoc || firstNonJoinRelevantAssoc.onlyForeignKeyAccess) fkBaseName = `${baseName}_${k.as || k.ref.at(-1)}`
|
|
1548
|
-
// e.g. if foreign key is accessed via infix filter - use join alias to access key in target
|
|
1549
|
-
else fkBaseName = k.ref.at(-1)
|
|
1550
|
-
const fkPath = [...csnPath, k.ref.at(-1)]
|
|
1551
|
-
if (fkElement.elements) {
|
|
1552
|
-
// structured key
|
|
1553
|
-
for (const e of Object.values(fkElement.elements)) {
|
|
1554
|
-
let alias
|
|
1555
|
-
if (columnAlias) {
|
|
1556
|
-
const fkName = k.as
|
|
1557
|
-
? `${k.as}_${e.name}` // foreign key might also be re-named: `assoc { id as foo }`
|
|
1558
|
-
: `${k.ref.join('_')}_${e.name}`
|
|
1559
|
-
alias = `${columnAlias}_${fkName}`
|
|
1560
|
-
}
|
|
1561
|
-
flatColumns.push(
|
|
1562
|
-
...getFlatColumnsFor(
|
|
1563
|
-
e,
|
|
1564
|
-
{ baseName: fkBaseName, columnAlias: alias, tableAlias },
|
|
1565
|
-
[...fkPath],
|
|
1566
|
-
excludeAndReplace,
|
|
1567
|
-
isWildcard,
|
|
1568
|
-
),
|
|
1569
|
-
)
|
|
1570
|
-
}
|
|
1571
|
-
} else if (fkElement.isAssociation) {
|
|
1572
|
-
// assoc as key
|
|
1573
|
-
flatColumns.push(
|
|
1574
|
-
...getFlatColumnsFor(
|
|
1575
|
-
fkElement,
|
|
1576
|
-
{ baseName, columnAlias, tableAlias },
|
|
1577
|
-
csnPath,
|
|
1578
|
-
excludeAndReplace,
|
|
1579
|
-
isWildcard,
|
|
1580
|
-
),
|
|
1581
|
-
)
|
|
1582
|
-
} else {
|
|
1583
|
-
// leaf reached
|
|
1584
|
-
let flatColumn
|
|
1585
|
-
if (columnAlias) {
|
|
1586
|
-
// if the column has an explicit alias AND the original ref
|
|
1587
|
-
// directly resolves to the foreign key, we must not append the fk name to the column alias
|
|
1588
|
-
// e.g. `assoc.fk as FOO` => columns.alias = FOO
|
|
1589
|
-
// `assoc as FOO` => columns.alias = FOO_fk
|
|
1590
|
-
let columnAliasWithFlatFk
|
|
1591
|
-
if (!(column.as && fkElement === column.$refLinks?.at(-1).definition))
|
|
1592
|
-
columnAliasWithFlatFk = `${columnAlias}_${k.as || k.ref.join('_')}`
|
|
1593
|
-
flatColumn = { ref: [fkBaseName], as: columnAliasWithFlatFk || columnAlias }
|
|
1594
|
-
} else flatColumn = { ref: [fkBaseName] }
|
|
1595
|
-
if (tableAlias) flatColumn.ref.unshift(tableAlias)
|
|
1596
|
-
|
|
1597
|
-
// in a flat model, we must assign the foreign key rather than the key in the target
|
|
1598
|
-
const flatForeignKey = getDefinition(element.parent.name)?.elements[fkBaseName]
|
|
1599
|
-
|
|
1600
|
-
setElementOnColumns(flatColumn, flatForeignKey || fkElement)
|
|
1601
|
-
defineProperty(flatColumn, '_csnPath', csnPath)
|
|
1602
|
-
flatColumns.push(flatColumn)
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
return flatColumns
|
|
1607
|
-
} else if (element.elements && element.type !== 'cds.Map') {
|
|
1517
|
+
if (element.keys) return flattenForeignKeys()
|
|
1518
|
+
if (element.elements && element.type !== 'cds.Map') return flattenStructElements()
|
|
1519
|
+
return buildScalarColumn()
|
|
1520
|
+
|
|
1521
|
+
function flattenStructElements() {
|
|
1608
1522
|
const flatRefs = []
|
|
1609
|
-
Object.values(element.elements)
|
|
1523
|
+
for (const e of Object.values(element.elements)) {
|
|
1610
1524
|
const alias = columnAlias ? `${columnAlias}_${e.name}` : null
|
|
1611
1525
|
flatRefs.push(
|
|
1612
1526
|
...getFlatColumnsFor(
|
|
1613
1527
|
e,
|
|
1614
1528
|
{ baseName, columnAlias: alias, tableAlias },
|
|
1615
|
-
|
|
1529
|
+
csnPath,
|
|
1616
1530
|
excludeAndReplace,
|
|
1617
1531
|
isWildcard,
|
|
1618
1532
|
),
|
|
1619
1533
|
)
|
|
1620
|
-
}
|
|
1534
|
+
}
|
|
1621
1535
|
return flatRefs
|
|
1622
1536
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
flatRef
|
|
1626
|
-
if (
|
|
1627
|
-
|
|
1628
|
-
columnAlias = baseName
|
|
1537
|
+
|
|
1538
|
+
function buildScalarColumn() {
|
|
1539
|
+
const flatRef = tableAlias ? { ref: [tableAlias, baseName] } : { ref: [baseName] }
|
|
1540
|
+
if (column.cast) {
|
|
1541
|
+
flatRef.cast = column.cast
|
|
1542
|
+
if (!columnAlias) columnAlias = baseName
|
|
1543
|
+
}
|
|
1544
|
+
if (column.sort) flatRef.sort = column.sort
|
|
1545
|
+
if (columnAlias) flatRef.as = columnAlias
|
|
1546
|
+
setElementOnColumns(flatRef, element)
|
|
1547
|
+
defineProperty(flatRef, '_csnPath', csnPath)
|
|
1548
|
+
return [flatRef]
|
|
1629
1549
|
}
|
|
1630
|
-
if (column.sort) flatRef.sort = column.sort
|
|
1631
|
-
if (columnAlias) flatRef.as = columnAlias
|
|
1632
|
-
setElementOnColumns(flatRef, element)
|
|
1633
|
-
defineProperty(flatRef, '_csnPath', csnPath)
|
|
1634
|
-
return [flatRef]
|
|
1635
1550
|
|
|
1636
1551
|
function getReplacement(from) {
|
|
1637
1552
|
return from?.find(replacement => {
|
|
@@ -1639,6 +1554,101 @@ function cqn4sql(originalQuery, model) {
|
|
|
1639
1554
|
return nameOfExcludedColumn === element.name
|
|
1640
1555
|
})
|
|
1641
1556
|
}
|
|
1557
|
+
|
|
1558
|
+
function resolveJoinRelevantNames() {
|
|
1559
|
+
const leafAssocIndex = column.$refLinks.findIndex(
|
|
1560
|
+
link => link.definition.isAssociation && link.onlyForeignKeyAccess,
|
|
1561
|
+
)
|
|
1562
|
+
firstNonJoinRelevantAssoc =
|
|
1563
|
+
column.$refLinks[leafAssocIndex] || [...column.$refLinks].reverse().find(link => link.definition.isAssociation)
|
|
1564
|
+
stepAfterAssoc = column.$refLinks.at(leafAssocIndex + 1) || column.$refLinks.at(-1)
|
|
1565
|
+
const targetElements = firstNonJoinRelevantAssoc.definition.elements || firstNonJoinRelevantAssoc.definition.foreignKeys
|
|
1566
|
+
if (targetElements && stepAfterAssoc.definition.name in targetElements) {
|
|
1567
|
+
element = firstNonJoinRelevantAssoc.definition
|
|
1568
|
+
baseName = getFullName(firstNonJoinRelevantAssoc.definition)
|
|
1569
|
+
columnAlias = column.as || column.ref.slice(0, -1).map(idOnly).join('_')
|
|
1570
|
+
} else {
|
|
1571
|
+
baseName = getFullName(column.$refLinks.at(-1).definition)
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
if (column.element && !isAssocOrStruct(column.element)) {
|
|
1575
|
+
columnAlias =
|
|
1576
|
+
column.as || (leafAssocIndex === -1 ? columnAlias : column.ref.slice(leafAssocIndex - 1).map(idOnly).join('_'))
|
|
1577
|
+
const res = { ref: [tableAlias, calculateElementName(column)], as: columnAlias }
|
|
1578
|
+
setElementOnColumns(res, column.element)
|
|
1579
|
+
return [res]
|
|
1580
|
+
}
|
|
1581
|
+
return null
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
function flattenForeignKeys() {
|
|
1585
|
+
const flatColumns = []
|
|
1586
|
+
for (const k of element.keys) {
|
|
1587
|
+
const fkElement = getElementForRef(k.ref, getDefinition(element.target))
|
|
1588
|
+
// if only one part of a foreign key is requested, only flatten the partial key
|
|
1589
|
+
const shouldFlatten =
|
|
1590
|
+
!$refLinks || // the association is passed as element, not as ref --> flatten full foreign key
|
|
1591
|
+
element === $refLinks.at(-1).definition || // the association is the leaf of the ref --> flatten full foreign key
|
|
1592
|
+
fkElement === stepAfterAssoc.definition // the foreign key is the leaf of the ref --> only flatten this specific foreign key
|
|
1593
|
+
if (!shouldFlatten) continue
|
|
1594
|
+
|
|
1595
|
+
// e.g. if foreign key is accessed via infix filter - use join alias to access key in target
|
|
1596
|
+
const fkBaseName = !firstNonJoinRelevantAssoc || firstNonJoinRelevantAssoc.onlyForeignKeyAccess
|
|
1597
|
+
? `${baseName}_${k.as || k.ref.at(-1)}`
|
|
1598
|
+
: k.ref.at(-1)
|
|
1599
|
+
const fkPath = [...csnPath, k.ref.at(-1)]
|
|
1600
|
+
|
|
1601
|
+
if (fkElement.elements) {
|
|
1602
|
+
// structured key
|
|
1603
|
+
for (const e of Object.values(fkElement.elements)) {
|
|
1604
|
+
let alias
|
|
1605
|
+
if (columnAlias) {
|
|
1606
|
+
const fkName = k.as
|
|
1607
|
+
? `${k.as}_${e.name}` // foreign key might also be re-named: `assoc { id as foo }`
|
|
1608
|
+
: `${k.ref.join('_')}_${e.name}`
|
|
1609
|
+
alias = `${columnAlias}_${fkName}`
|
|
1610
|
+
}
|
|
1611
|
+
flatColumns.push(
|
|
1612
|
+
...getFlatColumnsFor(
|
|
1613
|
+
e,
|
|
1614
|
+
{ baseName: fkBaseName, columnAlias: alias, tableAlias },
|
|
1615
|
+
fkPath,
|
|
1616
|
+
excludeAndReplace,
|
|
1617
|
+
isWildcard,
|
|
1618
|
+
),
|
|
1619
|
+
)
|
|
1620
|
+
}
|
|
1621
|
+
} else if (fkElement.isAssociation) {
|
|
1622
|
+
// assoc as key
|
|
1623
|
+
flatColumns.push(
|
|
1624
|
+
...getFlatColumnsFor(fkElement, { baseName, columnAlias, tableAlias }, csnPath, excludeAndReplace, isWildcard),
|
|
1625
|
+
)
|
|
1626
|
+
} else {
|
|
1627
|
+
// leaf reached
|
|
1628
|
+
let flatColumn
|
|
1629
|
+
if (columnAlias) {
|
|
1630
|
+
// if the column has an explicit alias AND the original ref
|
|
1631
|
+
// directly resolves to the foreign key, we must not append the fk name to the column alias
|
|
1632
|
+
// e.g. `assoc.fk as FOO` => columns.alias = FOO
|
|
1633
|
+
// `assoc as FOO` => columns.alias = FOO_fk
|
|
1634
|
+
let fkAlias = columnAlias
|
|
1635
|
+
if (!(column.as && fkElement === column.$refLinks?.at(-1).definition))
|
|
1636
|
+
fkAlias = `${columnAlias}_${k.as || k.ref.join('_')}`
|
|
1637
|
+
flatColumn = { ref: [fkBaseName], as: fkAlias }
|
|
1638
|
+
} else {
|
|
1639
|
+
flatColumn = { ref: [fkBaseName] }
|
|
1640
|
+
}
|
|
1641
|
+
if (tableAlias) flatColumn.ref.unshift(tableAlias)
|
|
1642
|
+
|
|
1643
|
+
// in a flat model, we must assign the foreign key rather than the key in the target
|
|
1644
|
+
const flatForeignKey = getDefinition(element.parent.name)?.elements[fkBaseName]
|
|
1645
|
+
setElementOnColumns(flatColumn, flatForeignKey || fkElement)
|
|
1646
|
+
defineProperty(flatColumn, '_csnPath', csnPath)
|
|
1647
|
+
flatColumns.push(flatColumn)
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
return flatColumns
|
|
1651
|
+
}
|
|
1642
1652
|
}
|
|
1643
1653
|
|
|
1644
1654
|
/**
|
|
@@ -1726,6 +1736,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
1726
1736
|
transformedTokenStream[i + 1] = whereExists
|
|
1727
1737
|
// skip newly created subquery from being iterated
|
|
1728
1738
|
i += 1
|
|
1739
|
+
} else if (token !== null && typeof token === 'object' && '#' in token) {
|
|
1740
|
+
// Enum token: resolve to its value
|
|
1741
|
+
transformedTokenStream.push(resolveEnumToken(token, tokenStream, i))
|
|
1729
1742
|
} else if (token.list) {
|
|
1730
1743
|
if (token.list.length === 0) {
|
|
1731
1744
|
// replace `[not] in <empty list>` to harmonize behavior across dbs
|
|
@@ -1743,8 +1756,13 @@ function cqn4sql(originalQuery, model) {
|
|
|
1743
1756
|
transformedTokenStream.push({ list: [] })
|
|
1744
1757
|
}
|
|
1745
1758
|
} else {
|
|
1746
|
-
|
|
1747
|
-
|
|
1759
|
+
let { list } = token
|
|
1760
|
+
// Resolve enum tokens in list items using context from the parent token stream
|
|
1761
|
+
if (list.some(e => e !== null && typeof e === 'object' && '#' in e)) {
|
|
1762
|
+
const enumDef = findEnumDefinition(tokenStream, i)
|
|
1763
|
+
list = list.map(item => (item !== null && typeof item === 'object' && '#' in item) ? resolveEnumToken(item, tokenStream, i, enumDef) : item)
|
|
1764
|
+
}
|
|
1765
|
+
if (list.every(e => 'val' in e))
|
|
1748
1766
|
// no need for transformation
|
|
1749
1767
|
transformedTokenStream.push({ list })
|
|
1750
1768
|
else transformedTokenStream.push({ list: getTransformedTokenStream(list, { $baseLink, prop: 'list' }) })
|
|
@@ -1776,7 +1794,6 @@ function cqn4sql(originalQuery, model) {
|
|
|
1776
1794
|
ops.push(rhs)
|
|
1777
1795
|
rhs = tokenStream[i + 3]
|
|
1778
1796
|
indexRhs += 1
|
|
1779
|
-
rhsDef = rhs?.$refLinks?.at(-1)?.definition
|
|
1780
1797
|
}
|
|
1781
1798
|
|
|
1782
1799
|
if (notSupportedOps.some(([firstOp]) => firstOp === next))
|
|
@@ -1846,6 +1863,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1846
1863
|
}
|
|
1847
1864
|
}
|
|
1848
1865
|
|
|
1866
|
+
if (result.cast) result.cast = resolveEnumCastType(result.cast)
|
|
1849
1867
|
transformedTokenStream.push(result)
|
|
1850
1868
|
}
|
|
1851
1869
|
}
|
|
@@ -2634,6 +2652,116 @@ function cqn4sql(originalQuery, model) {
|
|
|
2634
2652
|
}
|
|
2635
2653
|
return result
|
|
2636
2654
|
}
|
|
2655
|
+
|
|
2656
|
+
/**
|
|
2657
|
+
* Resolves an enum token to a value literal.
|
|
2658
|
+
*
|
|
2659
|
+
* If the token already has a `val`, it is used directly.
|
|
2660
|
+
* Otherwise, the enum value is resolved by looking up the symbol
|
|
2661
|
+
* in the enum definition found from the surrounding context.
|
|
2662
|
+
*
|
|
2663
|
+
* @param {object} token - The enum token with a `#` property.
|
|
2664
|
+
* @param {object[]} tokenStream - The surrounding token stream for context discovery.
|
|
2665
|
+
* @param {number} index - The index of the enum token in the token stream.
|
|
2666
|
+
* @param {object} [enumDef] - An already-discovered enum definition (optimization for lists).
|
|
2667
|
+
* @returns {object} A value token `{ val: resolvedValue }`.
|
|
2668
|
+
*/
|
|
2669
|
+
function resolveEnumToken(token, tokenStream, index, enumDef) {
|
|
2670
|
+
if ('val' in token) {
|
|
2671
|
+
const result = { val: token.val }
|
|
2672
|
+
if (token.cast) result.cast = resolveEnumCastType(token.cast)
|
|
2673
|
+
return result
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
// Check if the token itself has a cast with an enum type
|
|
2677
|
+
if (!enumDef && token.cast?.type) {
|
|
2678
|
+
const typeDef = model.definitions[token.cast.type]
|
|
2679
|
+
if (typeDef?.enum) enumDef = typeDef.enum
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
if (!enumDef) enumDef = findEnumDefinition(tokenStream, index)
|
|
2683
|
+
if (!enumDef) {
|
|
2684
|
+
throw new Error(`Can't resolve enum value "#${token['#']}"`)
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
const entry = enumDef[token['#']]
|
|
2688
|
+
if (!entry) {
|
|
2689
|
+
throw new Error(`Unknown enum symbol "#${token['#']}"`)
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
const result = { val: 'val' in entry ? entry.val : token['#'] }
|
|
2693
|
+
if (token.cast) result.cast = resolveEnumCastType(token.cast)
|
|
2694
|
+
return result
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
/**
|
|
2698
|
+
* If `cast.type` refers to a user-defined enum type, resolves it to the
|
|
2699
|
+
* underlying scalar CDS built-in type so that the SQL builder (`cqn2sql`)
|
|
2700
|
+
* can render a valid SQL type name.
|
|
2701
|
+
*
|
|
2702
|
+
* Example: `{ type: 'enums.Priority' }` → `{ type: 'cds.Integer' }`
|
|
2703
|
+
*
|
|
2704
|
+
* Non-enum types (including CDS built-ins) are returned unchanged.
|
|
2705
|
+
*
|
|
2706
|
+
* @param {object} cast - The cast descriptor with a `type` property.
|
|
2707
|
+
* @returns {object} The cast descriptor with the resolved type.
|
|
2708
|
+
*/
|
|
2709
|
+
function resolveEnumCastType(cast) {
|
|
2710
|
+
if (!cast?.type) return cast
|
|
2711
|
+
let def = model.definitions[cast.type]
|
|
2712
|
+
while (def?.enum) {
|
|
2713
|
+
const baseType = def.type
|
|
2714
|
+
if (!baseType) return cast // no base type declared – leave as-is
|
|
2715
|
+
if (cds.builtin.types[baseType]) return { ...cast, type: baseType }
|
|
2716
|
+
def = model.definitions[baseType]
|
|
2717
|
+
}
|
|
2718
|
+
return cast
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
/**
|
|
2722
|
+
* Scans the token stream around the given index to find an element
|
|
2723
|
+
* definition that has an `enum` property, which can be used to resolve
|
|
2724
|
+
* enum symbols to their values.
|
|
2725
|
+
*
|
|
2726
|
+
* @param {object[]} tokenStream - The token stream to scan.
|
|
2727
|
+
* @param {number} index - The index of the enum token.
|
|
2728
|
+
* @returns {object|null} The enum definition object, or null if not found.
|
|
2729
|
+
*/
|
|
2730
|
+
function findEnumDefinition(tokenStream, index) {
|
|
2731
|
+
// Scan backward
|
|
2732
|
+
for (let j = index - 1; j >= 0; j--) {
|
|
2733
|
+
const t = tokenStream[j]
|
|
2734
|
+
if (typeof t === 'string') continue // operators, keywords
|
|
2735
|
+
if (t !== null && typeof t === 'object' && '#' in t) continue // other enum tokens
|
|
2736
|
+
if ('val' in t && !t.ref) continue // plain value literals
|
|
2737
|
+
|
|
2738
|
+
const def = t.$refLinks?.at(-1)?.definition
|
|
2739
|
+
if (def?.enum) return def.enum
|
|
2740
|
+
if (t.cast?.type) {
|
|
2741
|
+
const typeDef = model.definitions[t.cast.type]
|
|
2742
|
+
if (typeDef?.enum) return typeDef.enum
|
|
2743
|
+
}
|
|
2744
|
+
if (def) break // found a ref without enum type, stop
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
// Scan forward
|
|
2748
|
+
for (let j = index + 1; j < tokenStream.length; j++) {
|
|
2749
|
+
const t = tokenStream[j]
|
|
2750
|
+
if (typeof t === 'string') continue
|
|
2751
|
+
if (t !== null && typeof t === 'object' && '#' in t) continue
|
|
2752
|
+
if ('val' in t && !t.ref) continue
|
|
2753
|
+
|
|
2754
|
+
const def = t.$refLinks?.at(-1)?.definition
|
|
2755
|
+
if (def?.enum) return def.enum
|
|
2756
|
+
if (t.cast?.type) {
|
|
2757
|
+
const typeDef = model.definitions[t.cast.type]
|
|
2758
|
+
if (typeDef?.enum) return typeDef.enum
|
|
2759
|
+
}
|
|
2760
|
+
if (def) break
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
return null
|
|
2764
|
+
}
|
|
2637
2765
|
}
|
|
2638
2766
|
|
|
2639
2767
|
function calculateElementName(token) {
|
package/lib/infer/index.js
CHANGED
|
@@ -11,7 +11,7 @@ const cdsTypes = cds.builtin.types
|
|
|
11
11
|
* @param {import('@sap/cds/apis/csn').CSN} [model]
|
|
12
12
|
* @returns {import('./cqn').Query} = q with .target and .elements
|
|
13
13
|
*/
|
|
14
|
-
function infer(originalQuery, model) {
|
|
14
|
+
function infer(originalQuery, model, useTechnicalAlias = true) {
|
|
15
15
|
if (!model) throw new Error('Please specify a model')
|
|
16
16
|
const inferred = originalQuery
|
|
17
17
|
|
|
@@ -34,7 +34,7 @@ function infer(originalQuery, model) {
|
|
|
34
34
|
|
|
35
35
|
let $combinedElements
|
|
36
36
|
|
|
37
|
-
const sources = inferTarget(_.into || _.from || _.entity, {}) // IMPORTANT: _.into has to go before _.from for INSERT.into().from(SELECT)
|
|
37
|
+
const sources = inferTarget(_.into || _.from || _.entity, {}, useTechnicalAlias) // IMPORTANT: _.into has to go before _.from for INSERT.into().from(SELECT)
|
|
38
38
|
const aliases = Object.keys(sources)
|
|
39
39
|
const target = aliases.length === 1 ? getDefinitionFromSources(sources, aliases[0]) : originalQuery
|
|
40
40
|
Object.defineProperties(inferred, {
|
|
@@ -80,7 +80,7 @@ function infer(originalQuery, model) {
|
|
|
80
80
|
* Each key is a query source alias, and its value is the corresponding CSN Definition.
|
|
81
81
|
* @returns {object} The updated `querySources` object with inferred sources from the `from` clause.
|
|
82
82
|
*/
|
|
83
|
-
function inferTarget(from, querySources, useTechnicalAlias
|
|
83
|
+
function inferTarget(from, querySources, useTechnicalAlias) {
|
|
84
84
|
const { ref } = from
|
|
85
85
|
// Given a from clause `Root:parent[$main.name = name].parent as Foo`
|
|
86
86
|
// we need to first resolve until to the last step of the from.ref
|
package/package.json
CHANGED