@cap-js/db-service 1.12.1 → 1.13.0
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 +14 -0
- package/lib/cqn2sql.js +16 -5
- package/lib/cqn4sql.js +65 -53
- package/lib/deep-queries.js +1 -1
- package/lib/infer/index.js +11 -7
- package/lib/infer/join-tree.js +9 -2
- package/lib/utils.js +27 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@
|
|
|
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
|
+
## [1.13.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.12.1...db-service-v1.13.0) (2024-10-01)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* Add quoted mode support ([#681](https://github.com/cap-js/cds-dbs/issues/681)) ([43c7a6c](https://github.com/cap-js/cds-dbs/commit/43c7a6c1bed836a1210eb9c2ff5c7ffc0e498d76))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
* **deep-queries:** properly return insert result ([#803](https://github.com/cap-js/cds-dbs/issues/803)) ([8d800e2](https://github.com/cap-js/cds-dbs/commit/8d800e2858f02663b8ed3845909e5891af390517))
|
|
18
|
+
* dont use virtual key for `UPDATE … where (<key>) in <subquery>` ([#800](https://github.com/cap-js/cds-dbs/issues/800)) ([d25af70](https://github.com/cap-js/cds-dbs/commit/d25af70b23688cd22c7b87f20a7848c9653ae9a9))
|
|
19
|
+
* reject all path expressions w/o foreign keys ([#806](https://github.com/cap-js/cds-dbs/issues/806)) ([cd271a8](https://github.com/cap-js/cds-dbs/commit/cd271a8aa973afe0926ac1fa956cc64260140b98))
|
|
20
|
+
|
|
7
21
|
## [1.12.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.12.0...db-service-v1.12.1) (2024-09-03)
|
|
8
22
|
|
|
9
23
|
|
package/lib/cqn2sql.js
CHANGED
|
@@ -34,6 +34,12 @@ class CQN2SQLRenderer {
|
|
|
34
34
|
this.class = new.target // for IntelliSense
|
|
35
35
|
this.class._init() // is a noop for subsequent calls
|
|
36
36
|
this.model = srv?.model
|
|
37
|
+
|
|
38
|
+
// Overwrite smart quoting
|
|
39
|
+
if (cds.env.sql.names === 'quoted') {
|
|
40
|
+
this.class.prototype.name = (name) => name.id || name
|
|
41
|
+
this.class.prototype.quote = (s) => `"${String(s).replace(/"/g, '""')}"`
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
static _add_mixins(aspect, mixins) {
|
|
@@ -221,7 +227,7 @@ class CQN2SQLRenderer {
|
|
|
221
227
|
let sql = `SELECT`
|
|
222
228
|
if (distinct) sql += ` DISTINCT`
|
|
223
229
|
if (!_empty(columns)) sql += ` ${columns}`
|
|
224
|
-
if (!_empty(from)) sql += ` FROM ${this.from(from)}`
|
|
230
|
+
if (!_empty(from)) sql += ` FROM ${this.from(from, q)}`
|
|
225
231
|
else sql += this.from_dummy()
|
|
226
232
|
if (!_empty(where)) sql += ` WHERE ${this.where(where)}`
|
|
227
233
|
if (!_empty(groupBy)) sql += ` GROUP BY ${this.groupBy(groupBy)}`
|
|
@@ -311,8 +317,8 @@ class CQN2SQLRenderer {
|
|
|
311
317
|
*/
|
|
312
318
|
column_expr(x, q) {
|
|
313
319
|
if (x === '*') return '*'
|
|
314
|
-
|
|
315
|
-
let sql = x.param !== true && typeof x.val === 'number' ? this.expr({ param: false, __proto__: x }): this.expr(x)
|
|
320
|
+
|
|
321
|
+
let sql = x.param !== true && typeof x.val === 'number' ? this.expr({ param: false, __proto__: x }) : this.expr(x)
|
|
316
322
|
let alias = this.column_alias4(x, q)
|
|
317
323
|
if (alias) sql += ' as ' + this.quote(alias)
|
|
318
324
|
return sql
|
|
@@ -332,11 +338,16 @@ class CQN2SQLRenderer {
|
|
|
332
338
|
* @param {import('./infer/cqn').source} from
|
|
333
339
|
* @returns {string} SQL
|
|
334
340
|
*/
|
|
335
|
-
from(from) {
|
|
341
|
+
from(from, q) {
|
|
336
342
|
const { ref, as } = from
|
|
337
343
|
const _aliased = as ? s => s + ` as ${this.quote(as)}` : s => s
|
|
338
344
|
if (ref) {
|
|
339
|
-
|
|
345
|
+
let z = ref[0]
|
|
346
|
+
if (cds.env.sql.names === 'quoted') {
|
|
347
|
+
// use SELECT.from to infer query, cds.infer also expects a query
|
|
348
|
+
const { target } = q || SELECT.from(from)
|
|
349
|
+
z = target?.['@cds.persistence.name'] || ref[0]
|
|
350
|
+
}
|
|
340
351
|
if (z.args) {
|
|
341
352
|
return _aliased(`${this.quote(this.name(z))}${this.from_args(z.args)}`)
|
|
342
353
|
}
|
package/lib/cqn4sql.js
CHANGED
|
@@ -4,6 +4,7 @@ const cds = require('@sap/cds')
|
|
|
4
4
|
|
|
5
5
|
const infer = require('./infer')
|
|
6
6
|
const { computeColumnsToBeSearched } = require('./search')
|
|
7
|
+
const { prettyPrintRef } = require('./utils')
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* For operators of <eqOps>, this is replaced by comparing all leaf elements with null, combined with and.
|
|
@@ -130,15 +131,14 @@ function cqn4sql(originalQuery, model) {
|
|
|
130
131
|
// calculate the primary keys of the target entity, there is always exactly
|
|
131
132
|
// one query source for UPDATE / DELETE
|
|
132
133
|
const queryTarget = Object.values(inferred.sources)[0].definition
|
|
133
|
-
const keys = Object.values(queryTarget.elements).filter(e => e.key === true)
|
|
134
134
|
const primaryKey = { list: [] }
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
135
|
+
for (const k of Object.keys(queryTarget.elements)) {
|
|
136
|
+
const e = queryTarget.elements[k]
|
|
137
|
+
if (e.key === true && !e.virtual) {
|
|
138
|
+
subquery.SELECT.columns.push({ ref: [e.name] })
|
|
139
|
+
primaryKey.list.push({ ref: [transformedFrom.as, e.name] })
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
142
|
|
|
143
143
|
const transformedSubquery = cqn4sql(subquery, model)
|
|
144
144
|
|
|
@@ -763,6 +763,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
763
763
|
function expandColumn(column) {
|
|
764
764
|
let outerAlias
|
|
765
765
|
let subqueryFromRef
|
|
766
|
+
const ref = column.$refLinks[0].definition.kind === 'entity' ? column.ref.slice(1) : column.ref
|
|
767
|
+
const assoc = column.$refLinks.at(-1)
|
|
768
|
+
ensureValidForeignKeys(assoc.definition, column.ref, 'expand')
|
|
766
769
|
if (column.isJoinRelevant) {
|
|
767
770
|
// all n-1 steps of the expand column are already transformed into joins
|
|
768
771
|
// find the last join relevant association. That is the n-1 assoc in the ref path.
|
|
@@ -782,24 +785,17 @@ function cqn4sql(originalQuery, model) {
|
|
|
782
785
|
outerAlias = transformedQuery.SELECT.from.as
|
|
783
786
|
subqueryFromRef = [
|
|
784
787
|
...(transformedQuery.SELECT.from.ref || /* subq in from */ [transformedQuery.SELECT.from.target.name]),
|
|
785
|
-
...
|
|
788
|
+
...ref,
|
|
786
789
|
]
|
|
787
790
|
}
|
|
788
791
|
|
|
789
792
|
// this is the alias of the column which holds the correlated subquery
|
|
790
|
-
const columnAlias =
|
|
791
|
-
column.as ||
|
|
792
|
-
(column.$refLinks[0].definition.kind === 'entity'
|
|
793
|
-
? column.ref.slice(1).map(idOnly).join('_') // omit explicit table alias from name of column
|
|
794
|
-
: column.ref.map(idOnly).join('_'))
|
|
793
|
+
const columnAlias = column.as || ref.map(idOnly).join('_')
|
|
795
794
|
|
|
796
795
|
// if there is a group by on the main query, all
|
|
797
796
|
// columns of the expand must be in the groupBy
|
|
798
797
|
if (transformedQuery.SELECT.groupBy) {
|
|
799
|
-
const baseRef =
|
|
800
|
-
column.$refLinks[0].definition.SELECT || column.$refLinks[0].definition.kind === 'entity'
|
|
801
|
-
? column.ref.slice(1)
|
|
802
|
-
: column.ref
|
|
798
|
+
const baseRef = column.$refLinks[0].definition.SELECT || ref
|
|
803
799
|
|
|
804
800
|
return _subqueryForGroupBy(column, baseRef, columnAlias)
|
|
805
801
|
}
|
|
@@ -810,10 +806,12 @@ function cqn4sql(originalQuery, model) {
|
|
|
810
806
|
|
|
811
807
|
// `SELECT from Authors { books.genre as genreOfBooks { name } } becomes `SELECT from Books:genre as genreOfBooks`
|
|
812
808
|
const from = { ref: subqueryFromRef, as: uniqueSubqueryAlias }
|
|
813
|
-
const subqueryBase =
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
809
|
+
const subqueryBase = {}
|
|
810
|
+
for (const [key, value] of Object.entries(column)) {
|
|
811
|
+
if (!(key in { ref: true, expand: true })) {
|
|
812
|
+
subqueryBase[key] = value;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
817
815
|
const subquery = {
|
|
818
816
|
SELECT: {
|
|
819
817
|
...subqueryBase,
|
|
@@ -1067,13 +1065,12 @@ function cqn4sql(originalQuery, model) {
|
|
|
1067
1065
|
*/
|
|
1068
1066
|
function getColumnsForWildcard(exclude = [], replace = [], baseName = null) {
|
|
1069
1067
|
const wildcardColumns = []
|
|
1070
|
-
Object.keys(inferred.$combinedElements)
|
|
1071
|
-
|
|
1072
|
-
.forEach(k => {
|
|
1068
|
+
for (const k of Object.keys(inferred.$combinedElements)) {
|
|
1069
|
+
if (!exclude.includes(k)) {
|
|
1073
1070
|
const { index, tableAlias } = inferred.$combinedElements[k][0]
|
|
1074
1071
|
const element = tableAlias.elements[k]
|
|
1075
1072
|
// ignore FK for odata csn / ignore blobs from wildcard expansion
|
|
1076
|
-
if (isManagedAssocInFlatMode(element) || element.type === 'cds.LargeBinary')
|
|
1073
|
+
if (isManagedAssocInFlatMode(element) || element.type === 'cds.LargeBinary') continue
|
|
1077
1074
|
// for wildcard on subquery in from, just reference the elements
|
|
1078
1075
|
if (tableAlias.SELECT && !element.elements && !element.target) {
|
|
1079
1076
|
wildcardColumns.push(index ? { ref: [index, k] } : { ref: [k] })
|
|
@@ -1089,7 +1086,8 @@ function cqn4sql(originalQuery, model) {
|
|
|
1089
1086
|
)
|
|
1090
1087
|
wildcardColumns.push(...flatColumns)
|
|
1091
1088
|
}
|
|
1092
|
-
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1093
1091
|
return wildcardColumns
|
|
1094
1092
|
|
|
1095
1093
|
/**
|
|
@@ -1383,6 +1381,8 @@ function cqn4sql(originalQuery, model) {
|
|
|
1383
1381
|
}”`,
|
|
1384
1382
|
)
|
|
1385
1383
|
}
|
|
1384
|
+
const { definition: fkSource } = next
|
|
1385
|
+
ensureValidForeignKeys(fkSource, ref)
|
|
1386
1386
|
whereExistsSubSelects.push(getWhereExistsSubquery(current, next, step.where, true, step.args))
|
|
1387
1387
|
}
|
|
1388
1388
|
|
|
@@ -1419,12 +1419,12 @@ function cqn4sql(originalQuery, model) {
|
|
|
1419
1419
|
const keys = def.keys // use key aspect on entity
|
|
1420
1420
|
const keyValComparisons = []
|
|
1421
1421
|
const flatKeys = []
|
|
1422
|
-
Object.values(keys)
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
.forEach(v => {
|
|
1422
|
+
for (const v of Object.values(keys)) {
|
|
1423
|
+
if (v !== backlinkFor($baseLink.definition)?.[0]) {
|
|
1424
|
+
// up__ID already part of inner where exists, no need to add it explicitly here
|
|
1426
1425
|
flatKeys.push(...getFlatColumnsFor(v, { tableAlias: $baseLink.alias }))
|
|
1427
|
-
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
1428
|
if (flatKeys.length > 1)
|
|
1429
1429
|
throw new Error('Filters can only be applied to managed associations which result in a single foreign key')
|
|
1430
1430
|
flatKeys.forEach(c => keyValComparisons.push([...[c, '=', token]]))
|
|
@@ -1584,10 +1584,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1584
1584
|
if (!def.$refLinks) return def
|
|
1585
1585
|
const leaf = def.$refLinks[def.$refLinks.length - 1]
|
|
1586
1586
|
const first = def.$refLinks[0]
|
|
1587
|
-
const tableAlias = getTableAlias(
|
|
1588
|
-
def,
|
|
1589
|
-
def.ref.length > 1 && first.definition.isAssociation ? first : $baseLink,
|
|
1590
|
-
)
|
|
1587
|
+
const tableAlias = getTableAlias(def, def.ref.length > 1 && first.definition.isAssociation ? first : $baseLink)
|
|
1591
1588
|
if (leaf.definition.parent.kind !== 'entity')
|
|
1592
1589
|
// we need the base name
|
|
1593
1590
|
return getFlatColumnsFor(leaf.definition, {
|
|
@@ -1671,23 +1668,23 @@ function cqn4sql(originalQuery, model) {
|
|
|
1671
1668
|
const refReverse = [...from.ref].reverse()
|
|
1672
1669
|
const $refLinksReverse = [...transformedFrom.$refLinks].reverse()
|
|
1673
1670
|
for (let i = 0; i < refReverse.length; i += 1) {
|
|
1674
|
-
const
|
|
1671
|
+
const current = $refLinksReverse[i]
|
|
1675
1672
|
|
|
1676
|
-
let
|
|
1673
|
+
let next = $refLinksReverse[i + 1]
|
|
1677
1674
|
const nextStep = refReverse[i + 1] // only because we want the filter condition
|
|
1678
1675
|
|
|
1679
|
-
if (
|
|
1676
|
+
if (current.definition.target && next) {
|
|
1680
1677
|
const { where, args } = nextStep
|
|
1681
|
-
if (isStructured(
|
|
1678
|
+
if (isStructured(next.definition)) {
|
|
1682
1679
|
// find next association / entity in the ref because this is actually our real nextStep
|
|
1683
1680
|
const nextStepIndex =
|
|
1684
1681
|
2 +
|
|
1685
1682
|
$refLinksReverse
|
|
1686
1683
|
.slice(i + 2)
|
|
1687
1684
|
.findIndex(rl => rl.definition.isAssociation || rl.definition.kind === 'entity')
|
|
1688
|
-
|
|
1685
|
+
next = $refLinksReverse[nextStepIndex]
|
|
1689
1686
|
}
|
|
1690
|
-
let as = getLastStringSegment(
|
|
1687
|
+
let as = getLastStringSegment(next.alias)
|
|
1691
1688
|
/**
|
|
1692
1689
|
* for an `expand` subquery, we do not need to add
|
|
1693
1690
|
* the table alias of the `expand` host to the join tree
|
|
@@ -1697,8 +1694,10 @@ function cqn4sql(originalQuery, model) {
|
|
|
1697
1694
|
if (!(inferred.SELECT?.expand === true)) {
|
|
1698
1695
|
as = getNextAvailableTableAlias(as)
|
|
1699
1696
|
}
|
|
1700
|
-
|
|
1701
|
-
|
|
1697
|
+
next.alias = as
|
|
1698
|
+
const { definition: fkSource } = current
|
|
1699
|
+
ensureValidForeignKeys(fkSource, from.ref)
|
|
1700
|
+
whereExistsSubSelects.push(getWhereExistsSubquery(current, next, where, false, args))
|
|
1702
1701
|
}
|
|
1703
1702
|
}
|
|
1704
1703
|
|
|
@@ -1743,6 +1742,14 @@ function cqn4sql(originalQuery, model) {
|
|
|
1743
1742
|
}
|
|
1744
1743
|
}
|
|
1745
1744
|
|
|
1745
|
+
function ensureValidForeignKeys(fkSource, ref, kind = null) {
|
|
1746
|
+
if (fkSource.keys && fkSource.keys.length === 0) {
|
|
1747
|
+
const path = prettyPrintRef(ref, model)
|
|
1748
|
+
if (kind === 'expand') throw new Error(`Can't expand “${fkSource.name}” as it has no foreign keys`)
|
|
1749
|
+
throw new Error(`Path step “${fkSource.name}” of “${path}” has no foreign keys`)
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1746
1753
|
function whereExistsSubqueries(whereExistsSubSelects) {
|
|
1747
1754
|
if (whereExistsSubSelects.length === 1) return whereExistsSubSelects[0]
|
|
1748
1755
|
whereExistsSubSelects.reduce((prev, cur) => {
|
|
@@ -1819,7 +1826,6 @@ function cqn4sql(originalQuery, model) {
|
|
|
1819
1826
|
const { on, keys } = assocRefLink.definition
|
|
1820
1827
|
const target = getDefinition(assocRefLink.definition.target)
|
|
1821
1828
|
let res
|
|
1822
|
-
// technically we could have multiple backlinks
|
|
1823
1829
|
if (keys) {
|
|
1824
1830
|
const fkPkPairs = getParentKeyForeignKeyPairs(assocRefLink.definition, targetSideRefLink, true)
|
|
1825
1831
|
const transformedOn = []
|
|
@@ -1853,7 +1859,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1853
1859
|
result[i] = asXpr(xpr)
|
|
1854
1860
|
continue
|
|
1855
1861
|
}
|
|
1856
|
-
if(lhs.args) {
|
|
1862
|
+
if (lhs.args) {
|
|
1857
1863
|
const args = calculateOnCondition(lhs.args)
|
|
1858
1864
|
result[i] = { ...lhs, args }
|
|
1859
1865
|
continue
|
|
@@ -1952,6 +1958,12 @@ function cqn4sql(originalQuery, model) {
|
|
|
1952
1958
|
}
|
|
1953
1959
|
})
|
|
1954
1960
|
} else if (backlink.keys) {
|
|
1961
|
+
// sanity check: error out if we can't produce a join
|
|
1962
|
+
if (backlink.keys.length === 0) {
|
|
1963
|
+
throw new Error(
|
|
1964
|
+
`Path step “${assocRefLink.alias}” is a self comparison with “${getFullName(backlink)}” that has no foreign keys`,
|
|
1965
|
+
)
|
|
1966
|
+
}
|
|
1955
1967
|
// managed backlink -> calculate fk-pk pairs
|
|
1956
1968
|
const fkPkPairs = getParentKeyForeignKeyPairs(backlink, targetSideRefLink)
|
|
1957
1969
|
fkPkPairs.forEach((pair, j) => {
|
|
@@ -2270,13 +2282,6 @@ function cqn4sql(originalQuery, model) {
|
|
|
2270
2282
|
}
|
|
2271
2283
|
}
|
|
2272
2284
|
|
|
2273
|
-
module.exports = Object.assign(cqn4sql, {
|
|
2274
|
-
// for own tests only:
|
|
2275
|
-
eqOps,
|
|
2276
|
-
notEqOps,
|
|
2277
|
-
notSupportedOps,
|
|
2278
|
-
})
|
|
2279
|
-
|
|
2280
2285
|
function calculateElementName(token) {
|
|
2281
2286
|
const nonJoinRelevantAssoc = [...token.$refLinks].findIndex(l => l.definition.isAssociation && l.onlyForeignKeyAccess)
|
|
2282
2287
|
let name
|
|
@@ -2364,3 +2369,10 @@ const refWithConditions = step => {
|
|
|
2364
2369
|
return appendix ? step.id + appendix : step
|
|
2365
2370
|
}
|
|
2366
2371
|
const is_regexp = x => x?.constructor?.name === 'RegExp' // NOTE: x instanceof RegExp doesn't work in repl
|
|
2372
|
+
|
|
2373
|
+
module.exports = Object.assign(cqn4sql, {
|
|
2374
|
+
// for own tests only:
|
|
2375
|
+
eqOps,
|
|
2376
|
+
notEqOps,
|
|
2377
|
+
notSupportedOps,
|
|
2378
|
+
})
|
package/lib/deep-queries.js
CHANGED
package/lib/infer/index.js
CHANGED
|
@@ -170,7 +170,8 @@ function infer(originalQuery, model) {
|
|
|
170
170
|
if (list) list.forEach(arg => attachRefLinksToArg(arg, $baseLink, expandOrExists))
|
|
171
171
|
if (!ref) return
|
|
172
172
|
init$refLinks(arg)
|
|
173
|
-
|
|
173
|
+
let i = 0
|
|
174
|
+
for (const step of ref) {
|
|
174
175
|
const id = step.id || step
|
|
175
176
|
if (i === 0) {
|
|
176
177
|
// infix filter never have table alias
|
|
@@ -231,7 +232,8 @@ function infer(originalQuery, model) {
|
|
|
231
232
|
step.where.forEach(walkTokenStream)
|
|
232
233
|
} else throw new Error('A filter can only be provided when navigating along associations')
|
|
233
234
|
}
|
|
234
|
-
|
|
235
|
+
i += 1
|
|
236
|
+
}
|
|
235
237
|
const { definition, target } = arg.$refLinks[arg.$refLinks.length - 1]
|
|
236
238
|
if (definition.value) {
|
|
237
239
|
// nested calculated element
|
|
@@ -1046,15 +1048,17 @@ function infer(originalQuery, model) {
|
|
|
1046
1048
|
if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
|
|
1047
1049
|
const { elements } = getDefinitionFromSources(sources, aliases[0])
|
|
1048
1050
|
// only one query source and no overwritten columns
|
|
1049
|
-
Object.keys(elements)
|
|
1050
|
-
|
|
1051
|
-
.forEach(k => {
|
|
1051
|
+
for (const k of Object.keys(elements)) {
|
|
1052
|
+
if (!exclude(k)) {
|
|
1052
1053
|
const element = elements[k]
|
|
1053
|
-
if (element.type !== 'cds.LargeBinary')
|
|
1054
|
+
if (element.type !== 'cds.LargeBinary') {
|
|
1055
|
+
queryElements[k] = element
|
|
1056
|
+
}
|
|
1054
1057
|
if (element.value) {
|
|
1055
1058
|
linkCalculatedElement(element)
|
|
1056
1059
|
}
|
|
1057
|
-
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1058
1062
|
return
|
|
1059
1063
|
}
|
|
1060
1064
|
|
package/lib/infer/join-tree.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { prettyPrintRef } = require('../utils')
|
|
4
|
+
|
|
3
5
|
// REVISIT: define following unknown types
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -177,13 +179,19 @@ class JoinTree {
|
|
|
177
179
|
}
|
|
178
180
|
|
|
179
181
|
// if no root node was found, the column is selected from a subquery
|
|
180
|
-
if(!node) return
|
|
182
|
+
if (!node) return
|
|
181
183
|
while (i < col.ref.length) {
|
|
182
184
|
const step = col.ref[i]
|
|
183
185
|
const { where, args } = step
|
|
184
186
|
const id = joinId(step, args, where)
|
|
185
187
|
const next = node.children.get(id)
|
|
186
188
|
const $refLink = col.$refLinks[i]
|
|
189
|
+
// sanity check: error out if we can't produce a join
|
|
190
|
+
if ($refLink.definition.keys && $refLink.definition.keys.length === 0) {
|
|
191
|
+
const path = prettyPrintRef(col.ref)
|
|
192
|
+
throw new Error(`Path step “${$refLink.alias}” of “${path}” has no foreign keys`)
|
|
193
|
+
}
|
|
194
|
+
|
|
187
195
|
if (next) {
|
|
188
196
|
// step already seen before
|
|
189
197
|
node = next
|
|
@@ -208,7 +216,6 @@ class JoinTree {
|
|
|
208
216
|
}
|
|
209
217
|
child.$refLink.alias = this.addNextAvailableTableAlias($refLink.alias, outerQueries)
|
|
210
218
|
}
|
|
211
|
-
//> REVISIT: remove fallback once UCSN is standard
|
|
212
219
|
const elements =
|
|
213
220
|
node.$refLink?.definition.isAssociation &&
|
|
214
221
|
(node.$refLink.definition.elements || node.$refLink.definition.foreignKeys)
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Formats a ref array into a string representation.
|
|
5
|
+
* If the first step is an entity, the separator is a colon, otherwise a dot.
|
|
6
|
+
*
|
|
7
|
+
* @param {Array} ref - The reference array to be formatted.
|
|
8
|
+
* @param {Object} model - The model object containing definitions.
|
|
9
|
+
* @returns {string} The formatted string representation of the reference.
|
|
10
|
+
*/
|
|
11
|
+
function prettyPrintRef(ref, model = null) {
|
|
12
|
+
return ref.reduce((acc, curr, j) => {
|
|
13
|
+
if (j > 0) {
|
|
14
|
+
if (j === 1 && model?.definitions[ref[0]]?.kind === 'entity') {
|
|
15
|
+
acc += ':'
|
|
16
|
+
} else {
|
|
17
|
+
acc += '.'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return acc + `${curr.id ? curr.id + '[…]' : curr}`
|
|
21
|
+
}, '')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// export the function to be used in other modules
|
|
25
|
+
module.exports = {
|
|
26
|
+
prettyPrintRef,
|
|
27
|
+
}
|
package/package.json
CHANGED