@cap-js/db-service 1.19.1 → 1.20.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/SQLService.js +50 -28
- package/lib/cqn2sql.js +13 -13
- package/lib/cqn4sql.js +14 -60
- package/lib/infer/index.js +26 -23
- package/lib/infer/join-tree.js +1 -1
- package/lib/utils.js +67 -5
- 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.20.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.19.1...db-service-v1.20.0) (2025-04-17)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* Result set streaming ([#702](https://github.com/cap-js/cds-dbs/issues/702)) ([2fe02ea](https://github.com/cap-js/cds-dbs/commit/2fe02eafd02993e5697efbdab90ad997fb2c9e00))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
* **`expand`:** proper subquery `from` construction ([#1126](https://github.com/cap-js/cds-dbs/issues/1126)) ([e343e79](https://github.com/cap-js/cds-dbs/commit/e343e7978acc0c181a012f61b6181b7c558aa178)), closes [#1114](https://github.com/cap-js/cds-dbs/issues/1114) [#1112](https://github.com/cap-js/cds-dbs/issues/1112)
|
|
18
|
+
* Improved support for special characters in column names ([#1141](https://github.com/cap-js/cds-dbs/issues/1141)) ([ba04697](https://github.com/cap-js/cds-dbs/commit/ba046971921d645e8571a80c27ef07988c8c01ad))
|
|
19
|
+
* **infer:** for localized queries, use `localized.<entity>` as `_target` ([#1140](https://github.com/cap-js/cds-dbs/issues/1140)) ([b08707b](https://github.com/cap-js/cds-dbs/commit/b08707b76a53c74f1c4388a8be4d0818506388c5))
|
|
20
|
+
|
|
7
21
|
## [1.19.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.19.0...db-service-v1.19.1) (2025-04-01)
|
|
8
22
|
|
|
9
23
|
|
package/lib/SQLService.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const cds = require('@sap/cds'),
|
|
2
2
|
DEBUG = cds.debug('sql|db')
|
|
3
|
-
const { Readable } = require('stream')
|
|
3
|
+
const { Readable, Transform } = require('stream')
|
|
4
|
+
const { pipeline } = require('stream/promises')
|
|
4
5
|
const { resolveView, getDBTable, getTransition } = require('@sap/cds/libx/_runtime/common/utils/resolveView')
|
|
5
6
|
const DatabaseService = require('./common/DatabaseService')
|
|
6
7
|
const cqn4sql = require('./cqn4sql')
|
|
@@ -117,7 +118,7 @@ class SQLService extends DatabaseService {
|
|
|
117
118
|
* Handler for SELECT
|
|
118
119
|
* @type {Handler}
|
|
119
120
|
*/
|
|
120
|
-
async onSELECT({ query, data }) {
|
|
121
|
+
async onSELECT({ query, data, iterator, objectMode }) {
|
|
121
122
|
// REVISIT: for custom joins, infer is called twice, which is bad
|
|
122
123
|
// --> make cds.infer properly work with custom joins and remove this
|
|
123
124
|
if (!(query._target instanceof cds.entity)) {
|
|
@@ -131,35 +132,56 @@ class SQLService extends DatabaseService {
|
|
|
131
132
|
const { sql, values, cqn } = this.cqn2sql(query, data)
|
|
132
133
|
const expand = query.SELECT.expand
|
|
133
134
|
delete query.SELECT.expand
|
|
135
|
+
const isOne = cqn.SELECT.one || query.SELECT.from?.ref?.[0].cardinality?.max === 1
|
|
134
136
|
|
|
135
137
|
let ps = await this.prepare(sql)
|
|
136
|
-
let rows = await ps.all(values)
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
138
|
+
let rows = iterator ? await ps.stream(values, isOne, objectMode) : await ps.all(values)
|
|
139
|
+
try {
|
|
140
|
+
if (rows.length)
|
|
141
|
+
if (expand) rows = rows.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))
|
|
142
|
+
|
|
143
|
+
if (!iterator) {
|
|
144
|
+
// REVISIT: remove after removing stream_compat feature flag
|
|
145
|
+
if (cds.env.features.stream_compat) {
|
|
146
|
+
if (query._streaming) {
|
|
147
|
+
if (!rows.length) return
|
|
148
|
+
this._changeToStreams(cqn.SELECT.columns, rows, true, true)
|
|
149
|
+
const result = rows[0]
|
|
150
|
+
|
|
151
|
+
// stream is always on position 0. Further properties like etag are inserted later.
|
|
152
|
+
let [key, val] = Object.entries(result)[0]
|
|
153
|
+
result.value = val
|
|
154
|
+
delete result[key]
|
|
155
|
+
|
|
156
|
+
return result
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
this._changeToStreams(cqn.SELECT.columns, rows, query.SELECT.one, false)
|
|
160
|
+
}
|
|
161
|
+
} else if (objectMode) {
|
|
162
|
+
const converter = (row) => this._changeToStreams(cqn.SELECT.columns, row, true)
|
|
163
|
+
const changeToStreams = new Transform({
|
|
164
|
+
objectMode: true,
|
|
165
|
+
transform(row, enc, cb) {
|
|
166
|
+
converter(row)
|
|
167
|
+
cb(null, row)
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
pipeline(rows, changeToStreams)
|
|
171
|
+
rows = changeToStreams
|
|
152
172
|
}
|
|
153
|
-
} else {
|
|
154
|
-
this._changeToStreams(cqn.SELECT.columns, rows, query.SELECT.one, false)
|
|
155
|
-
}
|
|
156
173
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
174
|
+
if (cqn.SELECT.count) {
|
|
175
|
+
// REVISIT: the runtime always expects that the count is preserved with .map, required for renaming in mocks
|
|
176
|
+
return SQLService._arrayWithCount(rows, await this.count(query, rows))
|
|
177
|
+
}
|
|
161
178
|
|
|
162
|
-
|
|
179
|
+
return iterator !== false && isOne ? rows[0] : rows
|
|
180
|
+
} catch (err) {
|
|
181
|
+
// Ensure that iterators receive pre stream errors
|
|
182
|
+
if (iterator) rows.emit('error', err)
|
|
183
|
+
throw err
|
|
184
|
+
}
|
|
163
185
|
}
|
|
164
186
|
|
|
165
187
|
/**
|
|
@@ -314,7 +336,7 @@ class SQLService extends DatabaseService {
|
|
|
314
336
|
* @returns {Promise<number>}
|
|
315
337
|
*/
|
|
316
338
|
async count(query, ret) {
|
|
317
|
-
if (ret) {
|
|
339
|
+
if (ret?.length) {
|
|
318
340
|
const { one, limit: _ } = query.SELECT,
|
|
319
341
|
n = ret.length
|
|
320
342
|
const [max, offset = 0] = one ? [1] : _ ? [_.rows?.val, _.offset?.val] : []
|
|
@@ -355,6 +377,7 @@ class SQLService extends DatabaseService {
|
|
|
355
377
|
// preserves $count for .map calls on array
|
|
356
378
|
static _arrayWithCount = function (a, count) {
|
|
357
379
|
const _map = a.map
|
|
380
|
+
|
|
358
381
|
const map = function (..._) {
|
|
359
382
|
return SQLService._arrayWithCount(_map.call(a, ..._), count)
|
|
360
383
|
}
|
|
@@ -475,7 +498,6 @@ SQLService.prototype.PreparedStatement = PreparedStatement
|
|
|
475
498
|
const _target_name4 = q => {
|
|
476
499
|
const target =
|
|
477
500
|
q._target_ref ||
|
|
478
|
-
q.from_into_ntt ||
|
|
479
501
|
q.SELECT?.from ||
|
|
480
502
|
q.INSERT?.into ||
|
|
481
503
|
q.UPSERT?.into ||
|
package/lib/cqn2sql.js
CHANGED
|
@@ -119,7 +119,7 @@ class CQN2SQLRenderer {
|
|
|
119
119
|
* @param {import('./infer/cqn').CREATE} q
|
|
120
120
|
*/
|
|
121
121
|
CREATE(q) {
|
|
122
|
-
let { target } = q
|
|
122
|
+
let { _target: target } = q
|
|
123
123
|
let query = target?.query || q.CREATE.as
|
|
124
124
|
if (!target || target._unresolved) {
|
|
125
125
|
const entity = q.CREATE.entity
|
|
@@ -213,7 +213,7 @@ class CQN2SQLRenderer {
|
|
|
213
213
|
* @param {import('./infer/cqn').DROP} q
|
|
214
214
|
*/
|
|
215
215
|
DROP(q) {
|
|
216
|
-
const { target } = q
|
|
216
|
+
const { _target: target } = q
|
|
217
217
|
const isView = target?.query || target?.projection || q.DROP.view
|
|
218
218
|
const name = target?.name || q.DROP.table?.ref?.[0] || q.DROP.view?.ref?.[0]
|
|
219
219
|
return (this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} IF EXISTS ${this.quote(this.name(name, q))}`)
|
|
@@ -307,8 +307,7 @@ class CQN2SQLRenderer {
|
|
|
307
307
|
}
|
|
308
308
|
: x => {
|
|
309
309
|
const name = this.column_name(x)
|
|
310
|
-
|
|
311
|
-
return `'$."${escaped}"',${this.output_converter4(x.element, this.quote(name))}`
|
|
310
|
+
return `${this.string(`$.${JSON.stringify(name)}`)},${this.output_converter4(x.element, this.quote(name))}`
|
|
312
311
|
}).flat()
|
|
313
312
|
|
|
314
313
|
if (isSimple) return `SELECT ${cols} FROM (${sql})`
|
|
@@ -429,8 +428,8 @@ class CQN2SQLRenderer {
|
|
|
429
428
|
return orderBy.map(c => {
|
|
430
429
|
const o = localized
|
|
431
430
|
? this.expr(c) +
|
|
432
|
-
|
|
433
|
-
|
|
431
|
+
(c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
|
|
432
|
+
(c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
|
|
434
433
|
: this.expr(c) + (c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
|
|
435
434
|
if (c.nulls) return o + ' NULLS ' + (c.nulls.toLowerCase() === 'first' ? 'FIRST' : 'LAST')
|
|
436
435
|
return o
|
|
@@ -524,13 +523,14 @@ class CQN2SQLRenderer {
|
|
|
524
523
|
// Include this.values for placeholders
|
|
525
524
|
/** @type {unknown[][]} */
|
|
526
525
|
this.entries = []
|
|
527
|
-
if (INSERT.entries[0] instanceof Readable) {
|
|
526
|
+
if (INSERT.entries[0] instanceof Readable && !INSERT.entries[0].readableObjectMode) {
|
|
528
527
|
INSERT.entries[0].type = 'json'
|
|
529
528
|
this.entries = [[...this.values, INSERT.entries[0]]]
|
|
530
529
|
} else {
|
|
531
|
-
const
|
|
530
|
+
const entries = INSERT.entries[0]?.[Symbol.iterator] || INSERT.entries[0]?.[Symbol.asyncIterator] || INSERT.entries[0] instanceof Readable ? INSERT.entries[0] : INSERT.entries
|
|
531
|
+
const stream = Readable.from(this.INSERT_entries_stream(entries), { objectMode: false })
|
|
532
532
|
stream.type = 'json'
|
|
533
|
-
stream._raw =
|
|
533
|
+
stream._raw = entries
|
|
534
534
|
this.entries = [[...this.values, stream]]
|
|
535
535
|
}
|
|
536
536
|
|
|
@@ -545,7 +545,7 @@ class CQN2SQLRenderer {
|
|
|
545
545
|
let buffer = '['
|
|
546
546
|
|
|
547
547
|
let sep = ''
|
|
548
|
-
for (const row of entries) {
|
|
548
|
+
for await (const row of entries) {
|
|
549
549
|
buffer += `${sep}{`
|
|
550
550
|
if (!sep) sep = ','
|
|
551
551
|
|
|
@@ -619,7 +619,7 @@ class CQN2SQLRenderer {
|
|
|
619
619
|
if (val != null && elements[this.columns[key]]?.type in this.BINARY_TYPES) {
|
|
620
620
|
val = Buffer.from(val, 'base64').toString(binaryEncoding)
|
|
621
621
|
}
|
|
622
|
-
buffer += `${sepsub}${val
|
|
622
|
+
buffer += `${sepsub}${val == null ? 'null' : JSON.stringify(val)}`
|
|
623
623
|
}
|
|
624
624
|
|
|
625
625
|
if (!sepsub) sepsub = ','
|
|
@@ -1143,8 +1143,8 @@ class CQN2SQLRenderer {
|
|
|
1143
1143
|
managed_extract(name, element, converter) {
|
|
1144
1144
|
const { UPSERT, INSERT } = this.cqn
|
|
1145
1145
|
const extract = !(INSERT?.entries || UPSERT?.entries) && (INSERT?.rows || UPSERT?.rows)
|
|
1146
|
-
? `value
|
|
1147
|
-
: `value
|
|
1146
|
+
? `value->>${this.string(`$[${this.columns.indexOf(name)}]`)}`
|
|
1147
|
+
: `value->>${this.string(`$.${JSON.stringify(name)}`)}`
|
|
1148
1148
|
const sql = converter?.(extract) || extract
|
|
1149
1149
|
return { extract, sql }
|
|
1150
1150
|
}
|
package/lib/cqn4sql.js
CHANGED
|
@@ -5,7 +5,7 @@ cds.infer.target ??= q => q._target || q.target // instanceof cds.entity ? q._ta
|
|
|
5
5
|
|
|
6
6
|
const infer = require('./infer')
|
|
7
7
|
const { computeColumnsToBeSearched } = require('./search')
|
|
8
|
-
const { prettyPrintRef, isCalculatedOnRead, isCalculatedElement, getImplicitAlias } = require('./utils')
|
|
8
|
+
const { prettyPrintRef, isCalculatedOnRead, isCalculatedElement, getImplicitAlias, getModelUtils } = require('./utils')
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* For operators of <eqOps>, this is replaced by comparing all leaf elements with null, combined with and.
|
|
@@ -78,6 +78,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
inferred = infer(inferred, model)
|
|
81
|
+
const { getLocalizedName, isLocalized, getDefinition } = getModelUtils(model, originalQuery) // TODO: pass model to getModelUtils
|
|
81
82
|
// if the query has custom joins we don't want to transform it
|
|
82
83
|
// TODO: move all the way to the top of this function once cds.infer supports joins as well
|
|
83
84
|
// we need to infer the query even if no transformation will happen because cds.infer can't calculate the target
|
|
@@ -225,7 +226,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
225
226
|
*/
|
|
226
227
|
function transformQueryForInsertUpsert(kind) {
|
|
227
228
|
const { as } = transformedQuery[kind].into
|
|
228
|
-
const target = cds.infer.target
|
|
229
|
+
const target = cds.infer.target(inferred) // REVISIT: we should reliably use inferred._target instead
|
|
229
230
|
transformedQuery[kind].into = { ref: [target.name] }
|
|
230
231
|
if (as) transformedQuery[kind].into.as = as
|
|
231
232
|
return transformedQuery
|
|
@@ -281,7 +282,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
281
282
|
const args = []
|
|
282
283
|
if (r.queryArtifact.SELECT) args.push({ SELECT: transformSubquery(r.queryArtifact).SELECT, as: r.alias })
|
|
283
284
|
else {
|
|
284
|
-
const id =
|
|
285
|
+
const id = getLocalizedName(r.queryArtifact)
|
|
285
286
|
args.push({ ref: [r.args ? { id, args: r.args } : id], as: r.alias })
|
|
286
287
|
}
|
|
287
288
|
from = { join: r.join || 'left', args, on: [] }
|
|
@@ -308,7 +309,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
308
309
|
),
|
|
309
310
|
)
|
|
310
311
|
|
|
311
|
-
const id =
|
|
312
|
+
const id = getDefinition(nextAssoc.$refLink.definition.target).name
|
|
312
313
|
const { args } = nextAssoc
|
|
313
314
|
const arg = {
|
|
314
315
|
ref: [args ? { id, args } : id],
|
|
@@ -801,8 +802,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
801
802
|
})
|
|
802
803
|
} else {
|
|
803
804
|
outerAlias = transformedQuery.SELECT.from.as
|
|
805
|
+
const getInnermostTarget = q => (q._target ? getInnermostTarget(q._target) : q)
|
|
804
806
|
subqueryFromRef = [
|
|
805
|
-
...(transformedQuery.SELECT.from.ref || /* subq in from */ transformedQuery.
|
|
807
|
+
...(transformedQuery.SELECT.from.ref || /* subq in from */ [getInnermostTarget(transformedQuery).name]),
|
|
806
808
|
...ref,
|
|
807
809
|
]
|
|
808
810
|
}
|
|
@@ -1070,7 +1072,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1070
1072
|
outerQueries.push(inferred)
|
|
1071
1073
|
Object.defineProperty(q, 'outerQueries', { value: outerQueries })
|
|
1072
1074
|
}
|
|
1073
|
-
const target = cds.infer.target
|
|
1075
|
+
const target = cds.infer.target(inferred) // REVISIT: we should reliably use inferred._target instead
|
|
1074
1076
|
if (isLocalized(target)) q.SELECT.localized = true
|
|
1075
1077
|
if (q.SELECT.from.ref && !q.SELECT.from.as) assignUniqueSubqueryAlias()
|
|
1076
1078
|
return cqn4sql(q, model)
|
|
@@ -1739,7 +1741,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1739
1741
|
* with the main query alias. see @function expandColumn()
|
|
1740
1742
|
* There is one exception:
|
|
1741
1743
|
* - if current and next have the same alias, we need to assign a new alias to the next
|
|
1742
|
-
*
|
|
1744
|
+
*
|
|
1743
1745
|
*/
|
|
1744
1746
|
if (!(inferred.SELECT?.expand === true && current.alias.toLowerCase() !== as.toLowerCase())) {
|
|
1745
1747
|
as = getNextAvailableTableAlias(as)
|
|
@@ -1786,7 +1788,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1786
1788
|
const subquerySource =
|
|
1787
1789
|
getDefinition(transformedFrom.$refLinks[0].definition.target) || transformedFrom.$refLinks[0].target
|
|
1788
1790
|
if (subquerySource.params && !args) args = {}
|
|
1789
|
-
const id =
|
|
1791
|
+
const id = getLocalizedName(subquerySource)
|
|
1790
1792
|
transformedFrom.ref = [args ? { id, args } : id]
|
|
1791
1793
|
|
|
1792
1794
|
return { transformedWhere, transformedFrom }
|
|
@@ -1994,9 +1996,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1994
1996
|
}
|
|
1995
1997
|
// assumption: if first step is the association itself, all following ref steps must be resolvable
|
|
1996
1998
|
// within target `assoc.assoc.fk` -> `assoc.assoc_fk`
|
|
1997
|
-
else if (
|
|
1998
|
-
lhsFirstDef === getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]
|
|
1999
|
-
)
|
|
1999
|
+
else if (lhsFirstDef === getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name])
|
|
2000
2000
|
result[i].ref = [assocRefLink.alias, lhs.ref.slice(lhs.ref[0] === '$self' ? 2 : 1).join('_')]
|
|
2001
2001
|
// naive assumption: if the path starts with an association which is not the association from
|
|
2002
2002
|
// which the on-condition originates, it must be a foreign key and hence resolvable in the source
|
|
@@ -2077,7 +2077,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2077
2077
|
// pseudo element
|
|
2078
2078
|
return element
|
|
2079
2079
|
if (element.kind === 'entity') return element
|
|
2080
|
-
else return getDefinition(
|
|
2080
|
+
else return getDefinition(getParentEntity(element.parent).name)
|
|
2081
2081
|
}
|
|
2082
2082
|
}
|
|
2083
2083
|
|
|
@@ -2164,8 +2164,8 @@ function cqn4sql(originalQuery, model) {
|
|
|
2164
2164
|
on.push(...(customWhere && hasLogicalOr(unmanagedOn) ? [asXpr(unmanagedOn)] : unmanagedOn))
|
|
2165
2165
|
}
|
|
2166
2166
|
|
|
2167
|
-
const subquerySource =
|
|
2168
|
-
const id =
|
|
2167
|
+
const subquerySource = getDefinition(nextDefinition.target) || nextDefinition
|
|
2168
|
+
const id = getLocalizedName(subquerySource)
|
|
2169
2169
|
if (subquerySource.params && !customArgs) customArgs = {}
|
|
2170
2170
|
const SELECT = {
|
|
2171
2171
|
from: {
|
|
@@ -2203,52 +2203,6 @@ function cqn4sql(originalQuery, model) {
|
|
|
2203
2203
|
return SELECT
|
|
2204
2204
|
}
|
|
2205
2205
|
|
|
2206
|
-
/**
|
|
2207
|
-
* If the query is `localized`, return the name of the `localized` entity for the `definition`.
|
|
2208
|
-
* If there is no `localized` entity for the `definition`, return the name of the `definition`
|
|
2209
|
-
*
|
|
2210
|
-
* @param {CSN.definition} definition
|
|
2211
|
-
* @returns the name of the localized entity for the given `definition` or `definition.name`
|
|
2212
|
-
*/
|
|
2213
|
-
function localized(definition) {
|
|
2214
|
-
if (!isLocalized(definition)) return definition.name
|
|
2215
|
-
const view = getDefinition(`localized.${definition.name}`)
|
|
2216
|
-
return view?.name || definition.name
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
/**
|
|
2220
|
-
* If a given query is required to be translated, the query has
|
|
2221
|
-
* the `.localized` property set to `true`. If that is the case,
|
|
2222
|
-
* and the definition has not set the `@cds.localized` annotation
|
|
2223
|
-
* to `false`, the given definition must be translated.
|
|
2224
|
-
*
|
|
2225
|
-
* @returns true if the given definition shall be localized
|
|
2226
|
-
*/
|
|
2227
|
-
function isLocalized(definition) {
|
|
2228
|
-
return (
|
|
2229
|
-
inferred.SELECT?.localized &&
|
|
2230
|
-
definition['@cds.localized'] !== false &&
|
|
2231
|
-
!inferred.SELECT.forUpdate &&
|
|
2232
|
-
!inferred.SELECT.forShareLock
|
|
2233
|
-
)
|
|
2234
|
-
}
|
|
2235
|
-
|
|
2236
|
-
/** returns the CSN definition for the given name from the model */
|
|
2237
|
-
function getDefinition(name) {
|
|
2238
|
-
if (!name) return null
|
|
2239
|
-
return model.definitions[name]
|
|
2240
|
-
}
|
|
2241
|
-
|
|
2242
|
-
/**
|
|
2243
|
-
* Get the csn definition of the target of a given association
|
|
2244
|
-
*
|
|
2245
|
-
* @param assoc
|
|
2246
|
-
* @returns the csn definition of the association target or null if it is not an association
|
|
2247
|
-
*/
|
|
2248
|
-
function assocTarget(assoc) {
|
|
2249
|
-
return getDefinition(assoc.target) || null
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
2206
|
/**
|
|
2253
2207
|
* For a given search expression return a function "search" which holds the search expression
|
|
2254
2208
|
* as well as the searchable columns as arguments.
|
package/lib/infer/index.js
CHANGED
|
@@ -4,7 +4,7 @@ const cds = require('@sap/cds')
|
|
|
4
4
|
|
|
5
5
|
const JoinTree = require('./join-tree')
|
|
6
6
|
const { pseudos } = require('./pseudos')
|
|
7
|
-
const { isCalculatedOnRead, getImplicitAlias } = require('../utils')
|
|
7
|
+
const { isCalculatedOnRead, getImplicitAlias, getModelUtils } = require('../utils')
|
|
8
8
|
const cdsTypes = cds.linked({
|
|
9
9
|
definitions: {
|
|
10
10
|
Timestamp: { type: 'cds.Timestamp' },
|
|
@@ -27,6 +27,8 @@ function infer(originalQuery, model) {
|
|
|
27
27
|
if (!model) throw new Error('Please specify a model')
|
|
28
28
|
const inferred = originalQuery
|
|
29
29
|
|
|
30
|
+
const { getDefinition } = getModelUtils(model, originalQuery)
|
|
31
|
+
|
|
30
32
|
// REVISIT: The more edge use cases we support, thes less optimized are we for the 90+% use cases
|
|
31
33
|
// e.g. there's a lot of overhead for infer( SELECT.from(Books) )
|
|
32
34
|
if (originalQuery.SET) throw new Error('”UNION” based queries are not supported')
|
|
@@ -111,13 +113,13 @@ function infer(originalQuery, model) {
|
|
|
111
113
|
|
|
112
114
|
inferArg(from, null, null, { inFrom: true })
|
|
113
115
|
const alias =
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
from.uniqueSubqueryAlias ||
|
|
117
|
+
from.as ||
|
|
118
|
+
(ref.length === 1
|
|
119
|
+
? getImplicitAlias(first, useTechnicalAlias)
|
|
120
|
+
: getImplicitAlias(ref.at(-1).id || ref.at(-1), useTechnicalAlias))
|
|
119
121
|
if (alias in querySources) throw new Error(`Duplicate alias "${alias}"`)
|
|
120
|
-
querySources[alias] = { definition: target, args }
|
|
122
|
+
querySources[alias] = { definition: getDefinition(target.name), args }
|
|
121
123
|
const last = from.$refLinks.at(-1)
|
|
122
124
|
last.alias = alias
|
|
123
125
|
} else if (from.args) {
|
|
@@ -209,7 +211,7 @@ function infer(originalQuery, model) {
|
|
|
209
211
|
if (col.func) {
|
|
210
212
|
if (col.args) {
|
|
211
213
|
// {func}.args are optional
|
|
212
|
-
applyToFunctionArgs(col.args, inferArg, [false, null, {dollarSelfRefs}])
|
|
214
|
+
applyToFunctionArgs(col.args, inferArg, [false, null, { dollarSelfRefs }])
|
|
213
215
|
}
|
|
214
216
|
queryElements[as] = getElementForCast(col)
|
|
215
217
|
}
|
|
@@ -350,7 +352,7 @@ function infer(originalQuery, model) {
|
|
|
350
352
|
}
|
|
351
353
|
|
|
352
354
|
function handleRef(col, inXpr) {
|
|
353
|
-
inferArg(col, queryElements, null,
|
|
355
|
+
inferArg(col, queryElements, null, { inXpr })
|
|
354
356
|
const { definition } = col.$refLinks[col.$refLinks.length - 1]
|
|
355
357
|
if (col.cast)
|
|
356
358
|
// final type overwritten -> element not visible anymore
|
|
@@ -399,7 +401,8 @@ function infer(originalQuery, model) {
|
|
|
399
401
|
*/
|
|
400
402
|
|
|
401
403
|
function inferArg(arg, queryElements = null, $baseLink = null, context = {}) {
|
|
402
|
-
const { inExists, inXpr, inCalcElement, baseColumn, inInfixFilter, inQueryModifier, inFrom, dollarSelfRefs } =
|
|
404
|
+
const { inExists, inXpr, inCalcElement, baseColumn, inInfixFilter, inQueryModifier, inFrom, dollarSelfRefs } =
|
|
405
|
+
context
|
|
403
406
|
if (arg.param || arg.SELECT) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
|
|
404
407
|
if (arg.args) applyToFunctionArgs(arg.args, inferArg, [null, $baseLink, context])
|
|
405
408
|
if (arg.list) arg.list.forEach(arg => inferArg(arg, null, $baseLink, context))
|
|
@@ -424,7 +427,7 @@ function infer(originalQuery, model) {
|
|
|
424
427
|
firstStepIsSelf = !firstStepIsTableAlias && arg.ref.length > 1 && ['$self', '$projection'].includes(arg.ref[0])
|
|
425
428
|
expandOnTableAlias = arg.ref.length === 1 && arg.ref[0] in sources && (arg.expand || arg.inline)
|
|
426
429
|
}
|
|
427
|
-
if(dollarSelfRefs && firstStepIsSelf) {
|
|
430
|
+
if (dollarSelfRefs && firstStepIsSelf) {
|
|
428
431
|
Object.defineProperty(arg, 'inXpr', { value: true, writable: true })
|
|
429
432
|
dollarSelfRefs.push(arg)
|
|
430
433
|
return
|
|
@@ -810,7 +813,8 @@ function infer(originalQuery, model) {
|
|
|
810
813
|
else alreadySeenCalcElements.add(calcElement)
|
|
811
814
|
const { ref, xpr } = calcElement.value
|
|
812
815
|
if (ref || xpr) {
|
|
813
|
-
|
|
816
|
+
const parentElementDefinition = getDefinition(calcElement.parent.name)
|
|
817
|
+
baseLink = { definition: parentElementDefinition, target: parentElementDefinition }
|
|
814
818
|
inferArg(calcElement.value, null, baseLink, { inCalcElement: true, ...context })
|
|
815
819
|
const basePath =
|
|
816
820
|
column.$refLinks?.length > 1
|
|
@@ -825,7 +829,13 @@ function infer(originalQuery, model) {
|
|
|
825
829
|
|
|
826
830
|
if (calcElement.value.args) {
|
|
827
831
|
const processArgument = (arg, calcElement, column) => {
|
|
828
|
-
|
|
832
|
+
const parentElementDefinition = getDefinition(calcElement.parent.name)
|
|
833
|
+
inferArg(
|
|
834
|
+
arg,
|
|
835
|
+
null,
|
|
836
|
+
{ definition: parentElementDefinition, target: parentElementDefinition },
|
|
837
|
+
{ inCalcElement: true },
|
|
838
|
+
)
|
|
829
839
|
const basePath =
|
|
830
840
|
column.$refLinks?.length > 1
|
|
831
841
|
? { $refLinks: column.$refLinks.slice(0, -1), ref: column.ref.slice(0, -1) }
|
|
@@ -879,8 +889,7 @@ function infer(originalQuery, model) {
|
|
|
879
889
|
step[nestedProp].forEach(a => {
|
|
880
890
|
// reset sub path for each nested argument
|
|
881
891
|
// e.g. case when <path> then <otherPath> else <anotherPath> end
|
|
882
|
-
if(!a.ref)
|
|
883
|
-
subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
|
|
892
|
+
if (!a.ref) subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
|
|
884
893
|
mergePathsIntoJoinTree(a, subPath)
|
|
885
894
|
})
|
|
886
895
|
}
|
|
@@ -891,7 +900,7 @@ function infer(originalQuery, model) {
|
|
|
891
900
|
const calcElementIsJoinRelevant = isColumnJoinRelevant(p)
|
|
892
901
|
if (calcElementIsJoinRelevant) {
|
|
893
902
|
if (!calcElement.value.isJoinRelevant)
|
|
894
|
-
Object.defineProperty(step, 'isJoinRelevant', { value: true, writable: true
|
|
903
|
+
Object.defineProperty(step, 'isJoinRelevant', { value: true, writable: true })
|
|
895
904
|
joinTree.mergeColumn(p, originalQuery.outerQueries)
|
|
896
905
|
} else {
|
|
897
906
|
// we need to explicitly set the value to false in this case,
|
|
@@ -960,7 +969,7 @@ function infer(originalQuery, model) {
|
|
|
960
969
|
if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
|
|
961
970
|
const { elements } = getDefinitionFromSources(sources, aliases[0])
|
|
962
971
|
// only one query source and no overwritten columns
|
|
963
|
-
for (const k
|
|
972
|
+
for (const k in elements) {
|
|
964
973
|
if (!exclude(k)) {
|
|
965
974
|
const element = elements[k]
|
|
966
975
|
if (element.type !== 'cds.LargeBinary') {
|
|
@@ -1083,12 +1092,6 @@ function infer(originalQuery, model) {
|
|
|
1083
1092
|
}
|
|
1084
1093
|
}
|
|
1085
1094
|
|
|
1086
|
-
/** returns the CSN definition for the given name from the model */
|
|
1087
|
-
function getDefinition(name) {
|
|
1088
|
-
if (!name) return null
|
|
1089
|
-
return model.definitions[name]
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
1095
|
function getDefinitionFromSources(sources, id) {
|
|
1093
1096
|
return sources[id].definition
|
|
1094
1097
|
}
|
package/lib/infer/join-tree.js
CHANGED
|
@@ -170,7 +170,7 @@ class JoinTree {
|
|
|
170
170
|
// find the correct query source
|
|
171
171
|
if (
|
|
172
172
|
r.queryArtifact === head.target ||
|
|
173
|
-
r.queryArtifact === head.target.
|
|
173
|
+
r.queryArtifact === head.target._target /** might as well be a query for order by */
|
|
174
174
|
)
|
|
175
175
|
node = r
|
|
176
176
|
})
|
package/lib/utils.js
CHANGED
|
@@ -40,32 +40,94 @@ function isCalculatedElement(def) {
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Calculates the implicit table alias for a given string.
|
|
43
|
-
*
|
|
43
|
+
*
|
|
44
44
|
* Based on the last part of the string, the implicit alias is calculated
|
|
45
45
|
* by taking the first character and prepending it with '$'.
|
|
46
46
|
* A leading '$' is removed if the last part already starts with '$'.
|
|
47
|
-
*
|
|
47
|
+
*
|
|
48
48
|
* @example
|
|
49
49
|
* getImplicitAlias('Books') => '$B'
|
|
50
50
|
* getImplicitAlias('bookshop.Books') => '$B'
|
|
51
51
|
* getImplicitAlias('bookshop.$B') => '$B'
|
|
52
|
-
*
|
|
52
|
+
*
|
|
53
53
|
* @param {string} str - The input string.
|
|
54
|
-
* @returns {string}
|
|
54
|
+
* @returns {string}
|
|
55
55
|
*/
|
|
56
56
|
function getImplicitAlias(str, useTechnicalAlias = true) {
|
|
57
57
|
const index = str.lastIndexOf('.')
|
|
58
|
-
if(useTechnicalAlias) {
|
|
58
|
+
if (useTechnicalAlias) {
|
|
59
59
|
const postfix = (index != -1 ? str.substring(index + 1) : str).replace(/^\$/, '')[0] || /* str === '$' */ '$'
|
|
60
60
|
return '$' + postfix
|
|
61
61
|
}
|
|
62
62
|
return index != -1 ? str.substring(index + 1) : str
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Shared utility functions which operate dynamically on the model / query.
|
|
67
|
+
*
|
|
68
|
+
* @param {CSN.model} model
|
|
69
|
+
* @param {CQL} query
|
|
70
|
+
*/
|
|
71
|
+
function getModelUtils(model, query) {
|
|
72
|
+
/**
|
|
73
|
+
* Returns the name of the localized entity for the given `definition`.
|
|
74
|
+
*
|
|
75
|
+
* If the query is `localized`, returns the name of the `localized` version of the `definition`.
|
|
76
|
+
* If there is no `localized` version of the `definition`, return the name of the `definition`
|
|
77
|
+
*
|
|
78
|
+
* @param {CSN.definition} definition
|
|
79
|
+
* @returns the name of the localized entity for the given `definition` or `definition.name`
|
|
80
|
+
*/
|
|
81
|
+
function getLocalizedName(definition) {
|
|
82
|
+
if (!isLocalized(definition)) return definition.name
|
|
83
|
+
const view = getDefinition(`localized.${definition.name}`)
|
|
84
|
+
return view?.name || definition.name
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Returns true if the definition shall be localized, in the context of the given query.
|
|
89
|
+
*
|
|
90
|
+
* If a given query is required to be translated, the query has
|
|
91
|
+
* the `.localized` property set to `true`. If that is the case,
|
|
92
|
+
* and the definition has not set the `@cds.localized` annotation
|
|
93
|
+
* to `false`, the given definition must be translated.
|
|
94
|
+
*
|
|
95
|
+
* @returns true if the given definition shall be localized
|
|
96
|
+
*/
|
|
97
|
+
function isLocalized(definition) {
|
|
98
|
+
return (
|
|
99
|
+
query.SELECT?.localized &&
|
|
100
|
+
definition?.['@cds.localized'] !== false &&
|
|
101
|
+
!query.SELECT.forUpdate &&
|
|
102
|
+
!query.SELECT.forShareLock
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Returns the (potentially localized) CSN definition for the given name from the model.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} name - The name of the definition to retrieve.
|
|
110
|
+
* @returns {Object|null} The CSN definition or null if not found. The definition may be localized.
|
|
111
|
+
*/
|
|
112
|
+
function getDefinition(name) {
|
|
113
|
+
if (!name) return null
|
|
114
|
+
const def = model.definitions[name]
|
|
115
|
+
if (!def || !isLocalized(def)) return def
|
|
116
|
+
return model.definitions[`localized.${def.name}`] || def
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
getLocalizedName,
|
|
121
|
+
isLocalized,
|
|
122
|
+
getDefinition,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
65
126
|
// export the function to be used in other modules
|
|
66
127
|
module.exports = {
|
|
67
128
|
prettyPrintRef,
|
|
68
129
|
isCalculatedOnRead,
|
|
69
130
|
isCalculatedElement,
|
|
70
131
|
getImplicitAlias,
|
|
132
|
+
getModelUtils,
|
|
71
133
|
}
|
package/package.json
CHANGED