@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
package/bin/import.js CHANGED
@@ -1,9 +1,12 @@
1
+ const { FILE, DIR } = require('./completion')
1
2
  module.exports = Object.assign(_import, {
2
3
  _name: 'import',
3
4
  staticCompletions: () => ({
4
5
  '--from': ['asyncapi', 'edmx', 'openapi'],
5
6
  '--as': ['cds', 'csn', 'json'],
6
- '_': { type: ':file', ext: ['json', 'xml', 'edmx'] },
7
+ '--out': DIR(),
8
+ '-o': DIR(),
9
+ '_': FILE('json,xml,edmx'),
7
10
  }),
8
11
  options: [ '--services', '--for', '--out', '--as', '--name', '--include-namespaces', '--dependencies', '--from', '--config', '--destination', '--group', '--into-namespace' ],
9
12
  shortcuts: [ '-s', '-4', '-o', '-as', '-n', '-ns', '-d',undefined, '-c', undefined, undefined, '-2', '-', '-f', '-b'],
package/bin/init.js CHANGED
@@ -10,6 +10,7 @@ module.exports = Object.assign(init, {
10
10
  staticCompletions: () => ({
11
11
  '--add': COMMA(require('../bin/completion/completionFs').readTemplates([])),
12
12
  '--java:package': DIR(),
13
+ '--base': DIR(),
13
14
  }),
14
15
  options: ['--add', '--java:mvn', '--java:package', '--lint:custom', '--lint:custom:example', '--no-db', '--base'],
15
16
  shortcuts: ['-a'],
package/bin/inspect.js CHANGED
@@ -9,7 +9,9 @@ const { FILE } = require('./completion');
9
9
  module.exports = Object.assign (inspect, {
10
10
  staticCompletions: () => ({
11
11
  '--kind': ['entity', 'aspect', 'type', 'service'],
12
- '_': FILE('cds,json'),
12
+ '--list': ['names', 'elements', 'keys', 'associations', 'compositions', 'ids', 'sources', 'all'],
13
+ '-l': ['names', 'elements', 'keys', 'associations', 'compositions', 'ids', 'sources', 'all'],
14
+ '_': FILE('cds,json', { dirs: true }),
13
15
  }),
14
16
  options: [ '--stats', '--list', '--kind' ],
15
17
  shortcuts: [ '-s', '-l', '-k' ],
package/bin/lint.js CHANGED
@@ -1,13 +1,15 @@
1
+ const { FILE } = require('./completion')
1
2
  const Linter = require('../lib/lint/Linter');
2
3
  const linter = new Linter();
3
4
 
4
- // cds cli exec expects an executable object with various additional properties.
5
- // As most of the linting functionality is bound to a class instance, we need to wrap the instance
6
- // to not lose the this-context and attach the additional properties to the function.
7
5
  const lint = (...args) => linter.lint(...args)
8
6
  const additionalProperties = Object.entries(linter.init())
9
7
  .map(([key, value]) => [key, { value }])
10
8
 
11
9
  Object.defineProperties(lint, Object.fromEntries(additionalProperties))
12
10
 
11
+ lint.staticCompletions = () => ({
12
+ '_': FILE('js,ts,cds', { dirs: true }),
13
+ })
14
+
13
15
  module.exports = lint
package/bin/minify.js CHANGED
@@ -119,4 +119,9 @@ class Dryifier extends Minifier {
119
119
  }
120
120
  }
121
121
 
122
- module.exports = Object.assign (Minify._for_cds_dk(), { Minify })
122
+ const { FILE } = require('./completion')
123
+ module.exports = Object.assign (Minify._for_cds_dk(), { Minify,
124
+ staticCompletions: () => ({
125
+ '_': FILE('cds,json', { dirs: true }),
126
+ })
127
+ })
package/bin/repl.js CHANGED
@@ -1,6 +1,15 @@
1
+ const { DIR } = require('./completion');
1
2
  module.exports = Object.assign (repl, {
2
- options: [ '--run', '--use', '--ql' ],
3
- shortcuts: [ '-r', '-u' ],
3
+ options: [ '--run', '--use' ],
4
+ flags: [ '--ql' ],
5
+ shortcuts: ['-r', '-u'],
6
+ staticCompletions: () => ({
7
+ '_': DIR(),
8
+ '--run': DIR(),
9
+ '-r': DIR(),
10
+ '--use': ['xl', 'ql', 'entities', 'services'],
11
+ '-u': ['xl', 'ql', 'entities', 'services'],
12
+ }),
4
13
  help: `
5
14
  # SYNOPSIS
6
15
 
package/bin/run.js CHANGED
@@ -1,6 +1,17 @@
1
+ const { DIR } = require('./completion');
1
2
  const serve = require ('./serve')
2
3
  module.exports = Object.assign (
3
- cds_run, serve, {help: `
4
+ cds_run, serve, {
5
+ staticCompletions: () => ({
6
+ '_': DIR(),
7
+ '--from': DIR(),
8
+ '-f': DIR(),
9
+ '--with': DIR(),
10
+ '-w': DIR(),
11
+ '--to': ['odata', 'fiori', 'rest'],
12
+ '-2': ['odata', 'fiori', 'rest'],
13
+ }),
14
+ help: `
4
15
  # SYNOPSIS
5
16
 
6
17
  *cds run* [<project>] [<options>]
package/bin/serve.js CHANGED
@@ -1,6 +1,17 @@
1
1
  // allows executing `cds serve` even when `cds` executable is removed from `@sap/cds`
2
-
3
- module.exports = require(_local('@sap/cds/bin/serve'))
2
+ const { DIR } = require('./completion');
3
+ const serve = require(_local('@sap/cds/bin/serve'))
4
+ module.exports = Object.assign(serve, {
5
+ staticCompletions: () => ({
6
+ '_': DIR(),
7
+ '--from': DIR(),
8
+ '-f': DIR(),
9
+ '--with': DIR(),
10
+ '-w': DIR(),
11
+ '--to': ['odata', 'fiori', 'rest'],
12
+ '-2': ['odata', 'fiori', 'rest'],
13
+ }),
14
+ })
4
15
 
5
16
  function _local (id) {
6
17
  return require.resolve (id, { paths: [ process.cwd(), __dirname ] })
package/bin/up.js CHANGED
@@ -1,6 +1,9 @@
1
+ const { FILE } = require('./completion');
2
+
1
3
  module.exports = Object.assign(up, {
2
4
  staticCompletions: () => ({
3
- '--to': ['cf', 'k8s'],
5
+ '--to': ['cf', 'k8s'],
6
+ '--overlay': FILE('mtaext')
4
7
  }),
5
8
  options: ['--to', '--namespace', '--overlay'],
6
9
  shortcuts: ['-2', '-n'],
@@ -59,6 +59,7 @@ function processCommands(bp) {
59
59
  }
60
60
  function processBuildpack(bp, name, tag, clearCache = false, image, publish = false) {
61
61
  let { type, path, env, builder } = bp.buildpack
62
+ const dependencyMirrors = Object.keys(process.env).filter(key => key.startsWith('BP_DEPENDENCY_MIRROR_') && process.env[key])
62
63
  type = type ? type.split(',').map(t => buildpacks.has(t.trim()) ? `paketo-buildpacks/${t.trim()}` : t.trim()) : []
63
64
  if (builders.has(builder)) builder = `paketobuildpacks/${builder}`
64
65
  const cacheArgs = clearCache ? ['--clear-cache'] : []
@@ -69,6 +70,7 @@ function processBuildpack(bp, name, tag, clearCache = false, image, publish = fa
69
70
  ...type.flatMap(t => appendSubcommand(t, '--buildpack')),
70
71
  ...appendSubcommand(builder, '--builder'),
71
72
  ...appendSubcommand(env, '--env'),
73
+ ...dependencyMirrors.flatMap(key => ['--env', key]),
72
74
  ...(publish ? ['--publish'] : []),
73
75
  ...cacheArgs
74
76
  ]]
@@ -3,10 +3,17 @@ const { DIR } = require('../completion')
3
3
 
4
4
  module.exports = Object.assign ( watch, {
5
5
  staticCompletions: () => ({
6
+ '_': DIR(),
7
+ '--from': DIR(),
8
+ '-f': DIR(),
9
+ '--with': DIR(),
10
+ '-w': DIR(),
11
+ '--to': ['odata', 'fiori', 'rest'],
12
+ '-2': ['odata', 'fiori', 'rest'],
6
13
  '--experimental-hot-reload': ['true', 'false'],
7
14
  '--livereload': ['false'],
8
15
  '--include': DIR(),
9
- '--exclude': DIR(),
16
+ '--exclude': DIR()
10
17
  }),
11
18
  flags: [ '--in-memory', '--in-memory?', '--with-mocks', '--with-mtx' ],
12
19
  options: [ '--port', ...watchOnlyOptions ],
package/lib/build/util.js CHANGED
@@ -8,7 +8,6 @@ const { exists } = cds.utils
8
8
  const execAsync = require('node:util').promisify(cp.exec)
9
9
  const { isRoot } = require('../util/fs')
10
10
 
11
- const IS_WIN = require('node:os').platform() === 'win32'
12
11
  const { SEVERITY_ERROR, FILE_EXT_CDS, NODEJS_MODEL_EXCLUDE_LIST } = require('./constants');
13
12
  const DEBUG = cds.debug('cli|build')
14
13
 
@@ -288,7 +287,7 @@ const _cachedWsRoots = new Map()
288
287
  * could contain globs. Glob resolution will be supported.
289
288
  * The result is cached by workspace root!
290
289
  * natively in the future (fs.glob is in experimental state as of Node23).
291
- * @param {import('fs').PathLike} wsRoot - the root directory of the workspace
290
+ * @param {string} wsRoot - the root directory of the workspace
292
291
  * @returns {Promise<string[]>} list of package paths, or an empty array if either
293
292
  * no workspaces are defined or have not been installed.
294
293
  */
@@ -302,8 +301,7 @@ async function _execNpmWs(wsRoot) {
302
301
  try {
303
302
  const cmdLine = 'npm ls --ws --json'
304
303
  DEBUG?.(`execute ${cmdLine}`)
305
- // @ts-expect-error - TODO: shell should be a string. But can probably be removed altogether?
306
- const { stdout } = await execAsync(cmdLine, { shell: IS_WIN, stdio: ['inherit', 'pipe', 'inherit'], cwd: wsRoot })
304
+ const { stdout } = await execAsync(cmdLine, { cwd: wsRoot })
307
305
  const jsonResult = JSON.parse(stdout)
308
306
  if (jsonResult.dependencies) {
309
307
  result = Object.keys(jsonResult.dependencies)
@@ -5,7 +5,7 @@ module.exports = {
5
5
  COMMAND_ADD: 'add',
6
6
 
7
7
  JAVA_LTS_VERSIONS: [25],
8
- MAVEN_ARCHETYPE_VERSION: '5.0.1',
8
+ MAVEN_ARCHETYPE_VERSION: '5.1.1',
9
9
 
10
10
  OPTIONS: Object.freeze({
11
11
  COMPLETION: 'completion',
@@ -247,6 +247,9 @@ function dataForField(element, artifact, csn, record, data, parent, depth, refer
247
247
  const range = { from: 0, to: prec }
248
248
  annotatedIntRange(element, range)
249
249
  if (scale === 0) return randomIntInRange(range)
250
+ const factor = Math.pow(10, scale)
251
+ range.from = Math.ceil(range.from * factor) / factor
252
+ range.to = Math.floor(range.to * factor) / factor
250
253
  // draw uniformly across the range and round to scale, so annotated bounds are never exceeded
251
254
  const val = +randomDecimalInRange(range).toFixed(scale)
252
255
  return +Math.min(Math.max(val, range.from), range.to).toFixed(scale)
@@ -4,6 +4,23 @@
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/db-service-v3.0.1...db-service-v3.1.0) (2026-09-07)
8
+
9
+
10
+ ### Added
11
+
12
+ * rank $search results by relevance ([#1725](https://github.com/cap-js/cds-dbs/issues/1725)) ([bc93160](https://github.com/cap-js/cds-dbs/commit/bc931601a721f7c9e00b89e6844ba064352b541d))
13
+
14
+
15
+ ### Fixed
16
+
17
+ * **cqn4sql:** reject overly nested query expressions instead of crashing ([#1712](https://github.com/cap-js/cds-dbs/issues/1712)) ([29a45b7](https://github.com/cap-js/cds-dbs/commit/29a45b7daadbc77433b882148700d73c00efae62))
18
+ * don't throw when releasing/destroying a resource that's no longer in the pool ([#1696](https://github.com/cap-js/cds-dbs/issues/1696)) ([5d3b277](https://github.com/cap-js/cds-dbs/commit/5d3b277a1cf7cac02e41f2f24975a8e9473e5b5e))
19
+ * exclude cds.Vector from wildcard select expansion ([#1716](https://github.com/cap-js/cds-dbs/issues/1716)) ([398bacc](https://github.com/cap-js/cds-dbs/commit/398bacc2a74883dcd93acba288deae362570a4c1))
20
+ * 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))
21
+ * **pool:** recover lost connection after stale acquire timeout ([#1724](https://github.com/cap-js/cds-dbs/issues/1724)) ([cf6af99](https://github.com/cap-js/cds-dbs/commit/cf6af991d02be7cf5f861e2a424d4d733b8f9238))
22
+ * pql groupby path expression ([#1700](https://github.com/cap-js/cds-dbs/issues/1700)) ([82b5e03](https://github.com/cap-js/cds-dbs/commit/82b5e0385e680321f30c352f0f78ade6d25259a6))
23
+
7
24
  ## [3.0.1](https://github.com/cap-js/cds-dbs/compare/db-service-v3.0.0...db-service-v3.0.1) (2026-06-29)
8
25
 
9
26
 
@@ -150,7 +150,7 @@ constructor (factory, options = {}) {
150
150
  return
151
151
  }
152
152
  const loan = this._loans.get(resource)
153
- if (!loan) throw new Error('Resource not currently part of this pool')
153
+ if (!loan) return
154
154
  this._loans.delete(resource)
155
155
  const pooledResource = loan.pooledResource
156
156
  pooledResource.idle()
@@ -164,7 +164,7 @@ constructor (factory, options = {}) {
164
164
  return
165
165
  }
166
166
  const loan = this._loans.get(resource)
167
- if (!loan) throw new Error('Resource not currently part of this pool')
167
+ if (!loan) return
168
168
  this._loans.delete(resource)
169
169
  const pooledResource = loan.pooledResource
170
170
  await this.#destroy(pooledResource)
@@ -212,16 +212,13 @@ constructor (factory, options = {}) {
212
212
  for (let i = 0; i < needed; i++) this.#createResource()
213
213
  }
214
214
  const dispense = async resource => {
215
- const request = this._queue.shift()
215
+ let request = this._queue.shift()
216
+ while (request && request.state !== RequestState.PENDING) request = this._queue.shift()
216
217
  if (!request) {
217
218
  resource.idle()
218
219
  this._available.add(resource)
219
220
  return false
220
221
  }
221
- if (request.state !== RequestState.PENDING) {
222
- this.#dispense()
223
- return false
224
- }
225
222
  this._loans.set(resource.obj, { pooledResource: resource })
226
223
  resource.update(ResourceState.ALLOCATED)
227
224
  request.resolve(resource.obj)
@@ -18,7 +18,7 @@ class CQN2PQLRenderer extends CQN2SQL {
18
18
 
19
19
  column_expr(x, q) {
20
20
  // omit alias when target is a single source
21
- if (q.SELECT.from.ref && x?.ref) x.ref = x.ref.slice(-1)
21
+ if (q.SELECT.from?.ref && x?.ref) x.ref = x.ref.slice(-1)
22
22
  return super.column_expr(x, q)
23
23
  }
24
24
 
@@ -1088,7 +1088,7 @@ class CQN2SQLRenderer {
1088
1088
 
1089
1089
  const extractions = this._managed = this.managed(columns.map(c => ({ name: c, sql: `NEW.${this.quote(c)}` })), elements)
1090
1090
  const sql = extractions.length > columns.length
1091
- ? `SELECT ${extractions.map(c => `${c.insert} AS ${this.quote(c.name)}`)} FROM (${this.SELECT(src)}) AS NEW`
1091
+ ? `SELECT ${extractions.map((c, i) => `${i < columns.length ? c.insert : c.onInsert} AS ${this.quote(c.name)}`)} FROM (${this.SELECT(src)}) AS NEW`
1092
1092
  : this.SELECT(src)
1093
1093
  if (extractions.length > columns.length) columns = this.columns = extractions.map(c => c.name)
1094
1094
  this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c))}) ${sql}`
@@ -1161,12 +1161,12 @@ class CQN2SQLRenderer {
1161
1161
  const extractions = this._managed
1162
1162
  if (this.values) this.values = [] // Clear previously computed values
1163
1163
  const src = this.cqn4sql(UPSERT.from || UPSERT.as)
1164
- const aliasedQuery = cds.ql.SELECT
1165
- .columns(src.SELECT.columns
1166
- .map((c, i) => ({ ref: [this.column_name(c)], as: this.columns[i] }))
1167
- )
1168
- .from(src)
1169
- sql = `SELECT ${extractions.map(c => `${c.upsert}`)} FROM (${this.SELECT(aliasedQuery)}) AS NEW LEFT JOIN ${this.quote(entity)} AS OLD ON ${keyCompare}`
1164
+ const aliasedQuery = `SELECT ${[
1165
+ ...src.SELECT.columns.map((c, i) => this.column_expr({ ref: [this.column_name(c)], as: this.columns[i] })),
1166
+ ...extractions.slice(src.SELECT.columns.length).map(c => `${elements[c.name].key ? c.onInsert : 'NULL'} AS ${this.quote(c.name)}`), // fill in missing default values
1167
+ ]} FROM (${this.SELECT(src)})`
1168
+
1169
+ sql = `SELECT ${extractions.map(c => elements[c.name].key ? `NEW.${this.quote(c.name)}` : c.upsert)} FROM(${aliasedQuery}) AS NEW LEFT JOIN ${this.quote(entity)} AS OLD ON ${keyCompare} `
1170
1170
  if (extractions.length > columns.length) columns = this.columns = extractions.map(c => c.name)
1171
1171
  this.entries = [this.values]
1172
1172
  }
@@ -1574,9 +1574,11 @@ class CQN2SQLRenderer {
1574
1574
 
1575
1575
  managed_extract(name, element, converter) {
1576
1576
  const { UPSERT, INSERT } = this.cqn
1577
- const extract = !(INSERT?.entries || UPSERT?.entries) && (INSERT?.rows || UPSERT?.rows)
1578
- ? `value->>${this.string(`$[${this.columns.indexOf(name)}]`)}`
1579
- : `value->>${this.string(`$.${JSON.stringify(name)}`)}`
1577
+ const extract = (INSERT?.entries || UPSERT?.entries)
1578
+ ? `value->>${this.string(`$.${JSON.stringify(name)}`)}`
1579
+ : (INSERT?.rows || UPSERT?.rows)
1580
+ ? `value->>${this.string(`$[${this.columns.indexOf(name)}]`)}`
1581
+ : `NEW.${this.quote(name)}` // in case of (INSERT?.from || UPSERT?.from)
1580
1582
  const sql = converter?.(extract) || extract
1581
1583
  return { extract, sql }
1582
1584
  }
@@ -1587,7 +1589,9 @@ class CQN2SQLRenderer {
1587
1589
  }
1588
1590
 
1589
1591
  managed_default(name, managed, src) {
1590
- return `(CASE WHEN json_type(value,${this.managed_extract(name).extract.slice(8)}) IS NULL THEN ${managed} ELSE ${src} END)`
1592
+ const { UPSERT, INSERT } = this.cqn
1593
+ const isJson = INSERT?.entries || UPSERT?.entries || INSERT?.rows || UPSERT?.rows
1594
+ return `(CASE WHEN ${isJson ? `json_type(value,${this.managed_extract(name).extract.slice(8)})` : `NEW.${this.quote(name)}`} IS NULL THEN ${managed} ELSE ${src} END)`
1591
1595
  }
1592
1596
  }
1593
1597
 
@@ -33,6 +33,9 @@ const notSupportedOps = [['>'], ['<'], ['>='], ['<='], ['*'], ['+'], ['-'], ['/'
33
33
 
34
34
  const allOps = eqOps.concat(eqOps).concat(notEqOps).concat(notSupportedOps)
35
35
 
36
+
37
+ const { depthGuarded, guardEntry } = require('./recursion-guard')
38
+
36
39
  const { pseudos } = require('./infer/pseudos')
37
40
  /**
38
41
  * Transforms a CDL style query into SQL-Like CQN:
@@ -55,7 +58,17 @@ const { pseudos } = require('./infer/pseudos')
55
58
  * @returns {object} transformedQuery the transformed query
56
59
  */
57
60
  function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
61
+ // guardEntry bounds subquery nesting and resets the depth budget per top-level call
62
+ return guardEntry(_cqn4sql)(originalQuery, model, useTechnicalAlias)
63
+ }
64
+
65
+ function _cqn4sql(originalQuery, model, useTechnicalAlias = true) {
58
66
  const getImplicitAlias = str => _getImplicitAlias(str, useTechnicalAlias)
67
+ // guard xpr and inline/expand-on-struct nesting
68
+ // eslint-disable-next-line no-func-assign
69
+ getTransformedTokenStream = depthGuarded(getTransformedTokenStream)
70
+ // eslint-disable-next-line no-func-assign
71
+ nestedProjectionOnStructure = depthGuarded(nestedProjectionOnStructure)
59
72
  let inferred = typeof originalQuery === 'string' ? cds.parse.cql(originalQuery) : cds.ql.clone(originalQuery)
60
73
  const hasCustomJoins =
61
74
  originalQuery.SELECT?.from.args && (!originalQuery.joinTree || originalQuery.joinTree.isInitial)
@@ -69,6 +82,9 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
69
82
  const { where, having } = transformSearch(searchTerm)
70
83
  if (where) inferred.SELECT.where = where
71
84
  else if (having) inferred.SELECT.having = having
85
+ // Defer the ranking ORDER BY to after infer(), where the outer table alias is known and the
86
+ // deep-search sub-select can be correlated to the outer row (see buildSearchRankOrderBy).
87
+ defineProperty(inferred, '$searchRank', searchTerm)
72
88
  }
73
89
  }
74
90
  // query modifiers can also be defined in from ref leaf infix filter
@@ -182,7 +198,7 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
182
198
  /**
183
199
  * If the target entity is annotated with persistence skip and has an underlying db entity,
184
200
  * we treat it as a runtime view and transform it into a CTE.
185
- *
201
+ *
186
202
  * @param {object} transformedQuery - The query object to be transformed.
187
203
  * @param {string} model - The data model used for inference and transformation.
188
204
  */
@@ -198,10 +214,10 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
198
214
  }
199
215
 
200
216
  /**
201
- * Recursively call cqn4sql for all nested runtime views to calculate cte and
217
+ * Recursively call cqn4sql for all nested runtime views to calculate cte and
202
218
  * add it as a with clause to the transformed query.
203
219
  * Alias the runtime view with a unique alias and update all references to the runtime view to point to the alias.
204
- *
220
+ *
205
221
  * @param {object} rootDefinition - The root definition of the query. This is used to recursively process nested runtime views.
206
222
  * @param {object} transformedQuery - The query object to be transformed.
207
223
  * @param {string} model - The data model used for infer and cqn4sql.
@@ -317,9 +333,29 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
317
333
 
318
334
  // Since all the expressions in the SELECT part of the query have been computed,
319
335
  // one can reference aliases of the queries columns in the orderBy clause.
320
- if (orderBy) {
321
- const transformedOrderBy = getTransformedOrderByGroupBy(orderBy, true)
336
+ let effectiveOrderBy = orderBy
337
+ // Rank by $search relevance — only when the score exists (HANA fuzzy, not opted out); else
338
+ // it would sort by a constant boolean.
339
+ const ranksSearch =
340
+ cds.db?.kind === 'hana' && cds.env.hana?.fuzzy !== false && cds.env.hana?.fuzzy?.ranked_search !== false
341
+ // count queries do not need ranked search
342
+ const isCountQuery = columns?.length === 1 && columns[0].func === 'count'
343
+ const searchRank = ranksSearch && !isCountQuery && inferred.$searchRank && buildSearchRankOrderBy(inferred.$searchRank)
344
+ if (searchRank) {
345
+ // precedence: user ordering, then rank, then the runtime's implicit key ordering
346
+ const implicitAt = (orderBy || []).findIndex(o => o.implicit)
347
+ const at = implicitAt === -1 ? (orderBy?.length ?? 0) : implicitAt
348
+ effectiveOrderBy = [...(orderBy || [])]
349
+ effectiveOrderBy.splice(at, 0, searchRank)
350
+ }
351
+ if (effectiveOrderBy) {
352
+ const transformedOrderBy = getTransformedOrderByGroupBy(effectiveOrderBy, true)
322
353
  if (transformedOrderBy.length) {
354
+ // the rank is the only order-by entry that is a correlated sub-select
355
+ if (searchRank?.$searchRank) {
356
+ const rank = transformedOrderBy.find(o => o.SELECT)
357
+ if (rank) correlateSearchRank(rank, transformedFrom.as)
358
+ }
323
359
  transformedQuery.SELECT.orderBy = transformedOrderBy
324
360
  }
325
361
  }
@@ -383,6 +419,8 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
383
419
  */
384
420
  function translateAssocsToJoins() {
385
421
  let from
422
+ // guard association-path length
423
+ const joinForBranch = depthGuarded(_joinForBranch)
386
424
  /**
387
425
  * remember already seen aliases, do not create a join for them again
388
426
  */
@@ -406,7 +444,7 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
406
444
  })
407
445
  return from.args.length > 1 ? from : from.args[0]
408
446
 
409
- function joinForBranch(lhs, node) {
447
+ function _joinForBranch(lhs, node) {
410
448
  const nextAssoc = inferred.joinTree.findNextAssoc(node)
411
449
  if (!nextAssoc || alreadySeen.has(nextAssoc.$refLink.alias)) return lhs.args.length > 1 ? lhs : lhs.args[0]
412
450
 
@@ -643,7 +681,7 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
643
681
  if (col.cast) ret.cast = resolveEnumCastType(col.cast)
644
682
  return ret
645
683
  }
646
- return copy(col)
684
+ return { ...col }
647
685
  }
648
686
 
649
687
  function handleEmptyColumns(columns) {
@@ -995,11 +1033,11 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
995
1033
  const keyName = k.as || k.ref.join('_')
996
1034
  const fkName = `${elemName}_${keyName}` // e.g., 'head_id'
997
1035
  const fkFullName = `${columnAlias}_${fkName}` // e.g., 'department_head_id'
998
-
1036
+
999
1037
  // Check if this FK is excluded
1000
1038
  if (exclude.some(e => (e.ref?.at(-1) || e.as || e) === fkName)) continue
1001
1039
  if (exclude.some(e => (e.ref?.at(-1) || e.as || e) === fkFullName)) continue
1002
-
1040
+
1003
1041
  const flatColumn = {
1004
1042
  ref: [joinAlias, fkName],
1005
1043
  as: fkFullName,
@@ -1017,7 +1055,7 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
1017
1055
  calcElement.as = fullName
1018
1056
  }
1019
1057
  res.push(calcElement)
1020
- }
1058
+ }
1021
1059
  else {
1022
1060
  // Scalar element
1023
1061
  const flatColumn = {
@@ -1322,7 +1360,7 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
1322
1360
  else if (col.xpr) transformedColumn = { xpr: getTransformedTokenStream(col.xpr) }
1323
1361
  else if (col.func) transformedColumn = { args: getTransformedFunctionArgs(col.args), func: col.func }
1324
1362
  // val
1325
- else transformedColumn = copy(col)
1363
+ else transformedColumn = { ...col }
1326
1364
  if (col.sort) transformedColumn.sort = col.sort
1327
1365
  if (col.nulls) transformedColumn.nulls = col.nulls
1328
1366
  res.push(transformedColumn)
@@ -1397,8 +1435,13 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
1397
1435
  if (!exclude.includes(k)) {
1398
1436
  const { index, tableAlias } = inferred.$combinedElements[k][0]
1399
1437
  const element = tableAlias.elements[k]
1400
- // ignore FK for odata csn (but not for subquery sources where FK is not a separate element) / ignore blobs from wildcard expansion
1401
- if ((!tableAlias.SELECT && isManagedAssocInFlatMode(element)) || element.type === 'cds.LargeBinary') continue
1438
+ // ignore FK for odata csn (but not for subquery sources where FK is not a separate element) / ignore blobs and vectors from wildcard expansion
1439
+ if (
1440
+ (!tableAlias.SELECT && isManagedAssocInFlatMode(element)) ||
1441
+ element.type === 'cds.LargeBinary' ||
1442
+ element.type === 'cds.Vector'
1443
+ )
1444
+ continue
1402
1445
  // for wildcard on subquery in from, just reference the elements
1403
1446
  if (tableAlias.SELECT && !element.elements && !element.target) {
1404
1447
  wildcardColumns.push(index ? { ref: [index, k] } : { ref: [k] })
@@ -1461,7 +1504,7 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
1461
1504
  * - `tableAlias` — table alias prepended to the column ref.
1462
1505
  * @param {string[]} [csnPath=[]] - Accumulated CSN element path (used for `_csnPath` metadata on leaf columns).
1463
1506
  * @param {{ exclude?: Array, replace?: Array }} [excludeAndReplace] - Columns to exclude or replace during wildcard expansion.
1464
- * @param {boolean} [isWildcard=false] - Whether this expansion originates from a wildcard; filters out LargeBinary.
1507
+ * @param {boolean} [isWildcard=false] - Whether this expansion originates from a wildcard; filters out LargeBinary and Vector.
1465
1508
  * @returns {object[]} Flat column(s) for the given element.
1466
1509
  */
1467
1510
  function getFlatColumnsFor(column, names, csnPath = [], excludeAndReplace, isWildcard = false) {
@@ -1474,7 +1517,7 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
1474
1517
  const { $refLinks, flatName, isJoinRelevant } = column
1475
1518
  let firstNonJoinRelevantAssoc, stepAfterAssoc
1476
1519
  let element = $refLinks ? $refLinks[$refLinks.length - 1].definition : column
1477
- if (isWildcard && element.type === 'cds.LargeBinary') return []
1520
+ if (isWildcard && (element.type === 'cds.LargeBinary' || element.type === 'cds.Vector')) return []
1478
1521
  if (element.on && !element.keys) return [] // unmanaged doesn't make it into columns
1479
1522
  if (element.virtual === true) return []
1480
1523
 
@@ -1812,8 +1855,7 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
1812
1855
  // reject virtual elements in expressions as they will lead to a sql error down the line
1813
1856
  if (lhsDef?.virtual) throw new Error(`Virtual elements are not allowed in expressions`)
1814
1857
 
1815
- let result = is_regexp(token?.val) ? token : copy(token) // REVISIT: too expensive!
1816
- // REVISIT: required because we copy the token above and lose the not enumerable "param: false"
1858
+ let result = typeof token !== 'object' || is_regexp(token?.val) ? token : { ...token }
1817
1859
  if (typeof token === 'object' && 'val' in token && 'param' in token) Object.defineProperty(result, 'param', { value: token.param })
1818
1860
  if (token.ref) {
1819
1861
  const { definition } = token.$refLinks.at(-1)
@@ -1830,7 +1872,6 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
1830
1872
  const stepToFind = token.ref[1]?.id || token.ref[1]
1831
1873
  const outerAlias = outerQuery.$combinedElements?.[stepToFind]?.[0].index
1832
1874
  if (outerAlias) {
1833
- let result = copy(token)
1834
1875
  result.ref = [outerAlias, token.flatName]
1835
1876
  transformedTokenStream.push(result)
1836
1877
  continue
@@ -2016,7 +2057,7 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
2016
2057
  */
2017
2058
  function getTransformedFrom(from, existingWhere = []) {
2018
2059
  const transformedWhere = []
2019
- let transformedFrom = copy(from) // REVISIT: too expensive!
2060
+ let transformedFrom = { ...from }
2020
2061
  if (from.$refLinks) defineProperty(transformedFrom, '$refLinks', [...from.$refLinks])
2021
2062
  if (from.args) {
2022
2063
  transformedFrom.args = []
@@ -2579,6 +2620,65 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
2579
2620
  return { xpr: [matchColumns.length === 1 ? matchColumns[0] : { list: matchColumns }, 'in', subquery] }
2580
2621
  }
2581
2622
 
2623
+ /**
2624
+ * Builds the ORDER BY entry ranking rows by $search relevance, sorted desc.
2625
+ *
2626
+ * Flat search: the score is on the outer row.
2627
+ * Deep search: the score lives in a semi-join, so emit a correlated scalar sub-select
2628
+ * (SELECT search(…, true) FROM <same source> WHERE innerKey = <outerAlias>.key) DESC.
2629
+ * Key comparisons are seeded unqualified (infer() binds them to the sub-select's own source);
2630
+ * correlateSearchRank redirects the rhs to the outer alias afterwards.
2631
+ *
2632
+ * @param {object} searchTerm the search term as returned by getSearch (func or xpr shape)
2633
+ * @returns {object|null} an orderBy entry, or null if there is nothing to rank by
2634
+ */
2635
+ function buildSearchRankOrderBy(searchTerm) {
2636
+ if (searchTerm.func) return { func: searchTerm.func, args: [...searchTerm.args, { val: true }], sort: 'desc' }
2637
+ if (!searchTerm.xpr) return null
2638
+
2639
+ const searchSelect = searchTerm.xpr[2]
2640
+ const searchFunc = searchSelect.SELECT.where[0]
2641
+ const innerKeys = searchSelect.SELECT.columns // unqualified pk refs, e.g. [{ ref: ['ID'] }]
2642
+
2643
+ const where = []
2644
+ for (let i = 0; i < innerKeys.length; i++) {
2645
+ if (i) where.push('and')
2646
+ // seeded unqualified on both sides; correlateSearchRank redirects the rhs to the outer row
2647
+ where.push({ ref: [...innerKeys[i].ref] }, '=', { ref: [...innerKeys[i].ref] })
2648
+ }
2649
+
2650
+ const entry = {
2651
+ __proto__: SELECT.from(searchSelect.SELECT.from)
2652
+ // the correlated outer row fans out to many child rows -> MAX makes it a single score
2653
+ .columns({ func: 'max', args: [{ func: searchFunc.func, args: [...searchFunc.args, { val: true }] }] })
2654
+ .where(where),
2655
+ sort: 'desc',
2656
+ }
2657
+ defineProperty(entry, '$searchRank', true)
2658
+ return entry
2659
+ }
2660
+
2661
+ /**
2662
+ * Correlates the (transformed) deep-search ranking sub-select to the outer row.
2663
+ *
2664
+ * buildSearchRankOrderBy seeds its WHERE as `ref = ref` comparisons on the sub-select's own
2665
+ * alias; this rewrites each rhs to `<outerAlias>.<key>`. Driven off the `=` operator (not a
2666
+ * fixed stride) so it holds for any key count.
2667
+ *
2668
+ * @param {object} entry the transformed orderBy entry produced from a `$searchRank` sub-select
2669
+ * @param {string} outerAlias the final table alias of the outer query source
2670
+ */
2671
+ function correlateSearchRank(entry, outerAlias) {
2672
+ const where = entry.SELECT.where
2673
+ for (let i = 1; i < where.length; i++) {
2674
+ // seeded comparisons are exactly `<ref> = <ref>`; rewrite the rhs ref to the outer row
2675
+ if (where[i] === '=' && where[i - 1]?.ref && where[i + 1]?.ref) {
2676
+ const rhs = where[i + 1]
2677
+ rhs.ref = [outerAlias, ...rhs.ref.slice(1)]
2678
+ }
2679
+ }
2680
+ }
2681
+
2582
2682
  /**
2583
2683
  * Calculates the name of the source which can be used to address the given node.
2584
2684
  *