@cap-js/db-service 1.15.1 → 1.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,32 @@
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.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.16.0...db-service-v1.16.1) (2024-12-16)
8
+
9
+
10
+ ### Fixed
11
+
12
+ * 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)
13
+ * 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))
14
+ * quoted mode ([#937](https://github.com/cap-js/cds-dbs/issues/937)) ([9e62b22](https://github.com/cap-js/cds-dbs/commit/9e62b22a1be90ada9f57cfa63505735d8b8eed88))
15
+ * 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))
16
+ * wildcard expand with aggregation ([#923](https://github.com/cap-js/cds-dbs/issues/923)) ([bbe7be0](https://github.com/cap-js/cds-dbs/commit/bbe7be00498ad083cf951daf344b7f5fd9f68ab9))
17
+
18
+ ## [1.16.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.15.1...db-service-v1.16.0) (2024-11-25)
19
+
20
+
21
+ ### Changed
22
+
23
+ * single column in `search` function is also treated as CQN `list` ([#898](https://github.com/cap-js/cds-dbs/issues/898)) ([f6593e6](https://github.com/cap-js/cds-dbs/commit/f6593e69de6df3e85a39c048794a56c7eb842c4c))
24
+
25
+
26
+ ### Fixed
27
+
28
+ * foreignKeys aspect on association may not be set ([#903](https://github.com/cap-js/cds-dbs/issues/903)) ([732a2f3](https://github.com/cap-js/cds-dbs/commit/732a2f385074f50b87ff9715b8bdf48d28a36309))
29
+ * insert on uuid with default value ([#911](https://github.com/cap-js/cds-dbs/issues/911)) ([545e489](https://github.com/cap-js/cds-dbs/commit/545e489ecd07b5a3ece9441d95804fb2f3d436fa))
30
+ * `session_context`, `current_date`, `current_time` and `current_timestamp` are treated as SAP HANA functions and are callable in upper case ([#910](https://github.com/cap-js/cds-dbs/issues/910)) ([50ebd10](https://github.com/cap-js/cds-dbs/commit/50ebd106b9ee5bf7e1026658b89401e904ffe051))
31
+ * wrap values in array if it is object, so it can be spreaded ([#882](https://github.com/cap-js/cds-dbs/issues/882)) ([11f3e8b](https://github.com/cap-js/cds-dbs/commit/11f3e8bdf37d57295c1f2ffb40e217f86ec7d423))
32
+
7
33
  ## [1.15.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.15.0...db-service-v1.15.1) (2024-11-18)
8
34
 
9
35
 
@@ -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
@@ -33,7 +33,7 @@ const StandardFunctions = {
33
33
  val = sub[2] || sub[3] || ''
34
34
  }
35
35
  arg.val = arg.__proto__.val = val
36
- const refs = ref.list || [ref]
36
+ const refs = ref.list
37
37
  const { toString } = ref
38
38
  return '(' + refs.map(ref2 => this.contains(this.tolower(toString(ref2)), this.tolower(arg))).join(' or ') + ')'
39
39
  },
@@ -159,10 +159,6 @@ const StandardFunctions = {
159
159
 
160
160
  // Date and Time Functions
161
161
 
162
- current_date: p => (p ? `current_date(${p})` : 'current_date'),
163
- current_time: p => (p ? `current_time(${p})` : 'current_time'),
164
- current_timestamp: p => (p ? `current_timestamp(${p})` : 'current_timestamp'),
165
-
166
162
  /**
167
163
  * Generates SQL statement that produces current point in time (date and time with time zone)
168
164
  * @returns {string}
@@ -257,20 +253,23 @@ const StandardFunctions = {
257
253
  ) - 0.5
258
254
  )
259
255
  ) * 86400
260
- )`,
256
+ )`
257
+ }
258
+
259
+ const HANAFunctions = {
260
+ // https://help.sap.com/docs/SAP_HANA_PLATFORM/4fe29514fd584807ac9f2a04f6754767/f12b86a6284c4aeeb449e57eb5dd3ebd.html
261
261
 
262
262
  /**
263
263
  * Generates SQL statement that calls the session_context function with the given parameter
264
264
  * @param {string} x session variable name or SQL expression
265
265
  * @returns {string}
266
266
  */
267
- session_context: x => `session_context('${x.val}')`,
268
- }
269
-
270
- const HANAFunctions = {
271
- // https://help.sap.com/docs/SAP_HANA_PLATFORM/4fe29514fd584807ac9f2a04f6754767/f12b86a6284c4aeeb449e57eb5dd3ebd.html
267
+ session_context: x => `session_context('${x.val}')`,
272
268
 
273
269
  // Time functions
270
+ current_date: p => (p ? `current_date(${p})` : 'current_date'),
271
+ current_time: p => (p ? `current_time(${p})` : 'current_time'),
272
+ current_timestamp: p => (p ? `current_timestamp(${p})` : 'current_timestamp'),
274
273
  /**
275
274
  * Generates SQL statement that calculates the difference in 100nanoseconds between two timestamps
276
275
  * @param {string} x left timestamp
package/lib/cqn2sql.js CHANGED
@@ -6,16 +6,9 @@ const _strict_booleans = _simple_queries < 2
6
6
 
7
7
  const { Readable } = require('stream')
8
8
 
9
- const DEBUG = (() => {
10
- const LOG = cds.log('sql-json')
11
- if (LOG._debug) return cds.debug('sql-json')
12
- return cds.debug('sql|sqlite')
13
- //if (DEBUG) {
14
- // return DEBUG
15
- // (sql, ...more) => DEBUG (sql.replace(/(?:SELECT[\n\r\s]+(json_group_array\()?[\n\r\s]*json_insert\((\n|\r|.)*?\)[\n\r\s]*\)?[\n\r\s]+as[\n\r\s]+_json_[\n\r\s]+FROM[\n\r\s]*\(|\)[\n\r\s]*(\)[\n\r\s]+AS )|\)$)/gim,(a,b,c,d) => d || ''), ...more)
16
- // FIXME: looses closing ) on INSERT queries
17
- //}
18
- })()
9
+ const DEBUG = cds.debug('sql|sqlite')
10
+ const LOG_SQL = cds.log('sql')
11
+ const LOG_SQLITE = cds.log('sqlite')
19
12
 
20
13
  class CQN2SQLRenderer {
21
14
  /**
@@ -28,10 +21,12 @@ class CQN2SQLRenderer {
28
21
  this.class = new.target // for IntelliSense
29
22
  this.class._init() // is a noop for subsequent calls
30
23
  this.model = srv?.model
31
-
32
24
  // Overwrite smart quoting
33
25
  if (cds.env.sql.names === 'quoted') {
34
- 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
+ }
35
30
  this.class.prototype.quote = (s) => `"${String(s).replace(/"/g, '""')}"`
36
31
  }
37
32
  }
@@ -90,10 +85,17 @@ class CQN2SQLRenderer {
90
85
  if (vars?.length && !this.values?.length) this.values = vars
91
86
  if (vars && Object.keys(vars).length && !this.values?.length) this.values = vars
92
87
  const sanitize_values = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
93
- DEBUG?.(
94
- this.sql,
95
- ...(sanitize_values && (this.entries || this.values?.length > 0) ? ['***'] : this.entries || this.values || []),
96
- )
88
+
89
+
90
+ if (DEBUG && (LOG_SQL._debug || LOG_SQLITE._debug)) {
91
+ let values = sanitize_values && (this.entries || this.values?.length > 0) ? ['***'] : this.entries || this.values || []
92
+ if (values && !Array.isArray(values)) {
93
+ values = [values]
94
+ }
95
+ DEBUG(this.sql, ...values)
96
+ }
97
+
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: ['?'] })
@@ -638,7 +635,7 @@ class CQN2SQLRenderer {
638
635
  */
639
636
  INSERT_rows(q) {
640
637
  const { INSERT } = q
641
- const entity = this.name(q.target?.name || INSERT.into.ref[0])
638
+ const entity = this.name(q.target?.name || INSERT.into.ref[0], q)
642
639
  const alias = INSERT.into.as
643
640
  const elements = q.elements || q.target?.elements
644
641
  const columns = this.columns = INSERT.columns || cds.error`Cannot insert rows without columns or elements`
@@ -684,7 +681,7 @@ class CQN2SQLRenderer {
684
681
  */
685
682
  INSERT_select(q) {
686
683
  const { INSERT } = q
687
- const entity = this.name(q.target.name)
684
+ const entity = this.name(q.target.name, q)
688
685
  const alias = INSERT.into.as
689
686
  const elements = q.elements || q.target?.elements || {}
690
687
  const columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
@@ -752,7 +749,7 @@ class CQN2SQLRenderer {
752
749
  .filter(c => keys.includes(c.name))
753
750
  .map(c => `${c.onInsert || c.sql} as ${this.quote(c.name)}`)
754
751
 
755
- const entity = this.name(q.target?.name || UPSERT.into.ref[0])
752
+ const entity = this.name(q.target?.name || UPSERT.into.ref[0], q)
756
753
  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
754
 
758
755
  const updateColumns = columns.filter(c => {
@@ -779,7 +776,7 @@ class CQN2SQLRenderer {
779
776
  UPDATE(q) {
780
777
  const { entity, with: _with, data, where } = q.UPDATE
781
778
  const elements = q.target?.elements
782
- let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity))}`
779
+ let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity, q))}`
783
780
  if (entity.as) sql += ` AS ${this.quote(entity.as)}`
784
781
 
785
782
  let columns = []
@@ -811,8 +808,9 @@ class CQN2SQLRenderer {
811
808
  * @param {import('./infer/cqn').DELETE} param0
812
809
  * @returns {string} SQL
813
810
  */
814
- DELETE({ DELETE: { from, where } }) {
815
- let sql = `DELETE FROM ${this.from(from)}`
811
+ DELETE(q) {
812
+ const { DELETE: { from, where } } = q
813
+ let sql = `DELETE FROM ${this.from(from, q)}`
816
814
  if (where) sql += ` WHERE ${this.where(where)}`
817
815
  return (this.sql = sql)
818
816
  }
@@ -1028,6 +1026,7 @@ class CQN2SQLRenderer {
1028
1026
  /**
1029
1027
  * Calculates the Database name of the given name
1030
1028
  * @param {string|import('./infer/cqn').ref} name
1029
+ * @param {import('./infer/cqn').Query} query
1031
1030
  * @returns {string} Database name
1032
1031
  */
1033
1032
  name(name) {
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
- searchIn.length > 1 ? { list: searchIn } : { ...searchIn[0] },
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 {
@@ -35,7 +35,7 @@ const generateUUIDandPropagateKeys = (entity, data, event) => {
35
35
  if (event === 'CREATE') {
36
36
  const keys = entity.keys
37
37
  for (const k in keys)
38
- if (keys[k].isUUID && !data[k] && !assoc4(keys[k])) //> skip key assocs, and foreign keys thereof
38
+ if (keys[k].isUUID && !data[k] && !assoc4(keys[k]) && !keys[k].default) //> skip key assocs, and foreign keys thereof
39
39
  data[k] = cds.utils.uuid()
40
40
  }
41
41
  for (const each in entity.elements) {