@cap-js/db-service 1.20.0 → 2.0.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,26 @@
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
+ ## [2.0.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.20.0...db-service-v2.0.0) (2025-05-07)
8
+
9
+
10
+ ### ⚠ BREAKING CHANGES
11
+
12
+ * update peer dependency to @sap/cds@9 ([#1178](https://github.com/cap-js/cds-dbs/issues/1178))
13
+
14
+
15
+ ### Fixed
16
+
17
+ * Adopt to recurse `DistanceTo` cqn format ([#1093](https://github.com/cap-js/cds-dbs/issues/1093)) ([246e0b3](https://github.com/cap-js/cds-dbs/commit/246e0b38840f7e132ea49cae335b6be7a55354b3))
18
+ * current_utctimestamp as default ([#1161](https://github.com/cap-js/cds-dbs/issues/1161)) ([7c6b2f5](https://github.com/cap-js/cds-dbs/commit/7c6b2f5a6837afbeb1e24daef9a49e25cf7e92f0))
19
+ * exists within expression is properly detected ([#1156](https://github.com/cap-js/cds-dbs/issues/1156)) ([5a7b50c](https://github.com/cap-js/cds-dbs/commit/5a7b50cb02776cf6052c79bd276421dd87161882))
20
+ * resilience for query re-use scenarios ([#1175](https://github.com/cap-js/cds-dbs/issues/1175)) ([2352767](https://github.com/cap-js/cds-dbs/commit/2352767465ea88db77dc89bcaa76e268583146e1))
21
+
22
+
23
+ ### Changed
24
+
25
+ * update peer dependency to @sap/cds@9 ([#1178](https://github.com/cap-js/cds-dbs/issues/1178)) ([0507edd](https://github.com/cap-js/cds-dbs/commit/0507edd4e1dcb98983b1fb65ade1344d978b7524))
26
+
7
27
  ## [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
28
 
9
29
 
@@ -26,9 +26,9 @@ module.exports = class InsertResult {
26
26
  * Lazy access to auto-generated keys.
27
27
  */
28
28
  get [iterator]() {
29
- // For INSERT.as(SELECT.from(...)) return a dummy iterator with correct length
29
+ // For INSERT.from(SELECT.from(...)) return a dummy iterator with correct length
30
30
  const { INSERT } = this.query
31
- if (INSERT.as) {
31
+ if (INSERT.from || INSERT.as) {
32
32
  return (super[iterator] = function* () {
33
33
  for (let i = 0; i < this.affectedRows; i++) yield {}
34
34
  })
@@ -81,7 +81,7 @@ module.exports = class InsertResult {
81
81
  */
82
82
  get affectedRows() {
83
83
  const { INSERT: _ } = this.query
84
- if (_.as) return (super.affectedRows = this.affectedRows4(this.results[0] || this.results))
84
+ if (_.from || _.as) return (super.affectedRows = this.affectedRows4(this.results[0] || this.results))
85
85
  else return (super.affectedRows = _.entries?.length || _.rows?.length || this.results.length || 1)
86
86
  }
87
87
 
package/lib/SQLService.js CHANGED
@@ -495,16 +495,16 @@ class PreparedStatement {
495
495
  }
496
496
  SQLService.prototype.PreparedStatement = PreparedStatement
497
497
 
498
+ /** @param {import('@sap/cds').ql.Query} q */
498
499
  const _target_name4 = q => {
499
- const target =
500
- q._target_ref ||
501
- q.SELECT?.from ||
502
- q.INSERT?.into ||
503
- q.UPSERT?.into ||
504
- q.UPDATE?.entity ||
505
- q.DELETE?.from ||
506
- q.CREATE?.entity ||
507
- q.DROP?.entity
500
+ const target = q._subject
501
+ || q.SELECT?.from
502
+ || q.INSERT?.into
503
+ || q.UPSERT?.into
504
+ || q.UPDATE?.entity
505
+ || q.DELETE?.from
506
+ || q.CREATE?.entity
507
+ || q.DROP?.entity
508
508
  if (target?.SET?.op === 'union') throw new cds.error('UNION-based queries are not supported')
509
509
  if (!target?.ref) return target
510
510
  const [first] = target.ref
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const cds = require('@sap/cds')
4
+
3
5
  // OData: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_CanonicalFunctions
4
6
  const StandardFunctions = {
5
7
  /**
@@ -18,10 +20,15 @@ const StandardFunctions = {
18
20
  } catch {
19
21
  val = sub[2] || sub[3] || ''
20
22
  }
21
- arg.val = arg.__proto__.val = val
23
+ arg.val = val
22
24
  const refs = ref.list
23
- const { toString } = ref
24
- return '(' + refs.map(ref2 => this.contains(this.tolower(toString(ref2)), this.tolower(arg))).join(' or ') + ')'
25
+ return `(${refs.map(ref => this.expr({
26
+ func: 'contains',
27
+ args: [
28
+ { func: 'tolower', args: [ref] },
29
+ { func: 'tolower', args: [arg] },
30
+ ]
31
+ })).join(' or ')})`
25
32
  },
26
33
 
27
34
  // ==============================
@@ -141,7 +148,7 @@ const StandardFunctions = {
141
148
  * @returns {string} - SQL statement
142
149
  */
143
150
  now: function () {
144
- return this.session_context({ val: '$now' })
151
+ return this.expr({ func: 'session_context', args: [{ val: '$now' }] })
145
152
  },
146
153
 
147
154
  /**
@@ -184,6 +191,226 @@ const HANAFunctions = {
184
191
  * @returns {string} - SQL statement
185
192
  */
186
193
  current_timestamp: p => (p ? `current_timestamp(${p})` : 'current_timestamp'),
194
+
195
+ /**
196
+ * Generates SQL statement for the hierarchy function
197
+ * @param {string} [p] -
198
+ * @returns {string} - SQL statement
199
+ */
200
+ HIERARCHY: function (args) {
201
+ let uniqueCounter = this._with?.length ?? 0
202
+ let src = args.xpr[1]
203
+
204
+ // Ensure that the orderBy column are exposed by the source for hierarchy sorting
205
+ const orderBy = args.xpr.find((_, i, arr) => /ORDER/i.test(arr[i - 2]) && /BY/i.test(arr[i - 1]))
206
+
207
+ const passThroughColumns = src.SELECT.columns.map(c => ({ ref: ['Source', this.column_name(c)] }))
208
+ src.as = 'H' + (uniqueCounter++)
209
+ src = this.expr(this.with(src))
210
+
211
+ let recursive = cds.ql(`
212
+ SELECT
213
+ 1 as HIERARCHY_LEVEL,
214
+ NODE_ID as HIERARCHY_ROOT_ID
215
+ FROM ${src} AS Source
216
+ WHERE parent_ID IS NULL
217
+ UNION ALL
218
+ SELECT
219
+ Parent.HIERARCHY_LEVEL + 1,
220
+ Parent.HIERARCHY_ROOT_ID
221
+ FROM ${src} AS Source
222
+ JOIN H${uniqueCounter} AS Parent ON Source.PARENT_ID=Parent.NODE_ID
223
+ ORDER BY HIERARCHY_LEVEL DESC${orderBy ? `,${orderBy}` : ''}`)
224
+ recursive.as = 'H' + (uniqueCounter++)
225
+ recursive.SET.args[0].SELECT.columns = [...recursive.SET.args[0].SELECT.columns, ...passThroughColumns]
226
+ recursive.SET.args[1].SELECT.columns = [...recursive.SET.args[1].SELECT.columns, ...passThroughColumns]
227
+ recursive = this.expr(this.with(recursive))
228
+
229
+ let ranked = cds.ql(`
230
+ SELECT
231
+ HIERARCHY_LEVEL,
232
+ row_number() over () as HIERARCHY_RANK,
233
+ HIERARCHY_ROOT_ID
234
+ FROM ${recursive} AS Source`)
235
+ ranked.as = 'H' + (uniqueCounter++)
236
+ ranked.SELECT.columns = [...ranked.SELECT.columns, ...passThroughColumns]
237
+ ranked = this.expr(this.with(ranked))
238
+
239
+ let Hierarchy = cds.ql(`
240
+ SELECT
241
+ HIERARCHY_LEVEL,
242
+ HIERARCHY_RANK,
243
+ (SELECT HIERARCHY_RANK FROM ${ranked} AS Ranked WHERE Ranked.NODE_ID = Source.PARENT_ID) AS HIERARCHY_PARENT_RANK,
244
+ (SELECT HIERARCHY_RANK FROM ${ranked} AS Ranked WHERE Ranked.NODE_ID = Source.HIERARCHY_ROOT_ID) AS HIERARCHY_ROOT_RANK,
245
+ coalesce(
246
+ (SELECT MIN(HIERARCHY_RANK) FROM ${ranked} AS Ranked WHERE Ranked.HIERARCHY_RANK > Source.HIERARCHY_RANK AND Ranked.HIERARCHY_LEVEL <= Source.HIERARCHY_LEVEL),
247
+ (SELECT MAX(HIERARCHY_RANK) + 1 FROM ${ranked})
248
+ ) - Source.HIERARCHY_RANK AS HIERARCHY_TREE_SIZE
249
+ FROM ${ranked} AS Source`)
250
+ Hierarchy.as = 'H' + (uniqueCounter++)
251
+ Hierarchy.SELECT.columns = [...Hierarchy.SELECT.columns, ...passThroughColumns]
252
+ Hierarchy = this.expr(this.with(Hierarchy))
253
+
254
+ return Hierarchy
255
+ },
256
+
257
+ /**
258
+ * Generates SQL statement for the hierarchy_descendants function
259
+ * @param {string} [p] -
260
+ * @returns {string} - SQL statement
261
+ */
262
+ HIERARCHY_DESCENDANTS: function (args) {
263
+ // Find Hierarchy function call source query
264
+ const passThroughColumns = args.xpr[1].args[0].xpr[1].SELECT.columns.map(c => ({ ref: [this.column_name(c)] }))
265
+ // REVISIT: currently only supports func: HIERARCHY as source
266
+ const src = this.expr(args.xpr[1])
267
+
268
+ let uniqueCounter = this._with?.length ?? 0
269
+
270
+ let alias = args.xpr.find((_, i, arr) => /AS/i.test(arr[i - 1]))
271
+ const where = args.xpr.find((a, i, arr) => a.xpr && /WHERE/i.test(arr[i - 1]) && /START/i.test(arr[i - 2]))
272
+ const distance = args.xpr.find((a, i, arr) => typeof a.val === 'number' && (/DISTANCE/i.test(arr[i - 1]) || /DISTANCE/i.test(arr[i - 2])))
273
+ const distanceFrom = args.xpr.find((a, i, arr) => /FROM/.test(a) && /DISTANCE/i.test(arr[i - 1]))
274
+
275
+ if (alias.startsWith('"') && alias.endsWith('"')) alias = alias.slice(1, -1).replace(/""/g, '"')
276
+
277
+ let HierarchyDescendants = cds.ql(`
278
+ SELECT
279
+ HIERARCHY_LEVEL,
280
+ HIERARCHY_PARENT_RANK,
281
+ HIERARCHY_RANK,
282
+ HIERARCHY_ROOT_RANK,
283
+ HIERARCHY_TREE_SIZE,
284
+ 0 as HIERARCHY_DISTANCE
285
+ FROM ${src} AS ![${alias}]
286
+ UNION ALL
287
+ SELECT
288
+ Source.HIERARCHY_LEVEL,
289
+ Source.HIERARCHY_PARENT_RANK,
290
+ Source.HIERARCHY_RANK,
291
+ Source.HIERARCHY_ROOT_RANK,
292
+ Source.HIERARCHY_TREE_SIZE,
293
+ Child.HIERARCHY_DISTANCE + 1
294
+ FROM ${src} AS Source
295
+ JOIN H${uniqueCounter} AS Child ON Source.PARENT_ID=Child.NODE_ID`)
296
+ HierarchyDescendants.as = 'H' + uniqueCounter
297
+ HierarchyDescendants.SET.args[0].SELECT.where = where.xpr
298
+ HierarchyDescendants.SET.args[0].SELECT.columns = [...HierarchyDescendants.SET.args[0].SELECT.columns, ...passThroughColumns.map(r => ({ ref: [alias, r.ref[0]] }))]
299
+ HierarchyDescendants.SET.args[1].SELECT.columns = [...HierarchyDescendants.SET.args[1].SELECT.columns, ...passThroughColumns.map(r => ({ ref: ['Source', r.ref[0]] }))]
300
+
301
+ HierarchyDescendants = this.with(HierarchyDescendants)
302
+ HierarchyDescendants.as = 'HierarchyDescendants'
303
+
304
+ return this.expr({
305
+ SELECT: {
306
+ columns: [
307
+ { ref: ['HIERARCHY_LEVEL'] },
308
+ { ref: ['HIERARCHY_PARENT_RANK'] },
309
+ { ref: ['HIERARCHY_RANK'] },
310
+ { ref: ['HIERARCHY_ROOT_RANK'] },
311
+ { ref: ['HIERARCHY_TREE_SIZE'] },
312
+ {
313
+ SELECT: {
314
+ columns: [{ func: 'MAX', args: [{ ref: ['HIERARCHY_DISTANCE'] }] }],
315
+ from: HierarchyDescendants,
316
+ where: [{ ref: [HierarchyDescendants.as, 'HIERARCHY_RANK'] }, '=', { ref: [src, 'HIERARCHY_RANK'] }]
317
+ },
318
+ as: 'HIERARCHY_DISTANCE',
319
+ },
320
+ ...passThroughColumns,
321
+ ],
322
+ from: { ref: [src] },
323
+ where: [
324
+ { ref: ['HIERARCHY_RANK'] },
325
+ 'IN',
326
+ {
327
+ SELECT: {
328
+ columns: [{ ref: ['HIERARCHY_RANK'] }],
329
+ from: HierarchyDescendants,
330
+ where: [{ ref: ['HIERARCHY_DISTANCE'] }, distanceFrom ? '>=' : '=', distance]
331
+ }
332
+ }
333
+ ]
334
+ }
335
+ })
336
+ },
337
+
338
+ /**
339
+ * Generates SQL statement for the hierarchy_ancestors function
340
+ * @param {string} [p] -
341
+ * @returns {string} - SQL statement
342
+ */
343
+ HIERARCHY_ANCESTORS: function (args) {
344
+ // Find Hierarchy function call source query
345
+ const passThroughColumns = args.xpr[1].args[0].xpr[1].SELECT.columns.map(c => ({ ref: [this.column_name(c)] }))
346
+ // REVISIT: currently only supports func: HIERARCHY as source
347
+ const src = this.expr(args.xpr[1])
348
+
349
+ let uniqueCounter = this._with?.length ?? 0
350
+
351
+ let alias = args.xpr.find((_, i, arr) => /AS/i.test(arr[i - 1]))
352
+ const where = args.xpr.find((a, i, arr) => a.xpr && /WHERE/i.test(arr[i - 1]) && /START/i.test(arr[i - 2]))
353
+
354
+ if (alias.startsWith('"') && alias.endsWith('"')) alias = alias.slice(1, -1).replace(/""/g, '"')
355
+
356
+ let HierarchyAncestors = cds.ql(`
357
+ SELECT
358
+ HIERARCHY_LEVEL,
359
+ HIERARCHY_PARENT_RANK,
360
+ HIERARCHY_RANK,
361
+ HIERARCHY_ROOT_RANK,
362
+ HIERARCHY_TREE_SIZE,
363
+ 0 as HIERARCHY_DISTANCE
364
+ FROM ${src} AS ![${alias}]
365
+ UNION ALL
366
+ SELECT
367
+ Source.HIERARCHY_LEVEL,
368
+ Source.HIERARCHY_PARENT_RANK,
369
+ Source.HIERARCHY_RANK,
370
+ Source.HIERARCHY_ROOT_RANK,
371
+ Source.HIERARCHY_TREE_SIZE,
372
+ Child.HIERARCHY_DISTANCE - 1
373
+ FROM ${src} AS Source
374
+ JOIN H${uniqueCounter} AS Child ON Source.NODE_ID=Child.PARENT_ID`)
375
+ HierarchyAncestors.as = 'H' + uniqueCounter
376
+ HierarchyAncestors.SET.args[0].SELECT.where = where.xpr
377
+ HierarchyAncestors.SET.args[0].SELECT.columns = [...HierarchyAncestors.SET.args[0].SELECT.columns, ...passThroughColumns.map(r => ({ ref: [alias, r.ref[0]] }))]
378
+ HierarchyAncestors.SET.args[1].SELECT.columns = [...HierarchyAncestors.SET.args[1].SELECT.columns, ...passThroughColumns.map(r => ({ ref: ['Source', r.ref[0]] }))]
379
+
380
+ HierarchyAncestors = this.with(HierarchyAncestors)
381
+ HierarchyAncestors.as = 'HierarchyAncestors'
382
+ return this.expr({
383
+ SELECT: {
384
+ columns: [
385
+ { ref: ['HIERARCHY_LEVEL'] },
386
+ { ref: ['HIERARCHY_PARENT_RANK'] },
387
+ { ref: ['HIERARCHY_RANK'] },
388
+ { ref: ['HIERARCHY_ROOT_RANK'] },
389
+ { ref: ['HIERARCHY_TREE_SIZE'] },
390
+ {
391
+ SELECT: {
392
+ columns: [{ func: 'MIN', args: [{ ref: ['HIERARCHY_DISTANCE'] }] }],
393
+ from: HierarchyAncestors,
394
+ where: [{ ref: [HierarchyAncestors.as, 'HIERARCHY_RANK'] }, '=', { ref: [src, 'HIERARCHY_RANK'] }]
395
+ },
396
+ as: 'HIERARCHY_DISTANCE',
397
+ },
398
+ ...passThroughColumns,
399
+ ],
400
+ from: { ref: [src] },
401
+ where: [
402
+ { ref: ['HIERARCHY_RANK'] },
403
+ 'IN',
404
+ {
405
+ SELECT: {
406
+ columns: [{ ref: ['HIERARCHY_RANK'] }],
407
+ from: HierarchyAncestors,
408
+ }
409
+ }
410
+ ]
411
+ }
412
+ })
413
+ },
187
414
  }
188
415
 
189
416
  for (let each in HANAFunctions) HANAFunctions[each.toUpperCase()] = HANAFunctions[each]
package/lib/cqn2sql.js CHANGED
@@ -82,6 +82,9 @@ class CQN2SQLRenderer {
82
82
  /** @type {unknown[]} */
83
83
  this.values = [] // prepare values, filled in by subroutines
84
84
  this[kind]((this.cqn = q)) // actual sql rendering happens here
85
+ if (this._with?.length) {
86
+ this.render_with()
87
+ }
85
88
  if (vars?.length && !this.values?.length) this.values = vars
86
89
  if (vars && Object.keys(vars).length && !this.values?.length) this.values = vars
87
90
  const sanitize_values = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
@@ -95,10 +98,28 @@ class CQN2SQLRenderer {
95
98
  DEBUG(this.sql, values)
96
99
  }
97
100
 
98
-
99
101
  return this
100
102
  }
101
103
 
104
+ render_with() {
105
+ const sql = this.sql
106
+ let recursive = false
107
+ const values = this.values
108
+ const prefix = this._with.map(q => {
109
+ const values = this.values = []
110
+ let sql
111
+ if ('SELECT' in q) sql = `${this.quote(q.as)} AS (${this.SELECT(q)})`
112
+ else if ('SET' in q) {
113
+ recursive = true
114
+ const { SET } = q
115
+ sql = `${this.quote(q.as)}(${SET.args[0].SELECT.columns?.map(c => this.quote(this.column_name(c))) || ''}) AS (${this.SELECT(SET.args[0])} ${SET.op?.toUpperCase() || 'UNION'} ${SET.all ? 'ALL' : ''} ${this.SELECT(SET.args[1])}${SET.orderBy ? ` ORDER BY ${this.orderBy(SET.orderBy)}` : ''})`
116
+ }
117
+ return { sql, values }
118
+ })
119
+ this.sql = `WITH${recursive ? ' RECURSIVE' : ''} ${prefix.map(p => p.sql)} ${sql}`
120
+ this.values = [...prefix.map(p => p.values).flat(), ...values]
121
+ }
122
+
102
123
  /**
103
124
  * Links the incoming query with the current service model
104
125
  * @param {import('./infer/cqn').Query} q
@@ -258,8 +279,273 @@ class CQN2SQLRenderer {
258
279
  return (this.sql = sql)
259
280
  }
260
281
 
261
- SELECT_recurse() {
262
- cds.error`Feature "recurse" queries not supported.`
282
+ SELECT_recurse(q) {
283
+ let { from, columns, where, orderBy, recurse, _internal } = q.SELECT
284
+
285
+ const requiredComputedColumns = { PARENT_ID: true, NODE_ID: true }
286
+ if (!_internal) requiredComputedColumns.RANK = true
287
+ const addComputedColumn = (name) => {
288
+ if (requiredComputedColumns[name]) return
289
+ requiredComputedColumns[name] = true
290
+ }
291
+
292
+ // The hierarchy functions will output the following columns. Which might clash with the entity columns
293
+ const reservedColumnNames = {
294
+ PARENT_ID: 1, NODE_ID: 1,
295
+ HIERARCHY_RANK: 1, HIERARCHY_DISTANCE: 1, HIERARCHY_LEVEL: 1, HIERARCHY_TREE_SIZE: 1
296
+ }
297
+ const availableComputedColumns = {
298
+ // Input computed columns
299
+ PARENT_ID: false,
300
+ NODE_ID: false,
301
+
302
+ // Output computed columns
303
+ RANK: { xpr: [{ ref: ['HIERARCHY_RANK'] }, '-', { val: 1, param: false }], as: 'RANK' },
304
+ Distance: { func: where?.length ? 'min' : 'max', args: [{ ref: ['HIERARCHY_DISTANCE'] }], as: 'Distance' },
305
+ DistanceFromRoot: { xpr: [{ ref: ['HIERARCHY_LEVEL'] }, '-', { val: 1, param: false }], as: 'DistanceFromRoot' },
306
+ DrillState: false,
307
+ LimitedDescendantCount: { xpr: [{ ref: ['HIERARCHY_TREE_SIZE'] }, '-', { val: 1, param: false }], as: 'LimitedDescendantCount' },
308
+ }
309
+
310
+ const columnsFiltered = columns
311
+ .filter(x => {
312
+ if (x.element?.isAssociation) return false
313
+ const name = this.column_name(x)
314
+ if (name === '$$RN$$') return false
315
+ // REVISIT: ensure that the selected column is one of the hierarchy computed columns by unifying their common definition
316
+ if (x.element?.['@Core.Computed'] && name in availableComputedColumns) {
317
+ addComputedColumn(name)
318
+ return false
319
+ }
320
+ return true
321
+ })
322
+ const columnsOut = []
323
+ const columnsIn = []
324
+ const target = q._target || q.target
325
+ for (const name in target.elements) {
326
+ const ref = { ref: [name] }
327
+ const element = target.elements[name]
328
+ if (element.virtual || element.value || element.isAssociation) continue
329
+ if (element['@Core.Computed'] && name in availableComputedColumns) continue
330
+ if (name.toUpperCase() in reservedColumnNames) ref.as = `$$${name}$$`
331
+ columnsIn.push(ref)
332
+ if (from.args || columnsFiltered.find(c => this.column_name(c) === name)) {
333
+ columnsOut.push(ref.as ? { ref: [ref.as], as: name } : ref)
334
+ }
335
+ }
336
+
337
+ const nodeKeys = []
338
+ const parentKeys = []
339
+ const association = target.elements[recurse.ref[0]]
340
+ association._foreignKeys.forEach(fk => {
341
+ nodeKeys.push(this.quote(fk.childElement.name))
342
+ parentKeys.push(this.quote(fk.parentElement.name))
343
+ })
344
+
345
+ columnsIn.push(
346
+ nodeKeys.length === 1
347
+ ? { ref: nodeKeys, as: 'NODE_ID' }
348
+ : { func: 'HIERARCHY_COMPOSITE_ID', args: nodeKeys.map(n => ({ ref: [n] })), as: 'NODE_ID' },
349
+ parentKeys.length === 1
350
+ ? { ref: parentKeys, as: 'PARENT_ID' }
351
+ : { func: 'HIERARCHY_COMPOSITE_ID', args: parentKeys.map(n => ({ ref: [n] })), as: 'PARENT_ID' },
352
+ )
353
+
354
+ if (orderBy) {
355
+ orderBy = orderBy.map(r => {
356
+ const col = r.ref.at(-1)
357
+ if (!columnsIn.find(c => this.column_name(c) === col)) {
358
+ columnsIn.push({ ref: [col] })
359
+ }
360
+ return { ...r, ref: [col] }
361
+ })
362
+ }
363
+
364
+ // In the case of join operations make sure to compute the hierarchy from the source table only
365
+ const stableFrom = getStableFrom(from)
366
+ const alias = stableFrom.as
367
+ const source = () => ({
368
+ func: 'HIERARCHY',
369
+ args: [{ xpr: ['SOURCE', { SELECT: { columns: columnsIn, from: stableFrom } }, ...(orderBy ? ['SIBLING', 'ORDER', 'BY', `${this.orderBy(orderBy)}`] : [])] }],
370
+ as: alias
371
+ })
372
+
373
+ const expandedByNr = { list: [] } // DistanceTo(...,null)
374
+ const expandedByOne = { list: [] } // DistanceTo(...,1)
375
+ const expandedByZero = { list: [] } // not DistanceTo(...,null)
376
+ let expandedFilter = []
377
+ let distanceType = 'DistanceFromRoot'
378
+ let distanceVal
379
+
380
+ if (recurse.where) {
381
+ distanceType = 'Distance'
382
+ if (recurse.where[0] === 'and') recurse.where = recurse.where.slice(1)
383
+ expandedFilter = [...recurse.where]
384
+ collectDistanceTo(expandedFilter)
385
+ }
386
+
387
+ const direction = where?.length ? 'ANCESTORS' : 'DESCENDANTS'
388
+ // Ensure that the distance value is being computed
389
+ if (distanceType) addComputedColumn(distanceType)
390
+
391
+ let distanceClause = []
392
+ if (distanceType === 'Distance') {
393
+ const isOne = expandedByOne.list.length
394
+ distanceClause = ['DISTANCE', ...(
395
+ isOne
396
+ ? [{ val: 1 }]
397
+ : ['FROM', { val: 1 }]
398
+ )]
399
+ where = [{ ref: ['NODE_ID'] }, 'IN', isOne ? expandedByOne : expandedByNr]
400
+ expandedFilter = []
401
+ }
402
+
403
+ availableComputedColumns.DrillState = {
404
+ xpr: [ // When the node doesn't have children make it a leaf
405
+ 'CASE', 'WHEN', { ref: ['HIERARCHY_TREE_SIZE'] }, '=', { val: 1, param: false }, 'THEN', { val: 'leaf', param: false },
406
+ ...(where?.length // When there is a where filter the final node will always be a leaf
407
+ ? ['WHEN', { func: where?.length ? 'min' : 'max', args: [{ ref: ['HIERARCHY_DISTANCE'] }] }, '=', { val: 0, param: false }, 'THEN', { val: 'leaf', param: false }]
408
+ : []
409
+ ), // When having expanded by 0 level nodes make sure they are collapsed
410
+ ...(expandedByZero.list.length
411
+ ? ['WHEN', { ref: ['NODE_ID'] }, 'IN', expandedByZero, 'THEN', { val: 'collapsed', param: false }]
412
+ : []
413
+ ), // When having expanded by null or one nodes compute them as expanded
414
+ ...(expandedByNr.list.length || expandedByOne.list.length
415
+ ? ['WHEN', { ref: ['NODE_ID'] }, 'IN', { list: [...expandedByNr.list, ...expandedByOne.list] }, 'THEN', { val: 'expanded', param: false }]
416
+ : []
417
+ ), // When having expanded by one level node make its children collapsed
418
+ ...(expandedByOne.list.length
419
+ ? ['WHEN', { ref: ['PARENT_ID'] }, 'IN', expandedByOne, 'THEN', { val: 'collapsed', param: false }]
420
+ : []
421
+ ), // When using DistanceFromRoot compute all entries within the levels as expanded
422
+ ...(distanceType === 'DistanceFromRoot' && distanceVal
423
+ ? [
424
+ 'WHEN', { ref: ['HIERARCHY_LEVEL'] }, '<>', { val: distanceVal.val + 1 },
425
+ 'THEN', { val: 'expanded', param: false },
426
+ ]
427
+ : []
428
+ ), // Default to expanded when default filter behavior is truthy
429
+ 'ELSE', { val: (recurse.where && !expandedByZero.list.length) && distanceType ? 'collapsed' : 'expanded', param: false },
430
+ 'END',
431
+ ],
432
+ as: 'DrillState'
433
+ }
434
+
435
+ for (const name in requiredComputedColumns) {
436
+ const def = availableComputedColumns[name]
437
+ if (def) columnsOut.push(def)
438
+ }
439
+ if (_internal) columnsOut.push({ ref: ['NODE_ID'] })
440
+
441
+ const graph = distanceType === 'DistanceFromRoot' && !where
442
+ ? { SELECT: { columns: columnsOut, from: source(), where: expandedFilter } }
443
+ : {
444
+ SELECT: {
445
+ columns: columnsOut,
446
+ from: {
447
+ func: `HIERARCHY_${direction}`,
448
+ args: [{
449
+ xpr: [
450
+ 'SOURCE', source(), 'AS', this.quote(alias),
451
+ 'START', 'WHERE', {
452
+ xpr: where // Requires special where logic before being put into the args
453
+ ? from.args
454
+ ? [{ ref: ['NODE_ID'] }, 'IN', { SELECT: { columns: [columnsIn.find(c => c.as === 'NODE_ID')], from, where: where } }]
455
+ : this.is_comparator?.({ xpr: where }) ?? true ? where : [...where, '=', { val: true, param: false }]
456
+ : [{ ref: ['PARENT_ID'] }, '=', { val: null }]
457
+ },
458
+ ...distanceClause
459
+ ]
460
+ }]
461
+ },
462
+ where: expandedFilter.length ? expandedFilter : undefined,
463
+ orderBy: [{ ref: ['HIERARCHY_RANK'], sort: 'asc' }],
464
+ groupBy: [{ ref: ['NODE_ID'] },{ ref: ['PARENT_ID'] }, { ref: ['HIERARCHY_RANK'] }, { ref: ['HIERARCHY_LEVEL'] }, { ref: ['HIERARCHY_TREE_SIZE'] }, ...columnsOut.filter(c => c.ref)],
465
+ }
466
+ }
467
+
468
+ // Only apply result join if the columns contain a references which doesn't start with the source alias
469
+ if (from.args && columns.find(c => c.ref?.[0] === alias)) {
470
+ graph.as = alias
471
+ return this.from(setStableFrom(from, graph))
472
+ }
473
+
474
+ return `(${this.SELECT(graph)})${alias ? ` AS ${this.quote(alias)}` : ''} `
475
+
476
+ function collectDistanceTo(where, innot = false) {
477
+ for (let i = 0; i < where.length; i++) {
478
+ const c = where[i]
479
+ if (c === 'not') {
480
+ distanceType = 'DistanceFromRoot'
481
+ innot = true
482
+ }
483
+ else if (c.func === 'DistanceTo') {
484
+ const expr = c.args[0]
485
+ // { func: 'HIERARCHY_COMPOSITE_ID', args: nodeKeys.map(n => ({ val: cur[n] })) }
486
+ const to = c.args[1].val
487
+ const list = to === 1
488
+ ? expandedByOne
489
+ : innot
490
+ ? expandedByZero
491
+ : expandedByNr
492
+
493
+ if (!list._where) {
494
+ list._where = []
495
+ where.splice(i, 1,
496
+ ...(to === 1
497
+ ? [{ ref: ['PARENT_ID'] }, 'IN', list]
498
+ : [{ ref: ['NODE_ID'] }, 'IN', {
499
+ SELECT: {
500
+ _internal: true,
501
+ columns: [{ ref: ['NODE_ID'], element: { '@Core.Computed': true } }],
502
+ from: q.SELECT.from,
503
+ recurse: {
504
+ ref: recurse.ref,
505
+ where: list._where,
506
+ },
507
+ },
508
+ target,
509
+ }])
510
+ )
511
+ i += 2
512
+ } else {
513
+ // Remove current entry from where
514
+ if (where[i - 1] === 'not') {
515
+ where.splice(i - 2, 3)
516
+ i -= 3
517
+ } else {
518
+ where.splice(i - 1, 2)
519
+ i -= 2
520
+ }
521
+ }
522
+ list.list.push(expr)
523
+ list._where.push(c)
524
+ }
525
+ else if (c.ref?.[0] === 'DistanceFromRoot') {
526
+ distanceType = 'DistanceFromRoot'
527
+ where[i] = { ref: ['HIERARCHY_LEVEL'] }
528
+ i += 2
529
+ distanceVal = where[i]
530
+ where[i] = { val: where[i].val + 1 }
531
+ }
532
+ }
533
+ }
534
+
535
+ function getStableFrom(from) {
536
+ if (from.args) return getStableFrom(from.args[0])
537
+ return from
538
+ }
539
+
540
+ function setStableFrom(from, src) {
541
+ if (from.args) {
542
+ const ret = { ...from }
543
+ ret.args = [...ret.args]
544
+ ret.args[0] = setStableFrom(ret.args[0], src)
545
+ return ret
546
+ }
547
+ return src
548
+ }
263
549
  }
264
550
 
265
551
  /**
@@ -371,6 +657,18 @@ class CQN2SQLRenderer {
371
657
  }
372
658
  if (from.SELECT) return _aliased(`(${this.SELECT(from)})`)
373
659
  if (from.join) return `${this.from(from.args[0])} ${from.join} JOIN ${this.from(from.args[1])}${from.on ? ` ON ${this.where(from.on)}` : ''}`
660
+ if (from.func) return _aliased(this.func(from))
661
+ }
662
+
663
+ /**
664
+ * Renders a FROM clause into generic SQL
665
+ * @param {import('./infer/cqn').source} from
666
+ * @returns {string} SQL
667
+ */
668
+ with(query) {
669
+ this._with ??= []
670
+ this._with.push(query)
671
+ return { ref: [query.as] }
374
672
  }
375
673
 
376
674
  /**
@@ -489,7 +787,7 @@ class CQN2SQLRenderer {
489
787
  ? this.INSERT_rows(q)
490
788
  : INSERT.values
491
789
  ? this.INSERT_values(q)
492
- : INSERT.as
790
+ : INSERT.from || INSERT.as
493
791
  ? this.INSERT_select(q)
494
792
  : cds.error`Missing .entries, .rows, or .values in ${q}`
495
793
  }
@@ -695,7 +993,7 @@ class CQN2SQLRenderer {
695
993
  c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
696
994
  ))
697
995
  this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns.map(c => this.quote(c))}) ${this.SELECT(
698
- this.cqn4sql(INSERT.as),
996
+ this.cqn4sql(INSERT.from || INSERT.as),
699
997
  )}`
700
998
  this.entries = [this.values]
701
999
  return this.sql
@@ -998,7 +1296,7 @@ class CQN2SQLRenderer {
998
1296
  } else {
999
1297
  cds.error`Invalid arguments provided for function '${func}' (${args})`
1000
1298
  }
1001
- const fn = this.class.Functions[func]?.apply(this.class.Functions, args) || `${func}(${args})`
1299
+ const fn = this.class.Functions[func]?.apply(this, args) || `${func}(${args})`
1002
1300
  if (xpr) return `${fn} ${this.xpr({ xpr })}`
1003
1301
  return fn
1004
1302
  }
@@ -1102,7 +1400,7 @@ class CQN2SQLRenderer {
1102
1400
 
1103
1401
  let onInsert = this.managed_session_context(element[cdsOnInsert]?.['='])
1104
1402
  || this.managed_session_context(element.default?.ref?.[0])
1105
- || (element.default?.val !== undefined && { val: element.default.val, param: false })
1403
+ || (element.default && { __proto__: element.default, param: false })
1106
1404
  let onUpdate = this.managed_session_context(element[cdsOnUpdate]?.['='])
1107
1405
 
1108
1406
  if (onInsert) onInsert = this.expr(onInsert)
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, getModelUtils } = 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.
@@ -845,10 +845,7 @@ function cqn4sql(originalQuery, model) {
845
845
  }
846
846
  const expanded = transformSubquery(subquery)
847
847
  const correlated = _correlate({ ...expanded, as: columnAlias }, outerAlias)
848
- Object.defineProperty(correlated, 'elements', {
849
- value: expanded.elements,
850
- writable: true,
851
- })
848
+ defineProperty(correlated, 'elements', expanded.elements)
852
849
  return correlated
853
850
 
854
851
  function _correlate(subq, outer) {
@@ -1070,7 +1067,7 @@ function cqn4sql(originalQuery, model) {
1070
1067
  else {
1071
1068
  const outerQueries = inferred.outerQueries || []
1072
1069
  outerQueries.push(inferred)
1073
- Object.defineProperty(q, 'outerQueries', { value: outerQueries })
1070
+ defineProperty(q, 'outerQueries', outerQueries)
1074
1071
  }
1075
1072
  const target = cds.infer.target(inferred) // REVISIT: we should reliably use inferred._target instead
1076
1073
  if (isLocalized(target)) q.SELECT.localized = true
@@ -1084,7 +1081,7 @@ function cqn4sql(originalQuery, model) {
1084
1081
  getImplicitAlias(last.id || last),
1085
1082
  inferred.outerQueries,
1086
1083
  )
1087
- Object.defineProperty(q.SELECT.from, 'uniqueSubqueryAlias', { value: uniqueSubqueryAlias })
1084
+ defineProperty(q.SELECT.from, 'uniqueSubqueryAlias', uniqueSubqueryAlias)
1088
1085
  }
1089
1086
  }
1090
1087
 
@@ -1311,7 +1308,7 @@ function cqn4sql(originalQuery, model) {
1311
1308
  const flatForeignKey = getDefinition(element.parent.name)?.elements[fkBaseName]
1312
1309
 
1313
1310
  setElementOnColumns(flatColumn, flatForeignKey || fkElement)
1314
- Object.defineProperty(flatColumn, '_csnPath', { value: csnPath, writable: true })
1311
+ defineProperty(flatColumn, '_csnPath', csnPath)
1315
1312
  flatColumns.push(flatColumn)
1316
1313
  }
1317
1314
  }
@@ -1343,7 +1340,7 @@ function cqn4sql(originalQuery, model) {
1343
1340
  if (column.sort) flatRef.sort = column.sort
1344
1341
  if (columnAlias) flatRef.as = columnAlias
1345
1342
  setElementOnColumns(flatRef, element)
1346
- Object.defineProperty(flatRef, '_csnPath', { value: csnPath, writable: true })
1343
+ defineProperty(flatRef, '_csnPath', csnPath)
1347
1344
  return [flatRef]
1348
1345
 
1349
1346
  function getReplacement(from) {
@@ -1676,7 +1673,7 @@ function cqn4sql(originalQuery, model) {
1676
1673
  const transformedWhere = []
1677
1674
  let transformedFrom = copy(from) // REVISIT: too expensive!
1678
1675
  if (from.$refLinks)
1679
- Object.defineProperty(transformedFrom, '$refLinks', { value: [...from.$refLinks], writable: true })
1676
+ defineProperty(transformedFrom, '$refLinks', [...from.$refLinks])
1680
1677
  if (from.args) {
1681
1678
  transformedFrom.args = []
1682
1679
  from.args.forEach(arg => {
@@ -1924,10 +1921,7 @@ function cqn4sql(originalQuery, model) {
1924
1921
  const refLinkFaker = thing => {
1925
1922
  const { ref } = thing
1926
1923
  const assocHost = getParentEntity(assocRefLink.definition)
1927
- Object.defineProperty(thing, '$refLinks', {
1928
- value: [],
1929
- writable: true,
1930
- })
1924
+ defineProperty(thing, '$refLinks', [])
1931
1925
  let pseudoPath = false
1932
1926
  ref.reduce((prev, res, i) => {
1933
1927
  if (res === '$self') {
@@ -2370,10 +2364,7 @@ function getParentEntity(element) {
2370
2364
  * @param {csn.Element} element
2371
2365
  */
2372
2366
  function setElementOnColumns(col, element) {
2373
- Object.defineProperty(col, 'element', {
2374
- value: element,
2375
- writable: true,
2376
- })
2367
+ defineProperty(col, 'element', element)
2377
2368
  }
2378
2369
 
2379
2370
  const getName = col => col.as || col.ref?.at(-1)
@@ -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, getModelUtils } = 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' },
@@ -46,7 +46,7 @@ function infer(originalQuery, model) {
46
46
 
47
47
  let $combinedElements
48
48
 
49
- const sources = inferTarget(_.from || _.into || _.entity, {})
49
+ const sources = inferTarget(_.into || _.from || _.entity, {}) // IMPORTANT: _.into has to go before _.from for INSERT.into().from(SELECT)
50
50
  const joinTree = new JoinTree(sources)
51
51
  const aliases = Object.keys(sources)
52
52
  const target = aliases.length === 1 ? getDefinitionFromSources(sources, aliases[0]) : originalQuery
@@ -75,7 +75,7 @@ function infer(originalQuery, model) {
75
75
  joinTree: { value: joinTree, writable: true, configurable: true }, // REVISIT: eliminate
76
76
  })
77
77
  // also enrich original query -> writable because it may be inferred again
78
- Object.defineProperty(originalQuery, 'elements', { value: elements, writable: true, configurable: true })
78
+ defineProperty(originalQuery, 'elements', elements)
79
79
  }
80
80
  return inferred
81
81
 
@@ -171,10 +171,7 @@ function infer(originalQuery, model) {
171
171
  * @param {csn.Element} element
172
172
  */
173
173
  function setElementOnColumns(col, element) {
174
- Object.defineProperty(col, 'element', {
175
- value: element,
176
- writable: true,
177
- })
174
+ defineProperty(col, 'element', element)
178
175
  }
179
176
 
180
177
  /**
@@ -245,7 +242,7 @@ function infer(originalQuery, model) {
245
242
  // link $refLinks -> special name resolution rules for orderBy
246
243
  orderBy.forEach(token => {
247
244
  let $baseLink
248
- let rejectJoinRelevantPath
245
+ let needsElementsOfQueryAsBase
249
246
  // first check if token ref is resolvable in query elements
250
247
  if (columns) {
251
248
  const firstStep = token.ref?.[0].id || token.ref?.[0]
@@ -253,14 +250,11 @@ function infer(originalQuery, model) {
253
250
  const columnName = c.as || c.flatName || c.ref?.at(-1).id || c.ref?.at(-1) || c.func
254
251
  return columnName === firstStep
255
252
  })
256
- const needsElementsOfQueryAsBase =
253
+ needsElementsOfQueryAsBase =
257
254
  tokenPointsToQueryElements &&
258
- queryElements[token.ref?.[0]] &&
259
- /* expand on structure can be addressed */ !queryElements[token.ref?.[0]].$assocExpand
255
+ queryElements[firstStep] &&
256
+ /* expand on structure can be addressed */ !queryElements[firstStep].$assocExpand
260
257
 
261
- // if the ref points into the query itself and follows an exposed association
262
- // to a non-fk column, we must reject the ref, as we can't join with the queries own results
263
- rejectJoinRelevantPath = needsElementsOfQueryAsBase
264
258
  if (needsElementsOfQueryAsBase) $baseLink = { definition: { elements: queryElements }, target: inferred }
265
259
  } else {
266
260
  // fallback to elements of query source
@@ -268,7 +262,9 @@ function infer(originalQuery, model) {
268
262
  }
269
263
 
270
264
  inferArg(token, queryElements, $baseLink, { inQueryModifier: true })
271
- if (token.isJoinRelevant && rejectJoinRelevantPath) {
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) {
272
268
  // reverse the array, find the last association and calculate the index of the association in non-reversed order
273
269
  const assocIndex =
274
270
  token.$refLinks.length - 1 - token.$refLinks.reverse().findIndex(link => link.definition.isAssociation)
@@ -406,18 +402,22 @@ function infer(originalQuery, model) {
406
402
  if (arg.param || arg.SELECT) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
407
403
  if (arg.args) applyToFunctionArgs(arg.args, inferArg, [null, $baseLink, context])
408
404
  if (arg.list) arg.list.forEach(arg => inferArg(arg, null, $baseLink, context))
409
- if (arg.xpr) arg.xpr.forEach(token => inferArg(token, queryElements, $baseLink, { ...context, inXpr: true })) // e.g. function in expression
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
410
409
 
411
410
  if (!arg.ref) {
412
411
  if (arg.expand && queryElements) queryElements[arg.as] = resolveExpand(arg)
413
412
  return
414
413
  }
415
414
 
416
- // initialize $refLinks
417
- Object.defineProperty(arg, '$refLinks', {
418
- value: [],
419
- writable: true,
420
- })
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
+
421
421
  // if any path step points to an artifact with `@cds.persistence.skip`
422
422
  // we must ignore the element from the queries elements
423
423
  let isPersisted = true
@@ -428,7 +428,7 @@ function infer(originalQuery, model) {
428
428
  expandOnTableAlias = arg.ref.length === 1 && arg.ref[0] in sources && (arg.expand || arg.inline)
429
429
  }
430
430
  if (dollarSelfRefs && firstStepIsSelf) {
431
- Object.defineProperty(arg, 'inXpr', { value: true, writable: true })
431
+ defineProperty(arg, 'inXpr', true)
432
432
  dollarSelfRefs.push(arg)
433
433
  return
434
434
  }
@@ -455,7 +455,7 @@ function infer(originalQuery, model) {
455
455
  const nextStep = arg.ref[1]?.id || arg.ref[1]
456
456
  if (isNonForeignKeyNavigation(element, nextStep)) {
457
457
  if (inExists) {
458
- Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
458
+ defineProperty($baseLink, 'pathExpressionInsideFilter', true)
459
459
  } else {
460
460
  rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
461
461
  }
@@ -519,7 +519,7 @@ function infer(originalQuery, model) {
519
519
  const nextStep = arg.ref[i + 1]?.id || arg.ref[i + 1]
520
520
  if (isNonForeignKeyNavigation(element, nextStep)) {
521
521
  if (inExists) {
522
- Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
522
+ defineProperty($baseLink, 'pathExpressionInsideFilter', true)
523
523
  } else {
524
524
  rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
525
525
  }
@@ -535,7 +535,7 @@ function infer(originalQuery, model) {
535
535
  } else if (id === '$dummy') {
536
536
  // `some.known.element.$dummy` -> no error; used by cds.ql to simulate joins
537
537
  arg.$refLinks.push({ definition: { name: '$dummy', parent: arg.$refLinks[i - 1].target } })
538
- Object.defineProperty(arg, 'isJoinRelevant', { value: true })
538
+ defineProperty(arg, 'isJoinRelevant', true)
539
539
  } else {
540
540
  const notFoundIn = pseudoPath ? arg.ref[i - 1] : getFullPathForLinkedArg(arg)
541
541
  stepNotFoundInPredecessor(id, notFoundIn)
@@ -561,7 +561,7 @@ function infer(originalQuery, model) {
561
561
  const definition = arg.$refLinks[i].definition
562
562
  if ((!definition.target && definition.kind !== 'entity') || (!inFrom && danglingFilter))
563
563
  throw new Error('A filter can only be provided when navigating along associations')
564
- if (!inFrom && !arg.expand) Object.defineProperty(arg, 'isJoinRelevant', { value: true })
564
+ if (!inFrom && !arg.expand)defineProperty(arg, 'isJoinRelevant', true)
565
565
  let skipJoinsForFilter = false
566
566
  step.where.forEach(token => {
567
567
  if (token === 'exists') {
@@ -590,7 +590,7 @@ function infer(originalQuery, model) {
590
590
  if (getDefinition(arg.$refLinks[i].definition.target)?.['@cds.persistence.skip'] === true) isPersisted = false
591
591
  if (!arg.ref[i + 1]) {
592
592
  const flatName = nameSegments.join('_')
593
- Object.defineProperty(arg, 'flatName', { value: flatName, writable: true })
593
+ defineProperty(arg, 'flatName', flatName)
594
594
  // if column is casted, we overwrite it's origin with the new type
595
595
  if (arg.cast) {
596
596
  const base = getElementForCast(arg)
@@ -635,7 +635,7 @@ function infer(originalQuery, model) {
635
635
  })
636
636
 
637
637
  // we need inner joins for the path expressions inside filter expressions after exists predicate
638
- if ($baseLink?.pathExpressionInsideFilter) Object.defineProperty(arg, 'join', { value: 'inner' })
638
+ if ($baseLink?.pathExpressionInsideFilter) defineProperty(arg, 'join', 'inner')
639
639
 
640
640
  // ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
641
641
  if (arg.expand) {
@@ -655,7 +655,7 @@ function infer(originalQuery, model) {
655
655
  ? { ref: [...baseColumn.ref, ...arg.ref], $refLinks: [...baseColumn.$refLinks, ...arg.$refLinks] }
656
656
  : arg
657
657
  if (isColumnJoinRelevant(colWithBase)) {
658
- Object.defineProperty(arg, 'isJoinRelevant', { value: true })
658
+ defineProperty(arg, 'isJoinRelevant', true)
659
659
  joinTree.mergeColumn(colWithBase, originalQuery.outerQueries)
660
660
  }
661
661
  }
@@ -761,7 +761,7 @@ function infer(originalQuery, model) {
761
761
  const res = $leafLink.definition.is2one
762
762
  ? new cds.struct({ elements: inferredExpandSubquery.elements })
763
763
  : new cds.array({ items: new cds.struct({ elements: inferredExpandSubquery.elements }) })
764
- return Object.defineProperty(res, '$assocExpand', { value: true })
764
+ return defineProperty(res, '$assocExpand', true)
765
765
  } else if ($leafLink.definition.elements) {
766
766
  let elements = {}
767
767
  expand.forEach(e => {
@@ -900,13 +900,13 @@ function infer(originalQuery, model) {
900
900
  const calcElementIsJoinRelevant = isColumnJoinRelevant(p)
901
901
  if (calcElementIsJoinRelevant) {
902
902
  if (!calcElement.value.isJoinRelevant)
903
- Object.defineProperty(step, 'isJoinRelevant', { value: true, writable: true })
903
+ defineProperty(step, 'isJoinRelevant',true)
904
904
  joinTree.mergeColumn(p, originalQuery.outerQueries)
905
905
  } else {
906
906
  // we need to explicitly set the value to false in this case,
907
907
  // e.g. `SELECT from booksCalc.Books { ID, author.{name }, author {name } }`
908
908
  // --> for the inline column, the name is join relevant, while for the expand, it is not
909
- Object.defineProperty(step, 'isJoinRelevant', { value: false, writable: true })
909
+ defineProperty(step, 'isJoinRelevant', false)
910
910
  }
911
911
  }
912
912
  }
@@ -1064,7 +1064,6 @@ function infer(originalQuery, model) {
1064
1064
  * @returns {object} a copy of @param base with all annotations of @param from
1065
1065
  * @TODO prototype based
1066
1066
  */
1067
- // REVISIT: TODO: inferred.elements should be linked
1068
1067
  function getCopyWithAnnos(from, base) {
1069
1068
  const result = { ...base }
1070
1069
  // REVISIT: we don't need to and hence should not handle annotations at runtime
@@ -1072,7 +1071,7 @@ function infer(originalQuery, model) {
1072
1071
  if (prop.startsWith('@')) result[prop] = from[prop]
1073
1072
  }
1074
1073
 
1075
- if (from.as && base.name !== from.as) Object.defineProperty(result, 'name', { value: from.as }) // TODO double check if this is needed
1074
+ if (from.as && base.name !== from.as) defineProperty(result, 'name', from.as) // TODO double check if this is needed
1076
1075
  // in subqueries we need the linked element if an outer query accesses it
1077
1076
  return Object.setPrototypeOf(result, base)
1078
1077
  }
package/lib/utils.js CHANGED
@@ -62,6 +62,14 @@ function getImplicitAlias(str, useTechnicalAlias = true) {
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
+
65
73
  /**
66
74
  * Shared utility functions which operate dynamically on the model / query.
67
75
  *
@@ -129,5 +137,6 @@ module.exports = {
129
137
  isCalculatedOnRead,
130
138
  isCalculatedElement,
131
139
  getImplicitAlias,
140
+ defineProperty,
132
141
  getModelUtils,
133
142
  }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.20.0",
3
+ "version": "2.0.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": {
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"
@@ -27,7 +27,7 @@
27
27
  "generic-pool": "^3.9.0"
28
28
  },
29
29
  "peerDependencies": {
30
- "@sap/cds": ">=7.9"
30
+ "@sap/cds": ">=9"
31
31
  },
32
- "license": "SEE LICENSE"
32
+ "license": "Apache-2.0"
33
33
  }