@cap-js/db-service 1.16.0 → 1.16.2

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,24 @@
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
+ ## [1.16.2](https://github.com/cap-js/cds-dbs/compare/db-service-v1.16.1...db-service-v1.16.2) (2024-12-18)
8
+
9
+
10
+ ### Fixed
11
+
12
+ * do not override .toJSON of buffers ([#949](https://github.com/cap-js/cds-dbs/issues/949)) ([ed52f72](https://github.com/cap-js/cds-dbs/commit/ed52f72206df6e683106ab0bbbecf4b778cf36b5))
13
+
14
+ ## [1.16.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.16.0...db-service-v1.16.1) (2024-12-16)
15
+
16
+
17
+ ### Fixed
18
+
19
+ * handle undefined DEBUG ([#942](https://github.com/cap-js/cds-dbs/issues/942)) ([dd2da3a](https://github.com/cap-js/cds-dbs/commit/dd2da3a8d8feb5beaae5860d493e9e1158dbf99f)), closes [#941](https://github.com/cap-js/cds-dbs/issues/941)
20
+ * only expand partial foreign key if explicitly requested ([#916](https://github.com/cap-js/cds-dbs/issues/916)) ([96911ad](https://github.com/cap-js/cds-dbs/commit/96911ada1831e71febb84d8a382b57d55d24c1bc))
21
+ * quoted mode ([#937](https://github.com/cap-js/cds-dbs/issues/937)) ([9e62b22](https://github.com/cap-js/cds-dbs/commit/9e62b22a1be90ada9f57cfa63505735d8b8eed88))
22
+ * sort property is case insensitive ([#924](https://github.com/cap-js/cds-dbs/issues/924)) ([2c72c87](https://github.com/cap-js/cds-dbs/commit/2c72c871d6c7f65797b8bd8692305149b3ea65f8))
23
+ * wildcard expand with aggregation ([#923](https://github.com/cap-js/cds-dbs/issues/923)) ([bbe7be0](https://github.com/cap-js/cds-dbs/commit/bbe7be00498ad083cf951daf344b7f5fd9f68ab9))
24
+
7
25
  ## [1.16.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.15.1...db-service-v1.16.0) (2024-11-25)
8
26
 
9
27
 
@@ -47,13 +47,14 @@ class DatabaseService extends cds.Service {
47
47
  * transaction with `BEGIN`
48
48
  * @returns this
49
49
  */
50
- async begin() {
50
+ async begin (min) {
51
51
  // We expect tx.begin() being called for an txed db service
52
52
  const ctx = this.context
53
53
 
54
54
  // If .begin is called explicitly it starts a new transaction and executes begin
55
- if (!ctx) return this.tx().begin()
55
+ if (!ctx) return this.tx().begin(min)
56
56
 
57
+ // REVISIT: can we revisit the below revisit now?
57
58
  // REVISIT: tenant should be undefined if !this.isMultitenant
58
59
  let isMultitenant = 'multiTenant' in this.options ? this.options.multiTenant : cds.env.requires.multitenancy
59
60
  let tenant = isMultitenant && ctx.tenant
@@ -63,10 +64,10 @@ class DatabaseService extends cds.Service {
63
64
 
64
65
  // Acquire a pooled connection
65
66
  this.dbc = await this.acquire()
66
- this.dbc.destroy = this.destroy.bind(this)
67
+ this.dbc.destroy = this.destroy.bind(this) // REVISIT: this is bad
67
68
 
68
69
  // Begin a session...
69
- try {
70
+ if (!min) try {
70
71
  await this.set(new SessionContext(ctx))
71
72
  await this.send('BEGIN')
72
73
  } catch (e) {
@@ -153,8 +154,8 @@ class DatabaseService extends cds.Service {
153
154
  */
154
155
  run(query, data, ...etc) {
155
156
  // Allow db.run('...',1,2,3,4)
156
- if (data !== undefined && typeof query === 'string' && typeof data !== 'object') data = [data, ...etc]
157
- return super.run(query, data)
157
+ if (data !== undefined && typeof query === 'string' && typeof data !== 'object') arguments[1] = [data, ...etc]
158
+ return super.run(...arguments) //> important to call like that for tagged template literal args
158
159
  }
159
160
 
160
161
  /**
@@ -1,14 +1,12 @@
1
1
  const { createPool } = require('generic-pool')
2
2
 
3
- class ConnectionPool {
4
- constructor(factory, tenant) {
5
- let bound_factory = { __proto__: factory, create: factory.create.bind(null, tenant) }
6
- return _track_connections4(createPool(bound_factory, factory.options))
7
- }
3
+ function ConnectionPool (factory, tenant) {
4
+ let bound_factory = { __proto__: factory, create: factory.create.bind(null, tenant) }
5
+ return createPool(bound_factory, factory.options)
8
6
  }
9
7
 
10
- // REVISIT: Is that really neccessary ?!
11
- function _track_connections4(pool) {
8
+ function TrackedConnectionPool (factory, tenant) {
9
+ const pool = new ConnectionPool (factory, tenant)
12
10
  const { acquire, release } = pool
13
11
  return Object.assign(pool, {
14
12
  async acquire() {
@@ -23,7 +21,6 @@ function _track_connections4(pool) {
23
21
  throw err
24
22
  }
25
23
  },
26
-
27
24
  release(dbc) {
28
25
  this._trackedConnections?.delete(dbc._beginStack)
29
26
  return release.call(this, dbc)
@@ -31,4 +28,5 @@ function _track_connections4(pool) {
31
28
  })
32
29
  }
33
30
 
34
- module.exports = ConnectionPool
31
+ const DEBUG = /\bpool\b/.test(process.env.DEBUG)
32
+ module.exports = DEBUG ? TrackedConnectionPool : ConnectionPool
package/lib/cqn2sql.js CHANGED
@@ -8,7 +8,7 @@ const { Readable } = require('stream')
8
8
 
9
9
  const DEBUG = cds.debug('sql|sqlite')
10
10
  const LOG_SQL = cds.log('sql')
11
- const LOG_SQLITE = cds.log('sqlite')
11
+ const LOG_SQLITE = cds.log('sqlite')
12
12
 
13
13
  class CQN2SQLRenderer {
14
14
  /**
@@ -21,10 +21,12 @@ class CQN2SQLRenderer {
21
21
  this.class = new.target // for IntelliSense
22
22
  this.class._init() // is a noop for subsequent calls
23
23
  this.model = srv?.model
24
-
25
24
  // Overwrite smart quoting
26
25
  if (cds.env.sql.names === 'quoted') {
27
- this.class.prototype.name = (name) => name.id || name
26
+ this.class.prototype.name = (name, query) => {
27
+ const e = name.id || name
28
+ return (query?.target || this.model?.definitions[e])?.['@cds.persistence.name'] || e
29
+ }
28
30
  this.class.prototype.quote = (s) => `"${String(s).replace(/"/g, '""')}"`
29
31
  }
30
32
  }
@@ -84,8 +86,8 @@ class CQN2SQLRenderer {
84
86
  if (vars && Object.keys(vars).length && !this.values?.length) this.values = vars
85
87
  const sanitize_values = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
86
88
 
87
-
88
- if (LOG_SQL._debug || LOG_SQLITE._debug) {
89
+
90
+ if (DEBUG && (LOG_SQL._debug || LOG_SQLITE._debug)) {
89
91
  let values = sanitize_values && (this.entries || this.values?.length > 0) ? ['***'] : this.entries || this.values || []
90
92
  if (values && !Array.isArray(values)) {
91
93
  values = [values]
@@ -93,7 +95,7 @@ class CQN2SQLRenderer {
93
95
  DEBUG(this.sql, ...values)
94
96
  }
95
97
 
96
-
98
+
97
99
  return this
98
100
  }
99
101
 
@@ -124,7 +126,7 @@ class CQN2SQLRenderer {
124
126
  target = typeof entity === 'string' ? { name: entity } : q.CREATE.entity
125
127
  }
126
128
 
127
- const name = this.name(target.name)
129
+ const name = this.name(target.name, q)
128
130
  // Don't allow place holders inside views
129
131
  delete this.values
130
132
  this.sql =
@@ -213,7 +215,7 @@ class CQN2SQLRenderer {
213
215
  const { target } = q
214
216
  const isView = target?.query || target?.projection || q.DROP.view
215
217
  const name = target?.name || q.DROP.table?.ref?.[0] || q.DROP.view?.ref?.[0]
216
- return (this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} IF EXISTS ${this.quote(this.name(name))}`)
218
+ return (this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} IF EXISTS ${this.quote(this.name(name, q))}`)
217
219
  }
218
220
 
219
221
  // SELECT Statements ------------------------------------------------
@@ -352,15 +354,10 @@ class CQN2SQLRenderer {
352
354
  const _aliased = as ? s => s + ` as ${this.quote(as)}` : s => s
353
355
  if (ref) {
354
356
  let z = ref[0]
355
- if (cds.env.sql.names === 'quoted') {
356
- // use SELECT.from to infer query, cds.infer also expects a query
357
- const { target } = q || SELECT.from(from)
358
- z = target?.['@cds.persistence.name'] || ref[0]
359
- }
360
357
  if (z.args) {
361
- return _aliased(`${this.quote(this.name(z))}${this.from_args(z.args)}`)
358
+ return _aliased(`${this.quote(this.name(z, q))}${this.from_args(z.args)}`)
362
359
  }
363
- return _aliased(this.quote(this.name(z)))
360
+ return _aliased(this.quote(this.name(z, q)))
364
361
  }
365
362
  if (from.SELECT) return _aliased(`(${this.SELECT(from)})`)
366
363
  if (from.join) return `${this.from(from.args[0])} ${from.join} JOIN ${this.from(from.args[1])}${from.on ? ` ON ${this.where(from.on)}` : ''}`
@@ -423,8 +420,8 @@ class CQN2SQLRenderer {
423
420
  ? c =>
424
421
  this.expr(c) +
425
422
  (c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
426
- (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
427
- : c => this.expr(c) + (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC'),
423
+ (c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
424
+ : c => this.expr(c) + (c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC'),
428
425
  )
429
426
  }
430
427
 
@@ -504,7 +501,7 @@ class CQN2SQLRenderer {
504
501
  this.columns = columns
505
502
 
506
503
  const alias = INSERT.into.as
507
- const entity = this.name(q.target?.name || INSERT.into.ref[0])
504
+ const entity = this.name(q.target?.name || INSERT.into.ref[0], q)
508
505
  if (!elements) {
509
506
  this.entries = INSERT.entries.map(e => columns.map(c => e[c]))
510
507
  const param = this.param.bind(this, { ref: ['?'] })
@@ -531,9 +528,6 @@ class CQN2SQLRenderer {
531
528
 
532
529
  async *INSERT_entries_stream(entries, binaryEncoding = 'base64') {
533
530
  const elements = this.cqn.target?.elements || {}
534
- const transformBase64 = binaryEncoding === 'base64'
535
- ? a => a
536
- : a => a != null ? Buffer.from(a, 'base64').toString(binaryEncoding) : a
537
531
  const bufferLimit = 65536 // 1 << 16
538
532
  let buffer = '['
539
533
 
@@ -564,8 +558,8 @@ class CQN2SQLRenderer {
564
558
 
565
559
  buffer += '"'
566
560
  } else {
567
- if (elements[key]?.type in this.BINARY_TYPES) {
568
- val = transformBase64(val)
561
+ if (val != null && elements[key]?.type in this.BINARY_TYPES) {
562
+ val = Buffer.from(val, 'base64').toString(binaryEncoding)
569
563
  }
570
564
  buffer += `${keyJSON}${JSON.stringify(val)}`
571
565
  }
@@ -583,9 +577,6 @@ class CQN2SQLRenderer {
583
577
 
584
578
  async *INSERT_rows_stream(entries, binaryEncoding = 'base64') {
585
579
  const elements = this.cqn.target?.elements || {}
586
- const transformBase64 = binaryEncoding === 'base64'
587
- ? a => a
588
- : a => a != null ? Buffer.from(a, 'base64').toString(binaryEncoding) : a
589
580
  const bufferLimit = 65536 // 1 << 16
590
581
  let buffer = '['
591
582
 
@@ -612,8 +603,8 @@ class CQN2SQLRenderer {
612
603
 
613
604
  buffer += '"'
614
605
  } else {
615
- if (elements[this.columns[key]]?.type in this.BINARY_TYPES) {
616
- val = transformBase64(val)
606
+ if (val != null && elements[this.columns[key]]?.type in this.BINARY_TYPES) {
607
+ val = Buffer.from(val, 'base64').toString(binaryEncoding)
617
608
  }
618
609
  buffer += `${sepsub}${val === undefined ? 'null' : JSON.stringify(val)}`
619
610
  }
@@ -638,7 +629,7 @@ class CQN2SQLRenderer {
638
629
  */
639
630
  INSERT_rows(q) {
640
631
  const { INSERT } = q
641
- const entity = this.name(q.target?.name || INSERT.into.ref[0])
632
+ const entity = this.name(q.target?.name || INSERT.into.ref[0], q)
642
633
  const alias = INSERT.into.as
643
634
  const elements = q.elements || q.target?.elements
644
635
  const columns = this.columns = INSERT.columns || cds.error`Cannot insert rows without columns or elements`
@@ -684,7 +675,7 @@ class CQN2SQLRenderer {
684
675
  */
685
676
  INSERT_select(q) {
686
677
  const { INSERT } = q
687
- const entity = this.name(q.target.name)
678
+ const entity = this.name(q.target.name, q)
688
679
  const alias = INSERT.into.as
689
680
  const elements = q.elements || q.target?.elements || {}
690
681
  const columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
@@ -752,7 +743,7 @@ class CQN2SQLRenderer {
752
743
  .filter(c => keys.includes(c.name))
753
744
  .map(c => `${c.onInsert || c.sql} as ${this.quote(c.name)}`)
754
745
 
755
- const entity = this.name(q.target?.name || UPSERT.into.ref[0])
746
+ const entity = this.name(q.target?.name || UPSERT.into.ref[0], q)
756
747
  sql = `SELECT ${managed.map(c => c.upsert)} FROM (SELECT value, ${extractkeys} from json_each(?)) as NEW LEFT JOIN ${this.quote(entity)} AS OLD ON ${keyCompare}`
757
748
 
758
749
  const updateColumns = columns.filter(c => {
@@ -779,7 +770,7 @@ class CQN2SQLRenderer {
779
770
  UPDATE(q) {
780
771
  const { entity, with: _with, data, where } = q.UPDATE
781
772
  const elements = q.target?.elements
782
- let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity))}`
773
+ let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity, q))}`
783
774
  if (entity.as) sql += ` AS ${this.quote(entity.as)}`
784
775
 
785
776
  let columns = []
@@ -811,8 +802,9 @@ class CQN2SQLRenderer {
811
802
  * @param {import('./infer/cqn').DELETE} param0
812
803
  * @returns {string} SQL
813
804
  */
814
- DELETE({ DELETE: { from, where } }) {
815
- let sql = `DELETE FROM ${this.from(from)}`
805
+ DELETE(q) {
806
+ const { DELETE: { from, where } } = q
807
+ let sql = `DELETE FROM ${this.from(from, q)}`
816
808
  if (where) sql += ` WHERE ${this.where(where)}`
817
809
  return (this.sql = sql)
818
810
  }
@@ -1028,6 +1020,7 @@ class CQN2SQLRenderer {
1028
1020
  /**
1029
1021
  * Calculates the Database name of the given name
1030
1022
  * @param {string|import('./infer/cqn').ref} name
1023
+ * @param {import('./infer/cqn').Query} query
1031
1024
  * @returns {string} Database name
1032
1025
  */
1033
1026
  name(name) {
@@ -1150,11 +1143,6 @@ class CQN2SQLRenderer {
1150
1143
  }
1151
1144
  }
1152
1145
 
1153
- // REVISIT: Workaround for JSON.stringify to work with buffers
1154
- Buffer.prototype.toJSON = function () {
1155
- return this.toString('base64')
1156
- }
1157
-
1158
1146
  Readable.prototype[require('node:util').inspect.custom] = Readable.prototype.toJSON = function () { return this._raw || `[object ${this.constructor.name}]` }
1159
1147
 
1160
1148
  const ObjectKeys = o => (o && [...ObjectKeys(o.__proto__), ...Object.keys(o)]) || []
package/lib/cqn4sql.js CHANGED
@@ -51,7 +51,7 @@ function cqn4sql(originalQuery, model) {
51
51
 
52
52
  if (!hasCustomJoins && inferred.SELECT?.search) {
53
53
  // we need an instance of query because the elements of the query are needed for the calculation of the search columns
54
- if (!inferred.SELECT.elements) Object.setPrototypeOf(inferred, Object.getPrototypeOf(SELECT()))
54
+ if (!inferred.SELECT.elements) Object.setPrototypeOf(inferred, SELECT.class.prototype)
55
55
  const searchTerm = getSearchTerm(inferred.SELECT.search, inferred)
56
56
  if (searchTerm) {
57
57
  // Search target can be a navigation, in that case use _target to get the correct entity
@@ -408,6 +408,7 @@ function cqn4sql(originalQuery, model) {
408
408
  const last = $refLinks?.[$refLinks.length - 1]
409
409
  if (last && !last.skipExpand && last.definition.isAssociation) {
410
410
  const expandedSubqueryColumn = expandColumn(col)
411
+ if (!expandedSubqueryColumn) return []
411
412
  setElementOnColumns(expandedSubqueryColumn, col.element)
412
413
  res.push(expandedSubqueryColumn)
413
414
  } else if (!last?.skipExpand) {
@@ -438,7 +439,10 @@ function cqn4sql(originalQuery, model) {
438
439
 
439
440
  let baseName
440
441
  if (col.ref.length >= 2) {
441
- baseName = col.ref.slice(col.ref[0] === tableAlias ? 1 : 0, col.ref.length - 1).join('_')
442
+ baseName = col.ref
443
+ .map(idOnly)
444
+ .slice(col.ref[0] === tableAlias ? 1 : 0, col.ref.length - 1)
445
+ .join('_')
442
446
  }
443
447
 
444
448
  let columnAlias = col.as || (col.isJoinRelevant ? col.flatName : null)
@@ -863,7 +867,7 @@ function cqn4sql(originalQuery, model) {
863
867
  * @param {Object} column - To expand.
864
868
  * @param {Array} baseRef - The base reference for the expanded column.
865
869
  * @param {string} subqueryAlias - The alias of the `expand` subquery column.
866
- * @returns {Object} - The subquery object.
870
+ * @returns {Object} - The subquery object or null if the expand has a wildcard.
867
871
  * @throws {Error} - If one of the `ref`s in the `column.expand` is not part of the GROUP BY clause.
868
872
  */
869
873
  function _subqueryForGroupBy(column, baseRef, subqueryAlias) {
@@ -873,7 +877,13 @@ function cqn4sql(originalQuery, model) {
873
877
 
874
878
  // to be attached to dummy query
875
879
  const elements = {}
880
+ const wildcardIndex = column.expand.findIndex(e => e === '*')
881
+ if (wildcardIndex !== -1) {
882
+ // expand with wildcard vanishes as expand is part of the group by (OData $apply + $expand)
883
+ return null
884
+ }
876
885
  const expandedColumns = column.expand.flatMap(expand => {
886
+ if (!expand.ref) return expand
877
887
  const fullRef = [...baseRef, ...expand.ref]
878
888
 
879
889
  if (expand.expand) {
@@ -976,7 +986,10 @@ function cqn4sql(originalQuery, model) {
976
986
  let baseName
977
987
  if (col.ref.length >= 2) {
978
988
  // leaf might be intermediate structure
979
- baseName = col.ref.slice(col.ref[0] === tableAlias ? 1 : 0, col.ref.length - 1).join('_')
989
+ baseName = col.ref
990
+ .map(idOnly)
991
+ .slice(col.ref[0] === tableAlias ? 1 : 0, col.ref.length - 1)
992
+ .join('_')
980
993
  }
981
994
  const flatColumns = getFlatColumnsFor(col, { baseName, tableAlias })
982
995
  /**
@@ -1160,10 +1173,9 @@ function cqn4sql(originalQuery, model) {
1160
1173
  else if (element.virtual === true) return []
1161
1174
  else if (!isJoinRelevant && flatName) baseName = flatName
1162
1175
  else if (isJoinRelevant) {
1163
- const leaf = column.$refLinks[column.$refLinks.length - 1]
1176
+ const leaf = column.$refLinks.at(-1)
1164
1177
  leafAssoc = [...column.$refLinks].reverse().find(link => link.definition.isAssociation)
1165
- let elements
1166
- elements = leafAssoc.definition.elements || leafAssoc.definition.foreignKeys
1178
+ let elements = leafAssoc.definition.elements || leafAssoc.definition.foreignKeys
1167
1179
  if (elements && leaf.definition.name in elements) {
1168
1180
  element = leafAssoc.definition
1169
1181
  baseName = getFullName(leafAssoc.definition)
@@ -1203,68 +1215,76 @@ function cqn4sql(originalQuery, model) {
1203
1215
 
1204
1216
  if (element.keys) {
1205
1217
  const flatColumns = []
1206
- element.keys.forEach(fk => {
1207
- const fkElement = getElementForRef(fk.ref, getDefinition(element.target))
1208
- let fkBaseName
1209
- if (!leafAssoc || leafAssoc.onlyForeignKeyAccess)
1210
- fkBaseName = `${baseName}_${fk.as || fk.ref[fk.ref.length - 1]}`
1211
- // e.g. if foreign key is accessed via infix filter - use join alias to access key in target
1212
- else fkBaseName = fk.ref[fk.ref.length - 1]
1213
- const fkPath = [...csnPath, fk.ref[fk.ref.length - 1]]
1214
- if (fkElement.elements) {
1215
- // structured key
1216
- Object.values(fkElement.elements).forEach(e => {
1217
- let alias
1218
- if (columnAlias) {
1219
- const fkName = fk.as
1220
- ? `${fk.as}_${e.name}` // foreign key might also be re-named: `assoc { id as foo }`
1221
- : `${fk.ref.join('_')}_${e.name}`
1222
- alias = `${columnAlias}_${fkName}`
1218
+ for (const k of element.keys) {
1219
+ // if only one part of a foreign key is requested, only flatten the partial key
1220
+ const keyElement = getElementForRef(k.ref, getDefinition(element.target))
1221
+ const flattenThisForeignKey =
1222
+ !$refLinks || // the association is passed as element, not as ref --> flatten full foreign key
1223
+ element === $refLinks.at(-1).definition || // the association is the leaf of the ref --> flatten full foreign key
1224
+ keyElement === $refLinks.at(-1).definition // the foreign key is the leaf of the ref --> only flatten this specific foreign key
1225
+ if (flattenThisForeignKey) {
1226
+ const fkElement = getElementForRef(k.ref, getDefinition(element.target))
1227
+ let fkBaseName
1228
+ if (!leafAssoc || leafAssoc.onlyForeignKeyAccess)
1229
+ fkBaseName = `${baseName}_${k.as || k.ref.at(-1)}`
1230
+ // e.g. if foreign key is accessed via infix filter - use join alias to access key in target
1231
+ else fkBaseName = k.ref.at(-1)
1232
+ const fkPath = [...csnPath, k.ref.at(-1)]
1233
+ if (fkElement.elements) {
1234
+ // structured key
1235
+ for (const e of Object.values(fkElement.elements)) {
1236
+ let alias
1237
+ if (columnAlias) {
1238
+ const fkName = k.as
1239
+ ? `${k.as}_${e.name}` // foreign key might also be re-named: `assoc { id as foo }`
1240
+ : `${k.ref.join('_')}_${e.name}`
1241
+ alias = `${columnAlias}_${fkName}`
1242
+ }
1243
+ flatColumns.push(
1244
+ ...getFlatColumnsFor(
1245
+ e,
1246
+ { baseName: fkBaseName, columnAlias: alias, tableAlias },
1247
+ [...fkPath],
1248
+ excludeAndReplace,
1249
+ isWildcard,
1250
+ ),
1251
+ )
1223
1252
  }
1253
+ } else if (fkElement.isAssociation) {
1254
+ // assoc as key
1224
1255
  flatColumns.push(
1225
1256
  ...getFlatColumnsFor(
1226
- e,
1227
- { baseName: fkBaseName, columnAlias: alias, tableAlias },
1228
- [...fkPath],
1257
+ fkElement,
1258
+ { baseName, columnAlias, tableAlias },
1259
+ csnPath,
1229
1260
  excludeAndReplace,
1230
1261
  isWildcard,
1231
1262
  ),
1232
1263
  )
1233
- })
1234
- } else if (fkElement.isAssociation) {
1235
- // assoc as key
1236
- flatColumns.push(
1237
- ...getFlatColumnsFor(
1238
- fkElement,
1239
- { baseName, columnAlias, tableAlias },
1240
- csnPath,
1241
- excludeAndReplace,
1242
- isWildcard,
1243
- ),
1244
- )
1245
- } else {
1246
- // leaf reached
1247
- let flatColumn
1248
- if (columnAlias) {
1249
- // if the column has an explicit alias AND the orignal ref
1250
- // directly resolves to the foreign key, we must not append the fk name to the column alias
1251
- // e.g. `assoc.fk as FOO` => columns.alias = FOO
1252
- // `assoc as FOO` => columns.alias = FOO_fk
1253
- let columnAliasWithFlatFk
1254
- if (!(column.as && fkElement === column.$refLinks?.at(-1).definition))
1255
- columnAliasWithFlatFk = `${columnAlias}_${fk.as || fk.ref.join('_')}`
1256
- flatColumn = { ref: [fkBaseName], as: columnAliasWithFlatFk || columnAlias }
1257
- } else flatColumn = { ref: [fkBaseName] }
1258
- if (tableAlias) flatColumn.ref.unshift(tableAlias)
1259
-
1260
- // in a flat model, we must assign the foreign key rather than the key in the target
1261
- const flatForeignKey = getDefinition(element.parent.name)?.elements[fkBaseName]
1262
-
1263
- setElementOnColumns(flatColumn, flatForeignKey || fkElement)
1264
- Object.defineProperty(flatColumn, '_csnPath', { value: csnPath, writable: true })
1265
- flatColumns.push(flatColumn)
1264
+ } else {
1265
+ // leaf reached
1266
+ let flatColumn
1267
+ if (columnAlias) {
1268
+ // if the column has an explicit alias AND the original ref
1269
+ // directly resolves to the foreign key, we must not append the fk name to the column alias
1270
+ // e.g. `assoc.fk as FOO` => columns.alias = FOO
1271
+ // `assoc as FOO` => columns.alias = FOO_fk
1272
+ let columnAliasWithFlatFk
1273
+ if (!(column.as && fkElement === column.$refLinks?.at(-1).definition))
1274
+ columnAliasWithFlatFk = `${columnAlias}_${k.as || k.ref.join('_')}`
1275
+ flatColumn = { ref: [fkBaseName], as: columnAliasWithFlatFk || columnAlias }
1276
+ } else flatColumn = { ref: [fkBaseName] }
1277
+ if (tableAlias) flatColumn.ref.unshift(tableAlias)
1278
+
1279
+ // in a flat model, we must assign the foreign key rather than the key in the target
1280
+ const flatForeignKey = getDefinition(element.parent.name)?.elements[fkBaseName]
1281
+
1282
+ setElementOnColumns(flatColumn, flatForeignKey || fkElement)
1283
+ Object.defineProperty(flatColumn, '_csnPath', { value: csnPath, writable: true })
1284
+ flatColumns.push(flatColumn)
1285
+ }
1266
1286
  }
1267
- })
1287
+ }
1268
1288
  return flatColumns
1269
1289
  } else if (element.elements) {
1270
1290
  const flatRefs = []
@@ -1297,7 +1317,7 @@ function cqn4sql(originalQuery, model) {
1297
1317
 
1298
1318
  function getReplacement(from) {
1299
1319
  return from?.find(replacement => {
1300
- const nameOfExcludedColumn = replacement.as || replacement.ref?.[replacement.ref.length - 1] || replacement
1320
+ const nameOfExcludedColumn = replacement.as || replacement.ref?.at(-1) || replacement
1301
1321
  return nameOfExcludedColumn === element.name
1302
1322
  })
1303
1323
  }
@@ -1581,7 +1601,10 @@ function cqn4sql(originalQuery, model) {
1581
1601
  if (leaf.definition.parent.kind !== 'entity')
1582
1602
  // we need the base name
1583
1603
  return getFlatColumnsFor(leaf.definition, {
1584
- baseName: def.ref.slice(0, def.ref.length - 1).join('_'),
1604
+ baseName: def.ref
1605
+ .map(idOnly)
1606
+ .slice(0, def.ref.length - 1)
1607
+ .join('_'),
1585
1608
  tableAlias,
1586
1609
  })
1587
1610
  return getFlatColumnsFor(leaf.definition, { tableAlias })
@@ -1726,7 +1749,8 @@ function cqn4sql(originalQuery, model) {
1726
1749
  transformedFrom.$refLinks.splice(0, transformedFrom.$refLinks.length - 1)
1727
1750
 
1728
1751
  let args = from.ref.at(-1).args
1729
- const subquerySource = transformedFrom.$refLinks[0].target
1752
+ const subquerySource =
1753
+ getDefinition(transformedFrom.$refLinks[0].definition.target) || transformedFrom.$refLinks[0].target
1730
1754
  if (subquerySource.params && !args) args = {}
1731
1755
  const id = localized(subquerySource)
1732
1756
  transformedFrom.ref = [args ? { id, args } : id]
@@ -2202,10 +2226,7 @@ function cqn4sql(originalQuery, model) {
2202
2226
  const xpr = search
2203
2227
  const searchFunc = {
2204
2228
  func: 'search',
2205
- args: [
2206
- { list: searchIn },
2207
- xpr.length === 1 && 'val' in xpr[0] ? xpr[0] : { xpr },
2208
- ],
2229
+ args: [{ list: searchIn }, xpr.length === 1 && 'val' in xpr[0] ? xpr[0] : { xpr }],
2209
2230
  }
2210
2231
  return searchFunc
2211
2232
  } else {