Skip to content

Commit 756d43d

Browse files
Merge branch 'main' into patrice/pg
2 parents 58ff0cf + 69636b6 commit 756d43d

22 files changed

Lines changed: 1516 additions & 108 deletions

.github/workflows/release-please.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
environment: npm
1515
steps:
1616
# v4.4.0
17-
- uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38
17+
- uses: googleapis/release-please-action@8bb7a2ed0f90c9802c83129a9488d235a1f31a7c
1818
id: release
1919
with:
2020
token: ${{secrets.CDS_DBS_TOKEN}}

db-service/lib/SQLService.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ const cds = require('@sap/cds'),
22
DEBUG = cds.debug('sql|db')
33
const { Readable, Transform } = require('stream')
44
const { pipeline } = require('stream/promises')
5-
const { resolveView, getDBTable, getTransition } = require('@sap/cds/libx/_runtime/common/utils/resolveView')
65
const DatabaseService = require('./common/DatabaseService')
76
const cqn4sql = require('./cqn4sql')
87

@@ -230,7 +229,8 @@ class SQLService extends DatabaseService {
230229
// REVISIT: It's not yet 100 % clear under which circumstances we can rely on db constraints
231230
return (super.onDELETE = /* cds.env.features.assert_integrity === 'db' ? this.onSIMPLE : */ deep_delete)
232231
async function deep_delete(/** @type {Request} */ req) {
233-
const transitions = getTransition(req.target, this, false, req.query.cmd || 'DELETE')
232+
const resolve = this.resolve
233+
const transitions = resolve.transitions4db(req.query, false)
234234
if (transitions.target !== transitions.queryTarget) {
235235
const keys = []
236236
const transitionsTarget = transitions.queryTarget.keys || transitions.queryTarget.elements
@@ -253,7 +253,7 @@ class SQLService extends DatabaseService {
253253
})
254254
return this.onDELETE({ query, target: transitions.target })
255255
}
256-
const table = getDBTable(req.target)
256+
const table = resolve.table(req.target)
257257
const { compositions } = table
258258
if (compositions) {
259259
// Transform CQL`DELETE from Foo[p1] WHERE p2` into CQL`DELETE from Foo[p1 and p2]`
@@ -404,10 +404,6 @@ class SQLService extends DatabaseService {
404404
*/
405405
cqn2sql(query, values) {
406406
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-
}
411407
let cqn2sql = new this.class.CQN2SQL(this)
412408
return cqn2sql.render(q, values)
413409
}

db-service/lib/cqn2sql.js

Lines changed: 121 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const cds = require('@sap/cds')
22
const cds_infer = require('./infer')
33
const cqn4sql = require('./cqn4sql')
4+
45
const _simple_queries = cds.env.features.sql_simple_queries
56
const _strict_booleans = _simple_queries < 2
67

@@ -26,7 +27,8 @@ class CQN2SQLRenderer {
2627
if (cds.env.sql.names === 'quoted') {
2728
this.class.prototype.name = (name, query) => {
2829
const e = name.id || name
29-
return (query?._target || this.model?.definitions[e])?.['@cds.persistence.name'] || e
30+
const entity = query?._target || this.model?.definitions[e]
31+
return (!entity?.['@cds.persistence.skip'] && entity?.['@cds.persistence.name']) || e
3032
}
3133
this.class.prototype.quote = (s) => `"${String(s).replace(/"/g, '""')}"`
3234
}
@@ -76,6 +78,7 @@ class CQN2SQLRenderer {
7678
*/
7779
render(q, vars) {
7880
const kind = q.kind || Object.keys(q)[0] // SELECT, INSERT, ...
81+
if (q._with) this._with = q._with
7982
/**
8083
* @type {string} the rendered SQL string
8184
*/
@@ -90,7 +93,6 @@ class CQN2SQLRenderer {
9093
if (vars && Object.keys(vars).length && !this.values?.length) this.values = vars
9194
const sanitize_values = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
9295

93-
9496
if (DEBUG && (LOG_SQL._debug || LOG_SQLITE._debug)) {
9597
let values = sanitize_values && (this.entries || this.values?.length > 0) ? ['***'] : this.entries || this.values || []
9698
if (values && !Array.isArray(values)) {
@@ -257,13 +259,15 @@ class CQN2SQLRenderer {
257259

258260
// REVISIT: When selecting from an entity that is not in the model the from.where are not normalized (as cqn4sql is skipped)
259261
if (!where && from?.ref?.length === 1 && from.ref[0]?.where) where = from.ref[0]?.where
260-
const columns = this.SELECT_columns(q)
262+
261263
let sql = `SELECT`
262264
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()
265+
if (recurse) sql += this.SELECT_recurse(q)
266+
else {
267+
sql += ` ${this.SELECT_columns(q)}`
268+
if (!_empty(from)) sql += ` FROM ${this.from(from, q)}`
269+
else sql += this.from_dummy()
270+
}
267271
if (!recurse && !_empty(where)) sql += ` WHERE ${this.where(where)}`
268272
if (!recurse && !_empty(groupBy)) sql += ` GROUP BY ${this.groupBy(groupBy)}`
269273
if (!recurse && !_empty(having)) sql += ` HAVING ${this.having(having)}`
@@ -347,10 +351,16 @@ class CQN2SQLRenderer {
347351
for (const name in target.elements) {
348352
const ref = { ref: [name] }
349353
const element = target.elements[name]
350-
if (element.virtual || element.value || element.isAssociation) continue
351-
if (element['@Core.Computed'] && name in availableComputedColumns) continue
354+
if (element.virtual || element.isAssociation) continue
355+
if (name in availableComputedColumns) continue
352356
if (name.toUpperCase() in reservedColumnNames) ref.as = `$$${name}$$`
353-
columnsIn.push(ref)
357+
// This only supports calculated elements within the scope of the own entity
358+
if ('value' in element) {
359+
const requested = columnsFiltered.find(c => this.column_name(c) === element.name)
360+
if (requested) columnsIn.push(requested)
361+
else continue
362+
}
363+
else columnsIn.push(ref)
354364
const foreignkey4 = element._foreignKey4
355365
if (
356366
from.args ||
@@ -497,13 +507,19 @@ class CQN2SQLRenderer {
497507
}
498508
}
499509

510+
const columnsQuery = cds.ql(q).clone()
511+
columnsQuery.SELECT.columns = columns.map(x => {
512+
if (x.element && 'value' in x.element) return { element: x.element, ref: [this.column_name(x)] }
513+
return x
514+
})
515+
const recurseColumns = this.SELECT_columns(columnsQuery)
500516
// Only apply result join if the columns contain a references which doesn't start with the source alias
501517
if (from.args && columns.find(c => c.ref?.[0] === alias)) {
502518
graph.as = alias
503-
return this.from(setStableFrom(from, graph))
519+
return ` ${recurseColumns} FROM ${this.from(setStableFrom(from, graph))}`
504520
}
505521

506-
return `(${this.SELECT(graph)})${alias ? ` AS ${this.quote(alias)}` : ''} `
522+
return ` ${recurseColumns} FROM (${this.SELECT(graph)})${alias ? ` AS ${this.quote(alias)}` : ''} `
507523

508524
function collectDistanceTo(where, innot = false) {
509525
for (let i = 0; i < where.length; i++) {
@@ -730,6 +746,38 @@ class CQN2SQLRenderer {
730746
return this.xpr({ xpr })
731747
}
732748

749+
/**
750+
* Renders a transformed where clause that maps the query target view to the source table
751+
* @param {import('./infer/cqn').source} alias
752+
* @param {import('./infer/cqn').predicate} where
753+
* @param {import('./infer/cqn').query} q
754+
* @returns SQL
755+
*/
756+
where_resolved(alias, where, q) {
757+
const transitions = this.srv.resolve.transitions4db(q)
758+
if (transitions.target === transitions.queryTarget) return this.where(where)
759+
760+
// view and table column refs to be matched
761+
const viewCols = []
762+
const tableCols = []
763+
764+
// Only match key columns when possible
765+
const elements = q._target.keys || q._target.elements
766+
for (const c in elements) {
767+
if (
768+
c in elements
769+
&& transitions.mapping.has(c)
770+
&& this.physical_column(elements, c)
771+
) {
772+
viewCols.push({ ref: [c] })
773+
tableCols.push(transitions.mapping.get(c))
774+
}
775+
}
776+
return tableCols.length > 0
777+
? this.where([{ list: tableCols }, 'in', SELECT.from(q._target).alias(alias).columns(viewCols).where(where)])
778+
: this.where(where)
779+
}
780+
733781
/**
734782
* Renders a HAVING clause into generic SQL
735783
* @param {import('./infer/cqn').predicate} xpr
@@ -835,15 +883,20 @@ class CQN2SQLRenderer {
835883
if (!elements && !INSERT.entries?.length) {
836884
return // REVISIT: mtx sends an insert statement without entries and no reference entity
837885
}
886+
const transitions = this.srv.resolve.transitions4db(q)
838887
const columns = elements
839-
? ObjectKeys(elements).filter(c => c in elements && !elements[c].virtual && !elements[c].value && !elements[c].isAssociation)
888+
? ObjectKeys(elements).filter(c => this.physical_column(elements, c)
889+
&& (c = transitions.mapping.get(c)?.ref?.[0] || c)
890+
&& c in transitions.target.elements
891+
&& this.physical_column(transitions.target.elements, c)
892+
)
840893
: ObjectKeys(INSERT.entries[0])
841894

842895
/** @type {string[]} */
843896
this.columns = columns
844897

845898
const alias = INSERT.into.as
846-
const entity = this.name(q._target?.name || INSERT.into.ref[0], q)
899+
const entity = q._target ? this.table_name(q) : INSERT.into.ref[0]
847900
if (!elements) {
848901
this.entries = INSERT.entries.map(e => columns.map(c => e[c]))
849902
const param = this.param.bind(this, { ref: ['?'] })
@@ -865,8 +918,8 @@ class CQN2SQLRenderer {
865918
}
866919

867920
const extractions = this._managed = this.managed(columns.map(c => ({ name: c })), elements)
868-
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))
869-
}) SELECT ${extractions.map(c => c.insert)} FROM json_each(?)`)
921+
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))
922+
}) SELECT ${extractions.slice(0, columns.length).map(c => c.insert)} FROM json_each(?)`)
870923
}
871924

872925
async *INSERT_entries_stream(entries, binaryEncoding = 'base64') {
@@ -972,7 +1025,7 @@ class CQN2SQLRenderer {
9721025
*/
9731026
INSERT_rows(q) {
9741027
const { INSERT } = q
975-
const entity = this.name(q._target?.name || INSERT.into.ref[0], q)
1028+
const entity = q._target ? this.table_name(q) : INSERT.into.ref[0]
9761029
const alias = INSERT.into.as
9771030
const elements = q.elements || q._target?.elements
9781031
const columns = this.columns = INSERT.columns || cds.error`Cannot insert rows without columns or elements`
@@ -997,7 +1050,8 @@ class CQN2SQLRenderer {
9971050
.slice(0, columns.length)
9981051
.map(c => c.converter(c.extract))
9991052

1000-
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))
1053+
const transitions = this.srv.resolve.transitions4db(q)
1054+
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))
10011055
}) SELECT ${extraction} FROM json_each(?)`)
10021056
}
10031057

@@ -1018,20 +1072,24 @@ class CQN2SQLRenderer {
10181072
*/
10191073
INSERT_select(q) {
10201074
const { INSERT } = q
1021-
const entity = this.name(q._target.name, q)
1075+
const entity = q._target ? this.table_name(q) : INSERT.into.ref[0]
10221076
const alias = INSERT.into.as
1077+
const src = this.cqn4sql(INSERT.from)
10231078
const elements = q.elements || q._target?.elements || {}
1024-
let columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
1025-
c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
1026-
))
1079+
const transitions = this.srv.resolve.transitions4db(q, this.srv)
1080+
let columns = (this.columns = (INSERT.columns || src.SELECT.columns?.map(c => this.column_name(c)) || ObjectKeys(src.elements) || ObjectKeys(elements))
1081+
.filter(c => this.physical_column(elements, c)
1082+
&& (c = transitions.mapping.get(c)?.ref?.[0] || c)
1083+
&& c in transitions.target.elements
1084+
&& this.physical_column(transitions.target.elements, c)
1085+
))
10271086

1028-
const src = this.cqn4sql(INSERT.from)
10291087
const extractions = this._managed = this.managed(columns.map(c => ({ name: c, sql: `NEW.${this.quote(c)}` })), elements)
10301088
const sql = extractions.length > columns.length
10311089
? `SELECT ${extractions.map(c => `${c.insert} AS ${this.quote(c.name)}`)} FROM (${this.SELECT(src)}) AS NEW`
10321090
: this.SELECT(src)
10331091
if (extractions.length > columns.length) columns = this.columns = extractions.map(c => c.name)
1034-
this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns.map(c => this.quote(c))}) ${sql}`
1092+
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}`
10351093
this.entries = [this.values]
10361094
return this.sql
10371095
}
@@ -1085,7 +1143,7 @@ class CQN2SQLRenderer {
10851143
.join(' AND ')
10861144

10871145
let columns = this.columns // this.columns is computed as part of this.INSERT
1088-
const entity = this.name(q._target?.name || UPSERT.into.ref[0], q)
1146+
const entity = q._target ? this.table_name(q) : this.name(UPSERT.into.ref[0], q)
10891147
if (UPSERT.entries || UPSERT.rows || UPSERT.values) {
10901148
const managed = this._managed.slice(0, columns.length)
10911149

@@ -1121,7 +1179,8 @@ class CQN2SQLRenderer {
11211179
else return true
11221180
}).map(c => `${this.quote(c)} = excluded.${this.quote(c)}`)
11231181

1124-
return (this.sql = `INSERT INTO ${this.quote(entity)} (${columns.map(c => this.quote(c))}) ${sql
1182+
const transitions = this.srv.resolve.transitions4db(q)
1183+
return (this.sql = `INSERT INTO ${this.quote(entity)} (${columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c))}) ${sql
11251184
} WHERE TRUE ON CONFLICT(${keys.map(c => this.quote(c))}) DO ${updateColumns.length ? `UPDATE SET ${updateColumns}` : 'NOTHING'}`)
11261185
}
11271186

@@ -1134,29 +1193,36 @@ class CQN2SQLRenderer {
11341193
*/
11351194
UPDATE(q) {
11361195
const { entity, with: _with, data, where } = q.UPDATE
1196+
const transitions = this.srv.resolve.transitions4db(q)
11371197
const elements = q._target?.elements
1138-
let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity, q))}`
1198+
let sql = `UPDATE ${this.quote(this.table_name(q))}`
11391199
if (entity.as) sql += ` AS ${this.quote(entity.as)}`
11401200

1141-
let columns = []
1142-
if (data) _add(data, val => this.val({ val }))
1143-
if (_with) _add(_with, x => this.expr(x))
1144-
function _add(data, sql4) {
1145-
for (let c in data) {
1146-
const columnExistsInDatabase =
1147-
elements && c in elements && !elements[c].virtual && !elements[c].isAssociation && !elements[c].value
1201+
const _add = (data, sql4) => {
1202+
for (let col in data) {
1203+
const c = transitions.mapping.get(col)?.ref?.[0] || col
1204+
const columnExistsInDatabase = elements
1205+
&& this.physical_column(elements, col)
1206+
&& c in transitions.target.elements
1207+
&& this.physical_column(transitions.target.elements, c)
11481208
if (!elements || columnExistsInDatabase) {
1149-
columns.push({ name: c, sql: sql4(data[c]) })
1209+
columns.push({ name: c, sql: sql4(data[col], col) })
11501210
}
11511211
}
11521212
}
11531213

1214+
let columns = []
1215+
if (data) _add(data, val => this.val({ val }))
1216+
if (_with) _add(_with, x => this.expr(x))
1217+
11541218
const extraction = this.managed(columns, elements)
1155-
.filter((c, i) => columns[i] || c.onUpdate)
1156-
.map((c, i) => `${this.quote(c.name)}=${!columns[i] ? c.onUpdate : c.sql}`)
1219+
.filter((c, i) => {
1220+
if (transitions.mapping.get(c.name)?.ref?.length > 1) return false
1221+
return columns[i] || c.onUpdate
1222+
}).map((c, i) => `${this.quote(transitions.mapping.get(c.name)?.ref?.[0] || c.name)}=${!columns[i] ? c.onUpdate : c.sql}`)
11571223

11581224
sql += ` SET ${extraction}`
1159-
if (where) sql += ` WHERE ${this.where(where)}`
1225+
if (where) sql += ` WHERE ${this.where_resolved(entity.as, where, q)}`
11601226
return (this.sql = sql)
11611227
}
11621228

@@ -1168,8 +1234,9 @@ class CQN2SQLRenderer {
11681234
* @returns {string} SQL
11691235
*/
11701236
DELETE(q) {
1171-
const { DELETE: { from, where } } = q
1172-
let sql = `DELETE FROM ${this.from(from, q)}`
1237+
const { DELETE: { where, from } } = q
1238+
let sql = `DELETE FROM ${this.quote(this.table_name(q))}`
1239+
if (from.as) sql += ` AS ${this.quote(from.as)}`
11731240
if (where) sql += ` WHERE ${this.where(where)}`
11741241
return (this.sql = sql)
11751242
}
@@ -1382,6 +1449,16 @@ class CQN2SQLRenderer {
13821449
return (typeof col.as === 'string' && col.as) || ('val' in col && col.val + '') || col.func || col.ref.at(-1)
13831450
}
13841451

1452+
/**
1453+
* Calculates the Database table name of the query target
1454+
* @param {import('./infer/cqn').Query} query
1455+
* @returns {string} Database table name
1456+
*/
1457+
table_name(q) {
1458+
const table = cds.db.resolve.table(q._target)
1459+
return this.name(table.name, { _target: table })
1460+
}
1461+
13851462
/**
13861463
* Calculates the Database name of the given name
13871464
* @param {string|import('./infer/cqn').ref} name
@@ -1489,6 +1566,10 @@ class CQN2SQLRenderer {
14891566
})
14901567
}
14911568

1569+
physical_column(elements, c) {
1570+
return elements[c] && !elements[c].virtual && !elements[c].value && !elements[c].isAssociation
1571+
}
1572+
14921573
managed_extract(name, element, converter) {
14931574
const { UPSERT, INSERT } = this.cqn
14941575
const extract = !(INSERT?.entries || UPSERT?.entries) && (INSERT?.rows || UPSERT?.rows)

0 commit comments

Comments
 (0)