@cap-js/db-service 1.6.4 → 1.7.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,27 @@
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.7.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.6.4...db-service-v1.7.0) (2024-03-22)
8
+
9
+
10
+ ### Added
11
+
12
+ * also support lowercase matchespattern function ([#528](https://github.com/cap-js/cds-dbs/issues/528)) ([6ea574e](https://github.com/cap-js/cds-dbs/commit/6ea574ee67ef5e42e4f8ccbe4fe91b46097de129))
13
+ * forUpdate and forShareLock ([#148](https://github.com/cap-js/cds-dbs/issues/148)) ([99a1170](https://github.com/cap-js/cds-dbs/commit/99a1170e61de4fd0c505834c25a9c03fc34da85b))
14
+ * **hana:** drop prepared statements after end of transaction ([#537](https://github.com/cap-js/cds-dbs/issues/537)) ([b1f864e](https://github.com/cap-js/cds-dbs/commit/b1f864e0a3a0e5efacd803d3709379cab76d61cc))
15
+ * **hana:** Add views with parameters support ([#488](https://github.com/cap-js/cds-dbs/issues/488)) ([3790ec0](https://github.com/cap-js/cds-dbs/commit/3790ec0178aab2cdb429272bb3e813b13441785c))
16
+ * **orderby:** allow to disable collations with [@cds](https://github.com/cds).collate: false ([#492](https://github.com/cap-js/cds-dbs/issues/492)) ([820f971](https://github.com/cap-js/cds-dbs/commit/820f971e1ad21fa8f8ca289c1e29b373365df484))
17
+
18
+
19
+ ### Fixed
20
+
21
+ * **cqn2sql:** Smart quoting of columns inside UPSERT rows ([#519](https://github.com/cap-js/cds-dbs/issues/519)) ([78fe10b](https://github.com/cap-js/cds-dbs/commit/78fe10b1df3691614dc77b1d4f82df10a1d641d3))
22
+ * Getting rid of quirks mode ([#514](https://github.com/cap-js/cds-dbs/issues/514)) ([c9aa6e8](https://github.com/cap-js/cds-dbs/commit/c9aa6e835761ace38447f37cad6a5f39cb0b910c))
23
+ * issue with reused select cqns ([#505](https://github.com/cap-js/cds-dbs/issues/505)) ([916d175](https://github.com/cap-js/cds-dbs/commit/916d1756422f0caf02c323052f2addafed39182a))
24
+ * joins without columns are rejected ([#535](https://github.com/cap-js/cds-dbs/issues/535)) ([eb9beda](https://github.com/cap-js/cds-dbs/commit/eb9beda728de60081d7afbfcd49305eeb241f3fb))
25
+ * **search:** dont search non string aggregations ([#527](https://github.com/cap-js/cds-dbs/issues/527)) ([c87900c](https://github.com/cap-js/cds-dbs/commit/c87900cb157041a6ff76c45192c1d33180840d0f))
26
+ * **search:** search on aggregated results in HAVING clause ([#524](https://github.com/cap-js/cds-dbs/issues/524)) ([61d348e](https://github.com/cap-js/cds-dbs/commit/61d348ebc2528b7f1c6da8c78a7455a438e1b7cf))
27
+
7
28
  ## [1.6.4](https://github.com/cap-js/cds-dbs/compare/db-service-v1.6.3...db-service-v1.6.4) (2024-02-28)
8
29
 
9
30
 
package/lib/SQLService.js CHANGED
@@ -123,10 +123,13 @@ class SQLService extends DatabaseService {
123
123
  }
124
124
 
125
125
  const { sql, values, cqn } = this.cqn2sql(query, data)
126
+ const expand = query.SELECT.expand
127
+ delete query.SELECT.expand
128
+
126
129
  let ps = await this.prepare(sql)
127
130
  let rows = await ps.all(values)
128
131
  if (rows.length)
129
- if (cqn.SELECT.expand) rows = rows.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))
132
+ if (expand) rows = rows.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))
130
133
 
131
134
  if (cds.env.features.stream_compat) {
132
135
  if (query._streaming) {
@@ -428,6 +431,8 @@ SQLService.prototype.PreparedStatement = PreparedStatement
428
431
 
429
432
  const _target_name4 = q => {
430
433
  const target =
434
+ q._target_ref ||
435
+ q.from_into_ntt ||
431
436
  q.SELECT?.from ||
432
437
  q.INSERT?.into ||
433
438
  q.UPSERT?.into ||
@@ -441,7 +446,7 @@ const _target_name4 = q => {
441
446
  return first.id || first
442
447
  }
443
448
 
444
- const _unquirked = q => {
449
+ const _unquirked = !cds.env.ql.quirks_mode ? q => q : q => {
445
450
  if (!q) return q
446
451
  else if (typeof q.SELECT?.from === 'string') q.SELECT.from = { ref: [q.SELECT.from] }
447
452
  else if (typeof q.INSERT?.into === 'string') q.INSERT.into = { ref: [q.INSERT.into] }
@@ -132,9 +132,9 @@ class DatabaseService extends cds.Service {
132
132
  const tenants = tenant ? [tenant] : Object.keys(this.pools)
133
133
  await Promise.all (tenants.map (async t => {
134
134
  const pool = this.pools[t]; if (!pool) return
135
+ delete this.pools[t]
135
136
  await pool.drain()
136
137
  await pool.clear()
137
- delete this.pools[t]
138
138
  }))
139
139
  }
140
140
 
@@ -99,6 +99,13 @@ const StandardFunctions = {
99
99
  * @returns {string}
100
100
  */
101
101
  matchesPattern: (x, y) => `(${x} regexp ${y})`,
102
+ /**
103
+ * Generates SQL statement that matches the given string against a regular expression
104
+ * @param {string} x
105
+ * @param {string} y
106
+ * @returns {string}
107
+ */
108
+ matchespattern: (x, y) => `(${x} regexp ${y})`,
102
109
  /**
103
110
  * Generates SQL statement that produces the lower case value of a given string
104
111
  * @param {string} x
package/lib/cqn2sql.js CHANGED
@@ -40,7 +40,9 @@ class CQN2SQLRenderer {
40
40
  for (let each in mixins) {
41
41
  const def = types[each]
42
42
  if (!def) continue
43
- Object.defineProperty(def, fqn, { value: mixins[each] })
43
+ const value = mixins[each]
44
+ if (value?.get) Object.defineProperty(def, fqn, { get: value.get })
45
+ else Object.defineProperty(def, fqn, { value })
44
46
  }
45
47
  return fqn
46
48
  }
@@ -193,7 +195,7 @@ class CQN2SQLRenderer {
193
195
  DROP(q) {
194
196
  const { target } = q
195
197
  const isView = target.query || target.projection
196
- return (this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} IF EXISTS ${this.name(target.name)}`)
198
+ return (this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} IF EXISTS ${this.quote(this.name(target.name))}`)
197
199
  }
198
200
 
199
201
  // SELECT Statements ------------------------------------------------
@@ -203,7 +205,13 @@ class CQN2SQLRenderer {
203
205
  * @param {import('./infer/cqn').SELECT} q
204
206
  */
205
207
  SELECT(q) {
206
- let { from, expand, where, groupBy, having, orderBy, limit, one, distinct, localized } = q.SELECT
208
+ let { from, expand, where, groupBy, having, orderBy, limit, one, distinct, localized, forUpdate, forShareLock } =
209
+ q.SELECT
210
+
211
+ if (from?.join && !q.SELECT.columns) {
212
+ throw new Error('CQN query using joins must specify the selected columns.')
213
+ }
214
+
207
215
  // REVISIT: When selecting from an entity that is not in the model the from.where are not normalized (as cqn4sql is skipped)
208
216
  if (!where && from?.ref?.length === 1 && from.ref[0]?.where) where = from.ref[0]?.where
209
217
  let columns = this.SELECT_columns(q)
@@ -217,6 +225,8 @@ class CQN2SQLRenderer {
217
225
  if (!_empty(orderBy)) sql += ` ORDER BY ${this.orderBy(orderBy, localized)}`
218
226
  if (one) limit = Object.assign({}, limit, { rows: { val: 1 } })
219
227
  if (limit) sql += ` LIMIT ${this.limit(limit)}`
228
+ if (forUpdate) sql += ` ${this.forUpdate(forUpdate)}`
229
+ else if (forShareLock) sql += ` ${this.forShareLock(forShareLock)}`
220
230
  // Expand cannot work without an inferred query
221
231
  if (expand) {
222
232
  if ('elements' in q) sql = this.SELECT_expand(q, sql)
@@ -303,12 +313,28 @@ class CQN2SQLRenderer {
303
313
  from(from) {
304
314
  const { ref, as } = from
305
315
  const _aliased = as ? s => s + ` as ${this.quote(as)}` : s => s
306
- if (ref) return _aliased(this.quote(this.name(ref[0])))
316
+ if (ref) {
317
+ const z = ref[0]
318
+ if (z.args) {
319
+ return _aliased(`${this.quote(this.name(z))}${this.from_args(z.args)}`)
320
+ }
321
+ return _aliased(this.quote(this.name(z)))
322
+ }
307
323
  if (from.SELECT) return _aliased(`(${this.SELECT(from)})`)
308
324
  if (from.join)
309
325
  return `${this.from(from.args[0])} ${from.join} JOIN ${this.from(from.args[1])} ON ${this.where(from.on)}`
310
326
  }
311
327
 
328
+ /**
329
+ * Renders a FROM clause into generic SQL
330
+ * @param {import('./infer/cqn').ref['ref'][0]['args']} args
331
+ * @returns {string} SQL
332
+ */
333
+ from_args(args) {
334
+ args
335
+ cds.error`Parameterized views are not supported by ${this.constructor.name}`
336
+ }
337
+
312
338
  /**
313
339
  * Renders a WHERE clause into generic SQL
314
340
  * @param {import('./infer/cqn').predicate} xpr
@@ -364,6 +390,32 @@ class CQN2SQLRenderer {
364
390
  return !offset ? rows.val : `${rows.val} OFFSET ${offset.val}`
365
391
  }
366
392
 
393
+ /**
394
+ * Renders an forUpdate clause into generic SQL
395
+ * @param {import('./infer/cqn').SELECT["SELECT"]["forUpdate"]} update
396
+ * @returns {string} SQL
397
+ */
398
+ forUpdate(update) {
399
+ const { wait, of } = update
400
+ let sql = 'FOR UPDATE'
401
+ if (!_empty(of)) sql += ` OF ${of.map(x => this.expr(x)).join(', ')}`
402
+ if (typeof wait === 'number') sql += ` WAIT ${wait}`
403
+ return sql
404
+ }
405
+
406
+ /**
407
+ * Renders an forShareLock clause into generic SQL
408
+ * @param {import('./infer/cqn').SELECT["SELECT"]["forShareLock"]} update
409
+ * @returns {string} SQL
410
+ */
411
+ forShareLock(lock) {
412
+ const { wait, of } = lock
413
+ let sql = 'FOR SHARE LOCK'
414
+ if (!_empty(of)) sql += ` OF ${of.map(x => this.expr(x)).join(', ')}`
415
+ if (typeof wait === 'number') sql += ` WAIT ${wait}`
416
+ return sql
417
+ }
418
+
367
419
  // INSERT Statements ------------------------------------------------
368
420
 
369
421
  /**
@@ -402,12 +454,12 @@ class CQN2SQLRenderer {
402
454
  : ObjectKeys(INSERT.entries[0])
403
455
 
404
456
  /** @type {string[]} */
405
- this.columns = columns.filter(elements ? c => !elements[c]?.['@cds.extension'] : () => true).map(c => this.quote(c))
457
+ this.columns = columns.filter(elements ? c => !elements[c]?.['@cds.extension'] : () => true)
406
458
 
407
459
  if (!elements) {
408
460
  this.entries = INSERT.entries.map(e => columns.map(c => e[c]))
409
461
  const param = this.param.bind(this, { ref: ['?'] })
410
- return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns}) VALUES (${columns.map(param)})`)
462
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))}) VALUES (${columns.map(param)})`)
411
463
  }
412
464
 
413
465
  const extractions = this.managed(
@@ -446,7 +498,7 @@ class CQN2SQLRenderer {
446
498
  this.entries = [[...this.values, stream]]
447
499
  }
448
500
 
449
- return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns
501
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))
450
502
  }) SELECT ${extraction} FROM json_each(?)`)
451
503
  }
452
504
 
@@ -573,12 +625,12 @@ class CQN2SQLRenderer {
573
625
  return converter?.(extract, element) || extract
574
626
  })
575
627
 
576
- this.columns = columns.map(c => this.quote(c))
628
+ this.columns = columns
577
629
 
578
630
  if (!elements) {
579
631
  this.entries = INSERT.rows
580
632
  const param = this.param.bind(this, { ref: ['?'] })
581
- return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns}) VALUES (${columns.map(param)})`)
633
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))}) VALUES (${columns.map(param)})`)
582
634
  }
583
635
 
584
636
  if (INSERT.rows[0] instanceof Readable) {
@@ -590,7 +642,7 @@ class CQN2SQLRenderer {
590
642
  this.entries = [[...this.values, stream]]
591
643
  }
592
644
 
593
- return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns
645
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))
594
646
  }) SELECT ${extraction} FROM json_each(?)`)
595
647
  }
596
648
 
@@ -617,7 +669,7 @@ class CQN2SQLRenderer {
617
669
  const columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
618
670
  c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
619
671
  ))
620
- this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns}) ${this.SELECT(
672
+ this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns.map(c => this.quote(c))}) ${this.SELECT(
621
673
  this.cqn4sql(INSERT.as),
622
674
  )}`
623
675
  this.entries = [this.values]
@@ -641,7 +693,7 @@ class CQN2SQLRenderer {
641
693
  /** @type {import('./converters').Converters} */
642
694
  static OutputConverters = {} // subclasses to override
643
695
 
644
- static localized = { String: true, UUID: false }
696
+ static localized = { String: { get() { return this['@cds.collate'] !== false } }, UUID: false }
645
697
 
646
698
  // UPSERT Statements ------------------------------------------------
647
699
 
package/lib/cqn4sql.js CHANGED
@@ -111,7 +111,7 @@ function cqn4sql(originalQuery, model) {
111
111
 
112
112
  // calculate the primary keys of the target entity, there is always exactly
113
113
  // one query source for UPDATE / DELETE
114
- const queryTarget = Object.values(originalQuery.sources)[0]
114
+ const queryTarget = Object.values(originalQuery.sources)[0].definition
115
115
  const keys = Object.values(queryTarget.elements).filter(e => e.key === true)
116
116
  const primaryKey = { list: [] }
117
117
  keys.forEach(k => {
@@ -181,10 +181,9 @@ function cqn4sql(originalQuery, model) {
181
181
 
182
182
  if (inferred.SELECT.search) {
183
183
  // Search target can be a navigation, in that case use _target to get the correct entity
184
- const where = transformSearchToWhere(inferred.SELECT.search, transformedFrom)
185
- if (where) {
186
- transformedQuery.SELECT.where = where
187
- }
184
+ const { where, having } = transformSearch(inferred.SELECT.search, transformedFrom) || {}
185
+ if (where) transformedQuery.SELECT.where = where
186
+ else if (having) transformedQuery.SELECT.having = having
188
187
  }
189
188
  return transformedQuery
190
189
  }
@@ -204,20 +203,26 @@ function cqn4sql(originalQuery, model) {
204
203
  }
205
204
 
206
205
  /**
207
- * Transforms a search expression to a WHERE clause for a SELECT operation.
206
+ * Transforms a search expression into a WHERE or HAVING clause for a SELECT operation, depending on the context of the query.
207
+ * The function decides whether to use a WHERE or HAVING clause based on the presence of aggregated columns in the search criteria.
208
208
  *
209
- * @param {object} search - The search expression which shall be applied to the searchable columns on the query source.
209
+ * @param {object} search - The search expression to be applied to the searchable columns within the query source.
210
210
  * @param {object} from - The FROM clause of the CQN statement.
211
211
  *
212
- * @returns {(Object|Array|undefined)} - If the target of the query contains searchable elements, the function returns an array that represents the WHERE clause.
213
- * If the SELECT query already contains a WHERE clause, this array includes the existing clause and appends an AND condition with the new 'contains' clause.
214
- * If the SELECT query does not contain a WHERE clause, the returned array solely consists of the 'contains' clause.
215
- * If the target entity of the query does not contain searchable elements, the function returns null.
212
+ * @returns {(Object|Array|null)} - The function returns an object representing the WHERE or HAVING clause of the query:
213
+ * - If the target of the query contains searchable elements, an array representing the WHERE or HAVING clause is returned.
214
+ * This includes appending to an existing clause with an AND condition or creating a new clause solely with the 'contains' clause.
215
+ * - If the SELECT query does not initially contain a WHERE or HAVING clause, the returned object solely consists of the 'contains' clause.
216
+ * - If the target entity of the query does not contain searchable elements, the function returns null.
216
217
  *
218
+ * Note: The WHERE clause is used for filtering individual rows before any aggregation occurs.
219
+ * The HAVING clause is utilized for conditions on aggregated data, applied after grouping operations.
217
220
  */
218
- function transformSearchToWhere(search, from) {
219
- const entity = from.$refLinks[0].definition._target || from.$refLinks[0].definition
220
- const searchIn = computeColumnsToBeSearched(inferred, entity, from.as)
221
+ function transformSearch(search, from) {
222
+ const entity = getDefinition(from.$refLinks[0].definition.target) || from.$refLinks[0].definition
223
+ // pass transformedQuery because we may need to search in the columns directly
224
+ // in case of aggregation
225
+ const searchIn = computeColumnsToBeSearched(transformedQuery, entity, from.as)
221
226
  if (searchIn.length > 0) {
222
227
  const xpr = search
223
228
  const contains = {
@@ -228,10 +233,16 @@ function cqn4sql(originalQuery, model) {
228
233
  ],
229
234
  }
230
235
 
231
- if (transformedQuery.SELECT.where) {
232
- return [asXpr(transformedQuery.SELECT.where), 'and', contains]
236
+ // if the query is grouped and the queries columns contain an aggregate function,
237
+ // we must put the search term into the `having` clause, as the search expression
238
+ // is defined on the aggregated result, not on the individual rows
239
+ let prop = 'where'
240
+
241
+ if (inferred.SELECT.groupBy && searchIn.some(c => c.func || c.xpr)) prop = 'having'
242
+ if (transformedQuery.SELECT[prop]) {
243
+ return { [prop]: [asXpr(transformedQuery.SELECT.where), 'and', contains] }
233
244
  } else {
234
- return [contains]
245
+ return { [prop]: [contains] }
235
246
  }
236
247
  } else {
237
248
  return null
@@ -255,9 +266,12 @@ function cqn4sql(originalQuery, model) {
255
266
  */
256
267
  const alreadySeen = new Map()
257
268
  inferred.joinTree._roots.forEach(r => {
258
- const args = r.queryArtifact.SELECT
259
- ? [{ SELECT: transformSubquery(r.queryArtifact).SELECT, as: r.alias }]
260
- : [{ ref: [localized(r.queryArtifact)], as: r.alias }]
269
+ const args = []
270
+ if (r.queryArtifact.SELECT) args.push({ SELECT: transformSubquery(r.queryArtifact).SELECT, as: r.alias })
271
+ else {
272
+ const id = localized(r.queryArtifact)
273
+ args.push({ ref: [r.args ? { id, args: r.args } : id], as: r.alias })
274
+ }
261
275
  from = { join: 'left', args, on: [] }
262
276
  r.children.forEach(c => {
263
277
  from = joinForBranch(from, c)
@@ -282,10 +296,13 @@ function cqn4sql(originalQuery, model) {
282
296
  ),
283
297
  )
284
298
 
299
+ const id = localized(getDefinition(nextAssoc.$refLink.definition.target))
300
+ const { args } = nextAssoc
285
301
  const arg = {
286
- ref: [localized(model.definitions[nextAssoc.$refLink.definition.target])],
302
+ ref: [args ? { id, args } : id],
287
303
  as: nextAssoc.$refLink.alias,
288
304
  }
305
+
289
306
  lhs.args.push(arg)
290
307
  alreadySeen.set(nextAssoc.$refLink.alias, true)
291
308
  if (nextAssoc.where) {
@@ -438,7 +455,7 @@ function cqn4sql(originalQuery, model) {
438
455
  const refNavigation = col.ref.slice(col.$refLinks[0].definition.kind !== 'element' ? 1 : 0).join('_')
439
456
  if (!columnAlias && col.flatName && col.flatName !== refNavigation) columnAlias = refNavigation
440
457
 
441
- if (col.$refLinks.some(link => link.definition._target?.['@cds.persistence.skip'] === true)) return
458
+ if (col.$refLinks.some(link => getDefinition(link.definition.target)?.['@cds.persistence.skip'] === true)) return
442
459
 
443
460
  const flatColumns = getFlatColumnsFor(col, { baseName, columnAlias, tableAlias })
444
461
  flatColumns.forEach(flatColumn => {
@@ -472,13 +489,17 @@ function cqn4sql(originalQuery, model) {
472
489
 
473
490
  function getTransformedColumn(col) {
474
491
  if (col.xpr) {
475
- return { xpr: getTransformedTokenStream(col.xpr) }
492
+ const xpr = { xpr: getTransformedTokenStream(col.xpr) }
493
+ if (col.cast) xpr.cast = col.cast
494
+ return xpr
476
495
  } else if (col.func) {
477
- return {
496
+ const func = {
478
497
  func: col.func,
479
498
  args: col.args && getTransformedTokenStream(col.args),
480
- as: col.func,
499
+ as: col.func, // may be overwritten by the explicit alias
481
500
  }
501
+ if (col.cast) func.cast = col.cast
502
+ return func
482
503
  } else {
483
504
  return copy(col)
484
505
  }
@@ -680,7 +701,7 @@ function cqn4sql(originalQuery, model) {
680
701
  // select from books { { * } as bar }
681
702
  // only possible if there is exactly one query source
682
703
  if (!baseRef.length) {
683
- const [tableAlias, definition] = Object.entries(inferred.sources)[0]
704
+ const [tableAlias, { definition }] = Object.entries(inferred.sources)[0]
684
705
  baseRef.push(tableAlias)
685
706
  baseRefLinks.push({ definition, source: definition })
686
707
  }
@@ -849,7 +870,8 @@ function cqn4sql(originalQuery, model) {
849
870
  } else if (pseudos.elements[col.ref?.[0]]) {
850
871
  res.push({ ...col })
851
872
  } else if (col.ref) {
852
- if (col.$refLinks.some(link => link.definition._target?.['@cds.persistence.skip'] === true)) continue
873
+ if (col.$refLinks.some(link => getDefinition(link.definition.target)?.['@cds.persistence.skip'] === true))
874
+ continue
853
875
  if (col.ref.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
854
876
  const dollarSelfReplacement = calculateDollarSelfColumn(col)
855
877
  res.push(...getTransformedOrderByGroupBy([dollarSelfReplacement], inOrderBy))
@@ -991,7 +1013,7 @@ function cqn4sql(originalQuery, model) {
991
1013
  */
992
1014
  function getElementForRef(ref, def) {
993
1015
  return ref.reduce((prev, res) => {
994
- return (prev?.elements || prev?.foreignKeys)?.[res] || prev?._target?.elements[res] // PLEASE REVIEW: should we add the .foreignKey check here for the non-ucsn case?
1016
+ return (prev?.elements || prev?.foreignKeys)?.[res] || getDefinition(prev?.target)?.elements[res] // PLEASE REVIEW: should we add the .foreignKey check here for the non-ucsn case?
995
1017
  }, def)
996
1018
  }
997
1019
 
@@ -1087,7 +1109,7 @@ function cqn4sql(originalQuery, model) {
1087
1109
  if (element.keys) {
1088
1110
  const flatColumns = []
1089
1111
  element.keys.forEach(fk => {
1090
- const fkElement = getElementForRef(fk.ref, element._target)
1112
+ const fkElement = getElementForRef(fk.ref, getDefinition(element.target))
1091
1113
  let fkBaseName
1092
1114
  if (!leafAssoc || leafAssoc.onlyForeignKeyAccess)
1093
1115
  fkBaseName = `${baseName}_${fk.as || fk.ref[fk.ref.length - 1]}`
@@ -1141,7 +1163,7 @@ function cqn4sql(originalQuery, model) {
1141
1163
  if (tableAlias) flatColumn.ref.unshift(tableAlias)
1142
1164
 
1143
1165
  // in a flat model, we must assign the foreign key rather than the key in the target
1144
- const flatForeignKey = model.definitions[element.parent.name]?.elements[fkBaseName]
1166
+ const flatForeignKey = getDefinition(element.parent.name)?.elements[fkBaseName]
1145
1167
 
1146
1168
  setElementOnColumns(flatColumn, flatForeignKey || fkElement)
1147
1169
  Object.defineProperty(flatColumn, '_csnPath', { value: csnPath, writable: true })
@@ -1260,7 +1282,7 @@ function cqn4sql(originalQuery, model) {
1260
1282
  }”`,
1261
1283
  )
1262
1284
  }
1263
- whereExistsSubSelects.push(getWhereExistsSubquery(current, next, step.where, true))
1285
+ whereExistsSubSelects.push(getWhereExistsSubquery(current, next, step.where, true, step.args))
1264
1286
  }
1265
1287
 
1266
1288
  const whereExists = { SELECT: whereExistsSubqueries(whereExistsSubSelects) }
@@ -1292,7 +1314,7 @@ function cqn4sql(originalQuery, model) {
1292
1314
  }
1293
1315
  } else if (tokenStream.length === 1 && token.val && $baseLink) {
1294
1316
  // infix filter - OData variant w/o mentioning key --> flatten out and compare each leaf to token.val
1295
- const def = $baseLink.definition._target || $baseLink.definition
1317
+ const def = getDefinition($baseLink.definition.target) || $baseLink.definition
1296
1318
  const keys = def.keys // use key aspect on entity
1297
1319
  const keyValComparisons = []
1298
1320
  const flatKeys = []
@@ -1552,7 +1574,7 @@ function cqn4sql(originalQuery, model) {
1552
1574
  const nextStep = refReverse[i + 1] // only because we want the filter condition
1553
1575
 
1554
1576
  if (stepLink.definition.target && nextStepLink) {
1555
- const { where } = nextStep
1577
+ const { where, args } = nextStep
1556
1578
  if (isStructured(nextStepLink.definition)) {
1557
1579
  // find next association / entity in the ref because this is actually our real nextStep
1558
1580
  const nextStepIndex =
@@ -1573,7 +1595,7 @@ function cqn4sql(originalQuery, model) {
1573
1595
  as = getNextAvailableTableAlias(as)
1574
1596
  }
1575
1597
  nextStepLink.alias = as
1576
- whereExistsSubSelects.push(getWhereExistsSubquery(stepLink, nextStepLink, where))
1598
+ whereExistsSubSelects.push(getWhereExistsSubquery(stepLink, nextStepLink, where, false, args))
1577
1599
  }
1578
1600
  }
1579
1601
 
@@ -1607,7 +1629,12 @@ function cqn4sql(originalQuery, model) {
1607
1629
 
1608
1630
  // adjust ref & $refLinks after associations have turned into where exists subqueries
1609
1631
  transformedFrom.$refLinks.splice(0, transformedFrom.$refLinks.length - 1)
1610
- transformedFrom.ref = [localized(transformedFrom.$refLinks[0].target)]
1632
+
1633
+ let args = from.ref.at(-1).args
1634
+ const subquerySource = transformedFrom.$refLinks[0].target
1635
+ if (subquerySource.params && !args) args = {}
1636
+ const id = localized(subquerySource)
1637
+ transformedFrom.ref = [args ? { id, args } : id]
1611
1638
 
1612
1639
  return { transformedWhere, transformedFrom }
1613
1640
  }
@@ -1661,7 +1688,7 @@ function cqn4sql(originalQuery, model) {
1661
1688
  */
1662
1689
  function backlinkFor(assoc) {
1663
1690
  if (!assoc.on) return null
1664
- const target = model.definitions[assoc.target]
1691
+ const target = getDefinition(assoc.target)
1665
1692
  // technically we could have multiple backlinks
1666
1693
  const backlinks = []
1667
1694
  for (let i = 0; i < assoc.on.length; i += 3) {
@@ -1687,7 +1714,7 @@ function cqn4sql(originalQuery, model) {
1687
1714
  */
1688
1715
  function onCondFor(assocRefLink, targetSideRefLink, inWhereOrJoin) {
1689
1716
  const { on, keys } = assocRefLink.definition
1690
- const target = model.definitions[assocRefLink.definition.target]
1717
+ const target = getDefinition(assocRefLink.definition.target)
1691
1718
  let res
1692
1719
  // technically we could have multiple backlinks
1693
1720
  if (keys) {
@@ -1738,10 +1765,11 @@ function cqn4sql(originalQuery, model) {
1738
1765
  if (res === '$self')
1739
1766
  // next is resolvable in entity
1740
1767
  return prev
1741
- const definition = prev?.elements?.[res] || prev?._target?.elements[res] || pseudos.elements[res]
1768
+ const definition =
1769
+ prev?.elements?.[res] || getDefinition(prev?.target)?.elements[res] || pseudos.elements[res]
1742
1770
  const target = getParentEntity(definition)
1743
1771
  thing.$refLinks[i] = { definition, target, alias: definition.name }
1744
- return prev?.elements?.[res] || prev?._target?.elements[res] || pseudos.elements[res]
1772
+ return prev?.elements?.[res] || getDefinition(prev?.target)?.elements[res] || pseudos.elements[res]
1745
1773
  }, assocHost)
1746
1774
  }
1747
1775
 
@@ -1843,7 +1871,7 @@ function cqn4sql(originalQuery, model) {
1843
1871
  result[i].ref.splice(0, 1, assocRefLink.alias)
1844
1872
  } else if (
1845
1873
  definition.name in
1846
- (targetSideRefLink.definition.elements || targetSideRefLink.definition._target.elements)
1874
+ (targetSideRefLink.definition.elements || getDefinition(targetSideRefLink.definition.target).elements)
1847
1875
  ) {
1848
1876
  // first step is association which refers to its foreign key by dot notation
1849
1877
  result[i].ref = [targetSideRefLink.alias, lhs.ref.join('_')]
@@ -1865,7 +1893,7 @@ function cqn4sql(originalQuery, model) {
1865
1893
  // pseudo element
1866
1894
  return element
1867
1895
  if (element.kind === 'entity') return element
1868
- else return model.definitions[localized(getParentEntity(element.parent))]
1896
+ else return getDefinition(localized(getParentEntity(element.parent)))
1869
1897
  }
1870
1898
  }
1871
1899
 
@@ -1880,11 +1908,11 @@ function cqn4sql(originalQuery, model) {
1880
1908
  function getParentKeyForeignKeyPairs(assoc, targetSideRefLink, flipSourceAndTarget = false) {
1881
1909
  const res = []
1882
1910
  const backlink = backlinkFor(assoc)?.[0]
1883
- const { keys, _target } = backlink || assoc
1911
+ const { keys, target } = backlink || assoc
1884
1912
  if (keys) {
1885
1913
  keys.forEach(fk => {
1886
1914
  const { ref, as } = fk
1887
- const elem = getElementForRef(ref, _target) // find the element (the target element of the foreign key) in the target of the (backlink) association
1915
+ const elem = getElementForRef(ref, getDefinition(target)) // find the element (the target element of the foreign key) in the target of the (backlink) association
1888
1916
  const flatParentKeys = getFlatColumnsFor(elem, { baseName: ref.slice(0, ref.length - 1).join('_') }) // it might be a structured element, so expand it into the full parent key tuple
1889
1917
  const flatAssociationName = getFullName(backlink || assoc) // get the name of the (backlink) association
1890
1918
  const flatForeignKeys = getFlatColumnsFor(elem, { baseName: flatAssociationName, columnAlias: as }) // the name of the (backlink) association is the base of the foreign key tuple, also respect aliased fk.
@@ -1933,7 +1961,7 @@ function cqn4sql(originalQuery, model) {
1933
1961
  * -> if it is, target and source side are flipped in the where exists subquery
1934
1962
  * @returns {CQN.SELECT}
1935
1963
  */
1936
- function getWhereExistsSubquery(current, next, customWhere = null, inWhere = false) {
1964
+ function getWhereExistsSubquery(current, next, customWhere = null, inWhere = false, customArgs = null) {
1937
1965
  const { definition } = current
1938
1966
  const { definition: nextDefinition } = next
1939
1967
  const on = []
@@ -1957,9 +1985,12 @@ function cqn4sql(originalQuery, model) {
1957
1985
  on.push(...['and', ...(hasLogicalOr(filter) ? [asXpr(filter)] : filter)])
1958
1986
  }
1959
1987
 
1988
+ const subquerySource = assocTarget(nextDefinition) || nextDefinition
1989
+ const id = localized(subquerySource)
1990
+ if (subquerySource.params && !customArgs) customArgs = {}
1960
1991
  const SELECT = {
1961
1992
  from: {
1962
- ref: [localized(assocTarget(nextDefinition) || nextDefinition)],
1993
+ ref: [customArgs ? { id, args: customArgs } : id],
1963
1994
  as: next.alias,
1964
1995
  },
1965
1996
  columns: [
@@ -1982,7 +2013,7 @@ function cqn4sql(originalQuery, model) {
1982
2013
  */
1983
2014
  function localized(definition) {
1984
2015
  if (!isLocalized(definition)) return definition.name
1985
- const view = model.definitions[`localized.${definition.name}`]
2016
+ const view = getDefinition(`localized.${definition.name}`)
1986
2017
  return view?.name || definition.name
1987
2018
  }
1988
2019
 
@@ -1995,7 +2026,18 @@ function cqn4sql(originalQuery, model) {
1995
2026
  * @returns true if the given definition shall be localized
1996
2027
  */
1997
2028
  function isLocalized(definition) {
1998
- return inferred.SELECT?.localized && definition['@cds.localized'] !== false
2029
+ return (
2030
+ inferred.SELECT?.localized &&
2031
+ definition['@cds.localized'] !== false &&
2032
+ !inferred.SELECT.forUpdate &&
2033
+ !inferred.SELECT.forShareLock
2034
+ )
2035
+ }
2036
+
2037
+ /** returns the CSN definition for the given name from the model */
2038
+ function getDefinition(name) {
2039
+ if (!name) return null
2040
+ return model.definitions[name]
1999
2041
  }
2000
2042
 
2001
2043
  /**
@@ -2005,7 +2047,7 @@ function cqn4sql(originalQuery, model) {
2005
2047
  * @returns the csn definition of the association target or null if it is not an association
2006
2048
  */
2007
2049
  function assocTarget(assoc) {
2008
- return model.definitions[assoc.target] || null
2050
+ return getDefinition(assoc.target) || null
2009
2051
  }
2010
2052
 
2011
2053
  /**
@@ -47,13 +47,16 @@ function infer(originalQuery, model) {
47
47
  Object.defineProperties(inferred, {
48
48
  // REVISIT: public, or for local reuse, or in cqn4sql only?
49
49
  sources: { value: sources, writable: true },
50
- target: { value: aliases.length === 1 ? sources[aliases[0]] : originalQuery, writable: true }, // REVISIT: legacy?
50
+ target: {
51
+ value: aliases.length === 1 ? getDefinitionFromSources(sources, aliases[0]) : originalQuery,
52
+ writable: true,
53
+ }, // REVISIT: legacy?
51
54
  })
52
55
  // also enrich original query -> writable because it may be inferred again
53
56
  Object.defineProperties(originalQuery, {
54
57
  sources: { value: sources, writable: true },
55
58
  target: {
56
- value: aliases.length === 1 ? sources[aliases[0]] : originalQuery,
59
+ value: aliases.length === 1 ? getDefinitionFromSources(sources, aliases[0]) : originalQuery,
57
60
  writable: true,
58
61
  },
59
62
  })
@@ -94,12 +97,13 @@ function infer(originalQuery, model) {
94
97
  function inferTarget(from, querySources) {
95
98
  const { ref } = from
96
99
  if (ref) {
97
- const first = ref[0].id || ref[0]
98
- let target = getDefinition(first, model)
100
+ const { id, args } = ref[0]
101
+ const first = id || ref[0]
102
+ let target = getDefinition(first) || cds.error`"${first}" not found in the definitions of your model`
99
103
  if (!target) throw new Error(`"${first}" not found in the definitions of your model`)
100
104
  if (ref.length > 1) {
101
105
  target = from.ref.slice(1).reduce((d, r) => {
102
- const next = d.elements[r.id || r]?._target || d.elements[r.id || r]
106
+ const next = getDefinition(d.elements[r.id || r]?.target) || d.elements[r.id || r]
103
107
  if (!next) throw new Error(`No association “${r.id || r}” in ${d.kind} “${d.name}”`)
104
108
  return next
105
109
  }, target)
@@ -113,16 +117,18 @@ function infer(originalQuery, model) {
113
117
  from.as ||
114
118
  (ref.length === 1 ? first.match(/[^.]+$/)[0] : ref[ref.length - 1].id || ref[ref.length - 1])
115
119
  if (alias in querySources) throw new Error(`Duplicate alias "${alias}"`)
116
- querySources[alias] = target
120
+ querySources[alias] = { definition: target, args }
117
121
  const last = from.$refLinks.at(-1)
118
122
  last.alias = alias
119
123
  } else if (from.args) {
120
124
  from.args.forEach(a => inferTarget(a, querySources))
121
125
  } else if (from.SELECT) {
122
126
  infer(from, model) // we need the .elements in the sources
123
- querySources[from.as || ''] = from
127
+ querySources[from.as || ''] = { definition: from }
124
128
  } else if (typeof from === 'string') {
125
- querySources[/([^.]*)$/.exec(from)[0]] = getDefinition(from, model)
129
+ // TODO: Create unique alias, what about duplicates?
130
+ const definition = getDefinition(from) || cds.error`"${from}" not found in the definitions of your model`
131
+ querySources[/([^.]*)$/.exec(from)[0]] = { definition }
126
132
  } else if (from.SET) {
127
133
  infer(from, model)
128
134
  }
@@ -168,7 +174,7 @@ function infer(originalQuery, model) {
168
174
  // we need to search for first step in ´model.definitions[infixAlias]`
169
175
  if ($baseLink) {
170
176
  const { definition } = $baseLink
171
- const elements = definition._target?.elements || definition.elements
177
+ const elements = getDefinition(definition.target)?.elements || definition.elements
172
178
  const e = elements?.[id] || cds.error`"${id}" not found in the elements of "${definition.name}"`
173
179
  if (e.target) {
174
180
  // only fk access in infix filter
@@ -188,22 +194,22 @@ function infer(originalQuery, model) {
188
194
  Object.defineProperty(arg, 'flatName', { value: ref.join('_'), writable: true })
189
195
  } else {
190
196
  // must be in model.definitions
191
- const definition = getDefinition(id, model)
197
+ const definition = getDefinition(id) || cds.error`"${id}" not found in the definitions of your model`
192
198
  arg.$refLinks[0] = { definition, target: definition }
193
199
  }
194
200
  } else {
195
201
  const recent = arg.$refLinks[i - 1]
196
- const { elements } = recent.definition._target || recent.definition
202
+ const { elements } = getDefinition(recent.definition.target) || recent.definition
197
203
  const e = elements[id]
198
204
  if (!e) throw new Error(`"${id}" not found in the elements of "${arg.$refLinks[i - 1].definition.name}"`)
199
- arg.$refLinks.push({ definition: e, target: e._target || e })
205
+ arg.$refLinks.push({ definition: e, target: getDefinition(e.target) || e })
200
206
  }
201
207
  arg.$refLinks[i].alias = !ref[i + 1] && arg.as ? arg.as : id.split('.').pop()
202
208
 
203
209
  // link refs in where
204
210
  if (step.where) {
205
211
  // REVISIT: why do we need to walk through these so early?
206
- if (arg.$refLinks[i].definition.kind === 'entity' || arg.$refLinks[i].definition._target) {
212
+ if (arg.$refLinks[i].definition.kind === 'entity' || getDefinition(arg.$refLinks[i].definition.target)) {
207
213
  let existsPredicate = false
208
214
  const walkTokenStream = token => {
209
215
  if (token === 'exists') {
@@ -243,7 +249,7 @@ function infer(originalQuery, model) {
243
249
  function inferCombinedElements() {
244
250
  const combinedElements = {}
245
251
  for (const index in sources) {
246
- const tableAlias = sources[index]
252
+ const tableAlias = getDefinitionFromSources(sources, index)
247
253
  for (const key in tableAlias.elements) {
248
254
  if (key in combinedElements) combinedElements[key].push({ index, tableAlias })
249
255
  else combinedElements[key] = [{ index, tableAlias }]
@@ -497,24 +503,30 @@ function infer(originalQuery, model) {
497
503
  nameSegments.push(id)
498
504
  } else if ($baseLink) {
499
505
  const { definition, target } = $baseLink
500
- const elements = definition._target?.elements || definition.elements
506
+ const elements = getDefinition(definition.target)?.elements || definition.elements
501
507
  if (elements && id in elements) {
502
508
  const element = elements[id]
503
509
  rejectNonFkAccess(element)
504
- const resolvableIn = definition.target ? definition._target : target
510
+ const resolvableIn = getDefinition(definition.target) || target
505
511
  column.$refLinks.push({ definition: elements[id], target: resolvableIn })
506
512
  } else {
507
513
  stepNotFoundInPredecessor(id, definition.name)
508
514
  }
509
515
  nameSegments.push(id)
510
516
  } else if (firstStepIsTableAlias) {
511
- column.$refLinks.push({ definition: sources[id], target: sources[id] })
517
+ column.$refLinks.push({
518
+ definition: getDefinitionFromSources(sources, id),
519
+ target: getDefinitionFromSources(sources, id),
520
+ })
512
521
  } else if (firstStepIsSelf) {
513
522
  column.$refLinks.push({ definition: { elements: queryElements }, target: { elements: queryElements } })
514
523
  } else if (column.ref.length > 1 && inferred.outerQueries?.find(outer => id in outer.sources)) {
515
524
  // outer query accessed via alias
516
525
  const outerAlias = inferred.outerQueries.find(outer => id in outer.sources)
517
- column.$refLinks.push({ definition: outerAlias.sources[id], target: outerAlias.sources[id] })
526
+ column.$refLinks.push({
527
+ definition: getDefinitionFromSources(outerAlias.sources, id),
528
+ target: getDefinitionFromSources(outerAlias.sources, id),
529
+ })
518
530
  } else if (id in $combinedElements) {
519
531
  if ($combinedElements[id].length > 1) stepIsAmbiguous(id) // exit
520
532
  const definition = $combinedElements[id][0].tableAlias.elements[id]
@@ -524,15 +536,15 @@ function infer(originalQuery, model) {
524
536
  } else if (expandOnTableAlias) {
525
537
  // expand on table alias
526
538
  column.$refLinks.push({
527
- definition: sources[id],
528
- target: sources[id],
539
+ definition: getDefinitionFromSources(sources, id),
540
+ target: getDefinitionFromSources(sources, id),
529
541
  })
530
542
  } else {
531
543
  stepNotFoundInCombinedElements(id) // REVISIT: fails with {__proto__:elements)
532
544
  }
533
545
  } else {
534
546
  const { definition } = column.$refLinks[i - 1]
535
- const elements = definition._target?.elements || definition.elements //> go for assoc._target first, instead of assoc as struct
547
+ const elements = getDefinition(definition.target)?.elements || definition.elements //> go for assoc._target first, instead of assoc as struct
536
548
  const element = elements?.[id]
537
549
 
538
550
  if (firstStepIsSelf && element?.isAssociation) {
@@ -543,7 +555,7 @@ function infer(originalQuery, model) {
543
555
  )
544
556
  }
545
557
 
546
- const target = definition._target || column.$refLinks[i - 1].target
558
+ const target = getDefinition(definition.target) || column.$refLinks[i - 1].target
547
559
  if (element) {
548
560
  if ($baseLink) rejectNonFkAccess(element)
549
561
  const $refLink = { definition: elements[id], target }
@@ -602,7 +614,8 @@ function infer(originalQuery, model) {
602
614
  }
603
615
 
604
616
  column.$refLinks[i].alias = !column.ref[i + 1] && column.as ? column.as : id.split('.').pop()
605
- if (column.$refLinks[i].definition._target?.['@cds.persistence.skip'] === true) isPersisted = false
617
+ if (getDefinition(column.$refLinks[i].definition.target)?.['@cds.persistence.skip'] === true)
618
+ isPersisted = false
606
619
  if (!column.ref[i + 1]) {
607
620
  const flatName = nameSegments.join('_')
608
621
  Object.defineProperty(column, 'flatName', { value: flatName, writable: true })
@@ -673,9 +686,7 @@ function infer(originalQuery, model) {
673
686
  // ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
674
687
  if (column.expand) {
675
688
  const { $refLinks } = column
676
- const skip = $refLinks.some(
677
- link => model.definitions[link.definition.target]?.['@cds.persistence.skip'] === true,
678
- )
689
+ const skip = $refLinks.some(link => getDefinition(link.definition.target)?.['@cds.persistence.skip'] === true)
679
690
  if (skip) {
680
691
  $refLinks[$refLinks.length - 1].skipExpand = true
681
692
  return
@@ -722,7 +733,8 @@ function infer(originalQuery, model) {
722
733
  if (inlineCol === '*') {
723
734
  const wildCardElements = {}
724
735
  // either the `.elements´ of the struct or the `.elements` of the assoc target
725
- const leafLinkElements = $leafLink.definition._target?.elements || $leafLink.definition.elements
736
+ const leafLinkElements =
737
+ getDefinition($leafLink.definition.target)?.elements || $leafLink.definition.elements
726
738
  Object.entries(leafLinkElements).forEach(([k, v]) => {
727
739
  const name = namePrefix ? `${namePrefix}_${k}` : k
728
740
  // if overwritten/excluded omit from wildcard elements
@@ -768,10 +780,11 @@ function infer(originalQuery, model) {
768
780
  function resolveExpand(col) {
769
781
  const { expand, $refLinks } = col
770
782
  const $leafLink = $refLinks?.[$refLinks.length - 1] || inferred.SELECT.from.$refLinks.at(-1) // fallback to anonymous expand
771
- if ($leafLink.definition._target) {
783
+ const target = getDefinition($leafLink.definition.target)
784
+ if (target) {
772
785
  const expandSubquery = {
773
786
  SELECT: {
774
- from: $leafLink.definition._target.name,
787
+ from: target.name,
775
788
  columns: expand.filter(c => !c.inline),
776
789
  },
777
790
  }
@@ -812,6 +825,7 @@ function infer(originalQuery, model) {
812
825
  function stepNotFoundInCombinedElements(step) {
813
826
  throw new Error(
814
827
  `"${step}" not found in the elements of ${Object.values(sources)
828
+ .map(s => s.definition)
815
829
  .map(def => `"${def.name || /* subquery */ def.as}"`)
816
830
  .join(', ')}`,
817
831
  )
@@ -972,12 +986,12 @@ function infer(originalQuery, model) {
972
986
  const exclude = _.excluding ? x => _.excluding.includes(x) : () => false
973
987
 
974
988
  if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
975
- const { elements } = sources[aliases[0]]
989
+ const { elements } = getDefinitionFromSources(sources, aliases[0])
976
990
  // only one query source and no overwritten columns
977
991
  Object.keys(elements)
978
992
  .filter(k => !exclude(k))
979
993
  .forEach(k => {
980
- const element = sources[aliases[0]].elements[k]
994
+ const element = elements[k]
981
995
  if (element.type !== 'cds.LargeBinary') queryElements[k] = element
982
996
  if (element.value) {
983
997
  linkCalculatedElement(element)
@@ -1105,9 +1119,14 @@ function infer(originalQuery, model) {
1105
1119
  }
1106
1120
  }
1107
1121
 
1108
- /** gets the CSN element for the given name from the model */
1109
- function getDefinition(name, model) {
1110
- return model.definitions[name] || cds.error`"${name}" not found in the definitions of your model`
1122
+ /** returns the CSN definition for the given name from the model */
1123
+ function getDefinition(name) {
1124
+ if (!name) return null
1125
+ return model.definitions[name]
1126
+ }
1127
+
1128
+ function getDefinitionFromSources(sources, id) {
1129
+ return sources[id].definition
1111
1130
  }
1112
1131
 
1113
1132
  /**
@@ -1129,6 +1148,7 @@ function infer(originalQuery, model) {
1129
1148
  }, '')
1130
1149
  }
1131
1150
  }
1151
+
1132
1152
  /**
1133
1153
  * Returns true if e is a foreign key of assoc.
1134
1154
  * this function is also compatible with unfolded csn (UCSN),
@@ -43,13 +43,17 @@ class Node {
43
43
  * @param {parent} parent
44
44
  * @param {where} where
45
45
  */
46
- constructor($refLink, parent, where = null) {
46
+ constructor($refLink, parent, where = null, args = null) {
47
47
  /** @type {$refLink} - A reference link to this node. */
48
48
  this.$refLink = $refLink
49
49
  /** @type {parent} - The parent Node of this node. */
50
50
  this.parent = parent
51
51
  /** @type {where} - An optional condition to be applied to this node. */
52
52
  this.where = where
53
+ /** @type {args} - optional parameter object to be applied to this node. */
54
+ const targetHasParams = $refLink.definition._target?.params || $refLink.definition._target?.['@cds.persistence.udf']
55
+ if (!args && targetHasParams) args = {} // if no args are provided, provide empty argument list
56
+ this.args = args
53
57
  /** @type {children} - A Map of children nodes belonging to this node. */
54
58
  this.children = new Map()
55
59
  }
@@ -63,9 +67,13 @@ class Root {
63
67
  * @param {[alias, queryArtifact]} querySource
64
68
  */
65
69
  constructor(querySource) {
66
- const [alias, queryArtifact] = querySource
70
+ let [alias, { definition, args }] = querySource
67
71
  /** @type {queryArtifact} - The artifact used to make the query. */
68
- this.queryArtifact = queryArtifact
72
+ this.queryArtifact = definition
73
+ /** @type {args} - optional parameter object to be applied to this node. */
74
+ const definitionHasParams = definition.params || definition['@cds.persistence.udf']
75
+ if (!args && definitionHasParams) args = {} // if no args are provided, provide empty argument list
76
+ this.args = args
69
77
  /** @type {alias} - The alias of the artifact. */
70
78
  this.alias = alias
71
79
  /** @type {parent} - The parent Node of this root, null for the root Node. */
@@ -170,8 +178,8 @@ class JoinTree {
170
178
 
171
179
  while (i < col.ref.length) {
172
180
  const step = col.ref[i]
173
- const { where } = step
174
- const id = where ? step.id + JSON.stringify(where) : step
181
+ const { where, args } = step
182
+ const id = joinId(step, args, where)
175
183
  const next = node.children.get(id)
176
184
  const $refLink = col.$refLinks[i]
177
185
  if (next) {
@@ -187,7 +195,7 @@ class JoinTree {
187
195
  node.$refLink.onlyForeignKeyAccess = false
188
196
  return true
189
197
  }
190
- const child = new Node($refLink, node, where)
198
+ const child = new Node($refLink, node, where, args)
191
199
  if (child.$refLink.definition.isAssociation) {
192
200
  if (child.where || col.inline) {
193
201
  // filter is always join relevant
@@ -212,6 +220,14 @@ class JoinTree {
212
220
  i += 1
213
221
  }
214
222
  return true
223
+
224
+ function joinId(step, args, where) {
225
+ let appendix
226
+ if (where && args) appendix = JSON.stringify(where) + JSON.stringify(args)
227
+ else if (where) appendix = JSON.stringify(where)
228
+ else if (args) appendix = JSON.stringify(args)
229
+ return appendix ? step.id + appendix : step
230
+ }
215
231
  }
216
232
 
217
233
  /**
package/lib/search.js CHANGED
@@ -10,10 +10,16 @@ const DRAFT_COLUMNS_UNION = {
10
10
  }
11
11
  const DEFAULT_SEARCHABLE_TYPE = 'cds.String'
12
12
 
13
+ // only those which return strings are relevant for search
14
+ const aggregateFunctions = {
15
+ MAX: true,
16
+ MIN: true,
17
+ }
18
+
13
19
  /**
14
20
  * This method gets all columns for an entity.
15
21
  * It includes the generated foreign keys from managed associations, structured elements and complex and custom types.
16
- * As well, it provides the annotations starting with '@' for each column.
22
+ * Moreover, it provides the annotations starting with '@' for each column.
17
23
  *
18
24
  * @param {object} entity - the csn entity
19
25
  * @param {object} [options]
@@ -120,37 +126,49 @@ const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }, a
120
126
  // aggregations case
121
127
  // in the new parser groupBy is moved to sub select.
122
128
  if (cqn._aggregated || /* new parser */ cqn.SELECT.groupBy || cqn.SELECT?.from?.SELECT?.groupBy) {
123
- cqn.SELECT.columns &&
124
- cqn.SELECT.columns.forEach(column => {
125
- if (column.func) {
126
- // exclude $count by SELECT of number of Items in a Collection
127
- if (
128
- cqn.SELECT.columns.length === 1 &&
129
- column.func === 'count' &&
130
- (column.as === '_counted_' || column.as === '$count')
131
- ) {
132
- return
133
- }
134
-
135
- toBeSearched.push(column)
129
+ cqn.SELECT.columns?.forEach(column => {
130
+ if (column.func || column.xpr) {
131
+ // exclude $count by SELECT of number of Items in a Collection
132
+ if (
133
+ cqn.SELECT.columns.length === 1 &&
134
+ column.func === 'count' &&
135
+ (column.as === '_counted_' || column.as === '$count')
136
+ ) {
136
137
  return
137
138
  }
138
139
 
139
- const columnRef = column.ref
140
- if (columnRef) {
141
- if (entity.elements[columnRef[columnRef.length - 1]]?._type !== DEFAULT_SEARCHABLE_TYPE) return
142
- column = { ref: [...column.ref] }
143
- if (alias) column.ref.unshift(alias)
144
- toBeSearched.push(column)
140
+ // only strings can be searched
141
+ if (column.element.type !== DEFAULT_SEARCHABLE_TYPE) {
142
+ if (column.xpr) return
143
+ if (column.func && !(column.func in aggregateFunctions)) return
145
144
  }
146
- })
145
+
146
+ const searchTerm = {}
147
+ if (column.func) {
148
+ searchTerm.func = column.func
149
+ searchTerm.args = column.args
150
+ } else if (column.xpr) {
151
+ searchTerm.xpr = column.xpr
152
+ }
153
+ toBeSearched.push(searchTerm)
154
+ return
155
+ }
156
+
157
+ // no need to set ref[0] to alias, because columns were already properly transformed
158
+ if (column.ref) {
159
+ if (column.element.type !== DEFAULT_SEARCHABLE_TYPE) return
160
+ column = { ref: [...column.ref] }
161
+ toBeSearched.push(column)
162
+ return
163
+ }
164
+ })
147
165
  } else {
148
166
  toBeSearched = entity.own('__searchableColumns') || entity.set('__searchableColumns', _getSearchableColumns(entity))
149
167
  if (cqn.SELECT.groupBy) toBeSearched = toBeSearched.filter(tbs => cqn.SELECT.groupBy.some(gb => gb.ref[0] === tbs))
150
168
  toBeSearched = toBeSearched.map(c => {
151
- const col = { ref: [c] }
152
- if (alias) col.ref.unshift(alias)
153
- return col
169
+ const column = { ref: [c] }
170
+ if (alias) column.ref.unshift(alias)
171
+ return column
154
172
  })
155
173
  }
156
174
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.6.4",
3
+ "version": "1.7.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": {
@@ -31,9 +31,5 @@
31
31
  "peerDependencies": {
32
32
  "@sap/cds": ">=7.6"
33
33
  },
34
- "license": "SEE LICENSE",
35
- "devDependencies": {
36
- "@typescript-eslint/eslint-plugin": "^6.2.0",
37
- "typescript": "^5.1.6"
38
- }
34
+ "license": "SEE LICENSE"
39
35
  }