@cap-js/db-service 2.8.1 → 2.9.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,36 @@
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.9.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.8.2...db-service-v2.9.0) (2026-03-09)
8
+
9
+
10
+ ### Added
11
+
12
+ * runtime views ([#1410](https://github.com/cap-js/cds-dbs/issues/1410)) ([5242675](https://github.com/cap-js/cds-dbs/commit/5242675c97472b86b81b3dc5fe0906141d276b02))
13
+ * support calculated elements in hierarchies ([#1456](https://github.com/cap-js/cds-dbs/issues/1456)) ([97c6f66](https://github.com/cap-js/cds-dbs/commit/97c6f6661f0ac4043245e021f2bf182f4e5d406f))
14
+
15
+
16
+ ### Fixed
17
+
18
+ * **`exists`:** detect join relevant path after exists ([#1412](https://github.com/cap-js/cds-dbs/issues/1412)) ([c5bad06](https://github.com/cap-js/cds-dbs/commit/c5bad06724ce6761379f91748490c6caac84153a)), closes [#1407](https://github.com/cap-js/cds-dbs/issues/1407)
19
+ * **cqn2sql:** Relied on inconstistent behavior of cds.ql.cloned queries ([#1500](https://github.com/cap-js/cds-dbs/issues/1500)) ([f9cb201](https://github.com/cap-js/cds-dbs/commit/f9cb2011219a86ae22f22fcc105e597b23209adf))
20
+ * enable expressions for `inline` ([#1512](https://github.com/cap-js/cds-dbs/issues/1512)) ([65f78e1](https://github.com/cap-js/cds-dbs/commit/65f78e1f3af83188462e9d44db67daa5d743ceb0))
21
+ * path expressions for scoped queries ([#1507](https://github.com/cap-js/cds-dbs/issues/1507)) ([0f1e234](https://github.com/cap-js/cds-dbs/commit/0f1e234b373f26a6244c715c9ca9d4a207a0faed))
22
+ * reject duplicated wildcards ([#1511](https://github.com/cap-js/cds-dbs/issues/1511)) ([b483062](https://github.com/cap-js/cds-dbs/commit/b483062e2ff5a8d0960dc2e7b71880af87ee8f78))
23
+ * the combination of `iterator` and `SELECT.one` ([#1514](https://github.com/cap-js/cds-dbs/issues/1514)) ([4b28579](https://github.com/cap-js/cds-dbs/commit/4b2857920a7a57bcfc09a9b5fb765283cf8bd70b))
24
+ * wildcard on inlined assoc ([#1513](https://github.com/cap-js/cds-dbs/issues/1513)) ([e520b97](https://github.com/cap-js/cds-dbs/commit/e520b97fd30394825b937b3613370c32c36c24a4))
25
+
26
+ ## [2.8.2](https://github.com/cap-js/cds-dbs/compare/db-service-v2.8.1...db-service-v2.8.2) (2026-02-03)
27
+
28
+
29
+ ### Fixed
30
+
31
+ * compare conversion for right hand `null` transformation ([#1469](https://github.com/cap-js/cds-dbs/issues/1469)) ([ec1d0c6](https://github.com/cap-js/cds-dbs/commit/ec1d0c6fa08db1f75e9b72eed382a507b49815cc))
32
+ * **cqn4sql:** calculated elements with function expr in from ([#1452](https://github.com/cap-js/cds-dbs/issues/1452)) ([970407e](https://github.com/cap-js/cds-dbs/commit/970407e29e4c98ee9c25d15277dff80c246b9523))
33
+ * hierarchy with $top ([#1460](https://github.com/cap-js/cds-dbs/issues/1460)) ([dfc6226](https://github.com/cap-js/cds-dbs/commit/dfc62261681ced388e9c35aa8ce3e49e1c09f4e2))
34
+ * search aggregate functions ([#1463](https://github.com/cap-js/cds-dbs/issues/1463)) ([a8db1f3](https://github.com/cap-js/cds-dbs/commit/a8db1f38e219bd7818c3cfc9f45e108bcab1dd95))
35
+ * support all types for casting in queries ([#1481](https://github.com/cap-js/cds-dbs/issues/1481)) ([8392232](https://github.com/cap-js/cds-dbs/commit/8392232aafdcfa025a7dce597bf65fb6344acd1f))
36
+
7
37
  ## [2.8.1](https://github.com/cap-js/cds-dbs/compare/db-service-v2.8.0...db-service-v2.8.1) (2025-12-19)
8
38
 
9
39
 
package/lib/SQLService.js CHANGED
@@ -2,9 +2,9 @@ const cds = require('@sap/cds'),
2
2
  DEBUG = cds.debug('sql|db')
3
3
  const { Readable, Transform } = require('stream')
4
4
  const { pipeline } = require('stream/promises')
5
- const { resolveView, getDBTable, getTransition } = require('@sap/cds/libx/_runtime/common/utils/resolveView')
6
5
  const DatabaseService = require('./common/DatabaseService')
7
6
  const cqn4sql = require('./cqn4sql')
7
+ const { resolveTable } = require('./utils')
8
8
 
9
9
  const BINARY_TYPES = {
10
10
  'cds.Binary': 1,
@@ -168,7 +168,7 @@ class SQLService extends DatabaseService {
168
168
  return SQLService._arrayWithCount(rows, await this.count(query, rows))
169
169
  }
170
170
 
171
- return iterator !== false && isOne ? rows[0] : rows
171
+ return !iterator && isOne ? rows[0] : rows
172
172
  } catch (err) {
173
173
  // Ensure that iterators receive pre stream errors
174
174
  if (iterator) rows.emit('error', err)
@@ -230,7 +230,8 @@ class SQLService extends DatabaseService {
230
230
  // REVISIT: It's not yet 100 % clear under which circumstances we can rely on db constraints
231
231
  return (super.onDELETE = /* cds.env.features.assert_integrity === 'db' ? this.onSIMPLE : */ deep_delete)
232
232
  async function deep_delete(/** @type {Request} */ req) {
233
- const transitions = getTransition(req.target, this, false, req.query.cmd || 'DELETE')
233
+ const resolve = this.resolve
234
+ const transitions = resolve.transitions(req.query)
234
235
  if (transitions.target !== transitions.queryTarget) {
235
236
  const keys = []
236
237
  const transitionsTarget = transitions.queryTarget.keys || transitions.queryTarget.elements
@@ -253,7 +254,7 @@ class SQLService extends DatabaseService {
253
254
  })
254
255
  return this.onDELETE({ query, target: transitions.target })
255
256
  }
256
- const table = getDBTable(req.target)
257
+ const table = resolveTable(req.target)
257
258
  const { compositions } = table
258
259
  if (compositions) {
259
260
  // Transform CQL`DELETE from Foo[p1] WHERE p2` into CQL`DELETE from Foo[p1 and p2]`
@@ -404,10 +405,6 @@ class SQLService extends DatabaseService {
404
405
  */
405
406
  cqn2sql(query, values) {
406
407
  let q = this.cqn4sql(query)
407
- let kind = q.kind || Object.keys(q)[0]
408
- if (kind in { INSERT: 1, DELETE: 1, UPSERT: 1, UPDATE: 1 }) {
409
- q = resolveView(q, this.model, this) // REVISIT: before resolveView was called on flat cqn obtained from cqn4sql -> is it correct to call on original q instead?
410
- }
411
408
  let cqn2sql = new this.class.CQN2SQL(this)
412
409
  return cqn2sql.render(q, values)
413
410
  }
package/lib/cqn2sql.js CHANGED
@@ -1,6 +1,8 @@
1
1
  const cds = require('@sap/cds')
2
2
  const cds_infer = require('./infer')
3
3
  const cqn4sql = require('./cqn4sql')
4
+ const { resolveTable } = require('./utils')
5
+
4
6
  const _simple_queries = cds.env.features.sql_simple_queries
5
7
  const _strict_booleans = _simple_queries < 2
6
8
 
@@ -26,7 +28,8 @@ class CQN2SQLRenderer {
26
28
  if (cds.env.sql.names === 'quoted') {
27
29
  this.class.prototype.name = (name, query) => {
28
30
  const e = name.id || name
29
- return (query?._target || this.model?.definitions[e])?.['@cds.persistence.name'] || e
31
+ const entity = query?._target || this.model?.definitions[e]
32
+ return (!entity?.['@cds.persistence.skip'] && entity?.['@cds.persistence.name']) || e
30
33
  }
31
34
  this.class.prototype.quote = (s) => `"${String(s).replace(/"/g, '""')}"`
32
35
  }
@@ -76,6 +79,7 @@ class CQN2SQLRenderer {
76
79
  */
77
80
  render(q, vars) {
78
81
  const kind = q.kind || Object.keys(q)[0] // SELECT, INSERT, ...
82
+ if (q._with) this._with = q._with
79
83
  /**
80
84
  * @type {string} the rendered SQL string
81
85
  */
@@ -90,7 +94,6 @@ class CQN2SQLRenderer {
90
94
  if (vars && Object.keys(vars).length && !this.values?.length) this.values = vars
91
95
  const sanitize_values = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
92
96
 
93
-
94
97
  if (DEBUG && (LOG_SQL._debug || LOG_SQLITE._debug)) {
95
98
  let values = sanitize_values && (this.entries || this.values?.length > 0) ? ['***'] : this.entries || this.values || []
96
99
  if (values && !Array.isArray(values)) {
@@ -257,13 +260,15 @@ class CQN2SQLRenderer {
257
260
 
258
261
  // REVISIT: When selecting from an entity that is not in the model the from.where are not normalized (as cqn4sql is skipped)
259
262
  if (!where && from?.ref?.length === 1 && from.ref[0]?.where) where = from.ref[0]?.where
260
- const columns = this.SELECT_columns(q)
263
+
261
264
  let sql = `SELECT`
262
265
  if (distinct) sql += ` DISTINCT`
263
- if (!_empty(columns)) sql += ` ${columns}`
264
- if (recurse) sql += ` FROM ${this.SELECT_recurse(q)}`
265
- else if (!_empty(from)) sql += ` FROM ${this.from(from, q)}`
266
- else sql += this.from_dummy()
266
+ if (recurse) sql += this.SELECT_recurse(q)
267
+ else {
268
+ sql += ` ${this.SELECT_columns(q)}`
269
+ if (!_empty(from)) sql += ` FROM ${this.from(from, q)}`
270
+ else sql += this.from_dummy()
271
+ }
267
272
  if (!recurse && !_empty(where)) sql += ` WHERE ${this.where(where)}`
268
273
  if (!recurse && !_empty(groupBy)) sql += ` GROUP BY ${this.groupBy(groupBy)}`
269
274
  if (!recurse && !_empty(having)) sql += ` HAVING ${this.having(having)}`
@@ -296,8 +301,9 @@ class CQN2SQLRenderer {
296
301
 
297
302
  // `where` needs to be wrapped to also support `where == ['exists', { SELECT }]` which is not allowed in `START WHERE`
298
303
  const clone = q.clone()
299
- clone.columns(keys)
304
+ clone.SELECT.columns = keys
300
305
  clone.SELECT.recurse = undefined
306
+ clone.SELECT.limit = undefined
301
307
  clone.SELECT.expand = undefined // omits JSON
302
308
  where = [{ list: keys }, 'in', clone]
303
309
  }
@@ -346,10 +352,16 @@ class CQN2SQLRenderer {
346
352
  for (const name in target.elements) {
347
353
  const ref = { ref: [name] }
348
354
  const element = target.elements[name]
349
- if (element.virtual || element.value || element.isAssociation) continue
350
- if (element['@Core.Computed'] && name in availableComputedColumns) continue
355
+ if (element.virtual || element.isAssociation) continue
356
+ if (name in availableComputedColumns) continue
351
357
  if (name.toUpperCase() in reservedColumnNames) ref.as = `$$${name}$$`
352
- columnsIn.push(ref)
358
+ // This only supports calculated elements within the scope of the own entity
359
+ if ('value' in element) {
360
+ const requested = columnsFiltered.find(c => this.column_name(c) === element.name)
361
+ if (requested) columnsIn.push(requested)
362
+ else continue
363
+ }
364
+ else columnsIn.push(ref)
353
365
  const foreignkey4 = element._foreignKey4
354
366
  if (
355
367
  from.args ||
@@ -379,7 +391,7 @@ class CQN2SQLRenderer {
379
391
  )
380
392
 
381
393
  if (orderBy) {
382
- orderBy = orderBy.map(r => {
394
+ orderBy = orderBy.filter(o => o.ref).map(r => {
383
395
  let col = r.ref.at(-1)
384
396
  if (col.toUpperCase() in reservedColumnNames) col = `$$${col}$$`
385
397
  if (!columnsIn.find(c => this.column_name(c) === col)) {
@@ -496,13 +508,19 @@ class CQN2SQLRenderer {
496
508
  }
497
509
  }
498
510
 
511
+ const columnsQuery = cds.ql(q).clone()
512
+ columnsQuery.SELECT.columns = columns.map(x => {
513
+ if (x.element && 'value' in x.element) return { element: x.element, ref: [this.column_name(x)] }
514
+ return x
515
+ })
516
+ const recurseColumns = this.SELECT_columns(columnsQuery)
499
517
  // Only apply result join if the columns contain a references which doesn't start with the source alias
500
518
  if (from.args && columns.find(c => c.ref?.[0] === alias)) {
501
519
  graph.as = alias
502
- return this.from(setStableFrom(from, graph))
520
+ return ` ${recurseColumns} FROM ${this.from(setStableFrom(from, graph))}`
503
521
  }
504
522
 
505
- return `(${this.SELECT(graph)})${alias ? ` AS ${this.quote(alias)}` : ''} `
523
+ return ` ${recurseColumns} FROM (${this.SELECT(graph)})${alias ? ` AS ${this.quote(alias)}` : ''} `
506
524
 
507
525
  function collectDistanceTo(where, innot = false) {
508
526
  for (let i = 0; i < where.length; i++) {
@@ -729,6 +747,38 @@ class CQN2SQLRenderer {
729
747
  return this.xpr({ xpr })
730
748
  }
731
749
 
750
+ /**
751
+ * Renders a transformed where clause that maps the query target view to the source table
752
+ * @param {import('./infer/cqn').source} alias
753
+ * @param {import('./infer/cqn').predicate} where
754
+ * @param {import('./infer/cqn').query} q
755
+ * @returns SQL
756
+ */
757
+ where_resolved(alias, where, q) {
758
+ const transitions = this.srv.resolve.transitions(q)
759
+ if (transitions.target === transitions.queryTarget) return this.where(where)
760
+
761
+ // view and table column refs to be matched
762
+ const viewCols = []
763
+ const tableCols = []
764
+
765
+ // Only match key columns when possible
766
+ const elements = q._target.keys || q._target.elements
767
+ for (const c in elements) {
768
+ if (
769
+ c in elements
770
+ && transitions.mapping.has(c)
771
+ && this.physical_column(elements, c)
772
+ ) {
773
+ viewCols.push({ ref: [c] })
774
+ tableCols.push(transitions.mapping.get(c))
775
+ }
776
+ }
777
+ return tableCols.length > 0
778
+ ? this.where([{ list: tableCols }, 'in', SELECT.from(q._target).alias(alias).columns(viewCols).where(where)])
779
+ : this.where(where)
780
+ }
781
+
732
782
  /**
733
783
  * Renders a HAVING clause into generic SQL
734
784
  * @param {import('./infer/cqn').predicate} xpr
@@ -834,15 +884,20 @@ class CQN2SQLRenderer {
834
884
  if (!elements && !INSERT.entries?.length) {
835
885
  return // REVISIT: mtx sends an insert statement without entries and no reference entity
836
886
  }
887
+ const transitions = this.srv.resolve.transitions(q)
837
888
  const columns = elements
838
- ? ObjectKeys(elements).filter(c => c in elements && !elements[c].virtual && !elements[c].value && !elements[c].isAssociation)
889
+ ? ObjectKeys(elements).filter(c => this.physical_column(elements, c)
890
+ && (c = transitions.mapping.get(c)?.ref?.[0] || c)
891
+ && c in transitions.target.elements
892
+ && this.physical_column(transitions.target.elements, c)
893
+ )
839
894
  : ObjectKeys(INSERT.entries[0])
840
895
 
841
896
  /** @type {string[]} */
842
897
  this.columns = columns
843
898
 
844
899
  const alias = INSERT.into.as
845
- const entity = this.name(q._target?.name || INSERT.into.ref[0], q)
900
+ const entity = q._target ? this.table_name(q) : INSERT.into.ref[0]
846
901
  if (!elements) {
847
902
  this.entries = INSERT.entries.map(e => columns.map(c => e[c]))
848
903
  const param = this.param.bind(this, { ref: ['?'] })
@@ -864,8 +919,8 @@ class CQN2SQLRenderer {
864
919
  }
865
920
 
866
921
  const extractions = this._managed = this.managed(columns.map(c => ({ name: c })), elements)
867
- return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))
868
- }) SELECT ${extractions.map(c => c.insert)} FROM json_each(?)`)
922
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c))
923
+ }) SELECT ${extractions.slice(0, columns.length).map(c => c.insert)} FROM json_each(?)`)
869
924
  }
870
925
 
871
926
  async *INSERT_entries_stream(entries, binaryEncoding = 'base64') {
@@ -971,7 +1026,7 @@ class CQN2SQLRenderer {
971
1026
  */
972
1027
  INSERT_rows(q) {
973
1028
  const { INSERT } = q
974
- const entity = this.name(q._target?.name || INSERT.into.ref[0], q)
1029
+ const entity = q._target ? this.table_name(q) : INSERT.into.ref[0]
975
1030
  const alias = INSERT.into.as
976
1031
  const elements = q.elements || q._target?.elements
977
1032
  const columns = this.columns = INSERT.columns || cds.error`Cannot insert rows without columns or elements`
@@ -996,7 +1051,8 @@ class CQN2SQLRenderer {
996
1051
  .slice(0, columns.length)
997
1052
  .map(c => c.converter(c.extract))
998
1053
 
999
- return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))
1054
+ const transitions = this.srv.resolve.transitions(q)
1055
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c))
1000
1056
  }) SELECT ${extraction} FROM json_each(?)`)
1001
1057
  }
1002
1058
 
@@ -1017,20 +1073,24 @@ class CQN2SQLRenderer {
1017
1073
  */
1018
1074
  INSERT_select(q) {
1019
1075
  const { INSERT } = q
1020
- const entity = this.name(q._target.name, q)
1076
+ const entity = q._target ? this.table_name(q) : INSERT.into.ref[0]
1021
1077
  const alias = INSERT.into.as
1078
+ const src = this.cqn4sql(INSERT.from)
1022
1079
  const elements = q.elements || q._target?.elements || {}
1023
- let columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
1024
- c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
1025
- ))
1080
+ const transitions = this.srv.resolve.transitions(q)
1081
+ let columns = (this.columns = (INSERT.columns || src.SELECT.columns?.map(c => this.column_name(c)) || ObjectKeys(src.elements) || ObjectKeys(elements))
1082
+ .filter(c => this.physical_column(elements, c)
1083
+ && (c = transitions.mapping.get(c)?.ref?.[0] || c)
1084
+ && c in transitions.target.elements
1085
+ && this.physical_column(transitions.target.elements, c)
1086
+ ))
1026
1087
 
1027
- const src = this.cqn4sql(INSERT.from)
1028
1088
  const extractions = this._managed = this.managed(columns.map(c => ({ name: c, sql: `NEW.${this.quote(c)}` })), elements)
1029
1089
  const sql = extractions.length > columns.length
1030
1090
  ? `SELECT ${extractions.map(c => `${c.insert} AS ${this.quote(c.name)}`)} FROM (${this.SELECT(src)}) AS NEW`
1031
1091
  : this.SELECT(src)
1032
1092
  if (extractions.length > columns.length) columns = this.columns = extractions.map(c => c.name)
1033
- this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns.map(c => this.quote(c))}) ${sql}`
1093
+ this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c))}) ${sql}`
1034
1094
  this.entries = [this.values]
1035
1095
  return this.sql
1036
1096
  }
@@ -1084,7 +1144,7 @@ class CQN2SQLRenderer {
1084
1144
  .join(' AND ')
1085
1145
 
1086
1146
  let columns = this.columns // this.columns is computed as part of this.INSERT
1087
- const entity = this.name(q._target?.name || UPSERT.into.ref[0], q)
1147
+ const entity = q._target ? this.table_name(q) : this.name(UPSERT.into.ref[0], q)
1088
1148
  if (UPSERT.entries || UPSERT.rows || UPSERT.values) {
1089
1149
  const managed = this._managed.slice(0, columns.length)
1090
1150
 
@@ -1120,7 +1180,8 @@ class CQN2SQLRenderer {
1120
1180
  else return true
1121
1181
  }).map(c => `${this.quote(c)} = excluded.${this.quote(c)}`)
1122
1182
 
1123
- return (this.sql = `INSERT INTO ${this.quote(entity)} (${columns.map(c => this.quote(c))}) ${sql
1183
+ const transitions = this.srv.resolve.transitions(q)
1184
+ return (this.sql = `INSERT INTO ${this.quote(entity)} (${columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c))}) ${sql
1124
1185
  } WHERE TRUE ON CONFLICT(${keys.map(c => this.quote(c))}) DO ${updateColumns.length ? `UPDATE SET ${updateColumns}` : 'NOTHING'}`)
1125
1186
  }
1126
1187
 
@@ -1133,29 +1194,36 @@ class CQN2SQLRenderer {
1133
1194
  */
1134
1195
  UPDATE(q) {
1135
1196
  const { entity, with: _with, data, where } = q.UPDATE
1197
+ const transitions = this.srv.resolve.transitions(q)
1136
1198
  const elements = q._target?.elements
1137
- let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity, q))}`
1199
+ let sql = `UPDATE ${this.quote(this.table_name(q))}`
1138
1200
  if (entity.as) sql += ` AS ${this.quote(entity.as)}`
1139
1201
 
1140
- let columns = []
1141
- if (data) _add(data, val => this.val({ val }))
1142
- if (_with) _add(_with, x => this.expr(x))
1143
- function _add(data, sql4) {
1144
- for (let c in data) {
1145
- const columnExistsInDatabase =
1146
- elements && c in elements && !elements[c].virtual && !elements[c].isAssociation && !elements[c].value
1202
+ const _add = (data, sql4) => {
1203
+ for (let col in data) {
1204
+ const c = transitions.mapping.get(col)?.ref?.[0] || col
1205
+ const columnExistsInDatabase = elements
1206
+ && this.physical_column(elements, col)
1207
+ && c in transitions.target.elements
1208
+ && this.physical_column(transitions.target.elements, c)
1147
1209
  if (!elements || columnExistsInDatabase) {
1148
- columns.push({ name: c, sql: sql4(data[c]) })
1210
+ columns.push({ name: c, sql: sql4(data[col], col) })
1149
1211
  }
1150
1212
  }
1151
1213
  }
1152
1214
 
1215
+ let columns = []
1216
+ if (data) _add(data, val => this.val({ val }))
1217
+ if (_with) _add(_with, x => this.expr(x))
1218
+
1153
1219
  const extraction = this.managed(columns, elements)
1154
- .filter((c, i) => columns[i] || c.onUpdate)
1155
- .map((c, i) => `${this.quote(c.name)}=${!columns[i] ? c.onUpdate : c.sql}`)
1220
+ .filter((c, i) => {
1221
+ if (transitions.mapping.get(c.name)?.ref?.length > 1) return false
1222
+ return columns[i] || c.onUpdate
1223
+ }).map((c, i) => `${this.quote(transitions.mapping.get(c.name)?.ref?.[0] || c.name)}=${!columns[i] ? c.onUpdate : c.sql}`)
1156
1224
 
1157
1225
  sql += ` SET ${extraction}`
1158
- if (where) sql += ` WHERE ${this.where(where)}`
1226
+ if (where) sql += ` WHERE ${this.where_resolved(entity.as, where, q)}`
1159
1227
  return (this.sql = sql)
1160
1228
  }
1161
1229
 
@@ -1167,8 +1235,9 @@ class CQN2SQLRenderer {
1167
1235
  * @returns {string} SQL
1168
1236
  */
1169
1237
  DELETE(q) {
1170
- const { DELETE: { from, where } } = q
1171
- let sql = `DELETE FROM ${this.from(from, q)}`
1238
+ const { DELETE: { where, from } } = q
1239
+ let sql = `DELETE FROM ${this.quote(this.table_name(q))}`
1240
+ if (from.as) sql += ` AS ${this.quote(from.as)}`
1172
1241
  if (where) sql += ` WHERE ${this.where(where)}`
1173
1242
  return (this.sql = sql)
1174
1243
  }
@@ -1224,7 +1293,7 @@ class CQN2SQLRenderer {
1224
1293
  ? _inline_null(xpr[i + 1]) || 'is'
1225
1294
  : '='
1226
1295
 
1227
- // Translate == to IS NOT NULL for rhs operand being NULL literal, otherwise ...
1296
+ // Translate == to IS NULL for rhs operand being NULL literal, otherwise ...
1228
1297
  // Translate == to IS NOT DISTINCT FROM, unless both operands cannot be NULL
1229
1298
  if (x === '==') return xpr[i + 1]?.val === null
1230
1299
  ? _inline_null(xpr[i + 1]) || 'is'
@@ -1232,7 +1301,7 @@ class CQN2SQLRenderer {
1232
1301
  ? '='
1233
1302
  : this.is_not_distinct_from_
1234
1303
 
1235
- // Translate != to IS NULL for rhs operand being NULL literal, otherwise...
1304
+ // Translate != to IS NOT NULL for rhs operand being NULL literal, otherwise...
1236
1305
  // Translate != to IS DISTINCT FROM, unless both operands cannot be NULL
1237
1306
  if (x === '!=') return xpr[i + 1]?.val === null
1238
1307
  ? _inline_null(xpr[i + 1]) || 'is not'
@@ -1381,6 +1450,16 @@ class CQN2SQLRenderer {
1381
1450
  return (typeof col.as === 'string' && col.as) || ('val' in col && col.val + '') || col.func || col.ref.at(-1)
1382
1451
  }
1383
1452
 
1453
+ /**
1454
+ * Calculates the Database table name of the query target
1455
+ * @param {import('./infer/cqn').Query} query
1456
+ * @returns {string} Database table name
1457
+ */
1458
+ table_name(q) {
1459
+ const table = resolveTable(q._target)
1460
+ return this.name(table.name, { _target: table })
1461
+ }
1462
+
1384
1463
  /**
1385
1464
  * Calculates the Database name of the given name
1386
1465
  * @param {string|import('./infer/cqn').ref} name
@@ -1488,6 +1567,10 @@ class CQN2SQLRenderer {
1488
1567
  })
1489
1568
  }
1490
1569
 
1570
+ physical_column(elements, c) {
1571
+ return elements[c] && !elements[c].virtual && !elements[c].value && !elements[c].isAssociation
1572
+ }
1573
+
1491
1574
  managed_extract(name, element, converter) {
1492
1575
  const { UPSERT, INSERT } = this.cqn
1493
1576
  const extract = !(INSERT?.entries || UPSERT?.entries) && (INSERT?.rows || UPSERT?.rows)