@cap-js/db-service 1.19.0 → 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 CHANGED
@@ -4,6 +4,27 @@
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
+
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)
22
+
23
+
24
+ ### Fixed
25
+
26
+ * **scoped queries:** wrap filter in `xpr` if needed ([#1105](https://github.com/cap-js/cds-dbs/issues/1105)) ([8f44df3](https://github.com/cap-js/cds-dbs/commit/8f44df37db7bc283933023dab92b348cf92e12bf))
27
+
7
28
  ## [1.19.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.18.0...db-service-v1.19.0) (2025-03-31)
8
29
 
9
30
 
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
- if (rows.length)
138
- if (expand) rows = rows.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))
139
-
140
- if (cds.env.features.stream_compat) {
141
- if (query._streaming) {
142
- this._changeToStreams(cqn.SELECT.columns, rows, true, true)
143
- if (!rows.length) return
144
-
145
- const result = rows[0]
146
- // stream is always on position 0. Further properties like etag are inserted later.
147
- let [key, val] = Object.entries(result)[0]
148
- result.value = val
149
- delete result[key]
150
-
151
- return result
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
- if (cqn.SELECT.count) {
158
- // REVISIT: the runtime always expects that the count is preserved with .map, required for renaming in mocks
159
- return SQLService._arrayWithCount(rows, await this.count(query, rows))
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
- return cqn.SELECT.one || query.SELECT.from?.ref?.[0].cardinality?.max === 1 ? rows[0] : rows
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
- const escaped = `${name.replace(/"/g, '""')}`
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
- (c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
433
- (c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
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 stream = Readable.from(this.INSERT_entries_stream(INSERT.entries), { objectMode: false })
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 = INSERT.entries
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 === undefined ? 'null' : JSON.stringify(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->>'$[${this.columns.indexOf(name)}]'`
1147
- : `value->>'$."${name.replace(/"/g, '""')}"'`
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 (inferred) // REVISIT: we should reliably use inferred._target instead
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 = localized(r.queryArtifact)
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 = localized(getDefinition(nextAssoc.$refLink.definition.target))
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.SELECT.from.SELECT.from.ref),
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 (inferred) // REVISIT: we should reliably use inferred._target instead
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)
@@ -1766,7 +1768,7 @@ function cqn4sql(originalQuery, model) {
1766
1768
  filterConditions.forEach(f => {
1767
1769
  transformedWhere.push('and')
1768
1770
  if (filterConditions.length > 1) transformedWhere.push(asXpr(f))
1769
- else if (f.length > 3) transformedWhere.push(asXpr(f))
1771
+ else if (f.length > 3 || f.includes('or') || f.includes('and')) transformedWhere.push(asXpr(f))
1770
1772
  else transformedWhere.push(...f)
1771
1773
  })
1772
1774
  } else {
@@ -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 = localized(subquerySource)
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(localized(getParentEntity(element.parent)))
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 = assocTarget(nextDefinition) || nextDefinition
2168
- const id = localized(subquerySource)
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.
@@ -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
- from.uniqueSubqueryAlias ||
115
- from.as ||
116
- (ref.length === 1
117
- ? getImplicitAlias(first, useTechnicalAlias)
118
- : getImplicitAlias(ref.at(-1).id || ref.at(-1), useTechnicalAlias));
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, { inXpr })
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 } = context
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
- baseLink = { definition: calcElement.parent, target: calcElement.parent }
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
- inferArg(arg, null, { definition: calcElement.parent, target: calcElement.parent }, { inCalcElement: true })
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 of Object.keys(elements)) {
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
  }
@@ -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.target /** might as well be a query for order by */
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
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": {