@cap-js/db-service 2.8.2 → 2.10.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 +35 -0
- package/lib/InsertResults.js +6 -3
- package/lib/SQLService.js +88 -50
- package/lib/cql-functions.js +1 -1
- package/lib/cqn2pql.js +116 -0
- package/lib/cqn2sql.js +131 -48
- package/lib/cqn4sql.js +588 -180
- package/lib/infer/index.js +77 -16
- package/lib/infer/join-tree.js +8 -6
- package/lib/utils.js +29 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,41 @@
|
|
|
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.10.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.9.0...db-service-v2.10.0) (2026-04-22)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* `cds.features.count_as_string` ([#1556](https://github.com/cap-js/cds-dbs/issues/1556)) ([00e0e60](https://github.com/cap-js/cds-dbs/commit/00e0e60d68edf0d42c1fce2fae3bb1286aca131e))
|
|
13
|
+
* **cqn4sql:** support for enums ([#1527](https://github.com/cap-js/cds-dbs/issues/1527)) ([27c4279](https://github.com/cap-js/cds-dbs/commit/27c4279c495fce8344c785e4489e3116d1a52c55))
|
|
14
|
+
* pql ([#1532](https://github.com/cap-js/cds-dbs/issues/1532)) ([943f76a](https://github.com/cap-js/cds-dbs/commit/943f76a3e4405eb91f0f4b929590212500c49c30))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
* `$self` reference to func column in `having` ([#1539](https://github.com/cap-js/cds-dbs/issues/1539)) ([9eac576](https://github.com/cap-js/cds-dbs/commit/9eac5762fc4d254a1bc54bded1dd6a492299f576)), closes [#1528](https://github.com/cap-js/cds-dbs/issues/1528)
|
|
20
|
+
* foreign key not included in wildcard select from subquery ([#1540](https://github.com/cap-js/cds-dbs/issues/1540)) ([0fde4ed](https://github.com/cap-js/cds-dbs/commit/0fde4eda21a389c68982f348e9e7c3680c00dcb3)), closes [#1127](https://github.com/cap-js/cds-dbs/issues/1127)
|
|
21
|
+
* sqlite generated key is named lastInsertRowid ([#1501](https://github.com/cap-js/cds-dbs/issues/1501)) ([a4d3437](https://github.com/cap-js/cds-dbs/commit/a4d34378297c8afdb13abb7e664165012c36eb8f))
|
|
22
|
+
|
|
23
|
+
## [2.9.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.8.2...db-service-v2.9.0) (2026-03-09)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
* runtime views ([#1410](https://github.com/cap-js/cds-dbs/issues/1410)) ([5242675](https://github.com/cap-js/cds-dbs/commit/5242675c97472b86b81b3dc5fe0906141d276b02))
|
|
29
|
+
* 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))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
* **`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)
|
|
35
|
+
* **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))
|
|
36
|
+
* enable expressions for `inline` ([#1512](https://github.com/cap-js/cds-dbs/issues/1512)) ([65f78e1](https://github.com/cap-js/cds-dbs/commit/65f78e1f3af83188462e9d44db67daa5d743ceb0))
|
|
37
|
+
* 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))
|
|
38
|
+
* reject duplicated wildcards ([#1511](https://github.com/cap-js/cds-dbs/issues/1511)) ([b483062](https://github.com/cap-js/cds-dbs/commit/b483062e2ff5a8d0960dc2e7b71880af87ee8f78))
|
|
39
|
+
* 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))
|
|
40
|
+
* wildcard on inlined assoc ([#1513](https://github.com/cap-js/cds-dbs/issues/1513)) ([e520b97](https://github.com/cap-js/cds-dbs/commit/e520b97fd30394825b937b3613370c32c36c24a4))
|
|
41
|
+
|
|
7
42
|
## [2.8.2](https://github.com/cap-js/cds-dbs/compare/db-service-v2.8.1...db-service-v2.8.2) (2026-02-03)
|
|
8
43
|
|
|
9
44
|
|
package/lib/InsertResults.js
CHANGED
|
@@ -69,9 +69,12 @@ module.exports = class InsertResult {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// If no generated keys in entries/rows/values we might have database-generated keys
|
|
72
|
-
const rows = this.results.slice(0, this.affectedRows) // only up to # of root entries
|
|
73
72
|
return (super[iterator] = function* () {
|
|
74
|
-
for (const
|
|
73
|
+
for (const row of this.results) {
|
|
74
|
+
const affectedRows = this.affectedRows4(row) - 1
|
|
75
|
+
const lastInsertRowid = this.insertedRowId4(row)
|
|
76
|
+
for (let i = lastInsertRowid - affectedRows; i<=lastInsertRowid;i++) yield { [k1]: i }
|
|
77
|
+
}
|
|
75
78
|
})
|
|
76
79
|
}
|
|
77
80
|
|
|
@@ -99,7 +102,7 @@ module.exports = class InsertResult {
|
|
|
99
102
|
* @returns {number}
|
|
100
103
|
*/
|
|
101
104
|
insertedRowId4(result) {
|
|
102
|
-
return result.
|
|
105
|
+
return result.lastInsertRowid
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
/**
|
package/lib/SQLService.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
const cds = require('@sap/cds')
|
|
2
|
-
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
|
+
const DEBUG = cds.log('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
|
+
|
|
9
|
+
// REVISIT: make string the default in next major
|
|
10
|
+
const _count_as_string = cds.env.features.count_as_string
|
|
11
|
+
const _count = _count_as_string ? { func: 'count', cast: { type: 'cds.String' } } : { func: 'count' }
|
|
8
12
|
|
|
9
13
|
const BINARY_TYPES = {
|
|
10
14
|
'cds.Binary': 1,
|
|
@@ -17,7 +21,7 @@ const BINARY_TYPES = {
|
|
|
17
21
|
* @param {*} obj
|
|
18
22
|
* @returns Boolean
|
|
19
23
|
*/
|
|
20
|
-
const _hasProps = (obj) => {
|
|
24
|
+
const _hasProps = (obj) => {
|
|
21
25
|
if (!obj) return false
|
|
22
26
|
for (const p in obj) {
|
|
23
27
|
return true
|
|
@@ -74,7 +78,7 @@ class SQLService extends DatabaseService {
|
|
|
74
78
|
_changeToStreams(columns, rows, one) {
|
|
75
79
|
if (!rows || !columns) return
|
|
76
80
|
if (!Array.isArray(rows)) rows = [rows]
|
|
77
|
-
if (!rows.length || !Object.keys(rows[0]).length) return
|
|
81
|
+
if (!rows.length || !Object.keys(rows[0]).length) return
|
|
78
82
|
|
|
79
83
|
let changes = false
|
|
80
84
|
for (let col of columns) {
|
|
@@ -149,7 +153,7 @@ class SQLService extends DatabaseService {
|
|
|
149
153
|
if (expand) rows = rows.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))
|
|
150
154
|
|
|
151
155
|
if (!iterator) {
|
|
152
|
-
|
|
156
|
+
this._changeToStreams(cqn.SELECT.columns, rows, query.SELECT.one)
|
|
153
157
|
} else if (objectMode) {
|
|
154
158
|
const converter = (row) => this._changeToStreams(cqn.SELECT.columns, row, true)
|
|
155
159
|
const changeToStreams = new Transform({
|
|
@@ -168,7 +172,7 @@ class SQLService extends DatabaseService {
|
|
|
168
172
|
return SQLService._arrayWithCount(rows, await this.count(query, rows))
|
|
169
173
|
}
|
|
170
174
|
|
|
171
|
-
return iterator
|
|
175
|
+
return !iterator && isOne ? rows[0] : rows
|
|
172
176
|
} catch (err) {
|
|
173
177
|
// Ensure that iterators receive pre stream errors
|
|
174
178
|
if (iterator) rows.emit('error', err)
|
|
@@ -198,7 +202,7 @@ class SQLService extends DatabaseService {
|
|
|
198
202
|
const ps = await this.prepare(sql)
|
|
199
203
|
const results = entries ? await Promise.all(entries.map(e => ps.run(e))) : await ps.run()
|
|
200
204
|
// REVISIT: results isn't an array, when no entries -> how could that work? when do we have no entries?
|
|
201
|
-
return results.reduce((total, affectedRows) =>
|
|
205
|
+
return results.reduce((total, affectedRows) => total + affectedRows.changes, 0)
|
|
202
206
|
}
|
|
203
207
|
|
|
204
208
|
/**
|
|
@@ -230,7 +234,8 @@ class SQLService extends DatabaseService {
|
|
|
230
234
|
// REVISIT: It's not yet 100 % clear under which circumstances we can rely on db constraints
|
|
231
235
|
return (super.onDELETE = /* cds.env.features.assert_integrity === 'db' ? this.onSIMPLE : */ deep_delete)
|
|
232
236
|
async function deep_delete(/** @type {Request} */ req) {
|
|
233
|
-
const
|
|
237
|
+
const resolve = this.resolve
|
|
238
|
+
const transitions = resolve.transitions(req.query)
|
|
234
239
|
if (transitions.target !== transitions.queryTarget) {
|
|
235
240
|
const keys = []
|
|
236
241
|
const transitionsTarget = transitions.queryTarget.keys || transitions.queryTarget.elements
|
|
@@ -253,7 +258,7 @@ class SQLService extends DatabaseService {
|
|
|
253
258
|
})
|
|
254
259
|
return this.onDELETE({ query, target: transitions.target })
|
|
255
260
|
}
|
|
256
|
-
const table =
|
|
261
|
+
const table = resolveTable(req.target)
|
|
257
262
|
const { compositions } = table
|
|
258
263
|
if (compositions) {
|
|
259
264
|
// Transform CQL`DELETE from Foo[p1] WHERE p2` into CQL`DELETE from Foo[p1 and p2]`
|
|
@@ -295,7 +300,7 @@ class SQLService extends DatabaseService {
|
|
|
295
300
|
* @type {Handler}
|
|
296
301
|
*/
|
|
297
302
|
async onEVENT({ event }) {
|
|
298
|
-
DEBUG
|
|
303
|
+
if(DEBUG._debug) DEBUG.debug(event) // in the other cases above DEBUG happens in cqn2sql
|
|
299
304
|
return await this.exec(event)
|
|
300
305
|
}
|
|
301
306
|
|
|
@@ -305,7 +310,7 @@ class SQLService extends DatabaseService {
|
|
|
305
310
|
*/
|
|
306
311
|
async onPlainSQL({ query, data }, next) {
|
|
307
312
|
if (typeof query === 'string') {
|
|
308
|
-
DEBUG
|
|
313
|
+
if(DEBUG._debug) DEBUG.debug(query, data)
|
|
309
314
|
const ps = await this.prepare(query)
|
|
310
315
|
const exec = this.hasResults(query) ? d => ps.all(d) : d => ps.run(d)
|
|
311
316
|
if (Array.isArray(data) && Array.isArray(data[0])) return await Promise.all(data.map(exec))
|
|
@@ -325,24 +330,24 @@ class SQLService extends DatabaseService {
|
|
|
325
330
|
* Derives and executes a query to fill in `$count` for given query
|
|
326
331
|
* @param {import('@sap/cds/apis/cqn').SELECT} query - SELECT CQN
|
|
327
332
|
* @param {unknown[]} ret - Results of the original query
|
|
328
|
-
* @returns {Promise<number>}
|
|
333
|
+
* @returns {Promise<number|string>}
|
|
329
334
|
*/
|
|
330
335
|
async count(query, ret) {
|
|
331
336
|
if (ret?.length) {
|
|
332
337
|
const { one, limit: _ } = query.SELECT,
|
|
333
338
|
n = ret.length
|
|
334
339
|
const [max, offset = 0] = one ? [1] : _ ? [_.rows?.val, _.offset?.val] : []
|
|
335
|
-
if (max === undefined || (n < max && (n || !offset))) return n + offset
|
|
340
|
+
if (max === undefined || (n < max && (n || !offset))) return _count_as_string ? `${n + offset}` : n + offset
|
|
336
341
|
}
|
|
337
342
|
|
|
338
343
|
// Keep original query columns when potentially used insde conditions
|
|
339
344
|
const { having, groupBy } = query.SELECT
|
|
340
345
|
let columns = []
|
|
341
|
-
if(
|
|
346
|
+
if (having?.length || groupBy?.length) {
|
|
342
347
|
columns = query.SELECT.columns.filter(c => !c.expand)
|
|
343
348
|
}
|
|
344
349
|
if (columns.length === 0) columns.push({ val: 1 })
|
|
345
|
-
const cq = SELECT.one([
|
|
350
|
+
const cq = SELECT.one([_count]).from(
|
|
346
351
|
cds.ql.clone(query, {
|
|
347
352
|
columns,
|
|
348
353
|
localized: false,
|
|
@@ -360,7 +365,7 @@ class SQLService extends DatabaseService {
|
|
|
360
365
|
* @param {import('@sap/cds/apis/cqn').SELECT} query - SELECT CQN
|
|
361
366
|
* @param {function} callback - Function to be invoked for each row
|
|
362
367
|
*/
|
|
363
|
-
foreach
|
|
368
|
+
foreach(query, callback) {
|
|
364
369
|
return query.foreach(callback)
|
|
365
370
|
}
|
|
366
371
|
|
|
@@ -390,40 +395,34 @@ class SQLService extends DatabaseService {
|
|
|
390
395
|
})
|
|
391
396
|
}
|
|
392
397
|
|
|
393
|
-
/** @param {unknown[]} args */
|
|
394
398
|
constructor(...args) {
|
|
395
399
|
super(...args)
|
|
396
|
-
/** @type {unknown} */
|
|
397
400
|
this.class = new.target // for IntelliSense
|
|
398
401
|
}
|
|
399
402
|
|
|
400
403
|
/**
|
|
401
404
|
* @param {import('@sap/cds/apis/cqn').Query} query
|
|
402
|
-
* @param {unknown} values
|
|
403
405
|
* @returns {typeof SQLService.CQN2SQL}
|
|
404
406
|
*/
|
|
405
407
|
cqn2sql(query, values) {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}
|
|
411
|
-
let cqn2sql = new this.class.CQN2SQL(this)
|
|
412
|
-
return cqn2sql.render(q, values)
|
|
408
|
+
const cqn2sql = new this.class.CQN2SQL(this)
|
|
409
|
+
const q = this.cqn4sql(query)
|
|
410
|
+
const sql = cqn2sql.render(q, values)
|
|
411
|
+
return sql
|
|
413
412
|
}
|
|
414
413
|
|
|
415
414
|
/**
|
|
416
415
|
* @param {import('@sap/cds/apis/cqn').Query} q
|
|
417
416
|
* @returns {import('./infer/cqn').Query}
|
|
418
417
|
*/
|
|
419
|
-
cqn4sql(q) {
|
|
418
|
+
cqn4sql(q, useTechnicalAlias=true) {
|
|
420
419
|
if (
|
|
421
420
|
!cds.env.features.db_strict &&
|
|
422
421
|
!q.SELECT?.from?.join &&
|
|
423
422
|
!q.SELECT?.from?.SELECT &&
|
|
424
423
|
!this.model?.definitions[_target_name4(q)]
|
|
425
424
|
) return q
|
|
426
|
-
else return cqn4sql(q, this.model)
|
|
425
|
+
else return cqn4sql(q, this.model, useTechnicalAlias)
|
|
427
426
|
}
|
|
428
427
|
|
|
429
428
|
/**
|
|
@@ -512,31 +511,70 @@ const _target_name4 = q => {
|
|
|
512
511
|
return first.id || first
|
|
513
512
|
}
|
|
514
513
|
|
|
515
|
-
const sqls = new (class extends SQLService {
|
|
516
|
-
get factory() {
|
|
517
|
-
return null
|
|
518
|
-
}
|
|
519
514
|
|
|
520
|
-
|
|
521
|
-
|
|
515
|
+
// Add support for cqn2pql if debug logging for pql is enabled, or if running in the REPL.
|
|
516
|
+
const DEBUG_PQL = cds.log('pql')
|
|
517
|
+
if (DEBUG_PQL._debug || cds.repl) {
|
|
518
|
+
|
|
519
|
+
// Add helper method to convert CQN to PQL, used below...
|
|
520
|
+
SQLService.prototype.cqn2pql = function cqn2pql (query, values) {
|
|
521
|
+
const CQN2PQL = cqn2pql.renderer ??= require('./cqn2pql')
|
|
522
|
+
return new CQN2PQL(this).render(query, values)
|
|
522
523
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
let { sql, values } = (cds.db || sqls).cqn2sql(this)
|
|
533
|
-
return { sql, values } // skipping .cqn property
|
|
524
|
+
|
|
525
|
+
// Add support for logging generated PQL if debug logging for pql is enabled.
|
|
526
|
+
if (DEBUG_PQL._debug) {
|
|
527
|
+
const $super = SQLService.prototype.cqn2sql
|
|
528
|
+
SQLService.prototype.cqn2sql = function (query, values) {
|
|
529
|
+
const q2 = this.cqn4sql(query, false) // FIXME: calling cqn4sql twice per query is utterly expensive, isn't it ?!?
|
|
530
|
+
const pql = this.cqn2pql(q2, values)
|
|
531
|
+
DEBUG_PQL.debug(pql.sql, pql.values ?? '')
|
|
532
|
+
return $super.call(this, query, values)
|
|
534
533
|
}
|
|
535
|
-
|
|
536
|
-
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// If running in the REPL, extend cds.ql.Query with helpers to inspect queries.
|
|
537
|
+
if (cds.repl) {
|
|
538
|
+
|
|
539
|
+
cds.extend(cds.ql.Query).with(
|
|
540
|
+
class {
|
|
541
|
+
forSQL() {
|
|
542
|
+
const cqn = db.srv.cqn4sql(this)
|
|
543
|
+
return this.flat(cqn)
|
|
544
|
+
}
|
|
545
|
+
forSql() { return this.forSQL() }
|
|
546
|
+
toSQL() {
|
|
547
|
+
if (this.SELECT) this.SELECT.expand = 'root' // Enforces using json functions always for top-level SELECTS
|
|
548
|
+
const { sql, values } = db.srv.cqn2sql(this)
|
|
549
|
+
return { sql, values } // skipping .cqn property
|
|
550
|
+
}
|
|
551
|
+
toSql() {
|
|
552
|
+
const { sql } = this.toSQL()
|
|
553
|
+
return sql
|
|
554
|
+
}
|
|
555
|
+
toPQL() {
|
|
556
|
+
const { sql, values } = db.srv.cqn2pql(this)
|
|
557
|
+
return { sql, values } // skipping .cqn property
|
|
558
|
+
}
|
|
559
|
+
toPql() {
|
|
560
|
+
const { sql } = this.toPQL()
|
|
561
|
+
return sql
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Dummy SQL service used in extensions to cds.ql above,
|
|
568
|
+
* if no real SQL service is available yet through cds.db.
|
|
569
|
+
*/
|
|
570
|
+
class db extends SQLService {
|
|
571
|
+
/** @returns {SQLService} */
|
|
572
|
+
static get srv() { return cds.db || (this.singleton ??= new this) }
|
|
573
|
+
get factory() { return null }
|
|
574
|
+
get model() { return cds.model }
|
|
537
575
|
}
|
|
538
|
-
}
|
|
539
|
-
|
|
576
|
+
}
|
|
577
|
+
}
|
|
540
578
|
|
|
541
579
|
Object.assign(SQLService, { _target_name4 })
|
|
542
580
|
module.exports = SQLService
|
package/lib/cql-functions.js
CHANGED
|
@@ -293,7 +293,7 @@ SELECT
|
|
|
293
293
|
(SELECT MAX(HIERARCHY_RANK) + 1 FROM ${ranked})
|
|
294
294
|
) - Source.HIERARCHY_RANK AS HIERARCHY_TREE_SIZE
|
|
295
295
|
FROM ${ranked} AS Source`)
|
|
296
|
-
Hierarchy.as =
|
|
296
|
+
Hierarchy.as = `H${uniqueCounter}`
|
|
297
297
|
Hierarchy.SELECT.columns = [...Hierarchy.SELECT.columns, ...passThroughColumns]
|
|
298
298
|
Hierarchy = this.expr(this.with(Hierarchy))
|
|
299
299
|
|
package/lib/cqn2pql.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
|
+
|
|
3
|
+
const CQN2SQL = require('./cqn2sql.js').class
|
|
4
|
+
|
|
5
|
+
class CQN2PQLRenderer extends CQN2SQL {
|
|
6
|
+
|
|
7
|
+
SELECT(q) {
|
|
8
|
+
this.values = undefined // inline all values
|
|
9
|
+
return (this.sql = super.SELECT(q)
|
|
10
|
+
.replaceAll('\n FROM', '\nFROM')
|
|
11
|
+
.replaceAll(/([^ ]) (FROM|WHERE|GROUP BY|HAVING|ORDER BY|LIMIT) /g, (a, b, c) => `${b}\n${c} `)
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
SELECT_columns(q) {
|
|
16
|
+
return super.SELECT_columns(q).map((c, i) => `${(i % 5 === 0) ? '\n ' : ' '}${c}${/ as /i.test(c) ? '\n' : ''}`).join(',')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
column_expr(x, q) {
|
|
20
|
+
// omit alias when target is a single source
|
|
21
|
+
if (q.SELECT.from.ref && x?.ref) x.ref = x.ref.slice(-1)
|
|
22
|
+
return super.column_expr(x, q)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
SELECT_expand(q, sql) { return sql }
|
|
26
|
+
|
|
27
|
+
INSERT_entries(q) {
|
|
28
|
+
super.INSERT_entries(q)
|
|
29
|
+
this.sql = this.sql
|
|
30
|
+
.replaceAll(/AS (.*?)([, ])(?=[^\n])/ig, (a, b, c) => `AS ${b}${c}\n${c === ',' ? ' ' : ''}`)
|
|
31
|
+
.replaceAll(/ *= */ig, ' = ')
|
|
32
|
+
.replaceAll('value AS "$$value$$"', 'value')
|
|
33
|
+
.replaceAll(' WHERE ', '\nWHERE ')
|
|
34
|
+
.replaceAll(' SELECT ', '\nSELECT')
|
|
35
|
+
.replaceAll('(SELECT ', '(SELECT\n ')
|
|
36
|
+
.replaceAll('))', ')\n)')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
INSERT_rows(q) {
|
|
40
|
+
super.INSERT_rows(q)
|
|
41
|
+
this.sql = this.sql.replaceAll('SELECT', '\nSELECT')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
UPSERT(q) {
|
|
45
|
+
super.UPSERT(q)
|
|
46
|
+
this.sql = this.sql
|
|
47
|
+
.replaceAll('INSERT', 'UPSERT')
|
|
48
|
+
.replaceAll(/AS (.*?)([, ])(?=[^\n])/ig, (a, b, c) => `AS ${b}${c}\n${c === ',' ? ' ' : ''}`)
|
|
49
|
+
.replaceAll(/ *= */ig, ' = ')
|
|
50
|
+
.replaceAll('value AS "$$value$$"', 'value')
|
|
51
|
+
.replaceAll(' WHERE ', '\nWHERE ')
|
|
52
|
+
.replaceAll(' SELECT ', '\nSELECT')
|
|
53
|
+
.replaceAll('(SELECT ', '(SELECT\n ')
|
|
54
|
+
.replaceAll('))', ')\n)')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
expr(x) {
|
|
58
|
+
const wrap = x.cast ? sql => `cast(${sql} as ${this.type4(x.cast)})` : sql => sql
|
|
59
|
+
if (typeof x === 'string') throw cds.error`Unsupported expr: ${x}`
|
|
60
|
+
if (x.param) return wrap(this.param(x))
|
|
61
|
+
if ('ref' in x) return wrap(this.ref(x))
|
|
62
|
+
if ('val' in x) return wrap(this.val(x))
|
|
63
|
+
if ('func' in x) return wrap(this.func(x))
|
|
64
|
+
if ('xpr' in x) return wrap(this.xpr(x))
|
|
65
|
+
if ('list' in x) return wrap(this.list(x))
|
|
66
|
+
if ('SELECT' in x) return wrap(`(\n ${this.SELECT(x).replaceAll('\n', '\n ')}\n )`)
|
|
67
|
+
else throw cds.error`Unsupported expr: ${x}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
quote(s) { return s }
|
|
71
|
+
|
|
72
|
+
managed(columns, elements) {
|
|
73
|
+
const keys = ObjectKeys(elements).filter(e => elements[e].key && !elements[e].isAssociation)
|
|
74
|
+
const keyZero = keys[0]
|
|
75
|
+
|
|
76
|
+
const ret = super.managed(columns, elements)
|
|
77
|
+
|
|
78
|
+
ret.forEach(c => {
|
|
79
|
+
const { name, insert, update, onInsert, onUpdate } = c
|
|
80
|
+
const element = elements?.[name]
|
|
81
|
+
c.upsert = keyZero && (
|
|
82
|
+
// upsert requires the keys to be provided for the existance join (default values optional)
|
|
83
|
+
element?.key
|
|
84
|
+
// If both insert and update have the same managed definition exclude the old value check
|
|
85
|
+
|| (onInsert && onUpdate && insert === update)
|
|
86
|
+
? `${insert} as ${name}`
|
|
87
|
+
: `!OLD.${keyZero} ? ${
|
|
88
|
+
// If key of old is null execute insert
|
|
89
|
+
insert
|
|
90
|
+
} : ${
|
|
91
|
+
// Else execute managed update or keep old if no new data if provided
|
|
92
|
+
onUpdate ? update : `(${this.managed_default(name, `OLD.${name}`, update)})`
|
|
93
|
+
} as ${name}`
|
|
94
|
+
)
|
|
95
|
+
if (c.upsert) c.upsert = '\n ' + c.upsert
|
|
96
|
+
})
|
|
97
|
+
return ret
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
managed_default(name, managed, src) {
|
|
101
|
+
return `!${src} ? ${managed} : ${src}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
managed_extract(name) {
|
|
105
|
+
const { UPSERT, INSERT } = this.cqn
|
|
106
|
+
const extract = !(INSERT?.entries || UPSERT?.entries) && (INSERT?.rows || UPSERT?.rows)
|
|
107
|
+
? `value[${this.columns.indexOf(name)}]`
|
|
108
|
+
: `value[${JSON.stringify(name)}]`
|
|
109
|
+
const sql = extract
|
|
110
|
+
return { extract, sql }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const ObjectKeys = o => (o && [...ObjectKeys(o.__proto__), ...Object.keys(o)]) || []
|
|
115
|
+
|
|
116
|
+
module.exports = CQN2PQLRenderer
|