@cap-js/db-service 1.6.2 → 1.6.4
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 +19 -0
- package/lib/SQLService.js +3 -0
- package/lib/cql-functions.js +1 -1
- package/lib/cqn2sql.js +16 -11
- package/lib/cqn4sql.js +64 -37
- package/lib/infer/index.js +1 -1
- package/lib/infer/join-tree.js +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -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
|
+
## [1.6.4](https://github.com/cap-js/cds-dbs/compare/db-service-v1.6.3...db-service-v1.6.4) (2024-02-28)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
* **`cqn2sql`:** smart quoting also for update statements ([#475](https://github.com/cap-js/cds-dbs/issues/475)) ([1688f77](https://github.com/cap-js/cds-dbs/commit/1688f77158c2df37673e969074f1b6d210267336))
|
|
13
|
+
* `INSERT` with first `undefined` value ([#484](https://github.com/cap-js/cds-dbs/issues/484)) ([c21e3c4](https://github.com/cap-js/cds-dbs/commit/c21e3c44140c44ff6378d1fdac32869d9c1c988c))
|
|
14
|
+
* Allow SELECT.join queries again with full infer call ([#469](https://github.com/cap-js/cds-dbs/issues/469)) ([5329ec0](https://github.com/cap-js/cds-dbs/commit/5329ec0a25036a1e42513e8bb9347b0ff8c7aa2d))
|
|
15
|
+
* optimize foreign key access in a join relevant path ([#481](https://github.com/cap-js/cds-dbs/issues/481)) ([5e30de4](https://github.com/cap-js/cds-dbs/commit/5e30de439b62167c4b6d487c4d5cda4f2f0a806d)), closes [#479](https://github.com/cap-js/cds-dbs/issues/479)
|
|
16
|
+
|
|
17
|
+
## [1.6.3](https://github.com/cap-js/cds-dbs/compare/db-service-v1.6.2...db-service-v1.6.3) (2024-02-20)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
* **`cqn4sql`:** be robust against `$self.<element>;` references ([#471](https://github.com/cap-js/cds-dbs/issues/471)) ([2921b0e](https://github.com/cap-js/cds-dbs/commit/2921b0e8ada33b172a001d89904893268e751efd))
|
|
23
|
+
* **`infer`:** Always use srv.model ([#451](https://github.com/cap-js/cds-dbs/issues/451)) ([41cf4a2](https://github.com/cap-js/cds-dbs/commit/41cf4a24cf2f5e2411be0dc647af6eb628a6d312))
|
|
24
|
+
* Throw 'new Error' instead of string on $search with multiple words ([#472](https://github.com/cap-js/cds-dbs/issues/472)) ([51be94d](https://github.com/cap-js/cds-dbs/commit/51be94d2333b4a4007f354c805d1b974b19d6d2d))
|
|
25
|
+
|
|
7
26
|
## [1.6.2](https://github.com/cap-js/cds-dbs/compare/db-service-v1.6.1...db-service-v1.6.2) (2024-02-16)
|
|
8
27
|
|
|
9
28
|
|
package/lib/SQLService.js
CHANGED
|
@@ -114,6 +114,9 @@ class SQLService extends DatabaseService {
|
|
|
114
114
|
* @type {Handler}
|
|
115
115
|
*/
|
|
116
116
|
async onSELECT({ query, data }) {
|
|
117
|
+
if (!query.target) {
|
|
118
|
+
try { this.infer(query) } catch (e) { /**/ }
|
|
119
|
+
}
|
|
117
120
|
if (query.target && !query.target._unresolved) {
|
|
118
121
|
// Will return multiple rows with objects inside
|
|
119
122
|
query.SELECT.expand = 'root'
|
package/lib/cql-functions.js
CHANGED
|
@@ -21,7 +21,7 @@ const StandardFunctions = {
|
|
|
21
21
|
* @returns {string}
|
|
22
22
|
*/
|
|
23
23
|
search: function (ref, arg) {
|
|
24
|
-
if (!('val' in arg)) throw `Only single value arguments are allowed for $search`
|
|
24
|
+
if (!('val' in arg)) throw new Error(`Only single value arguments are allowed for $search`)
|
|
25
25
|
const refs = ref.list || [ref],
|
|
26
26
|
{ toString } = ref
|
|
27
27
|
return '(' + refs.map(ref2 => this.contains(this.tolower(toString(ref2)), this.tolower(arg))).join(' or ') + ')'
|
package/lib/cqn2sql.js
CHANGED
|
@@ -31,6 +31,7 @@ class CQN2SQLRenderer {
|
|
|
31
31
|
this.context = srv?.context || cds.context // Using srv.context is required due to stakeholders doing unmanaged txs without cds.context being set
|
|
32
32
|
this.class = new.target // for IntelliSense
|
|
33
33
|
this.class._init() // is a noop for subsequent calls
|
|
34
|
+
this.model = srv?.model
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
static _add_mixins(aspect, mixins) {
|
|
@@ -94,6 +95,10 @@ class CQN2SQLRenderer {
|
|
|
94
95
|
return q.target ? q : cds_infer(q)
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
cqn4sql(q) {
|
|
99
|
+
return cqn4sql(q, this.model)
|
|
100
|
+
}
|
|
101
|
+
|
|
97
102
|
// CREATE Statements ------------------------------------------------
|
|
98
103
|
|
|
99
104
|
/**
|
|
@@ -108,8 +113,8 @@ class CQN2SQLRenderer {
|
|
|
108
113
|
delete this.values
|
|
109
114
|
this.sql =
|
|
110
115
|
!query || target['@cds.persistence.table']
|
|
111
|
-
? `CREATE TABLE ${name} ( ${this.CREATE_elements(target.elements)} )`
|
|
112
|
-
: `CREATE VIEW ${name} AS ${this.SELECT(cqn4sql(query))}`
|
|
116
|
+
? `CREATE TABLE ${this.quote(name)} ( ${this.CREATE_elements(target.elements)} )`
|
|
117
|
+
: `CREATE VIEW ${this.quote(name)} AS ${this.SELECT(this.cqn4sql(query))}`
|
|
113
118
|
this.values = []
|
|
114
119
|
return
|
|
115
120
|
}
|
|
@@ -460,10 +465,11 @@ class CQN2SQLRenderer {
|
|
|
460
465
|
|
|
461
466
|
let sepsub = ''
|
|
462
467
|
for (const key in row) {
|
|
468
|
+
let val = row[key]
|
|
469
|
+
if (val === undefined) continue
|
|
463
470
|
const keyJSON = `${sepsub}${JSON.stringify(key)}:`
|
|
464
471
|
if (!sepsub) sepsub = ','
|
|
465
472
|
|
|
466
|
-
let val = row[key]
|
|
467
473
|
if (val instanceof Readable) {
|
|
468
474
|
buffer += `${keyJSON}"`
|
|
469
475
|
|
|
@@ -479,7 +485,6 @@ class CQN2SQLRenderer {
|
|
|
479
485
|
|
|
480
486
|
buffer += '"'
|
|
481
487
|
} else {
|
|
482
|
-
if (val === undefined) continue
|
|
483
488
|
if (elements[key]?.type in BINARY_TYPES) {
|
|
484
489
|
val = transformBase64(val)
|
|
485
490
|
}
|
|
@@ -612,8 +617,8 @@ class CQN2SQLRenderer {
|
|
|
612
617
|
const columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
|
|
613
618
|
c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
|
|
614
619
|
))
|
|
615
|
-
this.sql = `INSERT INTO ${entity}${alias ? ' as ' + this.quote(alias) : ''} (${columns}) ${this.SELECT(
|
|
616
|
-
cqn4sql(INSERT.as),
|
|
620
|
+
this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns}) ${this.SELECT(
|
|
621
|
+
this.cqn4sql(INSERT.as),
|
|
617
622
|
)}`
|
|
618
623
|
this.entries = [this.values]
|
|
619
624
|
return this.sql
|
|
@@ -684,8 +689,8 @@ class CQN2SQLRenderer {
|
|
|
684
689
|
UPDATE(q) {
|
|
685
690
|
const { entity, with: _with, data, where } = q.UPDATE
|
|
686
691
|
const elements = q.target?.elements
|
|
687
|
-
let sql = `UPDATE ${this.name(entity.ref?.[0] || entity)}`
|
|
688
|
-
if (entity.as) sql += ` AS ${entity.as}`
|
|
692
|
+
let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity))}`
|
|
693
|
+
if (entity.as) sql += ` AS ${this.quote(entity.as)}`
|
|
689
694
|
|
|
690
695
|
let columns = []
|
|
691
696
|
if (data) _add(data, val => this.val({ val }))
|
|
@@ -819,8 +824,8 @@ class CQN2SQLRenderer {
|
|
|
819
824
|
*/
|
|
820
825
|
ref({ ref }) {
|
|
821
826
|
switch (ref[0]) {
|
|
822
|
-
case '$now':
|
|
823
|
-
case '$user': return this.func({ func: 'session_context', args: [{ val: '$user.'+ref[1]||'id', param: false }] }) // REVISIT: same here?
|
|
827
|
+
case '$now': return this.func({ func: 'session_context', args: [{ val: '$now', param: false }] }) // REVISIT: why do we need param: false here?
|
|
828
|
+
case '$user': return this.func({ func: 'session_context', args: [{ val: '$user.' + ref[1] || 'id', param: false }] }) // REVISIT: same here?
|
|
824
829
|
default: return ref.map(r => this.quote(r)).join('.')
|
|
825
830
|
}
|
|
826
831
|
}
|
|
@@ -988,6 +993,6 @@ const _empty = a => !a || a.length === 0
|
|
|
988
993
|
* @param {import('@sap/cds/apis/cqn').Query} q
|
|
989
994
|
* @param {import('@sap/cds/apis/csn').CSN} m
|
|
990
995
|
*/
|
|
991
|
-
module.exports = (q, m) => new CQN2SQLRenderer().render(cqn4sql(q, m)
|
|
996
|
+
module.exports = (q, m) => new CQN2SQLRenderer({ model: m }).render(cqn4sql(q, m))
|
|
992
997
|
module.exports.class = CQN2SQLRenderer
|
|
993
998
|
module.exports.classDefinition = CQN2SQLRenderer // class is a reserved typescript word
|
package/lib/cqn4sql.js
CHANGED
|
@@ -43,7 +43,7 @@ const { pseudos } = require('./infer/pseudos')
|
|
|
43
43
|
* @param {object} model
|
|
44
44
|
* @returns {object} transformedQuery the transformed query
|
|
45
45
|
*/
|
|
46
|
-
function cqn4sql(originalQuery, model
|
|
46
|
+
function cqn4sql(originalQuery, model) {
|
|
47
47
|
const inferred = infer(originalQuery, model)
|
|
48
48
|
if (originalQuery.SELECT?.from.args && !originalQuery.joinTree) return inferred
|
|
49
49
|
|
|
@@ -122,7 +122,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
122
122
|
primaryKey.list.push({ ref: [transformedFrom.as, k.name] })
|
|
123
123
|
})
|
|
124
124
|
|
|
125
|
-
const transformedSubquery = cqn4sql(subquery)
|
|
125
|
+
const transformedSubquery = cqn4sql(subquery, model)
|
|
126
126
|
|
|
127
127
|
// replace where condition of original query with the transformed subquery
|
|
128
128
|
// correlate UPDATE / DELETE query with subquery by primary key matches
|
|
@@ -838,9 +838,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
838
838
|
const calcElement = resolveCalculatedElement(col, true)
|
|
839
839
|
res.push(calcElement)
|
|
840
840
|
} else if (col.isJoinRelevant) {
|
|
841
|
-
const tableAlias
|
|
841
|
+
const tableAlias = getQuerySourceName(col)
|
|
842
|
+
const name = calculateElementName(col)
|
|
842
843
|
const transformedColumn = {
|
|
843
|
-
ref: [tableAlias
|
|
844
|
+
ref: [tableAlias, name],
|
|
844
845
|
}
|
|
845
846
|
if (col.sort) transformedColumn.sort = col.sort
|
|
846
847
|
if (col.nulls) transformedColumn.nulls = col.nulls
|
|
@@ -918,7 +919,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
918
919
|
if (q.SELECT.from.uniqueSubqueryAlias) return
|
|
919
920
|
const last = q.SELECT.from.ref.at(-1)
|
|
920
921
|
const uniqueSubqueryAlias = inferred.joinTree.addNextAvailableTableAlias(
|
|
921
|
-
getLastStringSegment(last.id||last),
|
|
922
|
+
getLastStringSegment(last.id || last),
|
|
922
923
|
originalQuery.outerQueries,
|
|
923
924
|
)
|
|
924
925
|
Object.defineProperty(q.SELECT.from, 'uniqueSubqueryAlias', { value: uniqueSubqueryAlias })
|
|
@@ -976,8 +977,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
976
977
|
*/
|
|
977
978
|
function isManagedAssocInFlatMode(e) {
|
|
978
979
|
return (
|
|
979
|
-
e.isAssociation && e.keys
|
|
980
|
-
&& (model.meta.transformation === 'odata' || model.meta.unfolded?.includes('structs'))
|
|
980
|
+
e.isAssociation && e.keys && (model.meta.transformation === 'odata' || model.meta.unfolded?.includes('structs'))
|
|
981
981
|
)
|
|
982
982
|
}
|
|
983
983
|
}
|
|
@@ -1038,7 +1038,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1038
1038
|
let leafAssoc
|
|
1039
1039
|
let element = $refLinks ? $refLinks[$refLinks.length - 1].definition : column
|
|
1040
1040
|
if (isWildcard && element.type === 'cds.LargeBinary') return []
|
|
1041
|
-
if (element.on && !element.keys)
|
|
1041
|
+
if (element.on && !element.keys)
|
|
1042
|
+
return [] // unmanaged doesn't make it into columns
|
|
1042
1043
|
else if (element.virtual === true) return []
|
|
1043
1044
|
else if (!isJoinRelevant && flatName) baseName = flatName
|
|
1044
1045
|
else if (isJoinRelevant) {
|
|
@@ -1046,10 +1047,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1046
1047
|
leafAssoc = [...column.$refLinks].reverse().find(link => link.definition.isAssociation)
|
|
1047
1048
|
let elements
|
|
1048
1049
|
elements = leafAssoc.definition.elements || leafAssoc.definition.foreignKeys
|
|
1049
|
-
if (elements && leaf.
|
|
1050
|
+
if (elements && leaf.definition.name in elements) {
|
|
1050
1051
|
element = leafAssoc.definition
|
|
1051
1052
|
baseName = getFullName(leafAssoc.definition)
|
|
1052
|
-
columnAlias = column.ref.slice(0, -1).map(idOnly).join('_')
|
|
1053
|
+
columnAlias = column.as || column.ref.slice(0, -1).map(idOnly).join('_')
|
|
1053
1054
|
} else baseName = getFullName(column.$refLinks[column.$refLinks.length - 1].definition)
|
|
1054
1055
|
} else if (!baseName && structsAreUnfoldedAlready) {
|
|
1055
1056
|
baseName = element.name // name is already fully constructed
|
|
@@ -1127,8 +1128,16 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1127
1128
|
} else {
|
|
1128
1129
|
// leaf reached
|
|
1129
1130
|
let flatColumn
|
|
1130
|
-
if (columnAlias)
|
|
1131
|
-
|
|
1131
|
+
if (columnAlias) {
|
|
1132
|
+
// if the column has an explicit alias AND the orignal ref
|
|
1133
|
+
// directly resolves to the foreign key, we must not append the fk name to the column alias
|
|
1134
|
+
// e.g. `assoc.fk as FOO` => columns.alias = FOO
|
|
1135
|
+
// `assoc as FOO` => columns.alias = FOO_fk
|
|
1136
|
+
let columnAliasWithFlatFk
|
|
1137
|
+
if (!(column.as && fkElement === column.$refLinks?.at(-1).definition))
|
|
1138
|
+
columnAliasWithFlatFk = `${columnAlias}_${fk.as || fk.ref.join('_')}`
|
|
1139
|
+
flatColumn = { ref: [fkBaseName], as: columnAliasWithFlatFk || columnAlias }
|
|
1140
|
+
} else flatColumn = { ref: [fkBaseName] }
|
|
1132
1141
|
if (tableAlias) flatColumn.ref.unshift(tableAlias)
|
|
1133
1142
|
|
|
1134
1143
|
// in a flat model, we must assign the foreign key rather than the key in the target
|
|
@@ -1276,7 +1285,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1276
1285
|
}
|
|
1277
1286
|
} else {
|
|
1278
1287
|
const { list } = token
|
|
1279
|
-
if (list.every(e => e.val))
|
|
1288
|
+
if (list.every(e => e.val))
|
|
1289
|
+
// no need for transformation
|
|
1280
1290
|
transformedTokenStream.push({ list })
|
|
1281
1291
|
else transformedTokenStream.push({ list: getTransformedTokenStream(list, $baseLink) })
|
|
1282
1292
|
}
|
|
@@ -1352,11 +1362,12 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1352
1362
|
// in that case, we have a baseLink `books` which we need to resolve the following steps
|
|
1353
1363
|
// however, the correct table alias has been assigned to the `author` step
|
|
1354
1364
|
// hence we need to ignore the alias of the `$baseLink`
|
|
1355
|
-
const
|
|
1365
|
+
const lastAssoc =
|
|
1356
1366
|
token.isJoinRelevant && [...token.$refLinks].reverse().find(l => l.definition.isAssociation)
|
|
1357
|
-
const tableAlias = getQuerySourceName(token,
|
|
1358
|
-
if ((!$baseLink ||
|
|
1359
|
-
|
|
1367
|
+
const tableAlias = getQuerySourceName(token, (!lastAssoc?.onlyForeignKeyAccess && lastAssoc) || $baseLink)
|
|
1368
|
+
if ((!$baseLink || lastAssoc) && token.isJoinRelevant) {
|
|
1369
|
+
let name = calculateElementName(token, getFullName)
|
|
1370
|
+
result.ref = [tableAlias, name]
|
|
1360
1371
|
} else if (tableAlias) {
|
|
1361
1372
|
result.ref = [tableAlias, token.flatName]
|
|
1362
1373
|
} else {
|
|
@@ -1774,13 +1785,13 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1774
1785
|
// naive assumption: if first step is the association itself, all following ref steps must be resolvable
|
|
1775
1786
|
// within target `assoc.assoc.fk` -> `assoc.assoc_fk`
|
|
1776
1787
|
else if (
|
|
1777
|
-
lhs.$refLinks[0]
|
|
1788
|
+
lhs.$refLinks[0]?.definition ===
|
|
1778
1789
|
getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]
|
|
1779
1790
|
)
|
|
1780
1791
|
result[i].ref = [assocRefLink.alias, lhs.ref.slice(1).join('_')]
|
|
1781
1792
|
// naive assumption: if the path starts with an association which is not the association from
|
|
1782
1793
|
// which the on-condition originates, it must be a foreign key and hence resolvable in the source
|
|
1783
|
-
else if (lhs.$refLinks[0]
|
|
1794
|
+
else if (lhs.$refLinks[0]?.definition.target) result[i].ref = [result[i].ref.join('_')]
|
|
1784
1795
|
}
|
|
1785
1796
|
}
|
|
1786
1797
|
if (backlink) {
|
|
@@ -1813,8 +1824,13 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1813
1824
|
result.splice(i, 3, ...(wrapInXpr ? [asXpr(backlinkOnCondition)] : backlinkOnCondition))
|
|
1814
1825
|
i += wrapInXpr ? 1 : backlinkOnCondition.length // skip inserted tokens
|
|
1815
1826
|
} else if (lhs.ref) {
|
|
1816
|
-
if (lhs.ref[0] === '$self')
|
|
1817
|
-
|
|
1827
|
+
if (lhs.ref[0] === '$self') {
|
|
1828
|
+
// $self in ref of length > 1
|
|
1829
|
+
// if $self is followed by association, the alias of the association must be used
|
|
1830
|
+
if (lhs.$refLinks[1].definition.isAssociation) result[i].ref.splice(0, 1)
|
|
1831
|
+
// otherwise $self is replaced by the alias of the entity
|
|
1832
|
+
else result[i].ref.splice(0, 1, targetSideRefLink.alias)
|
|
1833
|
+
} else if (lhs.ref.length > 1) {
|
|
1818
1834
|
if (
|
|
1819
1835
|
!(lhs.ref[0] in pseudos.elements) &&
|
|
1820
1836
|
lhs.ref[0] !== assocRefLink.alias &&
|
|
@@ -1826,7 +1842,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1826
1842
|
// first step is the association itself -> use it's name as it becomes the table alias
|
|
1827
1843
|
result[i].ref.splice(0, 1, assocRefLink.alias)
|
|
1828
1844
|
} else if (
|
|
1829
|
-
definition.name in
|
|
1845
|
+
definition.name in
|
|
1846
|
+
(targetSideRefLink.definition.elements || targetSideRefLink.definition._target.elements)
|
|
1830
1847
|
) {
|
|
1831
1848
|
// first step is association which refers to its foreign key by dot notation
|
|
1832
1849
|
result[i].ref = [targetSideRefLink.alias, lhs.ref.join('_')]
|
|
@@ -1991,21 +2008,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1991
2008
|
return model.definitions[assoc.target] || null
|
|
1992
2009
|
}
|
|
1993
2010
|
|
|
1994
|
-
/**
|
|
1995
|
-
* Calculate the flat name for a deeply nested element:
|
|
1996
|
-
* @example `entity E { struct: { foo: String} }` => `getFullName(foo)` => `struct_foo`
|
|
1997
|
-
*
|
|
1998
|
-
* @param {CSN.element} node an element
|
|
1999
|
-
* @param {object} name the last part of the name, e.g. the name of the deeply nested element
|
|
2000
|
-
* @returns the flat name of the element
|
|
2001
|
-
*/
|
|
2002
|
-
function getFullName(node, name = node.name) {
|
|
2003
|
-
// REVISIT: this is an unfortunate implementation
|
|
2004
|
-
if (!node.parent || node.parent.kind === 'entity') return name
|
|
2005
|
-
|
|
2006
|
-
return getFullName(node.parent, `${node.parent.name}_${name}`)
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
2011
|
/**
|
|
2010
2012
|
* Calculates the name of the source which can be used to address the given node.
|
|
2011
2013
|
*
|
|
@@ -2074,6 +2076,31 @@ module.exports = Object.assign(cqn4sql, {
|
|
|
2074
2076
|
notSupportedOps,
|
|
2075
2077
|
})
|
|
2076
2078
|
|
|
2079
|
+
function calculateElementName(token) {
|
|
2080
|
+
const nonJoinRelevantAssoc = [...token.$refLinks].findIndex(l => l.definition.isAssociation && l.onlyForeignKeyAccess)
|
|
2081
|
+
let name
|
|
2082
|
+
if (nonJoinRelevantAssoc)
|
|
2083
|
+
// calculate fk name
|
|
2084
|
+
name = token.ref.slice(nonJoinRelevantAssoc).join('_')
|
|
2085
|
+
else name = token.$refLinks[token.$refLinks.length - 1].definition.name
|
|
2086
|
+
return name
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
/**
|
|
2090
|
+
* Calculate the flat name for a deeply nested element:
|
|
2091
|
+
* @example `entity E { struct: { foo: String} }` => `getFullName(foo)` => `struct_foo`
|
|
2092
|
+
*
|
|
2093
|
+
* @param {CSN.element} node an element
|
|
2094
|
+
* @param {object} name the last part of the name, e.g. the name of the deeply nested element
|
|
2095
|
+
* @returns the flat name of the element
|
|
2096
|
+
*/
|
|
2097
|
+
function getFullName(node, name = node.name) {
|
|
2098
|
+
// REVISIT: this is an unfortunate implementation
|
|
2099
|
+
if (!node.parent || node.parent.kind === 'entity') return name
|
|
2100
|
+
|
|
2101
|
+
return getFullName(node.parent, `${node.parent.name}_${name}`)
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2077
2104
|
function copy(obj) {
|
|
2078
2105
|
const walk = function (par, prop) {
|
|
2079
2106
|
const val = prop ? par[prop] : par
|
package/lib/infer/index.js
CHANGED
|
@@ -22,7 +22,7 @@ for (const each in cdsTypes) cdsTypes[`cds.${each}`] = cdsTypes[each]
|
|
|
22
22
|
* @param {import('@sap/cds/apis/csn').CSN} [model]
|
|
23
23
|
* @returns {import('./cqn').Query} = q with .target and .elements
|
|
24
24
|
*/
|
|
25
|
-
function infer(originalQuery, model
|
|
25
|
+
function infer(originalQuery, model) {
|
|
26
26
|
if (!model) throw new Error('Please specify a model')
|
|
27
27
|
const inferred = typeof originalQuery === 'string' ? cds.parse.cql(originalQuery) : cds.ql.clone(originalQuery)
|
|
28
28
|
|
package/lib/infer/join-tree.js
CHANGED
|
@@ -181,6 +181,7 @@ class JoinTree {
|
|
|
181
181
|
col.$refLinks[i].alias = node.$refLink.alias
|
|
182
182
|
col.$refLinks[i].definition = node.$refLink.definition
|
|
183
183
|
col.$refLinks[i].target = node.$refLink.target
|
|
184
|
+
col.$refLinks[i].onlyForeignKeyAccess = node.$refLink.onlyForeignKeyAccess
|
|
184
185
|
} else {
|
|
185
186
|
if (col.expand && !col.ref[i + 1]) {
|
|
186
187
|
node.$refLink.onlyForeignKeyAccess = false
|
|
@@ -201,7 +202,7 @@ class JoinTree {
|
|
|
201
202
|
const elements =
|
|
202
203
|
node.$refLink?.definition.isAssociation &&
|
|
203
204
|
(node.$refLink.definition.elements || node.$refLink.definition.foreignKeys)
|
|
204
|
-
if (node.$refLink && (!elements || !(child.$refLink.
|
|
205
|
+
if (node.$refLink && (!elements || !(child.$refLink.definition.name in elements)))
|
|
205
206
|
// foreign key access
|
|
206
207
|
node.$refLink.onlyForeignKeyAccess = false
|
|
207
208
|
|
package/package.json
CHANGED