@cap-js/db-service 1.19.1 → 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/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
@@ -119,7 +140,7 @@ class CQN2SQLRenderer {
119
140
  * @param {import('./infer/cqn').CREATE} q
120
141
  */
121
142
  CREATE(q) {
122
- let { target } = q
143
+ let { _target: target } = q
123
144
  let query = target?.query || q.CREATE.as
124
145
  if (!target || target._unresolved) {
125
146
  const entity = q.CREATE.entity
@@ -213,7 +234,7 @@ class CQN2SQLRenderer {
213
234
  * @param {import('./infer/cqn').DROP} q
214
235
  */
215
236
  DROP(q) {
216
- const { target } = q
237
+ const { _target: target } = q
217
238
  const isView = target?.query || target?.projection || q.DROP.view
218
239
  const name = target?.name || q.DROP.table?.ref?.[0] || q.DROP.view?.ref?.[0]
219
240
  return (this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} IF EXISTS ${this.quote(this.name(name, 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
  /**
@@ -307,8 +593,7 @@ class CQN2SQLRenderer {
307
593
  }
308
594
  : x => {
309
595
  const name = this.column_name(x)
310
- const escaped = `${name.replace(/"/g, '""')}`
311
- return `'$."${escaped}"',${this.output_converter4(x.element, this.quote(name))}`
596
+ return `${this.string(`$.${JSON.stringify(name)}`)},${this.output_converter4(x.element, this.quote(name))}`
312
597
  }).flat()
313
598
 
314
599
  if (isSimple) return `SELECT ${cols} FROM (${sql})`
@@ -372,6 +657,18 @@ class CQN2SQLRenderer {
372
657
  }
373
658
  if (from.SELECT) return _aliased(`(${this.SELECT(from)})`)
374
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] }
375
672
  }
376
673
 
377
674
  /**
@@ -429,8 +726,8 @@ class CQN2SQLRenderer {
429
726
  return orderBy.map(c => {
430
727
  const o = localized
431
728
  ? this.expr(c) +
432
- (c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
433
- (c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
729
+ (c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
730
+ (c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
434
731
  : this.expr(c) + (c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
435
732
  if (c.nulls) return o + ' NULLS ' + (c.nulls.toLowerCase() === 'first' ? 'FIRST' : 'LAST')
436
733
  return o
@@ -490,7 +787,7 @@ class CQN2SQLRenderer {
490
787
  ? this.INSERT_rows(q)
491
788
  : INSERT.values
492
789
  ? this.INSERT_values(q)
493
- : INSERT.as
790
+ : INSERT.from || INSERT.as
494
791
  ? this.INSERT_select(q)
495
792
  : cds.error`Missing .entries, .rows, or .values in ${q}`
496
793
  }
@@ -524,13 +821,14 @@ class CQN2SQLRenderer {
524
821
  // Include this.values for placeholders
525
822
  /** @type {unknown[][]} */
526
823
  this.entries = []
527
- if (INSERT.entries[0] instanceof Readable) {
824
+ if (INSERT.entries[0] instanceof Readable && !INSERT.entries[0].readableObjectMode) {
528
825
  INSERT.entries[0].type = 'json'
529
826
  this.entries = [[...this.values, INSERT.entries[0]]]
530
827
  } else {
531
- const stream = Readable.from(this.INSERT_entries_stream(INSERT.entries), { objectMode: false })
828
+ const entries = INSERT.entries[0]?.[Symbol.iterator] || INSERT.entries[0]?.[Symbol.asyncIterator] || INSERT.entries[0] instanceof Readable ? INSERT.entries[0] : INSERT.entries
829
+ const stream = Readable.from(this.INSERT_entries_stream(entries), { objectMode: false })
532
830
  stream.type = 'json'
533
- stream._raw = INSERT.entries
831
+ stream._raw = entries
534
832
  this.entries = [[...this.values, stream]]
535
833
  }
536
834
 
@@ -545,7 +843,7 @@ class CQN2SQLRenderer {
545
843
  let buffer = '['
546
844
 
547
845
  let sep = ''
548
- for (const row of entries) {
846
+ for await (const row of entries) {
549
847
  buffer += `${sep}{`
550
848
  if (!sep) sep = ','
551
849
 
@@ -619,7 +917,7 @@ class CQN2SQLRenderer {
619
917
  if (val != null && elements[this.columns[key]]?.type in this.BINARY_TYPES) {
620
918
  val = Buffer.from(val, 'base64').toString(binaryEncoding)
621
919
  }
622
- buffer += `${sepsub}${val === undefined ? 'null' : JSON.stringify(val)}`
920
+ buffer += `${sepsub}${val == null ? 'null' : JSON.stringify(val)}`
623
921
  }
624
922
 
625
923
  if (!sepsub) sepsub = ','
@@ -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)
@@ -1143,8 +1441,8 @@ class CQN2SQLRenderer {
1143
1441
  managed_extract(name, element, converter) {
1144
1442
  const { UPSERT, INSERT } = this.cqn
1145
1443
  const extract = !(INSERT?.entries || UPSERT?.entries) && (INSERT?.rows || UPSERT?.rows)
1146
- ? `value->>'$[${this.columns.indexOf(name)}]'`
1147
- : `value->>'$."${name.replace(/"/g, '""')}"'`
1444
+ ? `value->>${this.string(`$[${this.columns.indexOf(name)}]`)}`
1445
+ : `value->>${this.string(`$.${JSON.stringify(name)}`)}`
1148
1446
  const sql = converter?.(extract) || extract
1149
1447
  return { extract, sql }
1150
1448
  }
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 (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
  }
@@ -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
- Object.defineProperty(correlated, 'elements', {
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
- Object.defineProperty(q, 'outerQueries', { value: outerQueries })
1070
+ defineProperty(q, 'outerQueries', outerQueries)
1072
1071
  }
1073
- const target = cds.infer.target (inferred) // REVISIT: we should reliably use inferred._target instead
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
- Object.defineProperty(q.SELECT.from, 'uniqueSubqueryAlias', { value: uniqueSubqueryAlias })
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
- Object.defineProperty(flatColumn, '_csnPath', { value: csnPath, writable: true })
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
- Object.defineProperty(flatRef, '_csnPath', { value: csnPath, writable: true })
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
- Object.defineProperty(transformedFrom, '$refLinks', { value: [...from.$refLinks], writable: true })
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 = localized(subquerySource)
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
- Object.defineProperty(thing, '$refLinks', {
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(localized(getParentEntity(element.parent)))
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 = assocTarget(nextDefinition) || nextDefinition
2168
- const id = localized(subquerySource)
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
- Object.defineProperty(col, 'element', {
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)