@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 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
 
@@ -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 each of rows) yield { [k1]: this.insertedRowId4(each) } // REVISIT: sqlite only returns a single lastID per row -> how is that with others?
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.lastID
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
- DEBUG = cds.debug('sql|db')
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
- this._changeToStreams(cqn.SELECT.columns, rows, query.SELECT.one)
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) => (total += affectedRows.changes), 0)
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?.(event) // in the other cases above DEBUG happens in cqn2sql
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?.(query, data)
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((having?.length || groupBy?.length)) {
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([{ func: 'count' }]).from(
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 (query, callback) {
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
- let q = this.cqn4sql(query)
408
- let cqn2sql = new this.class.CQN2SQL(this)
409
- return cqn2sql.render(q, values)
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
- get model() {
518
- return cds.model
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
- cds.extend(cds.ql.Query).with(
522
- class {
523
- forSQL() {
524
- let cqn = (cds.db || sqls).cqn4sql(this)
525
- return this.flat(cqn)
526
- }
527
- toSQL() {
528
- if (this.SELECT) this.SELECT.expand = 'root' // Enforces using json functions always for top-level SELECTS
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
- toSql() {
533
- return this.toSQL().sql
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
@@ -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 = 'H' + (uniqueCounter++)
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.debug('sql|sqlite')
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 && (LOG_SQL._debug || LOG_SQLITE._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: [{ func: 'count' }],
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 `xpr`, destructure and throw away the ref with the $self
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 { ...token, args: token.args.map(arg =>
840
- arg.ref ? augmentInlineXprRefs([arg], parentCol)[0] : arg
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
- * For each element, the function checks if it's a structure, an association or a scalar,
1443
- * and proceeds accordingly. If the element is a structure, it recursively fetches flat columns for all sub-elements.
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
- * Columns excluded in a wildcard expansion or replaced by other columns are also handled accordingly.
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
- * columnAlias: string
1452
- * tableAlias: string
1453
- * baseName: string
1454
- * }} names - configuration object for naming parameters:
1455
- * columnAlias - The explicit alias which the user has defined for the column.
1456
- * For instance `{ struct.foo as bar}` will be transformed into
1457
- * `{ struct_foo_leaf1 as bar_foo_leaf1, struct_foo_leaf2 as bar_foo_leaf2 }`.
1458
- * tableAlias - The table alias to prepend to the column name. Optional.
1459
- * baseName - The prefixes of the column reference (joined with '_'). Optional.
1460
- * @param {string} columnAlias - The explicit alias which the user has defined for the column.
1461
- * For instance `{ struct.foo as bar}` will be transformed into
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
- return [] // unmanaged doesn't make it into columns
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.push(element.name)
1515
+ csnPath = [...csnPath, element.name]
1534
1516
 
1535
- if (element.keys) {
1536
- const flatColumns = []
1537
- for (const k of element.keys) {
1538
- // if only one part of a foreign key is requested, only flatten the partial key
1539
- const keyElement = getElementForRef(k.ref, getDefinition(element.target))
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).forEach(e => {
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
- [...csnPath],
1529
+ csnPath,
1616
1530
  excludeAndReplace,
1617
1531
  isWildcard,
1618
1532
  ),
1619
1533
  )
1620
- })
1534
+ }
1621
1535
  return flatRefs
1622
1536
  }
1623
- const flatRef = tableAlias ? { ref: [tableAlias, baseName] } : { ref: [baseName] }
1624
- if (column.cast) {
1625
- flatRef.cast = column.cast
1626
- if (!columnAlias)
1627
- // provide an explicit alias
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
- const { list } = token
1747
- if (list.every(e => e.val))
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) {
@@ -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 = true) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "description": "CDS base database service",
5
5
  "homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service",
6
6
  "repository": {