@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.
- package/CHANGELOG.md +12 -0
- package/bin/bind.js +4 -0
- package/bin/build.js +1 -0
- package/bin/compile/index.js +12 -11
- package/bin/completion/index.js +21 -10
- package/bin/completion/scripts/cds.fish +179 -127
- package/bin/completion/scripts/cds.ps1 +88 -14
- package/bin/completion/scripts/cds.sh +71 -5
- package/bin/completion/scripts/cds.zsh +81 -15
- package/bin/completion/templates/cds-tmpl.ps1 +15 -6
- package/bin/deploy/index.js +7 -3
- package/bin/env.js +4 -1
- package/bin/export.js +9 -1
- package/bin/import.js +4 -1
- package/bin/init.js +1 -0
- package/bin/inspect.js +3 -1
- package/bin/lint.js +5 -3
- package/bin/minify.js +6 -1
- package/bin/repl.js +11 -2
- package/bin/run.js +12 -1
- package/bin/serve.js +13 -2
- package/bin/up.js +4 -1
- package/bin/util/containerize.js +2 -0
- package/bin/watch/index.js +8 -1
- package/lib/build/util.js +2 -4
- package/lib/init/constants.js +1 -1
- package/lib/init/template/data/as-json.js +3 -0
- package/node_modules/@cap-js/db-service/CHANGELOG.md +17 -0
- package/node_modules/@cap-js/db-service/lib/common/generic-pool.js +4 -7
- package/node_modules/@cap-js/db-service/lib/cqn2pql.js +1 -1
- package/node_modules/@cap-js/db-service/lib/cqn2sql.js +15 -11
- package/node_modules/@cap-js/db-service/lib/cqn4sql.js +119 -19
- package/node_modules/@cap-js/db-service/lib/infer/index.js +27 -16
- package/node_modules/@cap-js/db-service/lib/recursion-guard.js +40 -0
- package/node_modules/@cap-js/db-service/lib/search.js +1 -1
- package/node_modules/@cap-js/db-service/package.json +1 -1
- package/node_modules/@cap-js/sqlite/CHANGELOG.md +19 -0
- package/node_modules/@cap-js/sqlite/lib/SQLiteService.js +110 -14
- package/node_modules/@cap-js/sqlite/package.json +3 -3
- package/node_modules/@sap/cds-compiler/CHANGELOG.md +15 -0
- package/node_modules/@sap/cds-compiler/lib/compiler/resolve.js +7 -3
- package/node_modules/@sap/cds-compiler/lib/model/csnUtils.js +2 -0
- package/node_modules/@sap/cds-compiler/lib/transform/db/applyTransformations.js +12 -0
- package/node_modules/@sap/cds-compiler/lib/transform/db/associations.js +5 -3
- package/node_modules/@sap/cds-compiler/lib/transform/db/flattening.js +4 -3
- package/node_modules/@sap/cds-compiler/lib/transform/effective/flattening.js +17 -1
- package/node_modules/@sap/cds-compiler/lib/transform/effective/main.js +19 -0
- package/node_modules/@sap/cds-compiler/lib/transform/fioriTreeViews.js +12 -1
- package/node_modules/@sap/cds-compiler/lib/transform/forRelationalDB.js +1 -1
- package/node_modules/@sap/cds-compiler/lib/transform/odata/adaptAnnotationRefs.js +11 -5
- package/node_modules/@sap/cds-compiler/lib/transform/translateAssocsToJoins.js +29 -2
- package/node_modules/@sap/cds-compiler/package.json +1 -1
- package/node_modules/@sap/cds-mtxs/CHANGELOG.md +34 -0
- package/node_modules/@sap/cds-mtxs/bin/cds-mtx-migrate.js +26 -5
- package/node_modules/@sap/cds-mtxs/bin/cds-mtx.js +3 -0
- package/node_modules/@sap/cds-mtxs/lib/utils.js +5 -4
- package/node_modules/@sap/cds-mtxs/package.json +1 -1
- package/node_modules/@sap/cds-mtxs/srv/cf/abstract-provisioning-service.js +62 -7
- package/node_modules/@sap/cds-mtxs/srv/cf/saas-provisioning-service.js +1 -1
- package/node_modules/@sap/cds-mtxs/srv/cf/sms-provisioning-service.js +23 -3
- package/node_modules/@sap/cds-mtxs/srv/extensibility/compat/update.js +15 -0
- package/node_modules/@sap/cds-mtxs/srv/extensibility/linter/annotations.js +2 -2
- package/node_modules/@sap/cds-mtxs/srv/extensibility/linter/config.js +2 -0
- package/node_modules/@sap/cds-mtxs/srv/extensibility/linter/index.js +2 -0
- package/node_modules/@sap/cds-mtxs/srv/extensibility/linter/protected-namespace.js +45 -0
- package/node_modules/@sap/cds-mtxs/srv/extensibility-service.js +8 -3
- package/node_modules/@sap/cds-mtxs/srv/jobs-service.js +49 -10
- package/node_modules/@sap/cds-mtxs/srv/model-provider.js +11 -2
- package/node_modules/@sap/cds-mtxs/srv/plugins/common.js +19 -12
- package/node_modules/@sap/cds-mtxs/srv/plugins/hana/hdi-ctnr-mgr.js +6 -3
- package/node_modules/@sap/cds-mtxs/srv/plugins/hana/serverless.js +271 -0
- package/node_modules/@sap/cds-mtxs/srv/plugins/hana.js +3 -2
- package/node_modules/@sap/cds-mtxs/srv/worker/job-worker-pool.js +212 -0
- package/node_modules/@sap/cds-mtxs/srv/worker/job-worker.js +61 -0
- package/package.json +2 -2
- /package/node_modules/@sap/cds-mtxs/srv/worker/{pool.js → model-worker-pool.js} +0 -0
- /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
|
|
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 =
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
169
|
+
column.as === '$count'
|
|
170
170
|
) {
|
|
171
171
|
return
|
|
172
172
|
}
|
|
@@ -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
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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]
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|