@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 +30 -0
- package/lib/SQLService.js +5 -8
- package/lib/cqn2sql.js +127 -44
- package/lib/cqn4sql.js +314 -34
- package/lib/infer/index.js +84 -32
- package/lib/infer/join-tree.js +8 -6
- package/lib/infer/pseudos.js +12 -11
- package/lib/search.js +1 -1
- package/lib/utils.js +29 -0
- package/package.json +2 -2
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
263
|
+
|
|
261
264
|
let sql = `SELECT`
|
|
262
265
|
if (distinct) sql += ` DISTINCT`
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
|
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.
|
|
350
|
-
if (
|
|
355
|
+
if (element.virtual || element.isAssociation) continue
|
|
356
|
+
if (name in availableComputedColumns) continue
|
|
351
357
|
if (name.toUpperCase() in reservedColumnNames) ref.as = `$$${name}$$`
|
|
352
|
-
|
|
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 =>
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1024
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
1199
|
+
let sql = `UPDATE ${this.quote(this.table_name(q))}`
|
|
1138
1200
|
if (entity.as) sql += ` AS ${this.quote(entity.as)}`
|
|
1139
1201
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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[
|
|
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) =>
|
|
1155
|
-
|
|
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.
|
|
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: {
|
|
1171
|
-
let sql = `DELETE FROM ${this.
|
|
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
|
|
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)
|