@cap-js/db-service 1.17.2 → 1.19.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/lib/cqn2sql.js CHANGED
@@ -25,7 +25,7 @@ class CQN2SQLRenderer {
25
25
  if (cds.env.sql.names === 'quoted') {
26
26
  this.class.prototype.name = (name, query) => {
27
27
  const e = name.id || name
28
- return (query?.target || this.model?.definitions[e])?.['@cds.persistence.name'] || e
28
+ return (query?._target || this.model?.definitions[e])?.['@cds.persistence.name'] || e
29
29
  }
30
30
  this.class.prototype.quote = (s) => `"${String(s).replace(/"/g, '""')}"`
31
31
  }
@@ -92,7 +92,7 @@ class CQN2SQLRenderer {
92
92
  if (values && !Array.isArray(values)) {
93
93
  values = [values]
94
94
  }
95
- DEBUG(this.sql, ...values)
95
+ DEBUG(this.sql, values)
96
96
  }
97
97
 
98
98
 
@@ -105,7 +105,7 @@ class CQN2SQLRenderer {
105
105
  * @returns {import('./infer/cqn').Query}
106
106
  */
107
107
  infer(q) {
108
- return q.target ? q : cds_infer(q)
108
+ return q._target instanceof cds.entity ? q : cds_infer(q)
109
109
  }
110
110
 
111
111
  cqn4sql(q) {
@@ -226,7 +226,7 @@ class CQN2SQLRenderer {
226
226
  * @param {import('./infer/cqn').SELECT} q
227
227
  */
228
228
  SELECT(q) {
229
- let { from, expand, where, groupBy, having, orderBy, limit, one, distinct, localized, forUpdate, forShareLock } =
229
+ let { from, expand, where, groupBy, having, orderBy, limit, one, distinct, localized, forUpdate, forShareLock, recurse } =
230
230
  q.SELECT
231
231
 
232
232
  if (from?.join && !q.SELECT.columns) {
@@ -239,12 +239,13 @@ class CQN2SQLRenderer {
239
239
  let sql = `SELECT`
240
240
  if (distinct) sql += ` DISTINCT`
241
241
  if (!_empty(columns)) sql += ` ${columns}`
242
- if (!_empty(from)) sql += ` FROM ${this.from(from, q)}`
242
+ if (recurse) sql += ` FROM ${this.SELECT_recurse(q)}`
243
+ else if (!_empty(from)) sql += ` FROM ${this.from(from, q)}`
243
244
  else sql += this.from_dummy()
244
- if (!_empty(where)) sql += ` WHERE ${this.where(where)}`
245
- if (!_empty(groupBy)) sql += ` GROUP BY ${this.groupBy(groupBy)}`
246
- if (!_empty(having)) sql += ` HAVING ${this.having(having)}`
247
- if (!_empty(orderBy)) sql += ` ORDER BY ${this.orderBy(orderBy, localized)}`
245
+ if (!recurse && !_empty(where)) sql += ` WHERE ${this.where(where)}`
246
+ if (!recurse && !_empty(groupBy)) sql += ` GROUP BY ${this.groupBy(groupBy)}`
247
+ if (!recurse && !_empty(having)) sql += ` HAVING ${this.having(having)}`
248
+ if (!recurse && !_empty(orderBy)) sql += ` ORDER BY ${this.orderBy(orderBy, localized)}`
248
249
  if (one) limit = Object.assign({}, limit, { rows: { val: 1 } })
249
250
  if (limit) sql += ` LIMIT ${this.limit(limit)}`
250
251
  if (forUpdate) sql += ` ${this.forUpdate(forUpdate)}`
@@ -257,13 +258,23 @@ class CQN2SQLRenderer {
257
258
  return (this.sql = sql)
258
259
  }
259
260
 
261
+ SELECT_recurse() {
262
+ cds.error`Feature "recurse" queries not supported.`
263
+ }
264
+
260
265
  /**
261
266
  * Renders a column clause into generic SQL
262
267
  * @param {import('./infer/cqn').SELECT} param0
263
268
  * @returns {string} SQL
264
269
  */
265
270
  SELECT_columns(q) {
266
- return (q.SELECT.columns ?? ['*']).map(x => this.column_expr(x, q))
271
+ const ret = []
272
+ const arr = q.SELECT.columns ?? ['*']
273
+ for (const x of arr) {
274
+ if (x.SELECT?.count) arr.push(this.SELECT_count(x))
275
+ ret.push(this.column_expr(x, q))
276
+ }
277
+ return ret
267
278
  }
268
279
 
269
280
  /**
@@ -292,24 +303,12 @@ class CQN2SQLRenderer {
292
303
  ? x => {
293
304
  const name = this.column_name(x)
294
305
  const escaped = `${name.replace(/"/g, '""')}`
295
- let col = `${this.output_converter4(x.element, this.quote(name))} AS "${escaped}"`
296
- if (x.SELECT?.count) {
297
- // Return both the sub select and the count for @odata.count
298
- const qc = cds.ql.clone(x, { columns: [{ func: 'count' }], one: 1, limit: 0, orderBy: 0 })
299
- return [col, `${this.expr(qc)} AS "${escaped}@odata.count"`]
300
- }
301
- return col
306
+ return `${this.output_converter4(x.element, this.quote(name))} AS "${escaped}"`
302
307
  }
303
308
  : x => {
304
309
  const name = this.column_name(x)
305
310
  const escaped = `${name.replace(/"/g, '""')}`
306
- let col = `'$."${escaped}"',${this.output_converter4(x.element, this.quote(name))}`
307
- if (x.SELECT?.count) {
308
- // Return both the sub select and the count for @odata.count
309
- const qc = cds.ql.clone(x, { columns: [{ func: 'count' }], one: 1, limit: 0, orderBy: 0 })
310
- return [col, `'$."${escaped}@odata.count"',${this.expr(qc)}`]
311
- }
312
- return col
311
+ return `'$."${escaped}"',${this.output_converter4(x.element, this.quote(name))}`
313
312
  }).flat()
314
313
 
315
314
  if (isSimple) return `SELECT ${cols} FROM (${sql})`
@@ -322,6 +321,17 @@ class CQN2SQLRenderer {
322
321
  return `SELECT ${isRoot || SELECT.one ? obj.replace('jsonb', 'json') : `jsonb_group_array(${obj})`} as _json_ FROM (${sql})`
323
322
  }
324
323
 
324
+ SELECT_count(q) {
325
+ const countQuery = cds.ql.clone(q, {
326
+ columns: [{ func: 'count' }],
327
+ one: 0, limit: 0, orderBy: 0, expand: 0, count: 0
328
+ })
329
+ countQuery.as = q.as + '@odata.count'
330
+ countQuery.elements = undefined
331
+ countQuery.element = cds.builtin.types.Int64
332
+ return countQuery
333
+ }
334
+
325
335
  /**
326
336
  * Renders a SELECT column expression into generic SQL
327
337
  * @param {import('./infer/cqn').col} x
@@ -416,14 +426,15 @@ class CQN2SQLRenderer {
416
426
  * @returns {string[] | string} SQL
417
427
  */
418
428
  orderBy(orderBy, localized) {
419
- return orderBy.map(
420
- localized
421
- ? c =>
422
- this.expr(c) +
429
+ return orderBy.map(c => {
430
+ const o = localized
431
+ ? this.expr(c) +
423
432
  (c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
424
433
  (c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
425
- : c => this.expr(c) + (c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC'),
426
- )
434
+ : this.expr(c) + (c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
435
+ if (c.nulls) return o + ' NULLS ' + (c.nulls.toLowerCase() === 'first' ? 'FIRST' : 'LAST')
436
+ return o
437
+ })
427
438
  }
428
439
 
429
440
  /**
@@ -443,9 +454,10 @@ class CQN2SQLRenderer {
443
454
  * @returns {string} SQL
444
455
  */
445
456
  forUpdate(update) {
446
- const { wait, of } = update
457
+ const { wait, of, ignoreLocked } = update
447
458
  let sql = 'FOR UPDATE'
448
459
  if (!_empty(of)) sql += ` OF ${of.map(x => this.expr(x)).join(', ')}`
460
+ if (ignoreLocked) sql += ' IGNORE LOCKED'
449
461
  if (typeof wait === 'number') sql += ` WAIT ${wait}`
450
462
  return sql
451
463
  }
@@ -490,7 +502,7 @@ class CQN2SQLRenderer {
490
502
  */
491
503
  INSERT_entries(q) {
492
504
  const { INSERT } = q
493
- const elements = q.elements || q.target?.elements
505
+ const elements = q.elements || q._target?.elements
494
506
  if (!elements && !INSERT.entries?.length) {
495
507
  return // REVISIT: mtx sends an insert statement without entries and no reference entity
496
508
  }
@@ -502,7 +514,7 @@ class CQN2SQLRenderer {
502
514
  this.columns = columns
503
515
 
504
516
  const alias = INSERT.into.as
505
- const entity = this.name(q.target?.name || INSERT.into.ref[0], q)
517
+ const entity = this.name(q._target?.name || INSERT.into.ref[0], q)
506
518
  if (!elements) {
507
519
  this.entries = INSERT.entries.map(e => columns.map(c => e[c]))
508
520
  const param = this.param.bind(this, { ref: ['?'] })
@@ -528,7 +540,7 @@ class CQN2SQLRenderer {
528
540
  }
529
541
 
530
542
  async *INSERT_entries_stream(entries, binaryEncoding = 'base64') {
531
- const elements = this.cqn.target?.elements || {}
543
+ const elements = this.cqn._target?.elements || {}
532
544
  const bufferLimit = 65536 // 1 << 16
533
545
  let buffer = '['
534
546
 
@@ -577,7 +589,7 @@ class CQN2SQLRenderer {
577
589
  }
578
590
 
579
591
  async *INSERT_rows_stream(entries, binaryEncoding = 'base64') {
580
- const elements = this.cqn.target?.elements || {}
592
+ const elements = this.cqn._target?.elements || {}
581
593
  const bufferLimit = 65536 // 1 << 16
582
594
  let buffer = '['
583
595
 
@@ -630,9 +642,9 @@ class CQN2SQLRenderer {
630
642
  */
631
643
  INSERT_rows(q) {
632
644
  const { INSERT } = q
633
- const entity = this.name(q.target?.name || INSERT.into.ref[0], q)
645
+ const entity = this.name(q._target?.name || INSERT.into.ref[0], q)
634
646
  const alias = INSERT.into.as
635
- const elements = q.elements || q.target?.elements
647
+ const elements = q.elements || q._target?.elements
636
648
  const columns = this.columns = INSERT.columns || cds.error`Cannot insert rows without columns or elements`
637
649
 
638
650
  if (!elements) {
@@ -676,9 +688,9 @@ class CQN2SQLRenderer {
676
688
  */
677
689
  INSERT_select(q) {
678
690
  const { INSERT } = q
679
- const entity = this.name(q.target.name, q)
691
+ const entity = this.name(q._target.name, q)
680
692
  const alias = INSERT.into.as
681
- const elements = q.elements || q.target?.elements || {}
693
+ const elements = q.elements || q._target?.elements || {}
682
694
  const columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
683
695
  c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
684
696
  ))
@@ -719,15 +731,15 @@ class CQN2SQLRenderer {
719
731
  const { UPSERT } = q
720
732
 
721
733
  let sql = this.INSERT({ __proto__: q, INSERT: UPSERT })
722
- if (!q.target?.keys) return sql
734
+ if (!q._target?.keys) return sql
723
735
  const keys = []
724
- for (const k of ObjectKeys(q.target?.keys)) {
725
- const element = q.target.keys[k]
736
+ for (const k of ObjectKeys(q._target?.keys)) {
737
+ const element = q._target.keys[k]
726
738
  if (element.isAssociation || element.virtual) continue
727
739
  keys.push(k)
728
740
  }
729
741
 
730
- const elements = q.target?.elements || {}
742
+ const elements = q._target?.elements || {}
731
743
  // temporal data
732
744
  for (const k of ObjectKeys(elements)) {
733
745
  if (elements[k]['@cds.valid.from']) keys.push(k)
@@ -744,7 +756,7 @@ class CQN2SQLRenderer {
744
756
  .filter(c => keys.includes(c.name))
745
757
  .map(c => `${c.onInsert || c.sql} as ${this.quote(c.name)}`)
746
758
 
747
- const entity = this.name(q.target?.name || UPSERT.into.ref[0], q)
759
+ const entity = this.name(q._target?.name || UPSERT.into.ref[0], q)
748
760
  sql = `SELECT ${managed.map(c => c.upsert
749
761
  .replace(/value->/g, '"$$$$value$$$$"->')
750
762
  .replace(/json_type\(value,/g, 'json_type("$$$$value$$$$",'))
@@ -773,7 +785,7 @@ class CQN2SQLRenderer {
773
785
  */
774
786
  UPDATE(q) {
775
787
  const { entity, with: _with, data, where } = q.UPDATE
776
- const elements = q.target?.elements
788
+ const elements = q._target?.elements
777
789
  let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity, q))}`
778
790
  if (entity.as) sql += ` AS ${this.quote(entity.as)}`
779
791
 
package/lib/cqn4sql.js CHANGED
@@ -1,10 +1,11 @@
1
1
  'use strict'
2
2
 
3
3
  const cds = require('@sap/cds')
4
+ cds.infer.target ??= q => q._target || q.target // instanceof cds.entity ? q._target : q.target
4
5
 
5
6
  const infer = require('./infer')
6
7
  const { computeColumnsToBeSearched } = require('./search')
7
- const { prettyPrintRef, isCalculatedOnRead, isCalculatedElement } = require('./utils')
8
+ const { prettyPrintRef, isCalculatedOnRead, isCalculatedElement, getImplicitAlias } = require('./utils')
8
9
 
9
10
  /**
10
11
  * For operators of <eqOps>, this is replaced by comparing all leaf elements with null, combined with and.
@@ -60,6 +61,22 @@ function cqn4sql(originalQuery, model) {
60
61
  else if (having) inferred.SELECT.having = having
61
62
  }
62
63
  }
64
+ // query modifiers can also be defined in from ref leaf infix filter
65
+ // > SELECT from bookshop.Books[order by price] {ID}
66
+ if (inferred.SELECT?.from.ref) {
67
+ for (const [key, val] of Object.entries(inferred.SELECT.from.ref.at(-1))) {
68
+ if (key in { orderBy: 1, groupBy: 1 }) {
69
+ if (inferred.SELECT[key]) inferred.SELECT[key].push(...val)
70
+ else inferred.SELECT[key] = val
71
+ } else if (key === 'limit') {
72
+ // limit defined on the query has precedence
73
+ if (!inferred.SELECT.limit) inferred.SELECT.limit = val
74
+ } else if (key === 'having') {
75
+ if (!inferred.SELECT.having) inferred.SELECT.having = val
76
+ else inferred.SELECT.having.push('and', ...val)
77
+ }
78
+ }
79
+ }
63
80
  inferred = infer(inferred, model)
64
81
  // if the query has custom joins we don't want to transform it
65
82
  // TODO: move all the way to the top of this function once cds.infer supports joins as well
@@ -208,7 +225,8 @@ function cqn4sql(originalQuery, model) {
208
225
  */
209
226
  function transformQueryForInsertUpsert(kind) {
210
227
  const { as } = transformedQuery[kind].into
211
- transformedQuery[kind].into = { ref: [inferred.target.name] }
228
+ const target = cds.infer.target (inferred) // REVISIT: we should reliably use inferred._target instead
229
+ transformedQuery[kind].into = { ref: [target.name] }
212
230
  if (as) transformedQuery[kind].into.as = as
213
231
  return transformedQuery
214
232
  }
@@ -784,7 +802,7 @@ function cqn4sql(originalQuery, model) {
784
802
  } else {
785
803
  outerAlias = transformedQuery.SELECT.from.as
786
804
  subqueryFromRef = [
787
- ...(transformedQuery.SELECT.from.ref || /* subq in from */ [transformedQuery.SELECT.from.target.name]),
805
+ ...(transformedQuery.SELECT.from.ref || /* subq in from */ transformedQuery.SELECT.from.SELECT.from.ref),
788
806
  ...ref,
789
807
  ]
790
808
  }
@@ -800,18 +818,20 @@ function cqn4sql(originalQuery, model) {
800
818
  return _subqueryForGroupBy(column, baseRef, columnAlias)
801
819
  }
802
820
 
803
- // we need to respect the aliases of the outer query, so the columnAlias might not be suitable
804
- // as table alias for the correlated subquery
805
- const uniqueSubqueryAlias = getNextAvailableTableAlias(columnAlias, inferred.outerQueries)
821
+ // Alias in expand subquery is derived from but not equal to
822
+ // the alias of the column because to account for potential ambiguities
823
+ // the alias cannot be addressed anyways
824
+ const uniqueSubqueryAlias = getNextAvailableTableAlias(getImplicitAlias(columnAlias), inferred.outerQueries)
806
825
 
807
826
  // `SELECT from Authors { books.genre as genreOfBooks { name } } becomes `SELECT from Books:genre as genreOfBooks`
808
827
  const from = { ref: subqueryFromRef, as: uniqueSubqueryAlias }
809
828
  const subqueryBase = {}
810
- for (const [key, value] of Object.entries(column)) {
811
- if (!(key in { ref: true, expand: true })) {
829
+ const queryModifiers = { ...column }
830
+ for (const [key, value] of Object.entries(queryModifiers)) {
831
+ if (key in { limit: 1, orderBy: 1, groupBy: 1, excluding: 1, where: 1, having: 1, count: 1 })
812
832
  subqueryBase[key] = value
813
- }
814
833
  }
834
+
815
835
  const subquery = {
816
836
  SELECT: {
817
837
  ...subqueryBase,
@@ -853,6 +873,7 @@ function cqn4sql(originalQuery, model) {
853
873
  const existsSubqueryAlias = recent[existsIndex + 1].SELECT.from.as
854
874
  if (existsSubqueryAlias === x.ref?.[0]) return { ref: [outer, ...x.ref.slice(1)] }
855
875
  if (x.xpr) x.xpr = x.xpr.map(replaceAliasWithSubqueryAlias)
876
+ if (x.args) x.args = x.args.map(replaceAliasWithSubqueryAlias)
856
877
  return x
857
878
  }
858
879
  return subq
@@ -877,40 +898,48 @@ function cqn4sql(originalQuery, model) {
877
898
 
878
899
  // to be attached to dummy query
879
900
  const elements = {}
880
- const wildcardIndex = column.expand.findIndex(e => e === '*')
881
- if (wildcardIndex !== -1) {
901
+ const containsWildcard = column.expand.includes('*')
902
+ if (containsWildcard) {
882
903
  // expand with wildcard vanishes as expand is part of the group by (OData $apply + $expand)
883
904
  return null
884
905
  }
885
- const expandedColumns = column.expand.flatMap(expand => {
886
- if (!expand.ref) return expand
887
- const fullRef = [...baseRef, ...expand.ref]
888
-
889
- if (expand.expand) {
890
- const nested = _subqueryForGroupBy(expand, fullRef, expand.as || expand.ref.map(idOnly).join('_'))
891
- setElementOnColumns(nested, expand.element)
892
- elements[expand.as || expand.ref.map(idOnly).join('_')] = nested
893
- return nested
894
- }
906
+ const expandedColumns = column.expand
907
+ .flatMap(expand => {
908
+ if (!expand.ref) return expand
909
+ const fullRef = [...baseRef, ...expand.ref]
910
+
911
+ if (expand.expand) {
912
+ const nested = _subqueryForGroupBy(expand, fullRef, expand.as || expand.ref.map(idOnly).join('_'))
913
+ if (nested) {
914
+ setElementOnColumns(nested, expand.element)
915
+ elements[expand.as || expand.ref.map(idOnly).join('_')] = nested
916
+ }
917
+ return nested
918
+ }
895
919
 
896
- const groupByRef = groupByLookup.get(fullRef.map(refWithConditions).join('.'))
897
- if (!groupByRef) {
898
- throw new Error(
899
- `The expanded column "${fullRef.map(refWithConditions).join('.')}" must be part of the group by clause`,
900
- )
901
- }
920
+ const groupByRef = groupByLookup.get(fullRef.map(refWithConditions).join('.'))
921
+ if (!groupByRef) {
922
+ throw new Error(
923
+ `The expanded column "${fullRef.map(refWithConditions).join('.')}" must be part of the group by clause`,
924
+ )
925
+ }
902
926
 
903
- const copy = Object.create(groupByRef)
904
- // always alias for this special case, so that they nested element names match the expected result structure
905
- // otherwise we'd get `author { <outer>.author_ID }`, but we need `author { <outer>.author_ID as ID }`
906
- copy.as = expand.as || expand.ref.at(-1)
907
- const tableAlias = getTableAlias(copy)
908
- const res = getFlatColumnsFor(copy, { tableAlias })
909
- res.forEach(c => {
910
- elements[c.as || c.ref.at(-1)] = c.element
927
+ const copy = Object.create(groupByRef)
928
+ // always alias for this special case, so that they nested element names match the expected result structure
929
+ // otherwise we'd get `author { <outer>.author_ID }`, but we need `author { <outer>.author_ID as ID }`
930
+ copy.as = expand.as || expand.ref.at(-1)
931
+ const tableAlias = getTableAlias(copy)
932
+ const res = getFlatColumnsFor(copy, { tableAlias })
933
+ res.forEach(c => {
934
+ elements[c.as || c.ref.at(-1)] = c.element
935
+ })
936
+ return res
911
937
  })
912
- return res
913
- })
938
+ .filter(c => c)
939
+
940
+ if (expandedColumns.length === 0) {
941
+ return null
942
+ }
914
943
 
915
944
  const SELECT = {
916
945
  from: null,
@@ -1041,7 +1070,8 @@ function cqn4sql(originalQuery, model) {
1041
1070
  outerQueries.push(inferred)
1042
1071
  Object.defineProperty(q, 'outerQueries', { value: outerQueries })
1043
1072
  }
1044
- if (isLocalized(inferred.target)) q.SELECT.localized = true
1073
+ const target = cds.infer.target (inferred) // REVISIT: we should reliably use inferred._target instead
1074
+ if (isLocalized(target)) q.SELECT.localized = true
1045
1075
  if (q.SELECT.from.ref && !q.SELECT.from.as) assignUniqueSubqueryAlias()
1046
1076
  return cqn4sql(q, model)
1047
1077
 
@@ -1049,7 +1079,7 @@ function cqn4sql(originalQuery, model) {
1049
1079
  if (q.SELECT.from.uniqueSubqueryAlias) return
1050
1080
  const last = q.SELECT.from.ref.at(-1)
1051
1081
  const uniqueSubqueryAlias = inferred.joinTree.addNextAvailableTableAlias(
1052
- getLastStringSegment(last.id || last),
1082
+ getImplicitAlias(last.id || last),
1053
1083
  inferred.outerQueries,
1054
1084
  )
1055
1085
  Object.defineProperty(q.SELECT.from, 'uniqueSubqueryAlias', { value: uniqueSubqueryAlias })
@@ -1225,8 +1255,7 @@ function cqn4sql(originalQuery, model) {
1225
1255
  if (flattenThisForeignKey) {
1226
1256
  const fkElement = getElementForRef(k.ref, getDefinition(element.target))
1227
1257
  let fkBaseName
1228
- if (!leafAssoc || leafAssoc.onlyForeignKeyAccess)
1229
- fkBaseName = `${baseName}_${k.as || k.ref.at(-1)}`
1258
+ if (!leafAssoc || leafAssoc.onlyForeignKeyAccess) fkBaseName = `${baseName}_${k.as || k.ref.at(-1)}`
1230
1259
  // e.g. if foreign key is accessed via infix filter - use join alias to access key in target
1231
1260
  else fkBaseName = k.ref.at(-1)
1232
1261
  const fkPath = [...csnPath, k.ref.at(-1)]
@@ -1379,7 +1408,7 @@ function cqn4sql(originalQuery, model) {
1379
1408
  j = nextAssocIndex
1380
1409
  }
1381
1410
 
1382
- const as = getNextAvailableTableAlias(getLastStringSegment(next.alias))
1411
+ const as = getNextAvailableTableAlias(getImplicitAlias(next.alias))
1383
1412
  next.alias = as
1384
1413
  if (!next.definition.target) {
1385
1414
  let type = next.definition.type
@@ -1478,8 +1507,7 @@ function cqn4sql(originalQuery, model) {
1478
1507
  // reject associations in expression, except if we are in an infix filter -> $baseLink is set
1479
1508
  assertNoStructInXpr(token, $baseLink)
1480
1509
  // reject virtual elements in expressions as they will lead to a sql error down the line
1481
- if(definition?.virtual)
1482
- throw new Error(`Virtual elements are not allowed in expressions`)
1510
+ if (definition?.virtual) throw new Error(`Virtual elements are not allowed in expressions`)
1483
1511
 
1484
1512
  let result = is_regexp(token?.val) ? token : copy(token) // REVISIT: too expensive! //
1485
1513
  if (token.ref) {
@@ -1676,12 +1704,12 @@ function cqn4sql(originalQuery, model) {
1676
1704
  function _transformFrom() {
1677
1705
  if (typeof from === 'string') {
1678
1706
  // normalize to `ref`, i.e. for `UPDATE.entity('bookshop.Books')`
1679
- return { transformedFrom: { ref: [from], as: getLastStringSegment(from) } }
1707
+ return { transformedFrom: { ref: [from], as: getImplicitAlias(from) } }
1680
1708
  }
1681
1709
  transformedFrom.as =
1682
1710
  from.uniqueSubqueryAlias ||
1683
1711
  from.as ||
1684
- getLastStringSegment(transformedFrom.$refLinks[transformedFrom.$refLinks.length - 1].definition.name)
1712
+ getImplicitAlias(transformedFrom.$refLinks[transformedFrom.$refLinks.length - 1].definition.name)
1685
1713
  const whereExistsSubSelects = []
1686
1714
  const filterConditions = []
1687
1715
  const refReverse = [...from.ref].reverse()
@@ -1703,14 +1731,17 @@ function cqn4sql(originalQuery, model) {
1703
1731
  .findIndex(rl => rl.definition.isAssociation || rl.definition.kind === 'entity')
1704
1732
  next = $refLinksReverse[nextStepIndex]
1705
1733
  }
1706
- let as = getLastStringSegment(next.alias)
1734
+ let as = getImplicitAlias(next.alias)
1707
1735
  /**
1708
1736
  * for an `expand` subquery, we do not need to add
1709
1737
  * the table alias of the `expand` host to the join tree
1710
1738
  * --> This is an artificial query, which will later be correlated
1711
1739
  * with the main query alias. see @function expandColumn()
1740
+ * There is one exception:
1741
+ * - if current and next have the same alias, we need to assign a new alias to the next
1742
+ *
1712
1743
  */
1713
- if (!(inferred.SELECT?.expand === true)) {
1744
+ if (!(inferred.SELECT?.expand === true && current.alias.toLowerCase() !== as.toLowerCase())) {
1714
1745
  as = getNextAvailableTableAlias(as)
1715
1746
  }
1716
1747
  next.alias = as
@@ -1897,9 +1928,11 @@ function cqn4sql(originalQuery, model) {
1897
1928
  })
1898
1929
  let pseudoPath = false
1899
1930
  ref.reduce((prev, res, i) => {
1900
- if (res === '$self')
1931
+ if (res === '$self') {
1901
1932
  // next is resolvable in entity
1933
+ thing.$refLinks.push({ definition: prev, target: prev })
1902
1934
  return prev
1935
+ }
1903
1936
  if (res in pseudos.elements) {
1904
1937
  pseudoPath = true
1905
1938
  thing.$refLinks.push({ definition: pseudos.elements[res], target: pseudos })
@@ -1911,7 +1944,11 @@ function cqn4sql(originalQuery, model) {
1911
1944
  const definition =
1912
1945
  prev?.elements?.[res] || getDefinition(prev?.target)?.elements[res] || pseudos.elements[res]
1913
1946
  const target = getParentEntity(definition)
1914
- thing.$refLinks[i] = { definition, target, alias: definition.name }
1947
+ thing.$refLinks[i] = {
1948
+ definition,
1949
+ target,
1950
+ alias: definition === assocRefLink.definition ? assocRefLink.alias : definition.name,
1951
+ }
1915
1952
  return prev?.elements?.[res] || getDefinition(prev?.target)?.elements[res] || pseudos.elements[res]
1916
1953
  }, assocHost)
1917
1954
  }
@@ -1936,15 +1973,16 @@ function cqn4sql(originalQuery, model) {
1936
1973
  lhs.ref[0] in { $self: true, $projection: true } ? getParentEntity(assocRefLink.definition) : target,
1937
1974
  )
1938
1975
  else {
1939
- const lhsLeafArt = lhs.ref && lhs.$refLinks.at(-1).definition
1940
- const rhsLeafArt = rhs.ref && rhs.$refLinks.at(-1).definition
1976
+ const lhsLeafDef = lhs.ref && lhs.$refLinks.at(-1).definition
1977
+ const rhsLeafDef = rhs.ref && rhs.$refLinks.at(-1).definition
1978
+ const lhsFirstDef = lhs.$refLinks.find(r => r.definition.kind === 'element')?.definition
1941
1979
  // compare structures in on-condition
1942
- if ((lhsLeafArt?.target && rhsLeafArt?.target) || (lhsLeafArt?.elements && rhsLeafArt?.elements)) {
1980
+ if ((lhsLeafDef?.target && rhsLeafDef?.target) || (lhsLeafDef?.elements && rhsLeafDef?.elements)) {
1943
1981
  if (rhs.$refLinks[0].definition !== assocRefLink.definition) {
1944
1982
  rhs.ref.unshift(targetSideRefLink.alias)
1945
1983
  rhs.$refLinks.unshift(targetSideRefLink)
1946
1984
  }
1947
- if (lhs.$refLinks[0].definition !== assocRefLink.definition) {
1985
+ if (lhsFirstDef !== assocRefLink.definition) {
1948
1986
  lhs.ref.unshift(targetSideRefLink.alias)
1949
1987
  lhs.$refLinks.unshift(targetSideRefLink)
1950
1988
  }
@@ -1954,16 +1992,15 @@ function cqn4sql(originalQuery, model) {
1954
1992
  i += res.length
1955
1993
  continue
1956
1994
  }
1957
- // naive assumption: if first step is the association itself, all following ref steps must be resolvable
1995
+ // assumption: if first step is the association itself, all following ref steps must be resolvable
1958
1996
  // within target `assoc.assoc.fk` -> `assoc.assoc_fk`
1959
1997
  else if (
1960
- lhs.$refLinks[0]?.definition ===
1961
- getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]
1998
+ lhsFirstDef === getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]
1962
1999
  )
1963
- result[i].ref = [assocRefLink.alias, lhs.ref.slice(1).join('_')]
2000
+ result[i].ref = [assocRefLink.alias, lhs.ref.slice(lhs.ref[0] === '$self' ? 2 : 1).join('_')]
1964
2001
  // naive assumption: if the path starts with an association which is not the association from
1965
2002
  // which the on-condition originates, it must be a foreign key and hence resolvable in the source
1966
- else if (lhs.$refLinks[0]?.definition.target) result[i].ref = [result[i].ref.join('_')]
2003
+ else if (lhsFirstDef?.target) result[i].ref = [result[i].ref.join('_')]
1967
2004
  }
1968
2005
  }
1969
2006
  if (backlink) {
@@ -1986,7 +2023,7 @@ function cqn4sql(originalQuery, model) {
1986
2023
  // sanity check: error out if we can't produce a join
1987
2024
  if (backlink.keys.length === 0) {
1988
2025
  throw new Error(
1989
- `Path step “${assocRefLink.alias}” is a self comparison with “${getFullName(backlink)}” that has no foreign keys`,
2026
+ `Path step “${assocRefLink.definition.name}” is a self comparison with “${getFullName(backlink)}” that has no foreign keys`,
1990
2027
  )
1991
2028
  }
1992
2029
  // managed backlink -> calculate fk-pk pairs
@@ -2012,7 +2049,7 @@ function cqn4sql(originalQuery, model) {
2012
2049
  if (lhs.ref[0] !== assocRefLink.alias && lhs.ref[0] !== targetSideRefLink.alias) {
2013
2050
  // we need to find correct table alias for the structured access
2014
2051
  const { definition } = lhs.$refLinks[0]
2015
- if (definition === assocRefLink.definition) {
2052
+ if (definition === getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]) {
2016
2053
  // first step is the association itself -> use it's name as it becomes the table alias
2017
2054
  result[i].ref.splice(0, 1, assocRefLink.alias)
2018
2055
  } else if (
@@ -2030,6 +2067,7 @@ function cqn4sql(originalQuery, model) {
2030
2067
  }
2031
2068
  /**
2032
2069
  * Recursively calculates the containing entity for a given element.
2070
+ * If the query is localized, this function may return the localized entity.
2033
2071
  *
2034
2072
  * @param {CSN.element} element
2035
2073
  * @returns {CSN.definition} the entity containing the given element
@@ -2223,7 +2261,7 @@ function cqn4sql(originalQuery, model) {
2223
2261
  * - or null, if no searchable columns are found in neither in `@cds.search` nor in the target entity itself.
2224
2262
  */
2225
2263
  function getSearchTerm(search, query) {
2226
- const entity = query.SELECT.from.SELECT ? query.SELECT.from : query.target
2264
+ const entity = query.SELECT.from.SELECT ? query.SELECT.from : cds.infer.target(query) // REVISIT: we should reliably use inferred._target instead
2227
2265
  const searchIn = computeColumnsToBeSearched(inferred, entity)
2228
2266
  if (searchIn.length > 0) {
2229
2267
  const xpr = search
@@ -2284,7 +2322,7 @@ function cqn4sql(originalQuery, model) {
2284
2322
  if (
2285
2323
  inferred.SELECT?.from.uniqueSubqueryAlias &&
2286
2324
  !inferred.SELECT?.from.as &&
2287
- firstStep === getLastStringSegment(transformedQuery.SELECT.from.ref[0])
2325
+ getImplicitAlias(firstStep) === getImplicitAlias(transformedQuery.SELECT.from.ref[0])
2288
2326
  ) {
2289
2327
  return inferred.SELECT?.from.uniqueSubqueryAlias
2290
2328
  }
@@ -2293,7 +2331,7 @@ function cqn4sql(originalQuery, model) {
2293
2331
  }
2294
2332
 
2295
2333
  function getCombinedElementAlias(node) {
2296
- return getLastStringSegment(inferred.$combinedElements[node.ref[0].id || node.ref[0]]?.[0].index)
2334
+ return inferred.$combinedElements[node.ref[0].id || node.ref[0]]?.[0].index
2297
2335
  }
2298
2336
  }
2299
2337
  function getTransformedFunctionArgs(args, $baseLink = null) {
@@ -2366,17 +2404,6 @@ function hasLogicalOr(tokenStream) {
2366
2404
  return tokenStream.some(t => t in { OR: true, or: true })
2367
2405
  }
2368
2406
 
2369
- /**
2370
- * Returns the last segment of a string after the last dot.
2371
- *
2372
- * @param {string} str - The input string.
2373
- * @returns {string} The last segment of the string after the last dot. If there is no dot in the string, the function returns the original string.
2374
- */
2375
- function getLastStringSegment(str) {
2376
- const index = str.lastIndexOf('.')
2377
- return index != -1 ? str.substring(index + 1) : str
2378
- }
2379
-
2380
2407
  function getParentEntity(element) {
2381
2408
  if (element.kind === 'entity') return element
2382
2409
  else return getParentEntity(element.parent)
@@ -39,9 +39,9 @@ async function onDeep(req, next) {
39
39
  // const target = query.sources[Object.keys(query.sources)[0]]
40
40
  if (!this.model?.definitions[_target_name4(req.query)]) return next()
41
41
 
42
- const { target } = this.infer(query)
43
- if (!hasDeep(query, target)) return next()
42
+ if (!hasDeep(query)) return next()
44
43
 
44
+ const target = this.infer(query)._target
45
45
  const beforeData = query.INSERT ? [] : await this.run(getExpandForDeep(query, target, true))
46
46
  if (query.UPDATE && !beforeData.length) return 0
47
47
 
@@ -64,10 +64,10 @@ async function onDeep(req, next) {
64
64
  return rootResult ?? beforeData.length
65
65
  }
66
66
 
67
- const hasDeep = (q, target) => {
67
+ const hasDeep = (q) => {
68
68
  const data = q.INSERT?.entries || (q.UPDATE?.data && [q.UPDATE.data]) || (q.UPDATE?.with && [q.UPDATE.with])
69
69
  if (data)
70
- for (const c in target.compositions) {
70
+ for (const c in q._target.compositions) {
71
71
  for (const row of data) if (row[c] !== undefined) return true
72
72
  }
73
73
  }