@sap/cds-dk 10.1.0 → 10.1.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/bin/bind.js +4 -0
  3. package/bin/build.js +1 -0
  4. package/bin/compile/index.js +12 -11
  5. package/bin/completion/index.js +21 -10
  6. package/bin/completion/scripts/cds.fish +179 -127
  7. package/bin/completion/scripts/cds.ps1 +88 -14
  8. package/bin/completion/scripts/cds.sh +71 -5
  9. package/bin/completion/scripts/cds.zsh +81 -15
  10. package/bin/completion/templates/cds-tmpl.ps1 +15 -6
  11. package/bin/deploy/index.js +7 -3
  12. package/bin/env.js +4 -1
  13. package/bin/export.js +9 -1
  14. package/bin/import.js +4 -1
  15. package/bin/init.js +1 -0
  16. package/bin/inspect.js +3 -1
  17. package/bin/lint.js +5 -3
  18. package/bin/minify.js +6 -1
  19. package/bin/repl.js +11 -2
  20. package/bin/run.js +12 -1
  21. package/bin/serve.js +13 -2
  22. package/bin/up.js +4 -1
  23. package/bin/util/containerize.js +2 -0
  24. package/bin/watch/index.js +8 -1
  25. package/lib/build/util.js +2 -4
  26. package/lib/init/constants.js +1 -1
  27. package/lib/init/template/data/as-json.js +3 -0
  28. package/node_modules/@cap-js/db-service/CHANGELOG.md +17 -0
  29. package/node_modules/@cap-js/db-service/lib/common/generic-pool.js +4 -7
  30. package/node_modules/@cap-js/db-service/lib/cqn2pql.js +1 -1
  31. package/node_modules/@cap-js/db-service/lib/cqn2sql.js +15 -11
  32. package/node_modules/@cap-js/db-service/lib/cqn4sql.js +119 -19
  33. package/node_modules/@cap-js/db-service/lib/infer/index.js +27 -16
  34. package/node_modules/@cap-js/db-service/lib/recursion-guard.js +40 -0
  35. package/node_modules/@cap-js/db-service/lib/search.js +1 -1
  36. package/node_modules/@cap-js/db-service/package.json +1 -1
  37. package/node_modules/@cap-js/sqlite/CHANGELOG.md +19 -0
  38. package/node_modules/@cap-js/sqlite/lib/SQLiteService.js +110 -14
  39. package/node_modules/@cap-js/sqlite/package.json +3 -3
  40. package/node_modules/@sap/cds-compiler/CHANGELOG.md +15 -0
  41. package/node_modules/@sap/cds-compiler/lib/compiler/resolve.js +7 -3
  42. package/node_modules/@sap/cds-compiler/lib/model/csnUtils.js +2 -0
  43. package/node_modules/@sap/cds-compiler/lib/transform/db/applyTransformations.js +12 -0
  44. package/node_modules/@sap/cds-compiler/lib/transform/db/associations.js +5 -3
  45. package/node_modules/@sap/cds-compiler/lib/transform/db/flattening.js +4 -3
  46. package/node_modules/@sap/cds-compiler/lib/transform/effective/flattening.js +17 -1
  47. package/node_modules/@sap/cds-compiler/lib/transform/effective/main.js +19 -0
  48. package/node_modules/@sap/cds-compiler/lib/transform/fioriTreeViews.js +12 -1
  49. package/node_modules/@sap/cds-compiler/lib/transform/forRelationalDB.js +1 -1
  50. package/node_modules/@sap/cds-compiler/lib/transform/odata/adaptAnnotationRefs.js +11 -5
  51. package/node_modules/@sap/cds-compiler/lib/transform/translateAssocsToJoins.js +29 -2
  52. package/node_modules/@sap/cds-compiler/package.json +1 -1
  53. package/node_modules/@sap/cds-mtxs/CHANGELOG.md +34 -0
  54. package/node_modules/@sap/cds-mtxs/bin/cds-mtx-migrate.js +26 -5
  55. package/node_modules/@sap/cds-mtxs/bin/cds-mtx.js +3 -0
  56. package/node_modules/@sap/cds-mtxs/lib/utils.js +5 -4
  57. package/node_modules/@sap/cds-mtxs/package.json +1 -1
  58. package/node_modules/@sap/cds-mtxs/srv/cf/abstract-provisioning-service.js +62 -7
  59. package/node_modules/@sap/cds-mtxs/srv/cf/saas-provisioning-service.js +1 -1
  60. package/node_modules/@sap/cds-mtxs/srv/cf/sms-provisioning-service.js +23 -3
  61. package/node_modules/@sap/cds-mtxs/srv/extensibility/compat/update.js +15 -0
  62. package/node_modules/@sap/cds-mtxs/srv/extensibility/linter/annotations.js +2 -2
  63. package/node_modules/@sap/cds-mtxs/srv/extensibility/linter/config.js +2 -0
  64. package/node_modules/@sap/cds-mtxs/srv/extensibility/linter/index.js +2 -0
  65. package/node_modules/@sap/cds-mtxs/srv/extensibility/linter/protected-namespace.js +45 -0
  66. package/node_modules/@sap/cds-mtxs/srv/extensibility-service.js +8 -3
  67. package/node_modules/@sap/cds-mtxs/srv/jobs-service.js +49 -10
  68. package/node_modules/@sap/cds-mtxs/srv/model-provider.js +11 -2
  69. package/node_modules/@sap/cds-mtxs/srv/plugins/common.js +19 -12
  70. package/node_modules/@sap/cds-mtxs/srv/plugins/hana/hdi-ctnr-mgr.js +6 -3
  71. package/node_modules/@sap/cds-mtxs/srv/plugins/hana/serverless.js +271 -0
  72. package/node_modules/@sap/cds-mtxs/srv/plugins/hana.js +3 -2
  73. package/node_modules/@sap/cds-mtxs/srv/worker/job-worker-pool.js +212 -0
  74. package/node_modules/@sap/cds-mtxs/srv/worker/job-worker.js +61 -0
  75. package/package.json +2 -2
  76. /package/node_modules/@sap/cds-mtxs/srv/worker/{pool.js → model-worker-pool.js} +0 -0
  77. /package/node_modules/@sap/cds-mtxs/srv/worker/{load.js → model-worker.js} +0 -0
@@ -5,15 +5,19 @@ const cds = require('@sap/cds')
5
5
  const JoinTree = require('./join-tree')
6
6
  const { pseudos } = require('./pseudos')
7
7
  const { isCalculatedOnRead, getImplicitAlias, getModelUtils, defineProperty, hasOwnSkip, isRuntimeView } = require('../utils')
8
+ const { depthGuarded, guardEntry } = require('../recursion-guard')
8
9
  const cdsTypes = cds.builtin.types
9
10
  /**
10
11
  * @param {import('@sap/cds/apis/cqn').Query|string} originalQuery
11
12
  * @param {import('@sap/cds/apis/csn').CSN} [model]
12
13
  * @returns {import('./cqn').Query} = q with .target and .elements
13
14
  */
14
- function infer(originalQuery, model, useTechnicalAlias = true) {
15
+ function _infer(originalQuery, model, useTechnicalAlias = true) {
15
16
  if (!model) throw new Error('Please specify a model')
16
17
  const inferred = originalQuery
18
+ // guard func args / lists / xpr / exists nesting
19
+ // eslint-disable-next-line no-func-assign
20
+ inferArg = depthGuarded(inferArg)
17
21
 
18
22
  const { getDefinition } = getModelUtils(model, originalQuery)
19
23
 
@@ -291,19 +295,23 @@ function infer(originalQuery, model, useTechnicalAlias = true) {
291
295
  */
292
296
  function walkTokenStream(tokenStream, inXpr = false) {
293
297
  let skipJoins
294
- const processToken = t => {
295
- if (t === 'exists') {
296
- // no joins for infix filters along `exists <path>`
297
- skipJoins = true
298
- } else if (t.xpr) {
299
- // don't miss an exists within an expression
300
- t.xpr.forEach(processToken)
301
- } else {
302
- inferArg(t, queryElements, null, { inExists: skipJoins, inXpr, inQueryModifier: true })
303
- skipJoins = false
298
+ const processToken = depthGuarded(walker())
299
+ tokenStream.forEach(processToken)
300
+
301
+ function walker() {
302
+ return t => {
303
+ if (t === 'exists') {
304
+ // no joins for infix filters along `exists <path>`
305
+ skipJoins = true
306
+ } else if (t.xpr) {
307
+ // don't miss an exists within an expression
308
+ t.xpr.forEach(processToken)
309
+ } else {
310
+ inferArg(t, queryElements, null, { inExists: skipJoins, inXpr, inQueryModifier: true })
311
+ skipJoins = false
312
+ }
304
313
  }
305
314
  }
306
- tokenStream.forEach(processToken)
307
315
  }
308
316
  /**
309
317
  * Processes references starting with `$self`, which are intended to target other query elements.
@@ -631,7 +639,7 @@ function infer(originalQuery, model, useTechnicalAlias = true) {
631
639
  } else if (arg.inline && queryElements) {
632
640
  const elements = resolveInline(arg)
633
641
  for (const elName in elements) {
634
- if (queryElements[elName] !== undefined) rejectDuplicatedElement(elName)
642
+ if (queryElements[elName] !== undefined) rejectDuplicatedElement(elName)
635
643
  }
636
644
  Object.assign(queryElements, elements)
637
645
  } else {
@@ -667,7 +675,7 @@ function infer(originalQuery, model, useTechnicalAlias = true) {
667
675
  // ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
668
676
  if (arg.expand) {
669
677
  const { $refLinks } = arg
670
-
678
+
671
679
  const skip = $refLinks.some(link => {
672
680
  const def = getDefinition(link.definition.target)
673
681
  return hasOwnSkip(def) && !isRuntimeView(def)
@@ -1095,7 +1103,7 @@ function infer(originalQuery, model, useTechnicalAlias = true) {
1095
1103
  for (const k in elements) {
1096
1104
  if (!exclude(k)) {
1097
1105
  const element = elements[k]
1098
- if (element.type !== 'cds.LargeBinary') {
1106
+ if (element.type !== 'cds.LargeBinary' && element.type !== 'cds.Vector') {
1099
1107
  queryElements[k] = element
1100
1108
  }
1101
1109
  // only relevant if we actually select the calculated element
@@ -1115,7 +1123,7 @@ function infer(originalQuery, model, useTechnicalAlias = true) {
1115
1123
  }
1116
1124
  if (exclude(name) || name in queryElements) return true
1117
1125
  const element = tableAliases[0].tableAlias.elements[name]
1118
- if (element.type !== 'cds.LargeBinary') queryElements[name] = element
1126
+ if (element.type !== 'cds.LargeBinary' && element.type !== 'cds.Vector') queryElements[name] = element
1119
1127
  if (isCalculatedOnRead(element)) {
1120
1128
  linkCalculatedElement(element)
1121
1129
  }
@@ -1287,4 +1295,7 @@ function getMainAlias (query) {
1287
1295
  return mainAlias
1288
1296
  }
1289
1297
 
1298
+ // guardEntry bounds nested-subquery re-entry (expand, from, set)
1299
+ const infer = guardEntry(_infer)
1300
+
1290
1301
  module.exports = infer
@@ -0,0 +1,40 @@
1
+ 'use strict'
2
+
3
+ const cds = require('@sap/cds')
4
+
5
+ // shared across cqn4sql and infer to guard against stack overflow from user-controlled nesting depth
6
+ const MAX_RECURSION_DEPTH = 100
7
+
8
+ let recursionDepth = 0
9
+
10
+ function enterRecursion() {
11
+ if (++recursionDepth > MAX_RECURSION_DEPTH) throw cds.error(400, `Query exceeds the maximum nesting depth of ${MAX_RECURSION_DEPTH}`)
12
+ }
13
+
14
+ function depthGuarded(fn) {
15
+ return function (...args) {
16
+ enterRecursion()
17
+ try {
18
+ return fn.apply(this, args)
19
+ } finally {
20
+ recursionDepth--
21
+ }
22
+ }
23
+ }
24
+
25
+ // entry points (infer / cqn4sql): outermost call resets the counter, so a thrown
26
+ // depth error can't leave it polluted for the next request
27
+ function guardEntry(fn) {
28
+ return function (...args) {
29
+ const outermost = recursionDepth === 0
30
+ enterRecursion()
31
+ try {
32
+ return fn.apply(this, args)
33
+ } finally {
34
+ if (outermost) recursionDepth = 0
35
+ else recursionDepth--
36
+ }
37
+ }
38
+ }
39
+
40
+ module.exports = { depthGuarded, guardEntry }
@@ -166,7 +166,7 @@ const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }) =
166
166
  if (
167
167
  cqn.SELECT.columns.length === 1 &&
168
168
  column.func === 'count' &&
169
- (column.as === '_counted_' || column.as === '$count')
169
+ column.as === '$count'
170
170
  ) {
171
171
  return
172
172
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "3.0.1",
3
+ "version": "3.1.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": {
@@ -4,6 +4,25 @@
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
+ ## [3.1.0](https://github.com/cap-js/cds-dbs/compare/sqlite-v3.0.2...sqlite-v3.1.0) (2026-09-07)
8
+
9
+
10
+ ### Added
11
+
12
+ * Add support for `cds.Vector` on SQLite and Postgres ([#1676](https://github.com/cap-js/cds-dbs/issues/1676)) ([f0d2203](https://github.com/cap-js/cds-dbs/commit/f0d220324e8a1c7489966545000476bfd33f6b17))
13
+
14
+
15
+ ### Fixed
16
+
17
+ * INSERT.from(SELECT) used non-existent `value` ([#1565](https://github.com/cap-js/cds-dbs/issues/1565)) ([fd98844](https://github.com/cap-js/cds-dbs/commit/fd98844c83a78d6cf2b0b71344d3ee1d88528106))
18
+
19
+
20
+ ### Dependencies
21
+
22
+ * The following workspace dependencies were updated
23
+ * dependencies
24
+ * @cap-js/db-service bumped from ^3.0.1 to ^3.1.0
25
+
7
26
  ## [3.0.2](https://github.com/cap-js/cds-dbs/compare/sqlite-v3.0.1...sqlite-v3.0.2) (2026-07-01)
8
27
 
9
28
 
@@ -14,16 +14,6 @@ const sqliteKeywords = keywords.reduce((prev, curr) => {
14
14
  return prev
15
15
  }, {})
16
16
 
17
- // define date and time functions in js to allow for throwing errors
18
- const isTime = /^\d{1,2}:\d{1,2}:\d{1,2}$/
19
- const hasTimezone = /([+-]\d{1,2}:?\d{0,2}|Z)$/
20
- const toDate = (d, allowTime = false) => {
21
- const date = new Date(allowTime && isTime.test(d) ? `1970-01-01T${d}Z` : hasTimezone.test(d) ? d : d + 'Z')
22
- if (Number.isNaN(date.getTime())) throw new Error(`Value does not contain a valid ${allowTime ? 'time' : 'date'} "${d}"`)
23
- return date
24
- }
25
-
26
-
27
17
  class SQLiteService extends SQLService {
28
18
 
29
19
  get factory() {
@@ -46,6 +36,10 @@ class SQLiteService extends SQLService {
46
36
  dbc.function('hour', deterministic, d => d === null ? null : toDate(d, true).getUTCHours())
47
37
  dbc.function('minute', deterministic, d => d === null ? null : toDate(d, true).getUTCMinutes())
48
38
  dbc.function('second', deterministic, d => d === null ? null : toDate(d, true).getUTCSeconds())
39
+ dbc.function('COSINE_SIMILARITY', deterministic, (a, b) => cosineSimilarity(toFloatArray(a), toFloatArray(b)))
40
+ dbc.function('L2DISTANCE', deterministic, (a, b) => l2Distance(toFloatArray(a), toFloatArray(b)))
41
+ dbc.function('L2NORMALIZE', deterministic, v => v == null ? null : fromFloatArray(l2Normalize(toFloatArray(v)), v))
42
+ dbc.function('VECTOR_EMBEDDING', deterministic, (input, text_type, model_and_version) => input == null ? null : JSON.stringify(hashEmbedding(String(input))))
49
43
  if (database !== ':memory:') dbc.pragma?.('journal_mode = WAL') || dbc.exec('PRAGMA journal_mode = WAL')
50
44
  return dbc
51
45
  } catch (err) {
@@ -184,7 +178,7 @@ class SQLiteService extends SQLService {
184
178
  let ps = await this.prepare(sql)
185
179
  const vals = await this._prepareStreams(values)
186
180
  const { changes } = await ps.run(vals)
187
- return this._return_affected (changes)
181
+ return this._return_affected(changes)
188
182
  }
189
183
 
190
184
  onPlainSQL({ query, data }, next) {
@@ -273,9 +267,9 @@ class SQLiteService extends SQLService {
273
267
  // Reading decimal as string to not loose precision
274
268
  Decimal: cds.env.features.ieee754compatible
275
269
  ? (expr, elem) =>
276
- elem?.scale
277
- ? `CASE WHEN ${expr} IS NULL THEN NULL ELSE format('%.${elem.scale}f', ${expr}) END`
278
- : `CASE WHEN ${expr} IS NULL THEN NULL ELSE rtrim(rtrim(format('%.999f', ${expr}), '0'), '.') END`
270
+ elem?.scale
271
+ ? `CASE WHEN ${expr} IS NULL THEN NULL ELSE format('%.${elem.scale}f', ${expr}) END`
272
+ : `CASE WHEN ${expr} IS NULL THEN NULL ELSE rtrim(rtrim(format('%.999f', ${expr}), '0'), '.') END`
279
273
  : undefined,
280
274
  // Binary is not allowed in json objects
281
275
  Binary: expr => `${expr} || ''`,
@@ -321,4 +315,106 @@ function loadSQLite(driver) {
321
315
  }
322
316
  }
323
317
 
318
+ // define date and time functions in js to allow for throwing errors
319
+ const isTime = d => /^\d{1,2}:\d{1,2}:\d{1,2}$/.test(d)
320
+ const hasTimezone = d => /([+-]\d{1,2}:?\d{0,2}|Z)$/.test(d)
321
+ const toDate = (d, allowTime = false) => {
322
+ const date = new Date(allowTime && isTime(d) ? `1970-01-01T${d}Z` : hasTimezone(d) ? d : d + 'Z')
323
+ if (Number.isNaN(date.getTime())) throw new Error(`Value does not contain a valid ${allowTime ? 'time' : 'date'} "${d}"`)
324
+ return date
325
+ }
326
+
327
+ // Vector functions implemented in JavaScript for SQLite (registered as custom SQL functions)
328
+ function cosineSimilarity(a, b) {
329
+ if (a == null || b == null) return null
330
+ let dot = 0, normA = 0, normB = 0
331
+ for (let i = 0; i < a.length; i++) {
332
+ dot += a[i] * b[i]
333
+ normA += a[i] * a[i]
334
+ normB += b[i] * b[i]
335
+ }
336
+ const denom = Math.sqrt(normA) * Math.sqrt(normB)
337
+ return denom === 0 ? 0 : dot / denom
338
+ }
339
+
340
+ function l2Distance(a, b) {
341
+ if (a == null || b == null) return null
342
+ let sum = 0
343
+ for (let i = 0; i < a.length; i++) {
344
+ const diff = a[i] - b[i]
345
+ sum += diff * diff
346
+ }
347
+ return Math.sqrt(sum)
348
+ }
349
+
350
+ function l2Normalize(v) {
351
+ if (v == null) return null
352
+ let norm = 0
353
+ for (let i = 0; i < v.length; i++) norm += v[i] * v[i]
354
+ if (norm === 0) return v
355
+ norm = Math.sqrt(norm)
356
+ for (let i = 0; i < v.length; i++) v[i] /= norm
357
+ return v
358
+ }
359
+
360
+ /**
361
+ * Deterministic synchronous hash-based embedding function for SQLite.
362
+ * NOTE: If a synchronous embedding library becomes available for Node.js,
363
+ * it can be integrated here to replace the hash-based implementation.
364
+ */
365
+ function hashEmbedding(text, dimensions = 384) {
366
+ if (text == null) return null
367
+ const vector = new Float32Array(dimensions)
368
+ const normalized = text.toLowerCase()
369
+ const ngramSize = 3
370
+
371
+ if (normalized.length >= ngramSize) {
372
+ for (let i = 0; i <= normalized.length - ngramSize; i++)
373
+ project(ngramHash(normalized, i, ngramSize), vector, dimensions)
374
+ } else {
375
+ for (let i = 0; i < normalized.length; i++)
376
+ project(normalized.charCodeAt(i), vector, dimensions)
377
+ }
378
+ return Array.from(l2Normalize(vector))
379
+ }
380
+
381
+ function ngramHash(text, start, len) {
382
+ let hash = 0x811c9dc5
383
+ for (let i = start; i < start + len; i++) {
384
+ hash ^= text.charCodeAt(i)
385
+ hash = Math.imul(hash, 0x01000193)
386
+ }
387
+ return hash
388
+ }
389
+
390
+ function project(hash, vector, dimensions) {
391
+ for (let band = 0; band < 4; band++) {
392
+ const h = rehash(hash, band)
393
+ vector[Math.abs(h % dimensions)] += ((h >>> 16) & 1) === 0 ? 1.0 : -1.0
394
+ }
395
+ }
396
+
397
+ function rehash(hash, band) {
398
+ let h = hash ^ Math.imul(band, 0x9e3779b9)
399
+ h ^= h >>> 16
400
+ h = Math.imul(h, 0x45d9f3b)
401
+ h ^= h >>> 16
402
+ return h
403
+ }
404
+
405
+ // Vector type conversion helpers for SQLite - handle various input formats (Buffer, string, array, typed arrays)
406
+ function toFloatArray(vector) {
407
+ if (vector == null) return null
408
+ if (vector instanceof Float32Array) return Array.from(vector)
409
+ if (Buffer.isBuffer(vector)) return JSON.parse(vector.toString('utf8'))
410
+ if (vector instanceof Uint8Array) return JSON.parse(Buffer.from(vector).toString('utf8'))
411
+ if (typeof vector === 'string') return JSON.parse(vector)
412
+ if (Array.isArray(vector)) return vector
413
+ throw new Error(`Unsupported vector type: ${typeof vector}`)
414
+ }
415
+
416
+ function fromFloatArray(arr, original) {
417
+ return original instanceof Float32Array ? new Float32Array(arr) : JSON.stringify(arr)
418
+ }
419
+
324
420
  module.exports = SQLiteService
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/sqlite",
3
- "version": "3.0.2",
3
+ "version": "3.1.0",
4
4
  "description": "CDS database service for SQLite",
5
5
  "homepage": "https://github.com/cap-js/cds-dbs/tree/main/sqlite#cds-database-service-for-sqlite",
6
6
  "repository": {
@@ -26,12 +26,12 @@
26
26
  "test": "cds-test"
27
27
  },
28
28
  "dependencies": {
29
- "@cap-js/db-service": "^3.0.1"
29
+ "@cap-js/db-service": "^3.1.0"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "@sap/cds": "^10",
33
33
  "sql.js": "^1.13.0",
34
- "better-sqlite3": "^12.0.0"
34
+ "better-sqlite3": "^12.0.0 || ^13.0.0"
35
35
  },
36
36
  "peerDependenciesMeta": {
37
37
  "sql.js": {
@@ -15,6 +15,21 @@ might use a deprecated flag only for a limited period of time.
15
15
 
16
16
 
17
17
 
18
+ ## Version 7.1.1 - 2026-09-08
19
+
20
+ ### Bug Fixes
21
+
22
+ - **effective:**
23
+ + Fix dump in flattening for actions with entity parameters.
24
+ + Ensure consistent handling of reference-like annotation expressions during flattening.
25
+ - **sql:**
26
+ + Add missing `@hierarchy` columns to localized convenience views.
27
+ + Avoid crash on mixin backlink with renamed target element.
28
+ + The generated SQL for a reference `assoc.elem` where `assoc` is an association defined with duplicate backlink comparisons with `$self` is now more likely
29
+ to be correct. Usually, there is an error for the definition of such an `assoc`, but the corresponding message can be downgraded to a warning.
30
+
31
+
32
+
18
33
  ## Version 7.1.0 - 2026-08-27
19
34
 
20
35
  ### Features
@@ -1189,9 +1189,13 @@ function resolve( model ) {
1189
1189
  const backlinkPath = itemIsPath ? item : right;
1190
1190
  const last = backlinkPath.path[backlinkPath.path.length - 1];
1191
1191
  if (assoc.$backlink) {
1192
- const location = combinedLocation( item.path[0], right.path[right.path.length - 1] );
1193
- message( 'ref-duplicate-self-comparison', [ location, assoc ], { id: '$self' },
1194
- 'Duplicate comparison with a bare $(ID) reference in an ON-condition' );
1192
+ if (!assoc.on.$inferred) {
1193
+ const location = combinedLocation( item.path[0], right.path[right.path.length - 1] );
1194
+ message( 'ref-duplicate-self-comparison', [ location, assoc ], { id: '$self' },
1195
+ 'Duplicate comparison with a bare $(ID) reference in an ON-condition' );
1196
+ }
1197
+ if (last?._artifact !== assoc.$backlink._origin)
1198
+ setLink( assoc.$backlink, '_origin', null );
1195
1199
  return;
1196
1200
  }
1197
1201
  const backlinkRef = last?._artifact;
@@ -8,6 +8,7 @@ const {
8
8
  applyTransformationsOnNonDictionary,
9
9
  applyTransformationsOnDictionary,
10
10
  mergeTransformers,
11
+ isSimpleAnnoExprRef,
11
12
  } = require('../transform/db/applyTransformations');
12
13
  const { isBuiltinType, isAnnotationExpression, primaryExprProperties } = require('../base/builtins');
13
14
  const { ModelError, CompilerAssertion } = require('../base/error');
@@ -1512,6 +1513,7 @@ module.exports = {
1512
1513
  applyTransformationsOnNonDictionary,
1513
1514
  applyTransformationsOnDictionary,
1514
1515
  mergeTransformers,
1516
+ isSimpleAnnoExprRef,
1515
1517
  setDependencies,
1516
1518
  isPersistedOnDatabase,
1517
1519
  isPersistedAsView,
@@ -423,6 +423,17 @@ function mergeTransformers( transformers, that ) {
423
423
  return remapped;
424
424
  }
425
425
 
426
+ /**
427
+ * Return true if the annotation expression ref node is a simple reference:
428
+ * all ref steps are plain strings, and the node has neither `param` nor `cast`.
429
+ *
430
+ * @param {object} annotation An annotation expression node (has `ref`, optionally `param`, `cast`)
431
+ * @returns {boolean}
432
+ */
433
+ function isSimpleAnnoExprRef(annotation) {
434
+ return annotation.ref && annotation.ref.every(s => typeof s === 'string') && !annotation.param && !annotation.cast;
435
+ }
436
+
426
437
  module.exports = {
427
438
  mergeTransformers,
428
439
  transformExpression,
@@ -430,6 +441,7 @@ module.exports = {
430
441
  applyTransformations,
431
442
  applyTransformationsOnNonDictionary,
432
443
  applyTransformationsOnDictionary,
444
+ isSimpleAnnoExprRef,
433
445
  };
434
446
 
435
447
 
@@ -5,6 +5,7 @@ const {
5
5
  applyTransformationsOnNonDictionary,
6
6
  applyTransformations,
7
7
  implicitAs,
8
+ isSimpleAnnoExprRef,
8
9
  } = require('../../model/csnUtils');
9
10
 
10
11
  /**
@@ -138,7 +139,7 @@ function getFKAccessFinalizer( csn, options, csnUtils, pathDelimiter, processOnI
138
139
  const annoAfter = JSON.stringify(obj[anno]);
139
140
  if (annoBefore !== annoAfter &&
140
141
  (options.transformation !== 'effective' || obj[anno]['=']) &&
141
- !(obj[anno].ref && obj[anno].ref.every(s => typeof s === 'string')))
142
+ !(obj[anno].ref && isSimpleAnnoExprRef(obj[anno])))
142
143
  obj[anno]['='] = true;
143
144
  });
144
145
  }
@@ -251,10 +252,11 @@ function getFKAccessFinalizer( csn, options, csnUtils, pathDelimiter, processOnI
251
252
  else {
252
253
  refOwner.ref = [ ...ref.slice(0, i), newFkName ];
253
254
  }
254
- if (ctx?.annoExpr === refOwner)
255
- ctx.annoExpr['='] = refOwner.ref.every(s => typeof s === 'string')
255
+ if (ctx?.annoExpr === refOwner) {
256
+ ctx.annoExpr['='] = isSimpleAnnoExprRef(refOwner)
256
257
  ? refOwner.ref.join('.')
257
258
  : true;
259
+ }
258
260
  }
259
261
  }
260
262
  else {
@@ -11,7 +11,7 @@ const { isBuiltinType, isMagicVariable } = require('../../base/builtins');
11
11
  const transformUtils = require('../transformUtils');
12
12
  const { csnRefs } = require('../../base/csnRefs');
13
13
  const { setProp, forEach } = require('../../utils/objectUtils');
14
- const { transformExpression } = require('./applyTransformations');
14
+ const { transformExpression, isSimpleAnnoExprRef } = require('./applyTransformations');
15
15
  const { cloneCsnNonDict } = require('../../base/cloneCsn');
16
16
  const { EdmTypeFacetNames } = require('../../edm/EdmPrimitiveTypeDefinitions');
17
17
  const { adaptAnnotationsRefs } = require('../odata/adaptAnnotationRefs');
@@ -246,7 +246,8 @@ function getStructStepsFlattener( csn, options, messageFunctions, resolved, path
246
246
  adaptRefs.push((...args) => {
247
247
  const refChanged = fn(...args);
248
248
  if (refChanged && annotation['='])
249
- annotation['='] = true;
249
+ // For simple refs (all-string steps, no param/cast), = contains the string representation of the ref.
250
+ annotation['='] = isSimpleAnnoExprRef(annotation) ? annotation.ref.join('.') : true;
250
251
  });
251
252
  }
252
253
  else {
@@ -664,7 +665,7 @@ function handleManagedAssociationsAndCreateForeignKeys( csn, options, messageFun
664
665
  }
665
666
 
666
667
  if (options.transformation === 'effective') {
667
- adaptAnnotationsRefs(fks, csnUtils, messageFunctions, eltPath);
668
+ adaptAnnotationsRefs(fks, csnUtils, messageFunctions, eltPath, options);
668
669
  const validAnnoNames = Object.keys(element).filter(pn => pn[0] === '@' && findAnnotationExpression(element, pn));
669
670
  fks.forEach(fk => copyAnnotations(element, fk[1], false, {}, validAnnoNames));
670
671
  }
@@ -53,6 +53,18 @@ function flattenRefs(csn, options, csnUtils, messageFunctions) {
53
53
  const refFlattener = getStructStepsFlattener(csn, options, messageFunctions, resolved, '_', adaptRefs);
54
54
 
55
55
  forEachDefinition(csn, (def, defName) => {
56
+ // Unbound actions/functions/events: only flatten annotation expressions, not params.
57
+ // Their params may carry $self on-conditions that cannot be resolved without a query context.
58
+ if (def.kind === 'action' || def.kind === 'function' || def.kind === 'event') {
59
+ forEachMemberRecursively(def, (member, memberName, prop, path) => {
60
+ Object.keys(member).filter(pn => pn.startsWith('@') && member[pn]).forEach((pn) => {
61
+ transformAnnotationExpression(member, pn, [ refFlattener ], path);
62
+ adaptRefs.forEach(fn => fn());
63
+ adaptRefs.length = 0;
64
+ });
65
+ }, [ 'definitions', defName ]);
66
+ return;
67
+ }
56
68
  applyTransformationsOnNonDictionary(csn.definitions, defName, refFlattener, { processAnnotations: true, skipDict: { actions: 1 } }, [ 'definitions' ]);
57
69
 
58
70
  adaptRefs.forEach(fn => fn());
@@ -97,8 +109,12 @@ function flattenRefs(csn, options, csnUtils, messageFunctions) {
97
109
  });
98
110
 
99
111
  cleanup.forEach((obj) => {
100
- if (obj.ref && obj.ref[0] === '$self')
112
+ // strip leading $self from ref array and (if it is a string) from '='
113
+ if (obj.ref && obj.ref[0] === '$self') {
101
114
  obj.ref.shift();
115
+ if (typeof obj['='] === 'string' && obj['='].startsWith('$self.'))
116
+ obj['='] = obj['='].slice('$self.'.length);
117
+ }
102
118
  });
103
119
  }
104
120
 
@@ -103,6 +103,25 @@ function effectiveCsn( model, options, messageFunctions ) {
103
103
 
104
104
  resolveTypesInActionsAfterFlattening(csnUtils);
105
105
 
106
+ // Rewrite association keys in unbound action/function params to use flattened names.
107
+ // Not needed for bound actions: their params are reached via the parent entity and handled by flattenFKs in managedToUnmanaged.
108
+ forEachDefinition(csn, (def) => {
109
+ if (def.kind !== 'action' && def.kind !== 'function')
110
+ return;
111
+ if (!def.params)
112
+ return;
113
+ for (const param of Object.values(def.params)) {
114
+ if (!param.elements)
115
+ continue;
116
+ for (const element of Object.values(param.elements)) {
117
+ if (element.keys)
118
+ element.keys.forEach((key) => {
119
+ key.ref = [ key.ref.join('_') ];
120
+ });
121
+ }
122
+ }
123
+ });
124
+
106
125
  processCalculatedElementsInEntities(csn, options);
107
126
  // invalidate csnRef caches after flattening
108
127
  transformerUtils.csnUtils = getUtils(csn, 'init-all');
@@ -16,7 +16,7 @@ const { isBuiltinType } = require('../base/builtins');
16
16
  * - the foreign key is of scalar type
17
17
  * Otherwise, appropriate warnings/errors are raised.
18
18
  */
19
- function generateFioriTreeViewAnnotationsAndFields(def, defName, messageFunctions, csnUtils, transformers, options) {
19
+ function generateFioriTreeViewAnnotationsAndFields(def, defName, messageFunctions, csnUtils, transformers, options, csn) {
20
20
  const { error, warning } = messageFunctions;
21
21
  const { isAssociation } = csnUtils;
22
22
  const { setAnnotation, addElement, createScalarElement } = transformers;
@@ -157,6 +157,17 @@ function generateFioriTreeViewAnnotationsAndFields(def, defName, messageFunction
157
157
  }
158
158
 
159
159
  function addHierarchyFields(def, defName) {
160
+ addHierarchyFieldsTo(def, defName);
161
+
162
+ // The localized convenience view is a stand-in for the annotated view at
163
+ // runtime, so it must carry the same hierarchy columns. It is generated
164
+ // before this transform runs, hence it is augmented here explicitly.
165
+ const localizedView = csn?.definitions?.[`localized.${ defName }`];
166
+ if (localizedView)
167
+ addHierarchyFieldsTo(localizedView, `localized.${ defName }`);
168
+ }
169
+
170
+ function addHierarchyFieldsTo(def, defName) {
160
171
  // add elements in elements dictionary
161
172
  const limitedDescendantCount = createScalarElement('LimitedDescendantCount', 'cds.Integer');
162
173
  limitedDescendantCount.LimitedDescendantCount['@Core.Computed'] = true;
@@ -367,7 +367,7 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
367
367
  findAndMarkSqlServiceArtifacts(artifact, artifactName);
368
368
  if (artifact.$dummyService)
369
369
  createServiceDummy(artifact, artifactName, csn, messageFunctions);
370
- generateFioriTreeViews(artifact, artifactName, messageFunctions, csnUtils, { setAnnotation, addElement, createScalarElement }, options);
370
+ generateFioriTreeViews(artifact, artifactName, messageFunctions, csnUtils, { setAnnotation, addElement, createScalarElement }, options, csn);
371
371
  }, transformViews ]);
372
372
 
373
373
  // TODO: Could we maybe merge this with the final applyTransformations?
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { transformAnnotationExpression, implicitAs } = require('../../model/csnUtils');
3
+ const { transformAnnotationExpression, implicitAs, isSimpleAnnoExprRef } = require('../../model/csnUtils');
4
4
 
5
5
  /**
6
6
  * Used during annotating of foreign keys.
@@ -13,10 +13,10 @@ const { transformAnnotationExpression, implicitAs } = require('../../model/csnUt
13
13
  * @param {object} messageFunctions
14
14
  * @param {CSN.Path} elementPath
15
15
  */
16
- function adaptAnnotationsRefs(generatedForeignKeys, csnUtils, { error }, elementPath) {
16
+ function adaptAnnotationsRefs(generatedForeignKeys, csnUtils, { error }, elementPath, options) {
17
17
  if (Array.isArray(generatedForeignKeys?.[0])) {
18
18
  // ensure we are always called with an array of objects. TODO: Cleanup fk creation in for.effective to create array of objects
19
- adaptAnnotationsRefs(remapToArrayOfObjects(generatedForeignKeys), csnUtils, { error }, elementPath);
19
+ adaptAnnotationsRefs(remapToArrayOfObjects(generatedForeignKeys), csnUtils, { error }, elementPath, options);
20
20
  }
21
21
  else {
22
22
  const reportedErrorsForAnnoPath = {};
@@ -49,8 +49,14 @@ function adaptAnnotationsRefs(generatedForeignKeys, csnUtils, { error }, element
49
49
  if (gfkForRef.length === 1) {
50
50
  ref[0] = gfkForRef[0].prefix;
51
51
 
52
- if (ctx?.annoExpr?.['='])
53
- ctx.annoExpr['='] = true;
52
+ if (ctx?.annoExpr?.['=']) {
53
+ // TODO: Apply the same fix for OData once the OData behavior is aligned;
54
+ // when done, the `options` parameter can be removed from this function.
55
+ if (options?.transformation === 'effective')
56
+ ctx.annoExpr['='] = isSimpleAnnoExprRef(ctx.annoExpr) ? ctx.annoExpr.ref.join('.') : true;
57
+ else
58
+ ctx.annoExpr['='] = true;
59
+ }
54
60
  }
55
61
  else {
56
62
  // check if the annotation reference points to a structure that has been expanded,