@sap/cds-dk 10.1.0 → 10.1.2

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 (114) hide show
  1. package/CHANGELOG.md +24 -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/plugins/internal.js +0 -26
  26. package/lib/build/plugins/nodejs/index.js +8 -3
  27. package/lib/build/util.js +2 -4
  28. package/lib/cds.js +8 -2
  29. package/lib/init/constants.js +1 -1
  30. package/lib/init/merge.js +21 -17
  31. package/lib/init/template/cloud-logging/index.js +3 -3
  32. package/lib/init/template/data/as-json.js +3 -0
  33. package/lib/init/template/destination/index.js +3 -3
  34. package/lib/init/template/enterprise-messaging/index.js +4 -1
  35. package/lib/init/template/event-mesh/index.js +3 -0
  36. package/lib/init/template/multitenancy/index.js +2 -2
  37. package/lib/init/template/workzone-standard/index.js +1 -1
  38. package/node_modules/@cap-js/db-service/CHANGELOG.md +30 -0
  39. package/node_modules/@cap-js/db-service/lib/common/generic-pool.js +4 -7
  40. package/node_modules/@cap-js/db-service/lib/cqn2pql.js +1 -1
  41. package/node_modules/@cap-js/db-service/lib/cqn2sql.js +15 -11
  42. package/node_modules/@cap-js/db-service/lib/cqn4sql.js +125 -21
  43. package/node_modules/@cap-js/db-service/lib/infer/index.js +27 -16
  44. package/node_modules/@cap-js/db-service/lib/recursion-guard.js +40 -0
  45. package/node_modules/@cap-js/db-service/lib/search.js +1 -1
  46. package/node_modules/@cap-js/db-service/package.json +1 -1
  47. package/node_modules/@cap-js/sqlite/CHANGELOG.md +28 -0
  48. package/node_modules/@cap-js/sqlite/lib/SQLiteService.js +110 -14
  49. package/node_modules/@cap-js/sqlite/package.json +3 -3
  50. package/node_modules/@sap/cds/CHANGELOG.md +19 -0
  51. package/node_modules/@sap/cds/common.cds +3 -3
  52. package/node_modules/@sap/cds/lib/compile/minify.js +3 -1
  53. package/node_modules/@sap/cds/lib/env/cds-env.js +1 -1
  54. package/node_modules/@sap/cds/lib/env/cds-requires.js +1 -0
  55. package/node_modules/@sap/cds/libx/_runtime/common/generic/crud.js +7 -1
  56. package/node_modules/@sap/cds/libx/_runtime/common/utils/cqn.js +1 -1
  57. package/node_modules/@sap/cds/libx/_runtime/fiori/lean-draft.js +1 -1
  58. package/node_modules/@sap/cds/libx/_runtime/messaging/common-utils/normalizeIncomingMessage.js +1 -1
  59. package/node_modules/@sap/cds/libx/_runtime/messaging/kafka.js +1 -1
  60. package/node_modules/@sap/cds/libx/http/location.js +10 -11
  61. package/node_modules/@sap/cds/libx/odata/middleware/read.js +1 -1
  62. package/node_modules/@sap/cds/libx/queue/SchedulingService.js +4 -0
  63. package/node_modules/@sap/cds/libx/queue/index.js +44 -10
  64. package/node_modules/@sap/cds/libx/queue/processing.js +47 -16
  65. package/node_modules/@sap/cds/libx/queue/utils.js +3 -0
  66. package/node_modules/@sap/cds/package.json +1 -1
  67. package/node_modules/@sap/cds/tasks/enterprise-messaging-deploy.js +13 -5
  68. package/node_modules/@sap/cds-compiler/CHANGELOG.md +15 -0
  69. package/node_modules/@sap/cds-compiler/lib/compiler/resolve.js +7 -3
  70. package/node_modules/@sap/cds-compiler/lib/model/csnUtils.js +2 -0
  71. package/node_modules/@sap/cds-compiler/lib/transform/db/applyTransformations.js +12 -0
  72. package/node_modules/@sap/cds-compiler/lib/transform/db/associations.js +5 -3
  73. package/node_modules/@sap/cds-compiler/lib/transform/db/flattening.js +4 -3
  74. package/node_modules/@sap/cds-compiler/lib/transform/effective/flattening.js +17 -1
  75. package/node_modules/@sap/cds-compiler/lib/transform/effective/main.js +19 -0
  76. package/node_modules/@sap/cds-compiler/lib/transform/fioriTreeViews.js +12 -1
  77. package/node_modules/@sap/cds-compiler/lib/transform/forRelationalDB.js +1 -1
  78. package/node_modules/@sap/cds-compiler/lib/transform/odata/adaptAnnotationRefs.js +11 -5
  79. package/node_modules/@sap/cds-compiler/lib/transform/translateAssocsToJoins.js +29 -2
  80. package/node_modules/@sap/cds-compiler/package.json +1 -1
  81. package/node_modules/@sap/cds-mtxs/CHANGELOG.md +34 -0
  82. package/node_modules/@sap/cds-mtxs/bin/cds-mtx-migrate.js +26 -5
  83. package/node_modules/@sap/cds-mtxs/bin/cds-mtx.js +3 -0
  84. package/node_modules/@sap/cds-mtxs/lib/utils.js +5 -4
  85. package/node_modules/@sap/cds-mtxs/package.json +1 -1
  86. package/node_modules/@sap/cds-mtxs/srv/cf/abstract-provisioning-service.js +62 -7
  87. package/node_modules/@sap/cds-mtxs/srv/cf/saas-provisioning-service.js +1 -1
  88. package/node_modules/@sap/cds-mtxs/srv/cf/sms-provisioning-service.js +23 -3
  89. package/node_modules/@sap/cds-mtxs/srv/extensibility/compat/update.js +15 -0
  90. package/node_modules/@sap/cds-mtxs/srv/extensibility/linter/annotations.js +2 -2
  91. package/node_modules/@sap/cds-mtxs/srv/extensibility/linter/config.js +2 -0
  92. package/node_modules/@sap/cds-mtxs/srv/extensibility/linter/index.js +2 -0
  93. package/node_modules/@sap/cds-mtxs/srv/extensibility/linter/protected-namespace.js +45 -0
  94. package/node_modules/@sap/cds-mtxs/srv/extensibility-service.js +8 -3
  95. package/node_modules/@sap/cds-mtxs/srv/jobs-service.js +49 -10
  96. package/node_modules/@sap/cds-mtxs/srv/model-provider.js +11 -2
  97. package/node_modules/@sap/cds-mtxs/srv/plugins/common.js +19 -12
  98. package/node_modules/@sap/cds-mtxs/srv/plugins/hana/hdi-ctnr-mgr.js +6 -3
  99. package/node_modules/@sap/cds-mtxs/srv/plugins/hana/serverless.js +271 -0
  100. package/node_modules/@sap/cds-mtxs/srv/plugins/hana.js +3 -2
  101. package/node_modules/@sap/cds-mtxs/srv/worker/job-worker-pool.js +212 -0
  102. package/node_modules/@sap/cds-mtxs/srv/worker/job-worker.js +61 -0
  103. package/node_modules/proxy-addr/HISTORY.md +60 -54
  104. package/node_modules/proxy-addr/README.md +1 -1
  105. package/node_modules/proxy-addr/index.js +32 -0
  106. package/node_modules/proxy-addr/package.json +8 -4
  107. package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
  108. package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
  109. package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
  110. package/node_modules/yaml/dist/nodes/Alias.js +25 -23
  111. package/node_modules/yaml/package.json +1 -1
  112. package/package.json +2 -2
  113. /package/node_modules/@sap/cds-mtxs/srv/worker/{pool.js → model-worker-pool.js} +0 -0
  114. /package/node_modules/@sap/cds-mtxs/srv/worker/{load.js → model-worker.js} +0 -0
@@ -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,31 @@ 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 or static values do not need ranked search
342
+ const isCountQuery = columns?.length === 1 && typeof columns[0] === 'object' && (columns[0].func === 'count' || 'val' in columns[0])
343
+ // a user-provided ORDER BY takes precedence over the search rank — don't inject the ranking then
344
+ const hasExplicitOrderBy = (orderBy || []).some(o => !o.implicit)
345
+ const searchRank = ranksSearch && !isCountQuery && !hasExplicitOrderBy && inferred.$searchRank && buildSearchRankOrderBy(inferred.$searchRank)
346
+ if (searchRank) {
347
+ // precedence: user ordering, then rank, then the runtime's implicit key ordering
348
+ const implicitAt = (orderBy || []).findIndex(o => o.implicit)
349
+ const at = implicitAt === -1 ? (orderBy?.length ?? 0) : implicitAt
350
+ effectiveOrderBy = [...(orderBy || [])]
351
+ effectiveOrderBy.splice(at, 0, searchRank)
352
+ }
353
+ if (effectiveOrderBy) {
354
+ const transformedOrderBy = getTransformedOrderByGroupBy(effectiveOrderBy, true)
322
355
  if (transformedOrderBy.length) {
356
+ // the rank is the only order-by entry that is a correlated sub-select
357
+ if (searchRank?.$searchRank) {
358
+ const rank = transformedOrderBy.find(o => o.SELECT)
359
+ if (rank) correlateSearchRank(rank, transformedFrom.as)
360
+ }
323
361
  transformedQuery.SELECT.orderBy = transformedOrderBy
324
362
  }
325
363
  }
@@ -383,6 +421,8 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
383
421
  */
384
422
  function translateAssocsToJoins() {
385
423
  let from
424
+ // guard association-path length
425
+ const joinForBranch = depthGuarded(_joinForBranch)
386
426
  /**
387
427
  * remember already seen aliases, do not create a join for them again
388
428
  */
@@ -406,7 +446,7 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
406
446
  })
407
447
  return from.args.length > 1 ? from : from.args[0]
408
448
 
409
- function joinForBranch(lhs, node) {
449
+ function _joinForBranch(lhs, node) {
410
450
  const nextAssoc = inferred.joinTree.findNextAssoc(node)
411
451
  if (!nextAssoc || alreadySeen.has(nextAssoc.$refLink.alias)) return lhs.args.length > 1 ? lhs : lhs.args[0]
412
452
 
@@ -643,7 +683,7 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
643
683
  if (col.cast) ret.cast = resolveEnumCastType(col.cast)
644
684
  return ret
645
685
  }
646
- return copy(col)
686
+ return { ...col }
647
687
  }
648
688
 
649
689
  function handleEmptyColumns(columns) {
@@ -995,11 +1035,11 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
995
1035
  const keyName = k.as || k.ref.join('_')
996
1036
  const fkName = `${elemName}_${keyName}` // e.g., 'head_id'
997
1037
  const fkFullName = `${columnAlias}_${fkName}` // e.g., 'department_head_id'
998
-
1038
+
999
1039
  // Check if this FK is excluded
1000
1040
  if (exclude.some(e => (e.ref?.at(-1) || e.as || e) === fkName)) continue
1001
1041
  if (exclude.some(e => (e.ref?.at(-1) || e.as || e) === fkFullName)) continue
1002
-
1042
+
1003
1043
  const flatColumn = {
1004
1044
  ref: [joinAlias, fkName],
1005
1045
  as: fkFullName,
@@ -1017,7 +1057,7 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
1017
1057
  calcElement.as = fullName
1018
1058
  }
1019
1059
  res.push(calcElement)
1020
- }
1060
+ }
1021
1061
  else {
1022
1062
  // Scalar element
1023
1063
  const flatColumn = {
@@ -1322,7 +1362,7 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
1322
1362
  else if (col.xpr) transformedColumn = { xpr: getTransformedTokenStream(col.xpr) }
1323
1363
  else if (col.func) transformedColumn = { args: getTransformedFunctionArgs(col.args), func: col.func }
1324
1364
  // val
1325
- else transformedColumn = copy(col)
1365
+ else transformedColumn = { ...col }
1326
1366
  if (col.sort) transformedColumn.sort = col.sort
1327
1367
  if (col.nulls) transformedColumn.nulls = col.nulls
1328
1368
  res.push(transformedColumn)
@@ -1397,8 +1437,13 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
1397
1437
  if (!exclude.includes(k)) {
1398
1438
  const { index, tableAlias } = inferred.$combinedElements[k][0]
1399
1439
  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
1440
+ // ignore FK for odata csn (but not for subquery sources where FK is not a separate element) / ignore blobs and vectors from wildcard expansion
1441
+ if (
1442
+ (!tableAlias.SELECT && isManagedAssocInFlatMode(element)) ||
1443
+ element.type === 'cds.LargeBinary' ||
1444
+ element.type === 'cds.Vector'
1445
+ )
1446
+ continue
1402
1447
  // for wildcard on subquery in from, just reference the elements
1403
1448
  if (tableAlias.SELECT && !element.elements && !element.target) {
1404
1449
  wildcardColumns.push(index ? { ref: [index, k] } : { ref: [k] })
@@ -1461,7 +1506,7 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
1461
1506
  * - `tableAlias` — table alias prepended to the column ref.
1462
1507
  * @param {string[]} [csnPath=[]] - Accumulated CSN element path (used for `_csnPath` metadata on leaf columns).
1463
1508
  * @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.
1509
+ * @param {boolean} [isWildcard=false] - Whether this expansion originates from a wildcard; filters out LargeBinary and Vector.
1465
1510
  * @returns {object[]} Flat column(s) for the given element.
1466
1511
  */
1467
1512
  function getFlatColumnsFor(column, names, csnPath = [], excludeAndReplace, isWildcard = false) {
@@ -1474,7 +1519,7 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
1474
1519
  const { $refLinks, flatName, isJoinRelevant } = column
1475
1520
  let firstNonJoinRelevantAssoc, stepAfterAssoc
1476
1521
  let element = $refLinks ? $refLinks[$refLinks.length - 1].definition : column
1477
- if (isWildcard && element.type === 'cds.LargeBinary') return []
1522
+ if (isWildcard && (element.type === 'cds.LargeBinary' || element.type === 'cds.Vector')) return []
1478
1523
  if (element.on && !element.keys) return [] // unmanaged doesn't make it into columns
1479
1524
  if (element.virtual === true) return []
1480
1525
 
@@ -1733,8 +1778,10 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
1733
1778
  }
1734
1779
 
1735
1780
  const whereExists = { SELECT: whereExistsSubqueries(whereExistsSubSelects) }
1736
- transformedTokenStream[i + 1] = whereExists
1737
- // skip newly created subquery from being iterated
1781
+ // append, don't index by `i`: a preceding branch (e.g. flattening `<assoc> is null`)
1782
+ // may have changed the transformed stream's length, so `[i + 1]` would leave holes
1783
+ transformedTokenStream.push(whereExists)
1784
+ // skip the association ref which we just turned into the subquery
1738
1785
  i += 1
1739
1786
  } else if (token !== null && typeof token === 'object' && '#' in token) {
1740
1787
  // Enum token: resolve to its value
@@ -1812,8 +1859,7 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
1812
1859
  // reject virtual elements in expressions as they will lead to a sql error down the line
1813
1860
  if (lhsDef?.virtual) throw new Error(`Virtual elements are not allowed in expressions`)
1814
1861
 
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"
1862
+ let result = typeof token !== 'object' || is_regexp(token?.val) ? token : { ...token }
1817
1863
  if (typeof token === 'object' && 'val' in token && 'param' in token) Object.defineProperty(result, 'param', { value: token.param })
1818
1864
  if (token.ref) {
1819
1865
  const { definition } = token.$refLinks.at(-1)
@@ -1830,7 +1876,6 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
1830
1876
  const stepToFind = token.ref[1]?.id || token.ref[1]
1831
1877
  const outerAlias = outerQuery.$combinedElements?.[stepToFind]?.[0].index
1832
1878
  if (outerAlias) {
1833
- let result = copy(token)
1834
1879
  result.ref = [outerAlias, token.flatName]
1835
1880
  transformedTokenStream.push(result)
1836
1881
  continue
@@ -2016,7 +2061,7 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
2016
2061
  */
2017
2062
  function getTransformedFrom(from, existingWhere = []) {
2018
2063
  const transformedWhere = []
2019
- let transformedFrom = copy(from) // REVISIT: too expensive!
2064
+ let transformedFrom = { ...from }
2020
2065
  if (from.$refLinks) defineProperty(transformedFrom, '$refLinks', [...from.$refLinks])
2021
2066
  if (from.args) {
2022
2067
  transformedFrom.args = []
@@ -2579,6 +2624,65 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
2579
2624
  return { xpr: [matchColumns.length === 1 ? matchColumns[0] : { list: matchColumns }, 'in', subquery] }
2580
2625
  }
2581
2626
 
2627
+ /**
2628
+ * Builds the ORDER BY entry ranking rows by $search relevance, sorted desc.
2629
+ *
2630
+ * Flat search: the score is on the outer row.
2631
+ * Deep search: the score lives in a semi-join, so emit a correlated scalar sub-select
2632
+ * (SELECT search(…, true) FROM <same source> WHERE innerKey = <outerAlias>.key) DESC.
2633
+ * Key comparisons are seeded unqualified (infer() binds them to the sub-select's own source);
2634
+ * correlateSearchRank redirects the rhs to the outer alias afterwards.
2635
+ *
2636
+ * @param {object} searchTerm the search term as returned by getSearch (func or xpr shape)
2637
+ * @returns {object|null} an orderBy entry, or null if there is nothing to rank by
2638
+ */
2639
+ function buildSearchRankOrderBy(searchTerm) {
2640
+ if (searchTerm.func) return { func: searchTerm.func, args: [...searchTerm.args, { val: true }], sort: 'desc' }
2641
+ if (!searchTerm.xpr) return null
2642
+
2643
+ const searchSelect = searchTerm.xpr[2]
2644
+ const searchFunc = searchSelect.SELECT.where[0]
2645
+ const innerKeys = searchSelect.SELECT.columns // unqualified pk refs, e.g. [{ ref: ['ID'] }]
2646
+
2647
+ const where = []
2648
+ for (let i = 0; i < innerKeys.length; i++) {
2649
+ if (i) where.push('and')
2650
+ // seeded unqualified on both sides; correlateSearchRank redirects the rhs to the outer row
2651
+ where.push({ ref: [...innerKeys[i].ref] }, '=', { ref: [...innerKeys[i].ref] })
2652
+ }
2653
+
2654
+ const entry = {
2655
+ __proto__: SELECT.from(searchSelect.SELECT.from)
2656
+ // the correlated outer row fans out to many child rows -> MAX makes it a single score
2657
+ .columns({ func: 'max', args: [{ func: searchFunc.func, args: [...searchFunc.args, { val: true }] }] })
2658
+ .where(where),
2659
+ sort: 'desc',
2660
+ }
2661
+ defineProperty(entry, '$searchRank', true)
2662
+ return entry
2663
+ }
2664
+
2665
+ /**
2666
+ * Correlates the (transformed) deep-search ranking sub-select to the outer row.
2667
+ *
2668
+ * buildSearchRankOrderBy seeds its WHERE as `ref = ref` comparisons on the sub-select's own
2669
+ * alias; this rewrites each rhs to `<outerAlias>.<key>`. Driven off the `=` operator (not a
2670
+ * fixed stride) so it holds for any key count.
2671
+ *
2672
+ * @param {object} entry the transformed orderBy entry produced from a `$searchRank` sub-select
2673
+ * @param {string} outerAlias the final table alias of the outer query source
2674
+ */
2675
+ function correlateSearchRank(entry, outerAlias) {
2676
+ const where = entry.SELECT.where
2677
+ for (let i = 1; i < where.length; i++) {
2678
+ // seeded comparisons are exactly `<ref> = <ref>`; rewrite the rhs ref to the outer row
2679
+ if (where[i] === '=' && where[i - 1]?.ref && where[i + 1]?.ref) {
2680
+ const rhs = where[i + 1]
2681
+ rhs.ref = [outerAlias, ...rhs.ref.slice(1)]
2682
+ }
2683
+ }
2684
+ }
2685
+
2582
2686
  /**
2583
2687
  * Calculates the name of the source which can be used to address the given node.
2584
2688
  *
@@ -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.1",
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,34 @@
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.1](https://github.com/cap-js/cds-dbs/compare/sqlite-v3.1.0...sqlite-v3.1.1) (2026-09-15)
8
+
9
+
10
+ ### Dependencies
11
+
12
+ * The following workspace dependencies were updated
13
+ * dependencies
14
+ * @cap-js/db-service bumped from ^3.1.0 to ^3.1.1
15
+
16
+ ## [3.1.0](https://github.com/cap-js/cds-dbs/compare/sqlite-v3.0.2...sqlite-v3.1.0) (2026-09-07)
17
+
18
+
19
+ ### Added
20
+
21
+ * 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))
22
+
23
+
24
+ ### Fixed
25
+
26
+ * 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))
27
+
28
+
29
+ ### Dependencies
30
+
31
+ * The following workspace dependencies were updated
32
+ * dependencies
33
+ * @cap-js/db-service bumped from ^3.0.1 to ^3.1.0
34
+
7
35
  ## [3.0.2](https://github.com/cap-js/cds-dbs/compare/sqlite-v3.0.1...sqlite-v3.0.2) (2026-07-01)
8
36
 
9
37
 
@@ -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.1",
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.1"
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": {