@cap-js/db-service 1.3.1 → 1.3.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.
- package/CHANGELOG.md +9 -2
- package/README.md +5 -0
- package/lib/SQLService.js +17 -2
- package/lib/cqn4sql.js +44 -15
- package/lib/infer/index.js +29 -20
- package/lib/infer/join-tree.js +0 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Changelog
|
|
2
2
|
|
|
3
3
|
- All notable changes to this project are documented in this file.
|
|
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
|
-
##
|
|
7
|
+
## [1.3.2](https://github.com/cap-js/cds-dbs/compare/db-service-v1.3.1...db-service-v1.3.2) (2023-10-13)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- preserve $count for result of SELECT queries ([#280](https://github.com/cap-js/cds-dbs/issues/280)) ([23bef24](https://github.com/cap-js/cds-dbs/commit/23bef245e62952a57ed82afcfd238c0b294b2e9e))
|
|
13
|
+
|
|
14
|
+
## [1.3.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.3.0...db-service-v1.3.1) (2023-10-10)
|
|
8
15
|
|
|
9
16
|
### Fixed
|
|
10
17
|
|
package/README.md
CHANGED
|
@@ -12,6 +12,11 @@ This project is open to feature requests/suggestions, bug reports etc. via [GitH
|
|
|
12
12
|
|
|
13
13
|
Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md).
|
|
14
14
|
|
|
15
|
+
## Versioning
|
|
16
|
+
|
|
17
|
+
This library follows [Semantic Versioning](https://semver.org/).
|
|
18
|
+
All notable changes are documented in [CHANGELOG.md](CHANGELOG.md).
|
|
19
|
+
|
|
15
20
|
## Code of Conduct
|
|
16
21
|
|
|
17
22
|
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](CODE_OF_CONDUCT.md) at all times.
|
package/lib/SQLService.js
CHANGED
|
@@ -18,7 +18,7 @@ class SQLService extends DatabaseService {
|
|
|
18
18
|
init() {
|
|
19
19
|
this.on(['SELECT'], this.transformStreamFromCQN)
|
|
20
20
|
this.on(['UPDATE'], this.transformStreamIntoCQN)
|
|
21
|
-
this.on(['INSERT', 'UPSERT', 'UPDATE'], require('./fill-in-keys')) // REVISIT should be replaced by correct input processing eventually
|
|
21
|
+
this.on(['INSERT', 'UPSERT', 'UPDATE'], require('./fill-in-keys')) // REVISIT: should be replaced by correct input processing eventually
|
|
22
22
|
this.on(['INSERT', 'UPSERT', 'UPDATE'], require('./deep-queries').onDeep)
|
|
23
23
|
this.on(['SELECT'], this.onSELECT)
|
|
24
24
|
this.on(['INSERT'], this.onINSERT)
|
|
@@ -79,7 +79,10 @@ class SQLService extends DatabaseService {
|
|
|
79
79
|
let rows = await ps.all(values)
|
|
80
80
|
if (rows.length)
|
|
81
81
|
if (cqn.SELECT.expand) rows = rows.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))
|
|
82
|
-
if (cqn.SELECT.count)
|
|
82
|
+
if (cqn.SELECT.count) {
|
|
83
|
+
// REVISIT: the runtime always expects that the count is preserved with .map, required for renaming in mocks
|
|
84
|
+
return SQLService._arrayWithCount(rows, await this.count(query, rows))
|
|
85
|
+
}
|
|
83
86
|
return cqn.SELECT.one || query.SELECT.from?.ref?.[0].cardinality?.max === 1 ? rows[0] : rows
|
|
84
87
|
}
|
|
85
88
|
|
|
@@ -252,6 +255,17 @@ class SQLService extends DatabaseService {
|
|
|
252
255
|
*/
|
|
253
256
|
static CQN2SQL = require('./cqn2sql').class
|
|
254
257
|
|
|
258
|
+
// REVISIT: There must be a better way!
|
|
259
|
+
// preserves $count for .map calls on array
|
|
260
|
+
static _arrayWithCount = function (a, count) {
|
|
261
|
+
const _map = a.map
|
|
262
|
+
const map = function (..._) { return SQLService._arrayWithCount(_map.call(a, ..._), count) }
|
|
263
|
+
return Object.defineProperties(a, {
|
|
264
|
+
$count: { value: count, enumerable: false, configurable: true, writable: true },
|
|
265
|
+
map: { value: map, enumerable: false, configurable: true, writable: true }
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
255
269
|
/** @param {unknown[]} args */
|
|
256
270
|
constructor(...args) {
|
|
257
271
|
super(...args)
|
|
@@ -387,6 +401,7 @@ const _unquirked = q => {
|
|
|
387
401
|
return q
|
|
388
402
|
}
|
|
389
403
|
|
|
404
|
+
|
|
390
405
|
const sqls = new class extends SQLService { get factory() { return null } }
|
|
391
406
|
cds.extend(cds.ql.Query).with(
|
|
392
407
|
class {
|
package/lib/cqn4sql.js
CHANGED
|
@@ -12,7 +12,7 @@ const infer = require('./infer')
|
|
|
12
12
|
const eqOps = [['is'], ['='] /* ['=='] */]
|
|
13
13
|
/**
|
|
14
14
|
* For operators of <notEqOps>, do the same but use or instead of and.
|
|
15
|
-
* This ensures that not
|
|
15
|
+
* This ensures that not struct == <value> is the same as struct != <value>.
|
|
16
16
|
*/
|
|
17
17
|
const notEqOps = [['is', 'not'], ['<>'], ['!=']]
|
|
18
18
|
/**
|
|
@@ -668,12 +668,24 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
668
668
|
// everything after the wildcard, is a potential replacement
|
|
669
669
|
// in the wildcard expansion
|
|
670
670
|
const replace = []
|
|
671
|
+
|
|
672
|
+
const baseRef = col.ref || []
|
|
673
|
+
const baseRefLinks = col.$refLinks || []
|
|
674
|
+
|
|
675
|
+
// column has no ref, then it is an anonymous expand:
|
|
676
|
+
// select from books { { * } as bar }
|
|
677
|
+
// only possible if there is exactly one query source
|
|
678
|
+
if (!baseRef.length) {
|
|
679
|
+
const [tableAlias, definition] = Object.entries(inferred.sources)[0]
|
|
680
|
+
baseRef.push(tableAlias)
|
|
681
|
+
baseRefLinks.push({ definition, source: definition })
|
|
682
|
+
}
|
|
671
683
|
// we need to make the refs absolute
|
|
672
684
|
col[prop].slice(wildcardIndex + 1).forEach(c => {
|
|
673
685
|
const fakeColumn = { ...c }
|
|
674
686
|
if (fakeColumn.ref) {
|
|
675
|
-
fakeColumn.ref = [...
|
|
676
|
-
fakeColumn.$refLinks = [...
|
|
687
|
+
fakeColumn.ref = [...baseRef, ...fakeColumn.ref]
|
|
688
|
+
fakeColumn.$refLinks = [...baseRefLinks, ...c.$refLinks]
|
|
677
689
|
}
|
|
678
690
|
replace.push(fakeColumn)
|
|
679
691
|
})
|
|
@@ -682,15 +694,15 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
682
694
|
// fake the ref since excluding only has strings
|
|
683
695
|
col.excluding.forEach(c => {
|
|
684
696
|
const fakeColumn = {
|
|
685
|
-
ref: [...
|
|
697
|
+
ref: [...baseRef, c],
|
|
686
698
|
}
|
|
687
699
|
exclude.push(fakeColumn)
|
|
688
700
|
})
|
|
689
701
|
}
|
|
690
702
|
|
|
691
|
-
if (
|
|
692
|
-
res.push(...getColumnsForWildcard(exclude, replace))
|
|
693
|
-
else
|
|
703
|
+
if (baseRefLinks.at(-1).definition.kind === 'entity') {
|
|
704
|
+
res.push(...getColumnsForWildcard(exclude, replace, col.as))
|
|
705
|
+
} else
|
|
694
706
|
res.push(
|
|
695
707
|
...getFlatColumnsFor(col, { columnAlias: col.as, tableAlias: getQuerySourceName(col) }, [], {
|
|
696
708
|
exclude,
|
|
@@ -907,12 +919,14 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
907
919
|
*
|
|
908
920
|
* Furthermore, foreign keys (FK) for OData CSN and blobs are excluded from the wildcard expansion.
|
|
909
921
|
*
|
|
910
|
-
* @param {
|
|
911
|
-
* @param {
|
|
922
|
+
* @param {array} exclude - An optional list of columns to be excluded during the wildcard expansion.
|
|
923
|
+
* @param {array} replace - An optional list of columns to replace during the wildcard expansion.
|
|
924
|
+
* @param {string} baseName - the explicit alias of the column.
|
|
925
|
+
* Only possible for anonymous expands on implicit table alias: `select from books { { * } as FOO }`
|
|
912
926
|
*
|
|
913
927
|
* @returns {Array} Returns an array of explicit columns derived from the wildcard.
|
|
914
928
|
*/
|
|
915
|
-
function getColumnsForWildcard(exclude = [], replace = []) {
|
|
929
|
+
function getColumnsForWildcard(exclude = [], replace = [], baseName = null) {
|
|
916
930
|
const wildcardColumns = []
|
|
917
931
|
Object.keys(inferred.$combinedElements)
|
|
918
932
|
.filter(k => !exclude.includes(k))
|
|
@@ -927,7 +941,13 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
927
941
|
} else if (isCalculatedOnRead(element)) {
|
|
928
942
|
wildcardColumns.push(resolveCalculatedElement(replace.find(r => r.as === k) || element))
|
|
929
943
|
} else {
|
|
930
|
-
const flatColumns = getFlatColumnsFor(
|
|
944
|
+
const flatColumns = getFlatColumnsFor(
|
|
945
|
+
element,
|
|
946
|
+
{ tableAlias: index, baseName },
|
|
947
|
+
[],
|
|
948
|
+
{ exclude, replace },
|
|
949
|
+
true,
|
|
950
|
+
)
|
|
931
951
|
wildcardColumns.push(...flatColumns)
|
|
932
952
|
}
|
|
933
953
|
})
|
|
@@ -969,7 +989,16 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
969
989
|
* Columns excluded in a wildcard expansion or replaced by other columns are also handled accordingly.
|
|
970
990
|
*
|
|
971
991
|
* @param {object} column - The structured element which needs to be expanded.
|
|
972
|
-
* @param {
|
|
992
|
+
* @param {{
|
|
993
|
+
* columnAlias: string
|
|
994
|
+
* tableAlias: string
|
|
995
|
+
* baseName: string
|
|
996
|
+
* }} names - configuration object for naming parameters:
|
|
997
|
+
* columnAlias - The explicit alias which the user has defined for the column.
|
|
998
|
+
* For instance `{ struct.foo as bar}` will be transformed into
|
|
999
|
+
* `{ struct_foo_leaf1 as bar_foo_leaf1, struct_foo_leaf2 as bar_foo_leaf2 }`.
|
|
1000
|
+
* tableAlias - The table alias to prepend to the column name. Optional.
|
|
1001
|
+
* baseName - The prefixes of the column reference (joined with '_'). Optional.
|
|
973
1002
|
* @param {string} columnAlias - The explicit alias which the user has defined for the column.
|
|
974
1003
|
* For instance `{ struct.foo as bar}` will be transformed into
|
|
975
1004
|
* `{ struct_foo_leaf1 as bar_foo_leaf1, struct_foo_leaf2 as bar_foo_leaf2 }`.
|
|
@@ -1352,7 +1381,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1352
1381
|
if (flatRhs) {
|
|
1353
1382
|
const flatLhs = flattenWithBaseName(token)
|
|
1354
1383
|
|
|
1355
|
-
//
|
|
1384
|
+
//REVISIT: Early exit here? We kndow we cant compare the structs, however we do not know exactly why
|
|
1356
1385
|
// --> calculate error message or exit early? See test "proper error if structures cannot be compared / too many elements on lhs"
|
|
1357
1386
|
if (flatRhs.length !== flatLhs.length)
|
|
1358
1387
|
// make sure we can compare both structures
|
|
@@ -1419,10 +1448,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1419
1448
|
|
|
1420
1449
|
function assertNoStructInXpr(token, inInfixFilter = false) {
|
|
1421
1450
|
if (!inInfixFilter && token.$refLinks?.[token.$refLinks.length - 1].definition.target)
|
|
1422
|
-
//
|
|
1451
|
+
// REVISIT: let this through if not requested otherwise
|
|
1423
1452
|
rejectAssocInExpression()
|
|
1424
1453
|
if (isStructured(token.$refLinks?.[token.$refLinks.length - 1].definition))
|
|
1425
|
-
//
|
|
1454
|
+
// REVISIT: let this through if not requested otherwise
|
|
1426
1455
|
rejectStructInExpression()
|
|
1427
1456
|
|
|
1428
1457
|
function rejectAssocInExpression() {
|
package/lib/infer/index.js
CHANGED
|
@@ -173,7 +173,9 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
173
173
|
const nextStep = ref[1]?.id || ref[1]
|
|
174
174
|
// no unmanaged assoc in infix filter path
|
|
175
175
|
if (!expandOrExists && e.on)
|
|
176
|
-
throw new Error(
|
|
176
|
+
throw new Error(
|
|
177
|
+
`"${e.name}" in path "${arg.ref.map(idOnly).join('.')}" must not be an unmanaged association`,
|
|
178
|
+
)
|
|
177
179
|
// no non-fk traversal in infix filter
|
|
178
180
|
if (!expandOrExists && nextStep && !(nextStep in e.foreignKeys))
|
|
179
181
|
throw new Error(`Only foreign keys of "${e.name}" can be accessed in infix filter`)
|
|
@@ -339,13 +341,11 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
339
341
|
}
|
|
340
342
|
|
|
341
343
|
// walk over all paths in other query properties
|
|
342
|
-
if (where)
|
|
343
|
-
|
|
344
|
-
if (
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
walkTokenStream(having)
|
|
348
|
-
if (_.with) // consider UPDATE.with
|
|
344
|
+
if (where) walkTokenStream(where)
|
|
345
|
+
if (groupBy) groupBy.forEach(token => inferQueryElement(token, false))
|
|
346
|
+
if (having) walkTokenStream(having)
|
|
347
|
+
if (_.with)
|
|
348
|
+
// consider UPDATE.with
|
|
349
349
|
Object.values(_.with).forEach(val => inferQueryElement(val, false))
|
|
350
350
|
|
|
351
351
|
return queryElements
|
|
@@ -355,8 +355,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
355
355
|
* on the information whether the next token is resolved within an `exists` predicates.
|
|
356
356
|
* If such a token has an infix filter, it is not join relevant, because the filter
|
|
357
357
|
* condition is applied to the generated `exists <subquery>` condition.
|
|
358
|
-
*
|
|
359
|
-
* @param {array} tokenStream
|
|
358
|
+
*
|
|
359
|
+
* @param {array} tokenStream
|
|
360
360
|
*/
|
|
361
361
|
function walkTokenStream(tokenStream) {
|
|
362
362
|
let skipJoins
|
|
@@ -476,12 +476,10 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
476
476
|
// if any path step points to an artifact with `@cds.persistence.skip`
|
|
477
477
|
// we must ignore the element from the queries elements
|
|
478
478
|
let isPersisted = true
|
|
479
|
-
const firstStepIsTableAlias =
|
|
480
|
-
(column.ref.length > 1 && column.ref[0] in sources) ||
|
|
481
|
-
// nested projection on table alias
|
|
482
|
-
(column.ref.length === 1 && column.ref[0] in sources && column.inline)
|
|
479
|
+
const firstStepIsTableAlias = column.ref.length > 1 && column.ref[0] in sources
|
|
483
480
|
const firstStepIsSelf =
|
|
484
481
|
!firstStepIsTableAlias && column.ref.length > 1 && ['$self', '$projection'].includes(column.ref[0])
|
|
482
|
+
const expandOnTableAlias = column.ref.length === 1 && column.ref[0] in sources && (column.expand || column.inline)
|
|
485
483
|
const nameSegments = []
|
|
486
484
|
// if a (segment) of a (structured) foreign key is renamed, we must not include
|
|
487
485
|
// the aliased ref segments into the name of the final foreign key which is e.g. used in
|
|
@@ -535,6 +533,12 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
535
533
|
const $refLink = { definition, target: $combinedElements[id][0].tableAlias }
|
|
536
534
|
column.$refLinks.push($refLink)
|
|
537
535
|
nameSegments.push(id)
|
|
536
|
+
} else if (expandOnTableAlias) {
|
|
537
|
+
// expand on table alias
|
|
538
|
+
column.$refLinks.push({
|
|
539
|
+
definition: sources[id],
|
|
540
|
+
target: sources[id],
|
|
541
|
+
})
|
|
538
542
|
} else {
|
|
539
543
|
stepNotFoundInCombinedElements(id) // REVISIT: fails with {__proto__:elements)
|
|
540
544
|
}
|
|
@@ -545,9 +549,9 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
545
549
|
|
|
546
550
|
if (firstStepIsSelf && element?.isAssociation) {
|
|
547
551
|
throw new Error(
|
|
548
|
-
`Paths starting with “$self” must not contain steps of type “cds.Association”: ref: [ ${column.ref
|
|
549
|
-
idOnly
|
|
550
|
-
|
|
552
|
+
`Paths starting with “$self” must not contain steps of type “cds.Association”: ref: [ ${column.ref
|
|
553
|
+
.map(idOnly)
|
|
554
|
+
.join(', ')} ]`,
|
|
551
555
|
)
|
|
552
556
|
}
|
|
553
557
|
|
|
@@ -619,7 +623,12 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
619
623
|
if (insertIntoQueryElements) queryElements[column.as || flatName] = getCopyWithAnnos(column, base)
|
|
620
624
|
} else if (column.expand) {
|
|
621
625
|
const elements = resolveExpand(column)
|
|
622
|
-
|
|
626
|
+
let elementName
|
|
627
|
+
// expand on table alias
|
|
628
|
+
if (column.$refLinks.length === 1 && column.$refLinks[0].definition.kind === 'entity')
|
|
629
|
+
elementName = column.$refLinks[0].alias
|
|
630
|
+
else elementName = column.as || flatName
|
|
631
|
+
if (insertIntoQueryElements) queryElements[elementName] = elements
|
|
623
632
|
} else if (column.inline && insertIntoQueryElements) {
|
|
624
633
|
const elements = resolveInline(column)
|
|
625
634
|
queryElements = { ...queryElements, ...elements }
|
|
@@ -752,8 +761,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
752
761
|
*/
|
|
753
762
|
function resolveExpand(col) {
|
|
754
763
|
const { expand, $refLinks } = col
|
|
755
|
-
const $leafLink = $refLinks?.[$refLinks.length - 1]
|
|
756
|
-
if ($leafLink
|
|
764
|
+
const $leafLink = $refLinks?.[$refLinks.length - 1] || inferred.SELECT.from.$refLinks.at(-1) // fallback to anonymous expand
|
|
765
|
+
if ($leafLink.definition._target) {
|
|
757
766
|
const expandSubquery = {
|
|
758
767
|
SELECT: {
|
|
759
768
|
from: $leafLink.definition._target.name,
|
package/lib/infer/join-tree.js
CHANGED
|
@@ -148,7 +148,6 @@ class JoinTree {
|
|
|
148
148
|
*
|
|
149
149
|
* It begins by inferring the source of the given column, which is the table alias where the column is resolvable.
|
|
150
150
|
* Each step during this process represents a node in the join tree. If a node already exists in the tree, the current step is replaced by the already merged node.
|
|
151
|
-
* For each step, it checks whether it has been seen before. If so, it resets the $refLink to point to the already merged $refLink.
|
|
152
151
|
* If not, it creates a new Node and ensures proper aliasing and foreign key access.
|
|
153
152
|
*
|
|
154
153
|
* @param {object} col - The column object to be merged into the existing join tree. This object should have the properties $refLinks and ref.
|
package/package.json
CHANGED