@cap-js/db-service 1.19.1 → 1.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/lib/SQLService.js +50 -28
- package/lib/cqn2sql.js +14 -14
- package/lib/cqn4sql.js +22 -77
- package/lib/infer/index.js +56 -54
- package/lib/infer/join-tree.js +1 -1
- package/lib/search.js +25 -18
- package/lib/utils.js +76 -5
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@
|
|
|
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.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.20.0...db-service-v1.20.1) (2025-05-27)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
* **`search`:** do not search on non-projected elements ([#1198](https://github.com/cap-js/cds-dbs/issues/1198)) ([1461673](https://github.com/cap-js/cds-dbs/commit/14616730ba8c27e8ffa30c5962b881badfab991c))
|
|
13
|
+
* current_utctimestamp as default ([#1161](https://github.com/cap-js/cds-dbs/issues/1161)) ([c0cccad](https://github.com/cap-js/cds-dbs/commit/c0cccad921c45db96e14f0e2afeced6af69da4a2))
|
|
14
|
+
* exists within expression is properly detected ([#1156](https://github.com/cap-js/cds-dbs/issues/1156)) ([febe175](https://github.com/cap-js/cds-dbs/commit/febe1755186d291b92edbdf69cebbab68a53d0af))
|
|
15
|
+
* resilience for query re-use scenarios ([#1175](https://github.com/cap-js/cds-dbs/issues/1175)) ([fe9abd5](https://github.com/cap-js/cds-dbs/commit/fe9abd5e4c5e9153c0afad9164f240ae2eadf581))
|
|
16
|
+
|
|
17
|
+
## [1.20.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.19.1...db-service-v1.20.0) (2025-04-17)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
* Result set streaming ([#702](https://github.com/cap-js/cds-dbs/issues/702)) ([2fe02ea](https://github.com/cap-js/cds-dbs/commit/2fe02eafd02993e5697efbdab90ad997fb2c9e00))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
* **`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)
|
|
28
|
+
* 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))
|
|
29
|
+
* **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))
|
|
30
|
+
|
|
7
31
|
## [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
32
|
|
|
9
33
|
|
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 = ','
|
|
@@ -1102,7 +1102,7 @@ class CQN2SQLRenderer {
|
|
|
1102
1102
|
|
|
1103
1103
|
let onInsert = this.managed_session_context(element[cdsOnInsert]?.['='])
|
|
1104
1104
|
|| this.managed_session_context(element.default?.ref?.[0])
|
|
1105
|
-
|| (element.default
|
|
1105
|
+
|| (element.default && { __proto__: element.default, param: false })
|
|
1106
1106
|
let onUpdate = this.managed_session_context(element[cdsOnUpdate]?.['='])
|
|
1107
1107
|
|
|
1108
1108
|
if (onInsert) onInsert = this.expr(onInsert)
|
|
@@ -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, defineProperty, 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
|
}
|
|
@@ -843,10 +845,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
843
845
|
}
|
|
844
846
|
const expanded = transformSubquery(subquery)
|
|
845
847
|
const correlated = _correlate({ ...expanded, as: columnAlias }, outerAlias)
|
|
846
|
-
|
|
847
|
-
value: expanded.elements,
|
|
848
|
-
writable: true,
|
|
849
|
-
})
|
|
848
|
+
defineProperty(correlated, 'elements', expanded.elements)
|
|
850
849
|
return correlated
|
|
851
850
|
|
|
852
851
|
function _correlate(subq, outer) {
|
|
@@ -1068,9 +1067,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
1068
1067
|
else {
|
|
1069
1068
|
const outerQueries = inferred.outerQueries || []
|
|
1070
1069
|
outerQueries.push(inferred)
|
|
1071
|
-
|
|
1070
|
+
defineProperty(q, 'outerQueries', outerQueries)
|
|
1072
1071
|
}
|
|
1073
|
-
const target = cds.infer.target
|
|
1072
|
+
const target = cds.infer.target(inferred) // REVISIT: we should reliably use inferred._target instead
|
|
1074
1073
|
if (isLocalized(target)) q.SELECT.localized = true
|
|
1075
1074
|
if (q.SELECT.from.ref && !q.SELECT.from.as) assignUniqueSubqueryAlias()
|
|
1076
1075
|
return cqn4sql(q, model)
|
|
@@ -1082,7 +1081,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1082
1081
|
getImplicitAlias(last.id || last),
|
|
1083
1082
|
inferred.outerQueries,
|
|
1084
1083
|
)
|
|
1085
|
-
|
|
1084
|
+
defineProperty(q.SELECT.from, 'uniqueSubqueryAlias', uniqueSubqueryAlias)
|
|
1086
1085
|
}
|
|
1087
1086
|
}
|
|
1088
1087
|
|
|
@@ -1309,7 +1308,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1309
1308
|
const flatForeignKey = getDefinition(element.parent.name)?.elements[fkBaseName]
|
|
1310
1309
|
|
|
1311
1310
|
setElementOnColumns(flatColumn, flatForeignKey || fkElement)
|
|
1312
|
-
|
|
1311
|
+
defineProperty(flatColumn, '_csnPath', csnPath)
|
|
1313
1312
|
flatColumns.push(flatColumn)
|
|
1314
1313
|
}
|
|
1315
1314
|
}
|
|
@@ -1341,7 +1340,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1341
1340
|
if (column.sort) flatRef.sort = column.sort
|
|
1342
1341
|
if (columnAlias) flatRef.as = columnAlias
|
|
1343
1342
|
setElementOnColumns(flatRef, element)
|
|
1344
|
-
|
|
1343
|
+
defineProperty(flatRef, '_csnPath', csnPath)
|
|
1345
1344
|
return [flatRef]
|
|
1346
1345
|
|
|
1347
1346
|
function getReplacement(from) {
|
|
@@ -1674,7 +1673,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1674
1673
|
const transformedWhere = []
|
|
1675
1674
|
let transformedFrom = copy(from) // REVISIT: too expensive!
|
|
1676
1675
|
if (from.$refLinks)
|
|
1677
|
-
|
|
1676
|
+
defineProperty(transformedFrom, '$refLinks', [...from.$refLinks])
|
|
1678
1677
|
if (from.args) {
|
|
1679
1678
|
transformedFrom.args = []
|
|
1680
1679
|
from.args.forEach(arg => {
|
|
@@ -1739,7 +1738,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1739
1738
|
* with the main query alias. see @function expandColumn()
|
|
1740
1739
|
* There is one exception:
|
|
1741
1740
|
* - if current and next have the same alias, we need to assign a new alias to the next
|
|
1742
|
-
*
|
|
1741
|
+
*
|
|
1743
1742
|
*/
|
|
1744
1743
|
if (!(inferred.SELECT?.expand === true && current.alias.toLowerCase() !== as.toLowerCase())) {
|
|
1745
1744
|
as = getNextAvailableTableAlias(as)
|
|
@@ -1786,7 +1785,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1786
1785
|
const subquerySource =
|
|
1787
1786
|
getDefinition(transformedFrom.$refLinks[0].definition.target) || transformedFrom.$refLinks[0].target
|
|
1788
1787
|
if (subquerySource.params && !args) args = {}
|
|
1789
|
-
const id =
|
|
1788
|
+
const id = getLocalizedName(subquerySource)
|
|
1790
1789
|
transformedFrom.ref = [args ? { id, args } : id]
|
|
1791
1790
|
|
|
1792
1791
|
return { transformedWhere, transformedFrom }
|
|
@@ -1922,10 +1921,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1922
1921
|
const refLinkFaker = thing => {
|
|
1923
1922
|
const { ref } = thing
|
|
1924
1923
|
const assocHost = getParentEntity(assocRefLink.definition)
|
|
1925
|
-
|
|
1926
|
-
value: [],
|
|
1927
|
-
writable: true,
|
|
1928
|
-
})
|
|
1924
|
+
defineProperty(thing, '$refLinks', [])
|
|
1929
1925
|
let pseudoPath = false
|
|
1930
1926
|
ref.reduce((prev, res, i) => {
|
|
1931
1927
|
if (res === '$self') {
|
|
@@ -1994,9 +1990,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1994
1990
|
}
|
|
1995
1991
|
// assumption: if first step is the association itself, all following ref steps must be resolvable
|
|
1996
1992
|
// within target `assoc.assoc.fk` -> `assoc.assoc_fk`
|
|
1997
|
-
else if (
|
|
1998
|
-
lhsFirstDef === getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]
|
|
1999
|
-
)
|
|
1993
|
+
else if (lhsFirstDef === getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name])
|
|
2000
1994
|
result[i].ref = [assocRefLink.alias, lhs.ref.slice(lhs.ref[0] === '$self' ? 2 : 1).join('_')]
|
|
2001
1995
|
// naive assumption: if the path starts with an association which is not the association from
|
|
2002
1996
|
// which the on-condition originates, it must be a foreign key and hence resolvable in the source
|
|
@@ -2077,7 +2071,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2077
2071
|
// pseudo element
|
|
2078
2072
|
return element
|
|
2079
2073
|
if (element.kind === 'entity') return element
|
|
2080
|
-
else return getDefinition(
|
|
2074
|
+
else return getDefinition(getParentEntity(element.parent).name)
|
|
2081
2075
|
}
|
|
2082
2076
|
}
|
|
2083
2077
|
|
|
@@ -2164,8 +2158,8 @@ function cqn4sql(originalQuery, model) {
|
|
|
2164
2158
|
on.push(...(customWhere && hasLogicalOr(unmanagedOn) ? [asXpr(unmanagedOn)] : unmanagedOn))
|
|
2165
2159
|
}
|
|
2166
2160
|
|
|
2167
|
-
const subquerySource =
|
|
2168
|
-
const id =
|
|
2161
|
+
const subquerySource = getDefinition(nextDefinition.target) || nextDefinition
|
|
2162
|
+
const id = getLocalizedName(subquerySource)
|
|
2169
2163
|
if (subquerySource.params && !customArgs) customArgs = {}
|
|
2170
2164
|
const SELECT = {
|
|
2171
2165
|
from: {
|
|
@@ -2203,52 +2197,6 @@ function cqn4sql(originalQuery, model) {
|
|
|
2203
2197
|
return SELECT
|
|
2204
2198
|
}
|
|
2205
2199
|
|
|
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
2200
|
/**
|
|
2253
2201
|
* For a given search expression return a function "search" which holds the search expression
|
|
2254
2202
|
* as well as the searchable columns as arguments.
|
|
@@ -2416,10 +2364,7 @@ function getParentEntity(element) {
|
|
|
2416
2364
|
* @param {csn.Element} element
|
|
2417
2365
|
*/
|
|
2418
2366
|
function setElementOnColumns(col, element) {
|
|
2419
|
-
|
|
2420
|
-
value: element,
|
|
2421
|
-
writable: true,
|
|
2422
|
-
})
|
|
2367
|
+
defineProperty(col, 'element', element)
|
|
2423
2368
|
}
|
|
2424
2369
|
|
|
2425
2370
|
const getName = col => col.as || col.ref?.at(-1)
|
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, defineProperty } = 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')
|
|
@@ -73,7 +75,7 @@ function infer(originalQuery, model) {
|
|
|
73
75
|
joinTree: { value: joinTree, writable: true, configurable: true }, // REVISIT: eliminate
|
|
74
76
|
})
|
|
75
77
|
// also enrich original query -> writable because it may be inferred again
|
|
76
|
-
|
|
78
|
+
defineProperty(originalQuery, 'elements', elements)
|
|
77
79
|
}
|
|
78
80
|
return inferred
|
|
79
81
|
|
|
@@ -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) {
|
|
@@ -169,10 +171,7 @@ function infer(originalQuery, model) {
|
|
|
169
171
|
* @param {csn.Element} element
|
|
170
172
|
*/
|
|
171
173
|
function setElementOnColumns(col, element) {
|
|
172
|
-
|
|
173
|
-
value: element,
|
|
174
|
-
writable: true,
|
|
175
|
-
})
|
|
174
|
+
defineProperty(col, 'element', element)
|
|
176
175
|
}
|
|
177
176
|
|
|
178
177
|
/**
|
|
@@ -209,7 +208,7 @@ function infer(originalQuery, model) {
|
|
|
209
208
|
if (col.func) {
|
|
210
209
|
if (col.args) {
|
|
211
210
|
// {func}.args are optional
|
|
212
|
-
applyToFunctionArgs(col.args, inferArg, [false, null, {dollarSelfRefs}])
|
|
211
|
+
applyToFunctionArgs(col.args, inferArg, [false, null, { dollarSelfRefs }])
|
|
213
212
|
}
|
|
214
213
|
queryElements[as] = getElementForCast(col)
|
|
215
214
|
}
|
|
@@ -243,7 +242,7 @@ function infer(originalQuery, model) {
|
|
|
243
242
|
// link $refLinks -> special name resolution rules for orderBy
|
|
244
243
|
orderBy.forEach(token => {
|
|
245
244
|
let $baseLink
|
|
246
|
-
let
|
|
245
|
+
let needsElementsOfQueryAsBase
|
|
247
246
|
// first check if token ref is resolvable in query elements
|
|
248
247
|
if (columns) {
|
|
249
248
|
const firstStep = token.ref?.[0].id || token.ref?.[0]
|
|
@@ -251,14 +250,11 @@ function infer(originalQuery, model) {
|
|
|
251
250
|
const columnName = c.as || c.flatName || c.ref?.at(-1).id || c.ref?.at(-1) || c.func
|
|
252
251
|
return columnName === firstStep
|
|
253
252
|
})
|
|
254
|
-
|
|
253
|
+
needsElementsOfQueryAsBase =
|
|
255
254
|
tokenPointsToQueryElements &&
|
|
256
|
-
queryElements[
|
|
257
|
-
/* expand on structure can be addressed */ !queryElements[
|
|
255
|
+
queryElements[firstStep] &&
|
|
256
|
+
/* expand on structure can be addressed */ !queryElements[firstStep].$assocExpand
|
|
258
257
|
|
|
259
|
-
// if the ref points into the query itself and follows an exposed association
|
|
260
|
-
// to a non-fk column, we must reject the ref, as we can't join with the queries own results
|
|
261
|
-
rejectJoinRelevantPath = needsElementsOfQueryAsBase
|
|
262
258
|
if (needsElementsOfQueryAsBase) $baseLink = { definition: { elements: queryElements }, target: inferred }
|
|
263
259
|
} else {
|
|
264
260
|
// fallback to elements of query source
|
|
@@ -266,7 +262,9 @@ function infer(originalQuery, model) {
|
|
|
266
262
|
}
|
|
267
263
|
|
|
268
264
|
inferArg(token, queryElements, $baseLink, { inQueryModifier: true })
|
|
269
|
-
if
|
|
265
|
+
// if the ref points into the query itself and follows an exposed association
|
|
266
|
+
// to a non-fk column, we must reject the ref, as we can't join with the queries own results
|
|
267
|
+
if (token.isJoinRelevant && needsElementsOfQueryAsBase) {
|
|
270
268
|
// reverse the array, find the last association and calculate the index of the association in non-reversed order
|
|
271
269
|
const assocIndex =
|
|
272
270
|
token.$refLinks.length - 1 - token.$refLinks.reverse().findIndex(link => link.definition.isAssociation)
|
|
@@ -350,7 +348,7 @@ function infer(originalQuery, model) {
|
|
|
350
348
|
}
|
|
351
349
|
|
|
352
350
|
function handleRef(col, inXpr) {
|
|
353
|
-
inferArg(col, queryElements, null,
|
|
351
|
+
inferArg(col, queryElements, null, { inXpr })
|
|
354
352
|
const { definition } = col.$refLinks[col.$refLinks.length - 1]
|
|
355
353
|
if (col.cast)
|
|
356
354
|
// final type overwritten -> element not visible anymore
|
|
@@ -399,22 +397,27 @@ function infer(originalQuery, model) {
|
|
|
399
397
|
*/
|
|
400
398
|
|
|
401
399
|
function inferArg(arg, queryElements = null, $baseLink = null, context = {}) {
|
|
402
|
-
const { inExists, inXpr, inCalcElement, baseColumn, inInfixFilter, inQueryModifier, inFrom, dollarSelfRefs } =
|
|
400
|
+
const { inExists, inXpr, inCalcElement, baseColumn, inInfixFilter, inQueryModifier, inFrom, dollarSelfRefs } =
|
|
401
|
+
context
|
|
403
402
|
if (arg.param || arg.SELECT) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
|
|
404
403
|
if (arg.args) applyToFunctionArgs(arg.args, inferArg, [null, $baseLink, context])
|
|
405
404
|
if (arg.list) arg.list.forEach(arg => inferArg(arg, null, $baseLink, context))
|
|
406
|
-
if (arg.xpr)
|
|
405
|
+
if (arg.xpr)
|
|
406
|
+
arg.xpr.forEach((token, i) =>
|
|
407
|
+
inferArg(token, queryElements, $baseLink, { ...context, inXpr: true, inExists: arg.xpr[i - 1] === 'exists' }),
|
|
408
|
+
) // e.g. function in expression
|
|
407
409
|
|
|
408
410
|
if (!arg.ref) {
|
|
409
411
|
if (arg.expand && queryElements) queryElements[arg.as] = resolveExpand(arg)
|
|
410
412
|
return
|
|
411
413
|
}
|
|
412
414
|
|
|
413
|
-
//
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
415
|
+
// Before the arg is linked, it's meta information should be cleaned up.
|
|
416
|
+
// This may be important if one manipulates the arg object
|
|
417
|
+
// __after__ a query has been fired and re-uses the manipulated query
|
|
418
|
+
defineProperty(arg, '$refLinks', [])
|
|
419
|
+
defineProperty(arg, 'isJoinRelevant', false)
|
|
420
|
+
|
|
418
421
|
// if any path step points to an artifact with `@cds.persistence.skip`
|
|
419
422
|
// we must ignore the element from the queries elements
|
|
420
423
|
let isPersisted = true
|
|
@@ -424,8 +427,8 @@ 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) {
|
|
428
|
-
|
|
430
|
+
if (dollarSelfRefs && firstStepIsSelf) {
|
|
431
|
+
defineProperty(arg, 'inXpr', true)
|
|
429
432
|
dollarSelfRefs.push(arg)
|
|
430
433
|
return
|
|
431
434
|
}
|
|
@@ -452,7 +455,7 @@ function infer(originalQuery, model) {
|
|
|
452
455
|
const nextStep = arg.ref[1]?.id || arg.ref[1]
|
|
453
456
|
if (isNonForeignKeyNavigation(element, nextStep)) {
|
|
454
457
|
if (inExists) {
|
|
455
|
-
|
|
458
|
+
defineProperty($baseLink, 'pathExpressionInsideFilter', true)
|
|
456
459
|
} else {
|
|
457
460
|
rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
|
|
458
461
|
}
|
|
@@ -516,7 +519,7 @@ function infer(originalQuery, model) {
|
|
|
516
519
|
const nextStep = arg.ref[i + 1]?.id || arg.ref[i + 1]
|
|
517
520
|
if (isNonForeignKeyNavigation(element, nextStep)) {
|
|
518
521
|
if (inExists) {
|
|
519
|
-
|
|
522
|
+
defineProperty($baseLink, 'pathExpressionInsideFilter', true)
|
|
520
523
|
} else {
|
|
521
524
|
rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
|
|
522
525
|
}
|
|
@@ -532,7 +535,7 @@ function infer(originalQuery, model) {
|
|
|
532
535
|
} else if (id === '$dummy') {
|
|
533
536
|
// `some.known.element.$dummy` -> no error; used by cds.ql to simulate joins
|
|
534
537
|
arg.$refLinks.push({ definition: { name: '$dummy', parent: arg.$refLinks[i - 1].target } })
|
|
535
|
-
|
|
538
|
+
defineProperty(arg, 'isJoinRelevant', true)
|
|
536
539
|
} else {
|
|
537
540
|
const notFoundIn = pseudoPath ? arg.ref[i - 1] : getFullPathForLinkedArg(arg)
|
|
538
541
|
stepNotFoundInPredecessor(id, notFoundIn)
|
|
@@ -558,7 +561,7 @@ function infer(originalQuery, model) {
|
|
|
558
561
|
const definition = arg.$refLinks[i].definition
|
|
559
562
|
if ((!definition.target && definition.kind !== 'entity') || (!inFrom && danglingFilter))
|
|
560
563
|
throw new Error('A filter can only be provided when navigating along associations')
|
|
561
|
-
if (!inFrom && !arg.expand)
|
|
564
|
+
if (!inFrom && !arg.expand)defineProperty(arg, 'isJoinRelevant', true)
|
|
562
565
|
let skipJoinsForFilter = false
|
|
563
566
|
step.where.forEach(token => {
|
|
564
567
|
if (token === 'exists') {
|
|
@@ -587,7 +590,7 @@ function infer(originalQuery, model) {
|
|
|
587
590
|
if (getDefinition(arg.$refLinks[i].definition.target)?.['@cds.persistence.skip'] === true) isPersisted = false
|
|
588
591
|
if (!arg.ref[i + 1]) {
|
|
589
592
|
const flatName = nameSegments.join('_')
|
|
590
|
-
|
|
593
|
+
defineProperty(arg, 'flatName', flatName)
|
|
591
594
|
// if column is casted, we overwrite it's origin with the new type
|
|
592
595
|
if (arg.cast) {
|
|
593
596
|
const base = getElementForCast(arg)
|
|
@@ -632,7 +635,7 @@ function infer(originalQuery, model) {
|
|
|
632
635
|
})
|
|
633
636
|
|
|
634
637
|
// we need inner joins for the path expressions inside filter expressions after exists predicate
|
|
635
|
-
if ($baseLink?.pathExpressionInsideFilter)
|
|
638
|
+
if ($baseLink?.pathExpressionInsideFilter) defineProperty(arg, 'join', 'inner')
|
|
636
639
|
|
|
637
640
|
// ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
|
|
638
641
|
if (arg.expand) {
|
|
@@ -652,7 +655,7 @@ function infer(originalQuery, model) {
|
|
|
652
655
|
? { ref: [...baseColumn.ref, ...arg.ref], $refLinks: [...baseColumn.$refLinks, ...arg.$refLinks] }
|
|
653
656
|
: arg
|
|
654
657
|
if (isColumnJoinRelevant(colWithBase)) {
|
|
655
|
-
|
|
658
|
+
defineProperty(arg, 'isJoinRelevant', true)
|
|
656
659
|
joinTree.mergeColumn(colWithBase, originalQuery.outerQueries)
|
|
657
660
|
}
|
|
658
661
|
}
|
|
@@ -758,7 +761,7 @@ function infer(originalQuery, model) {
|
|
|
758
761
|
const res = $leafLink.definition.is2one
|
|
759
762
|
? new cds.struct({ elements: inferredExpandSubquery.elements })
|
|
760
763
|
: new cds.array({ items: new cds.struct({ elements: inferredExpandSubquery.elements }) })
|
|
761
|
-
return
|
|
764
|
+
return defineProperty(res, '$assocExpand', true)
|
|
762
765
|
} else if ($leafLink.definition.elements) {
|
|
763
766
|
let elements = {}
|
|
764
767
|
expand.forEach(e => {
|
|
@@ -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,13 +900,13 @@ function infer(originalQuery, model) {
|
|
|
891
900
|
const calcElementIsJoinRelevant = isColumnJoinRelevant(p)
|
|
892
901
|
if (calcElementIsJoinRelevant) {
|
|
893
902
|
if (!calcElement.value.isJoinRelevant)
|
|
894
|
-
|
|
903
|
+
defineProperty(step, 'isJoinRelevant',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,
|
|
898
907
|
// e.g. `SELECT from booksCalc.Books { ID, author.{name }, author {name } }`
|
|
899
908
|
// --> for the inline column, the name is join relevant, while for the expand, it is not
|
|
900
|
-
|
|
909
|
+
defineProperty(step, 'isJoinRelevant', false)
|
|
901
910
|
}
|
|
902
911
|
}
|
|
903
912
|
}
|
|
@@ -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') {
|
|
@@ -1055,7 +1064,6 @@ function infer(originalQuery, model) {
|
|
|
1055
1064
|
* @returns {object} a copy of @param base with all annotations of @param from
|
|
1056
1065
|
* @TODO prototype based
|
|
1057
1066
|
*/
|
|
1058
|
-
// REVISIT: TODO: inferred.elements should be linked
|
|
1059
1067
|
function getCopyWithAnnos(from, base) {
|
|
1060
1068
|
const result = { ...base }
|
|
1061
1069
|
// REVISIT: we don't need to and hence should not handle annotations at runtime
|
|
@@ -1063,7 +1071,7 @@ function infer(originalQuery, model) {
|
|
|
1063
1071
|
if (prop.startsWith('@')) result[prop] = from[prop]
|
|
1064
1072
|
}
|
|
1065
1073
|
|
|
1066
|
-
if (from.as && base.name !== from.as)
|
|
1074
|
+
if (from.as && base.name !== from.as) defineProperty(result, 'name', from.as) // TODO double check if this is needed
|
|
1067
1075
|
// in subqueries we need the linked element if an outer query accesses it
|
|
1068
1076
|
return Object.setPrototypeOf(result, base)
|
|
1069
1077
|
}
|
|
@@ -1083,12 +1091,6 @@ function infer(originalQuery, model) {
|
|
|
1083
1091
|
}
|
|
1084
1092
|
}
|
|
1085
1093
|
|
|
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
1094
|
function getDefinitionFromSources(sources, id) {
|
|
1093
1095
|
return sources[id].definition
|
|
1094
1096
|
}
|
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/search.js
CHANGED
|
@@ -79,7 +79,7 @@ const _getSearchableColumns = entity => {
|
|
|
79
79
|
const column = entity.elements[columnName]
|
|
80
80
|
if (column?.isAssociation || columnName.includes('.')) {
|
|
81
81
|
deepSearchCandidates.push({ ref: columnName.split('.') })
|
|
82
|
-
continue
|
|
82
|
+
continue
|
|
83
83
|
}
|
|
84
84
|
cdsSearchColumnMap.set(columnName, annotationValue)
|
|
85
85
|
}
|
|
@@ -93,8 +93,8 @@ const _getSearchableColumns = entity => {
|
|
|
93
93
|
// `@cds.search { element1: true }` or `@cds.search { element1 }`
|
|
94
94
|
if (annotatedColumnValue) return true
|
|
95
95
|
|
|
96
|
-
// calculated elements are only searchable if requested through `@cds.search`
|
|
97
|
-
if(column.value) return false
|
|
96
|
+
// calculated elements are only searchable if requested through `@cds.search`
|
|
97
|
+
if (column.value) return false
|
|
98
98
|
|
|
99
99
|
// if at least one element is explicitly annotated as searchable, e.g.:
|
|
100
100
|
// `@cds.search { element1: true }` or `@cds.search { element1 }`
|
|
@@ -112,16 +112,23 @@ const _getSearchableColumns = entity => {
|
|
|
112
112
|
|
|
113
113
|
if (deepSearchCandidates.length) {
|
|
114
114
|
deepSearchCandidates.forEach(c => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
let element = entity
|
|
116
|
+
for (let i = 0; i < c.ref.length; ++i) {
|
|
117
|
+
const curr = c.ref[i]
|
|
118
|
+
const next = element.elements?.[curr] ?? element._target?.elements?.[curr]
|
|
119
|
+
|
|
120
|
+
if (!next) { // e.g. if a search element is not part of a projection
|
|
121
|
+
element = undefined
|
|
122
|
+
break
|
|
122
123
|
}
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
|
|
125
|
+
if (next.isAssociation && i === c.ref.length - 1) {
|
|
126
|
+
_getSearchableColumns(next._target).forEach(r => searchableColumns.push({ ref: c.ref.concat(...r.ref) }))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
element = next
|
|
130
|
+
}
|
|
131
|
+
|
|
125
132
|
if (element?.type === DEFAULT_SEARCHABLE_TYPE) {
|
|
126
133
|
searchableColumns.push({ ref: c.ref })
|
|
127
134
|
}
|
|
@@ -129,9 +136,8 @@ const _getSearchableColumns = entity => {
|
|
|
129
136
|
}
|
|
130
137
|
|
|
131
138
|
return searchableColumns.map(column => {
|
|
132
|
-
if(column.ref)
|
|
133
|
-
|
|
134
|
-
return { ref: [ column.name ] }
|
|
139
|
+
if (column.ref) return column
|
|
140
|
+
return { ref: [column.name] }
|
|
135
141
|
})
|
|
136
142
|
}
|
|
137
143
|
|
|
@@ -183,15 +189,16 @@ const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }) =
|
|
|
183
189
|
}
|
|
184
190
|
})
|
|
185
191
|
} else {
|
|
186
|
-
if(entity.kind === 'entity') {
|
|
192
|
+
if (entity.kind === 'entity') {
|
|
187
193
|
// first check cache
|
|
188
|
-
toBeSearched =
|
|
194
|
+
toBeSearched =
|
|
195
|
+
entity.own('__searchableColumns') || entity.set('__searchableColumns', _getSearchableColumns(entity))
|
|
189
196
|
} else {
|
|
190
197
|
// if we search on a subquery, we don't have a cache
|
|
191
198
|
toBeSearched = _getSearchableColumns(entity)
|
|
192
199
|
}
|
|
193
200
|
toBeSearched = toBeSearched.map(c => {
|
|
194
|
-
const column = {ref: [...c.ref]}
|
|
201
|
+
const column = { ref: [...c.ref] }
|
|
195
202
|
return column
|
|
196
203
|
})
|
|
197
204
|
}
|
package/lib/utils.js
CHANGED
|
@@ -40,32 +40,103 @@ 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
|
+
function defineProperty(obj, prop, value) {
|
|
66
|
+
return Object.defineProperty(obj, prop, {
|
|
67
|
+
value,
|
|
68
|
+
writable: true,
|
|
69
|
+
configurable: true,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Shared utility functions which operate dynamically on the model / query.
|
|
75
|
+
*
|
|
76
|
+
* @param {CSN.model} model
|
|
77
|
+
* @param {CQL} query
|
|
78
|
+
*/
|
|
79
|
+
function getModelUtils(model, query) {
|
|
80
|
+
/**
|
|
81
|
+
* Returns the name of the localized entity for the given `definition`.
|
|
82
|
+
*
|
|
83
|
+
* If the query is `localized`, returns the name of the `localized` version of the `definition`.
|
|
84
|
+
* If there is no `localized` version of the `definition`, return the name of the `definition`
|
|
85
|
+
*
|
|
86
|
+
* @param {CSN.definition} definition
|
|
87
|
+
* @returns the name of the localized entity for the given `definition` or `definition.name`
|
|
88
|
+
*/
|
|
89
|
+
function getLocalizedName(definition) {
|
|
90
|
+
if (!isLocalized(definition)) return definition.name
|
|
91
|
+
const view = getDefinition(`localized.${definition.name}`)
|
|
92
|
+
return view?.name || definition.name
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Returns true if the definition shall be localized, in the context of the given query.
|
|
97
|
+
*
|
|
98
|
+
* If a given query is required to be translated, the query has
|
|
99
|
+
* the `.localized` property set to `true`. If that is the case,
|
|
100
|
+
* and the definition has not set the `@cds.localized` annotation
|
|
101
|
+
* to `false`, the given definition must be translated.
|
|
102
|
+
*
|
|
103
|
+
* @returns true if the given definition shall be localized
|
|
104
|
+
*/
|
|
105
|
+
function isLocalized(definition) {
|
|
106
|
+
return (
|
|
107
|
+
query.SELECT?.localized &&
|
|
108
|
+
definition?.['@cds.localized'] !== false &&
|
|
109
|
+
!query.SELECT.forUpdate &&
|
|
110
|
+
!query.SELECT.forShareLock
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Returns the (potentially localized) CSN definition for the given name from the model.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} name - The name of the definition to retrieve.
|
|
118
|
+
* @returns {Object|null} The CSN definition or null if not found. The definition may be localized.
|
|
119
|
+
*/
|
|
120
|
+
function getDefinition(name) {
|
|
121
|
+
if (!name) return null
|
|
122
|
+
const def = model.definitions[name]
|
|
123
|
+
if (!def || !isLocalized(def)) return def
|
|
124
|
+
return model.definitions[`localized.${def.name}`] || def
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
getLocalizedName,
|
|
129
|
+
isLocalized,
|
|
130
|
+
getDefinition,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
65
134
|
// export the function to be used in other modules
|
|
66
135
|
module.exports = {
|
|
67
136
|
prettyPrintRef,
|
|
68
137
|
isCalculatedOnRead,
|
|
69
138
|
isCalculatedElement,
|
|
70
139
|
getImplicitAlias,
|
|
140
|
+
defineProperty,
|
|
141
|
+
getModelUtils,
|
|
71
142
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/db-service",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.1",
|
|
4
4
|
"description": "CDS base database service",
|
|
5
5
|
"homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/cap-js/cds-dbs"
|
|
8
|
+
"url": "git+https://github.com/cap-js/cds-dbs.git"
|
|
9
9
|
},
|
|
10
10
|
"bugs": {
|
|
11
11
|
"url": "https://github.com/cap-js/cds-dbs/issues"
|