@cap-js/sqlite 0.1.0 → 0.2.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 +4 -0
- package/README.md +2 -2
- package/lib/db/DatabaseService.js +11 -10
- package/lib/db/sql/SQLService.js +24 -4
- package/lib/db/sql/cqn2sql.js +37 -21
- package/lib/db/sql/cqn4sql.js +38 -22
- package/lib/db/sql/deep.js +41 -44
- package/lib/db/sql/func.js +3 -2
- package/lib/db/sql/workarounds.js +1 -11
- package/lib/db/sqlite/SQLiteService.js +5 -0
- package/lib/ql/cds.infer.js +93 -22
- package/lib/ql/join-tree.js +0 -1
- package/package.json +9 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
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
|
+
## Version 0.2.0 - 2023-05-03
|
|
8
|
+
|
|
9
|
+
- Continuous improvements
|
|
10
|
+
|
|
7
11
|
## Version 0.1.0 - 2023-04-04
|
|
8
12
|
|
|
9
13
|
- Initial release
|
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ cds repl --profile better-sqlite
|
|
|
76
76
|
var { server } = await cds.test('bookshop')
|
|
77
77
|
var { Books, Authors } = cds.entities
|
|
78
78
|
await INSERT.into (Books) .entries ({ title: 'Unwritten Book' })
|
|
79
|
-
await INSERT.into (Authors) .entries ({ name: '
|
|
79
|
+
await INSERT.into (Authors) .entries ({ name: 'Upcoming Author' })
|
|
80
80
|
await SELECT `from ${Books} { title as book, author.name as author, genre.name as genre }`
|
|
81
81
|
await SELECT `from ${Authors} { books.title as book, name as author, books.genre.name as genre }`
|
|
82
82
|
await SELECT `from ${Books} { title as book, author[ID<170].name as author, genre.name as genre }`
|
|
@@ -174,7 +174,7 @@ The combination of the above-mentioned improvements commonly leads to significan
|
|
|
174
174
|
|
|
175
175
|
## Known Limitations & Changes
|
|
176
176
|
|
|
177
|
-
- Node v14 is no longer supported → will be dropped anyways with
|
|
177
|
+
- Node v14 is no longer supported → will be dropped anyways with upcoming cds7.
|
|
178
178
|
- JOINs and UNIONs by CQN are no longer supported → use plain SQL instead.
|
|
179
179
|
* CQNs with subqueries require table aliases to refer to elements of outer queries.
|
|
180
180
|
* CQNs with an empty columns array now throws an error.
|
|
@@ -43,16 +43,17 @@ class DatabaseService extends cds.Service {
|
|
|
43
43
|
const pool = this.pools[tenant] ??= new Pool (this.pools._factory, tenant)
|
|
44
44
|
const dbc = this.dbc = await pool.acquire()
|
|
45
45
|
this._release = (dbc) => pool.release(dbc)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
try {
|
|
47
|
+
// Setting session context variables
|
|
48
|
+
await this.set({
|
|
49
|
+
get '$user.id'(){ return _set (this, '$user.id', ctx.user?.id || 'anonymous') },
|
|
50
|
+
get '$user.locale'(){ return _set (this, '$user.locale', ctx.locale || cds.env.i18n.default_language) },
|
|
51
|
+
get '$valid.from'(){ return _set (this, '$valid.from', ctx._?.['VALID-FROM']?.toISOString() || ctx._?.['VALID-AT']?.toISOString() || '1970-01-01T00:00:00Z') },
|
|
52
|
+
get '$valid.to'(){ return _set (this, '$valid.to', ctx._?.['VALID-TO']?.toISOString() || _validTo4(ctx._?.['VALID-AT'])?.toISOString() || '9999-11-11T22:22:22Z') },
|
|
53
|
+
})
|
|
54
|
+
// Run BEGIN
|
|
55
|
+
await this.send('BEGIN')
|
|
56
|
+
} catch (e) { this._release(dbc); throw e }
|
|
56
57
|
return this
|
|
57
58
|
}
|
|
58
59
|
|
package/lib/db/sql/SQLService.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const cds = require('../../../cds'), DEBUG = cds.debug('sql|db')
|
|
2
|
+
const { resolveView } = require('@sap/cds/libx/_runtime/common/utils/resolveView')
|
|
2
3
|
const DatabaseService = require('../DatabaseService')
|
|
3
4
|
const cqn4sql = require('./cqn4sql')
|
|
4
5
|
const { target_name4 } = require('./utils')
|
|
@@ -11,7 +12,8 @@ class SQLService extends DatabaseService {
|
|
|
11
12
|
this.on([ 'INSERT', 'UPSERT', 'UPDATE', 'DELETE' ], require('./workarounds').input) // REVISIT should be replaced by correct input processing eventually
|
|
12
13
|
this.on([ 'INSERT', 'UPSERT', 'UPDATE', 'DELETE' ], require('./deep').onDeep)
|
|
13
14
|
this.on([ 'SELECT' ], this.onSELECT)
|
|
14
|
-
this.on([ 'INSERT'
|
|
15
|
+
this.on([ 'INSERT' ], this.onINSERT)
|
|
16
|
+
this.on([ 'UPSERT' ], this.onUPSERT)
|
|
15
17
|
this.on([ 'UPDATE' ], this.onUPDATE)
|
|
16
18
|
this.on([ 'DELETE', 'CREATE ENTITY', 'DROP ENTITY' ], this.onSIMPLE)
|
|
17
19
|
this.on([ 'BEGIN', 'COMMIT', 'ROLLBACK' ], this.onEVENT)
|
|
@@ -35,7 +37,6 @@ class SQLService extends DatabaseService {
|
|
|
35
37
|
return cqn.SELECT.one || query.SELECT.from.ref?.[0].cardinality?.max === 1 ? rows[0] || null : rows
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
/** Handler for INSERT & UPSERT, which support bulk queries */
|
|
39
40
|
async onINSERT ({ query, data }) {
|
|
40
41
|
const { sql, entries, cqn } = this.cqn2sql (query, data)
|
|
41
42
|
if(!sql) return // Do nothing when there is nothing to be done
|
|
@@ -44,6 +45,14 @@ class SQLService extends DatabaseService {
|
|
|
44
45
|
return new this.class.InsertResults (cqn, results)
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
async onUPSERT ({ query, data }) {
|
|
49
|
+
const { sql, entries } = this.cqn2sql (query, data)
|
|
50
|
+
if(!sql) return // Do nothing when there is nothing to be done
|
|
51
|
+
const ps = await this.prepare(sql)
|
|
52
|
+
const results = entries ? await Promise.all(entries.map(e => ps.run(e))) : await ps.run()
|
|
53
|
+
return results.reduce((lastValue, currentValue) => lastValue += currentValue.changes, 0)
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
/** Handler for UPDATE */
|
|
48
57
|
async onUPDATE (req) {
|
|
49
58
|
return this.onSIMPLE(req)
|
|
@@ -127,10 +136,21 @@ class SQLService extends DatabaseService {
|
|
|
127
136
|
}
|
|
128
137
|
cqn2sql(q,values) {
|
|
129
138
|
const cqn = this.cqn4sql(q)
|
|
130
|
-
|
|
139
|
+
|
|
140
|
+
const cmd = cqn.cmd || Object.keys(cqn)[0]
|
|
141
|
+
if (cmd in { INSERT:1, DELETE: 1, UPSERT: 1, UPDATE: 1 }) {
|
|
142
|
+
let resolvedCqn = resolveView(cqn, this.model, this)
|
|
143
|
+
if (resolvedCqn && resolvedCqn[cmd]._transitions?.[0].target) {
|
|
144
|
+
resolvedCqn = resolvedCqn || cqn
|
|
145
|
+
resolvedCqn.target = resolvedCqn?.[cmd]._transitions[0].target || cqn.target
|
|
146
|
+
}
|
|
147
|
+
return (new this.class.CQN2SQL(this.context)) .render (resolvedCqn, values)
|
|
148
|
+
}
|
|
149
|
+
return (new this.class.CQN2SQL(this.context)) .render (cqn, values)
|
|
131
150
|
}
|
|
132
151
|
cqn4sql(q) {
|
|
133
|
-
|
|
152
|
+
// REVISIT: move this check to cqn4sql?
|
|
153
|
+
if (!q.SELECT?.from?.join && !this.model?.definitions[target_name4(q)]) return _unquirked(q)
|
|
134
154
|
return cqn4sql (q, this.model)
|
|
135
155
|
}
|
|
136
156
|
|
package/lib/db/sql/cqn2sql.js
CHANGED
|
@@ -14,7 +14,8 @@ const DEBUG = (()=>{
|
|
|
14
14
|
|
|
15
15
|
class CQN2SQLRenderer {
|
|
16
16
|
|
|
17
|
-
constructor() {
|
|
17
|
+
constructor(context) {
|
|
18
|
+
this.context = cds.context || context
|
|
18
19
|
this.class = new.target // for IntelliSense
|
|
19
20
|
this.class._init() // is a noop for subsequent calls
|
|
20
21
|
}
|
|
@@ -55,15 +56,21 @@ class CQN2SQLRenderer {
|
|
|
55
56
|
CREATE(q) {
|
|
56
57
|
const { target } = q, { query } = target
|
|
57
58
|
const name = this.name(target.name)
|
|
58
|
-
|
|
59
|
+
// Don't allow place holders inside views
|
|
60
|
+
delete this.values
|
|
61
|
+
this.sql = (!query || target['@cds.persistence.table'])
|
|
59
62
|
? `CREATE TABLE ${name} ( ${this.CREATE_elements(target.elements)} )`
|
|
60
63
|
: `CREATE VIEW ${name} AS ${this.SELECT(cqn4sql(query))}`
|
|
64
|
+
this.values = []
|
|
65
|
+
return
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
CREATE_elements(elements) {
|
|
64
69
|
let sql = ''
|
|
65
70
|
for (let e in elements) {
|
|
66
|
-
const
|
|
71
|
+
const definition = elements[e]
|
|
72
|
+
if(definition.isAssociation) continue
|
|
73
|
+
const s = this.CREATE_element(definition)
|
|
67
74
|
if (s) sql += `${s}, `
|
|
68
75
|
}
|
|
69
76
|
return sql.slice(0, -2)
|
|
@@ -71,7 +78,7 @@ class CQN2SQLRenderer {
|
|
|
71
78
|
|
|
72
79
|
CREATE_element(element) {
|
|
73
80
|
const type = this.type4(element)
|
|
74
|
-
if (type) return element.name + ' ' + type
|
|
81
|
+
if (type) return this.quote(element.name) + ' ' + type
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
type4 (element) {
|
|
@@ -95,10 +102,12 @@ class CQN2SQLRenderer {
|
|
|
95
102
|
Composition: () => false,
|
|
96
103
|
array: () => 'NCLOB',
|
|
97
104
|
// HANA types
|
|
105
|
+
/* Disabled as these types are linked to normal cds types
|
|
106
|
+
'cds.hana.TINYINT': () => 'REAL',
|
|
98
107
|
'cds.hana.REAL': () => 'REAL',
|
|
99
108
|
'cds.hana.CHAR': e => `CHAR(${e.length || 1})`,
|
|
100
|
-
'cds.hana.ST_POINT': () => '
|
|
101
|
-
'cds.hana.ST_GEOMETRY': () => '
|
|
109
|
+
'cds.hana.ST_POINT': () => 'ST_POINT',
|
|
110
|
+
'cds.hana.ST_GEOMETRY': () => 'ST_GEO',*/
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
|
|
@@ -316,11 +325,16 @@ class CQN2SQLRenderer {
|
|
|
316
325
|
UPSERT(q) {
|
|
317
326
|
let { UPSERT } = q, sql = this.INSERT ({__proto__:q, INSERT:UPSERT })
|
|
318
327
|
let keys = q.target?.keys; if (!keys) return this.sql = sql // REVISIT: We should converge q.target and q._target
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
328
|
+
keys = Object.keys(keys).filter(k => !keys[k].isAssociation)
|
|
329
|
+
|
|
330
|
+
let updateColumns = q.UPSERT.entries ? Object.keys(q.UPSERT.entries[0]) : this.columns
|
|
331
|
+
updateColumns = updateColumns.filter(c => !keys.includes(c)).map (c => `${this.quote(c)} = excluded.${this.quote(c)}`)
|
|
332
|
+
|
|
333
|
+
keys = keys.map(k => this.quote(k))
|
|
334
|
+
const conflict = updateColumns.length
|
|
335
|
+
? ` ON CONFLICT(${ keys }) DO UPDATE SET ` + updateColumns
|
|
336
|
+
: ` ON CONFLICT(${ keys }) DO NOTHING`
|
|
337
|
+
return this.sql = `${sql} WHERE true ${conflict}`
|
|
324
338
|
}
|
|
325
339
|
|
|
326
340
|
|
|
@@ -385,7 +399,7 @@ class CQN2SQLRenderer {
|
|
|
385
399
|
|
|
386
400
|
xpr({ xpr }) {
|
|
387
401
|
return xpr.map((x,i) => {
|
|
388
|
-
if (x in {LIKE:1,like:1} && is_regexp(xpr[i+1]?.val)) return 'regexp'
|
|
402
|
+
if (x in {LIKE:1,like:1} && is_regexp(xpr[i+1]?.val)) return this.operator('regexp')
|
|
389
403
|
if (typeof x === 'string') return this.operator(x,i,xpr)
|
|
390
404
|
if (x.xpr) return `(${this.xpr(x)})`
|
|
391
405
|
else return this.expr(x)
|
|
@@ -409,6 +423,7 @@ class CQN2SQLRenderer {
|
|
|
409
423
|
|
|
410
424
|
val({ val }) {
|
|
411
425
|
switch (typeof val) {
|
|
426
|
+
case 'function': throw new Error('Function values not supported.')
|
|
412
427
|
case 'undefined': return 'NULL'
|
|
413
428
|
case 'boolean': return val
|
|
414
429
|
case 'number': return val // REVISIT for HANA
|
|
@@ -418,6 +433,7 @@ class CQN2SQLRenderer {
|
|
|
418
433
|
if (Buffer.isBuffer(val)) val = val.toString('base64')
|
|
419
434
|
else val = this.regex(val) || this.json(val)
|
|
420
435
|
}
|
|
436
|
+
if(!this.values) return this.string(val)
|
|
421
437
|
this.values.push(val)
|
|
422
438
|
return '?'
|
|
423
439
|
}
|
|
@@ -425,7 +441,7 @@ class CQN2SQLRenderer {
|
|
|
425
441
|
static Functions = require('./func')
|
|
426
442
|
func({ func, args }) {
|
|
427
443
|
args = (args||[]).map(e => e === '*' ? e : { __proto__:e, toString:(x=e)=>this.expr(x) })
|
|
428
|
-
return this.class.Functions[func]?.(
|
|
444
|
+
return this.class.Functions[func]?.apply(this.class.Functions, args) || `${func}(${args})`
|
|
429
445
|
}
|
|
430
446
|
|
|
431
447
|
list({ list }) {
|
|
@@ -465,12 +481,9 @@ class CQN2SQLRenderer {
|
|
|
465
481
|
const inputConverterKey = this.class._convertInput
|
|
466
482
|
// Ensure that missing managed columns are added
|
|
467
483
|
const requiredColumns = !elements ? [] : Object.keys(elements)
|
|
468
|
-
.filter(e => (elements[e]?.[annotation] || (!isUpdate && elements[e]?.default)) && !columns.find(c => c.name === e))
|
|
484
|
+
.filter(e => (elements[e]?.[annotation] || (!isUpdate && elements[e]?.default && !elements[e].virtual)) && !columns.find(c => c.name === e))
|
|
469
485
|
.map(name => ({name,sql:'NULL'}))
|
|
470
486
|
|
|
471
|
-
// try to call cds.context less frequent, and have a default
|
|
472
|
-
const ctx = cds.context || { timestamp: new Date, user: cds.User.anonymous }
|
|
473
|
-
|
|
474
487
|
return [...columns,...requiredColumns].map(({name,sql}) => {
|
|
475
488
|
const element = elements?.[name] || {}
|
|
476
489
|
let extract = sql ?? `value->>'$.${name}'`
|
|
@@ -479,11 +492,11 @@ class CQN2SQLRenderer {
|
|
|
479
492
|
switch (managed) {
|
|
480
493
|
case '$user.id':
|
|
481
494
|
case '$user':
|
|
482
|
-
managed = this.string(
|
|
495
|
+
managed = this.string(this.context.user.id)
|
|
483
496
|
break
|
|
484
497
|
case '$now':
|
|
485
498
|
// REVISIT fix for date precision
|
|
486
|
-
managed = this.string(
|
|
499
|
+
managed = this.string(this.context.timestamp.toISOString())
|
|
487
500
|
break
|
|
488
501
|
default:
|
|
489
502
|
managed = undefined
|
|
@@ -491,8 +504,7 @@ class CQN2SQLRenderer {
|
|
|
491
504
|
if(!isUpdate) {
|
|
492
505
|
const d = element.default
|
|
493
506
|
if(d && (d.val !== undefined || d.ref?.[0] === '$now')) {
|
|
494
|
-
|
|
495
|
-
extract = `(CASE WHEN json_type(value,'$.${name}') IS NULL THEN ${typeof defaultValue === 'string' ? this.string(defaultValue) : defaultValue} ELSE ${extract} END)`
|
|
507
|
+
extract = `(CASE WHEN json_type(value,'$.${name}') IS NULL THEN ${this.defaultValue(d.val)} ELSE ${extract} END)`
|
|
496
508
|
}
|
|
497
509
|
}
|
|
498
510
|
return {
|
|
@@ -501,6 +513,10 @@ class CQN2SQLRenderer {
|
|
|
501
513
|
}
|
|
502
514
|
})
|
|
503
515
|
}
|
|
516
|
+
|
|
517
|
+
defaultValue(defaultValue = (this.context.timestamp).toISOString()) {
|
|
518
|
+
return typeof defaultValue === 'string' ? this.string(defaultValue) : defaultValue
|
|
519
|
+
}
|
|
504
520
|
}
|
|
505
521
|
|
|
506
522
|
// REVISIT: Workaround for JSON.stringify to work with buffers
|
package/lib/db/sql/cqn4sql.js
CHANGED
|
@@ -26,24 +26,22 @@ const {pseudos} = require('../../ql/pseudos')
|
|
|
26
26
|
*
|
|
27
27
|
* @param {object} query
|
|
28
28
|
* @param {object} model
|
|
29
|
-
* @param {object} context
|
|
30
29
|
* @returns {object} transformedQuery the transformed query
|
|
31
30
|
*/
|
|
32
|
-
function cqn4sql(query, model = cds.context?.model || cds.model
|
|
31
|
+
function cqn4sql(query, model = cds.context?.model || cds.model) {
|
|
32
|
+
const inferred = infer(query, model)
|
|
33
33
|
if(query.SELECT?.from.args && !query.joinTree)
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
let inferred
|
|
37
|
-
// do not infer again - e.g. for subquery in from
|
|
38
|
-
if(skipInfer)
|
|
39
|
-
inferred = query
|
|
40
|
-
else
|
|
41
|
-
inferred = infer(query, model)
|
|
34
|
+
return inferred
|
|
42
35
|
|
|
43
36
|
const transformedQuery = cds.ql.clone(inferred)
|
|
44
|
-
|
|
45
|
-
if(inferred.INSERT || inferred.UPSERT) return inferred // nothing to do
|
|
46
37
|
const kind = inferred.cmd || Object.keys(inferred)[0]
|
|
38
|
+
if(inferred.INSERT || inferred.UPSERT) {
|
|
39
|
+
const {as} = transformedQuery[kind].into
|
|
40
|
+
transformedQuery[kind].into = {ref: [inferred.target.name]}
|
|
41
|
+
if(as)
|
|
42
|
+
transformedQuery[kind].into.as = as
|
|
43
|
+
return transformedQuery
|
|
44
|
+
}
|
|
47
45
|
const _ = inferred[kind]
|
|
48
46
|
if (_) {
|
|
49
47
|
const { from, entity, where } = _
|
|
@@ -445,7 +443,7 @@ function cqn4sql(query, model = cds.context?.model || cds.model, skipInfer = fal
|
|
|
445
443
|
where = subqueryFollowingExists(recent = where, i).SELECT.where
|
|
446
444
|
i = where.indexOf('exists')
|
|
447
445
|
}
|
|
448
|
-
const existsIndex = recent.
|
|
446
|
+
const existsIndex = recent.indexOf('exists')
|
|
449
447
|
recent.splice (existsIndex, 2, ...where.map (
|
|
450
448
|
(x) => {
|
|
451
449
|
return replaceAliasWithSubqueryAlias(x)
|
|
@@ -838,7 +836,11 @@ function cqn4sql(query, model = cds.context?.model || cds.model, skipInfer = fal
|
|
|
838
836
|
} else if(token.xpr) {
|
|
839
837
|
result.xpr = getTransformedTokenStream(token.xpr, $baseLink)
|
|
840
838
|
} else if(token.func && token.args) {
|
|
841
|
-
result.args =
|
|
839
|
+
result.args = token.args.map(t => {
|
|
840
|
+
if(!t.val) // this must not be touched
|
|
841
|
+
return getTransformedTokenStream([t], $baseLink)[0]
|
|
842
|
+
return t
|
|
843
|
+
})
|
|
842
844
|
}
|
|
843
845
|
|
|
844
846
|
transformedWhere.push(result)
|
|
@@ -1175,6 +1177,7 @@ function cqn4sql(query, model = cds.context?.model || cds.model, skipInfer = fal
|
|
|
1175
1177
|
// REVISIT: this whole section needs a refactoring, it is too complex and some edge cases may still be not considered...
|
|
1176
1178
|
const refLinkFaker = (thing) => {
|
|
1177
1179
|
const {ref} = thing
|
|
1180
|
+
const assocHost = getParentEntity(assocRefLink.definition)
|
|
1178
1181
|
Object.defineProperty(thing, '$refLinks', {
|
|
1179
1182
|
value: [],
|
|
1180
1183
|
writable: true,
|
|
@@ -1186,7 +1189,7 @@ function cqn4sql(query, model = cds.context?.model || cds.model, skipInfer = fal
|
|
|
1186
1189
|
const target = getParentEntity(prev)
|
|
1187
1190
|
thing.$refLinks[i] = {definition, target, alias: definition.name}
|
|
1188
1191
|
return prev?.elements?.[res] || prev?._target?.elements[res] || pseudos.elements[res]
|
|
1189
|
-
},
|
|
1192
|
+
}, assocHost)
|
|
1190
1193
|
};
|
|
1191
1194
|
|
|
1192
1195
|
// comparison in on condition needs to be expanded...
|
|
@@ -1272,12 +1275,18 @@ function cqn4sql(query, model = cds.context?.model || cds.model, skipInfer = fal
|
|
|
1272
1275
|
}
|
|
1273
1276
|
return result
|
|
1274
1277
|
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Recursively calculates the containing entity for a given element.
|
|
1280
|
+
*
|
|
1281
|
+
* @param {CSN.element} element
|
|
1282
|
+
* @returns {CSN.definition} the entity containing the given element
|
|
1283
|
+
*/
|
|
1275
1284
|
function getParentEntity(element) {
|
|
1276
1285
|
if(!element.kind) // pseudo element
|
|
1277
1286
|
return element
|
|
1278
1287
|
if(element.kind === 'entity')
|
|
1279
1288
|
return element
|
|
1280
|
-
else return getParentEntity(element.parent)
|
|
1289
|
+
else return model.definitions[localized(getParentEntity(element.parent))]
|
|
1281
1290
|
}
|
|
1282
1291
|
}
|
|
1283
1292
|
|
|
@@ -1372,7 +1381,7 @@ function cqn4sql(query, model = cds.context?.model || cds.model, skipInfer = fal
|
|
|
1372
1381
|
const SELECT = {
|
|
1373
1382
|
from: {
|
|
1374
1383
|
ref: [
|
|
1375
|
-
nextDefinition
|
|
1384
|
+
localized(assocTarget(nextDefinition) || nextDefinition)
|
|
1376
1385
|
],
|
|
1377
1386
|
as: next.alias
|
|
1378
1387
|
},
|
|
@@ -1397,9 +1406,8 @@ function cqn4sql(query, model = cds.context?.model || cds.model, skipInfer = fal
|
|
|
1397
1406
|
function localized(definition) {
|
|
1398
1407
|
if(!isLocalized(definition))
|
|
1399
1408
|
return definition.name
|
|
1400
|
-
const
|
|
1401
|
-
|
|
1402
|
-
return view?.name || model.definitions[`localized.${definition.name}`]?.name || definition.name
|
|
1409
|
+
const view = model.definitions[`localized.${definition.name}`]
|
|
1410
|
+
return view?.name || definition.name
|
|
1403
1411
|
}
|
|
1404
1412
|
|
|
1405
1413
|
/**
|
|
@@ -1414,6 +1422,16 @@ function cqn4sql(query, model = cds.context?.model || cds.model, skipInfer = fal
|
|
|
1414
1422
|
return inferred.SELECT?.localized && definition['@cds.localized'] !== false
|
|
1415
1423
|
}
|
|
1416
1424
|
|
|
1425
|
+
/**
|
|
1426
|
+
* Get the csn definition of the target of a given association
|
|
1427
|
+
*
|
|
1428
|
+
* @param assoc
|
|
1429
|
+
* @returns the csn definition of the association target or null if it is not an association
|
|
1430
|
+
*/
|
|
1431
|
+
function assocTarget(assoc) {
|
|
1432
|
+
return model.definitions[assoc.target] || null
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1417
1435
|
/**
|
|
1418
1436
|
* Calculate the flat name for a deeply nested element:
|
|
1419
1437
|
* @example `entity E { struct: { foo: String} }` => `getFullName(foo)` => `struct_foo`
|
|
@@ -1455,7 +1473,5 @@ module.exports = cqn4sql
|
|
|
1455
1473
|
function hasLogicalOr(tokenStream) {
|
|
1456
1474
|
return tokenStream.some(t => t in {'OR': true, 'or': true})
|
|
1457
1475
|
}
|
|
1458
|
-
// revisit don't do sqlite specific stuff here
|
|
1459
|
-
const _translations = cds.env.i18n.for_sqlite.reduce((all, l) => ((all[l] = true), all), {})
|
|
1460
1476
|
const idOnly = (ref) => ref.id || ref
|
|
1461
1477
|
const is_regexp = x => x?.constructor?.name === 'RegExp' // NOTE: x instanceof RegExp doesn't work in repl
|
package/lib/db/sql/deep.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const cds = require('@sap/cds')
|
|
2
|
-
const { getDBTable } = require('@sap/cds/libx/_runtime/common/utils/resolveView')
|
|
3
2
|
const { compareJson } = require('@sap/cds/libx/_runtime/cds-services/services/utils/compareJson')
|
|
4
3
|
const { target_name4 } = require('./utils')
|
|
5
4
|
|
|
@@ -17,7 +16,11 @@ async function onDeep (req, next) {
|
|
|
17
16
|
if (!hasDeep(query, target)) return next()
|
|
18
17
|
const beforeData = query.INSERT ? [] : await this.run(getExpandForDeep(query, target, true))
|
|
19
18
|
const queries = getDeepQueries(query, beforeData, target)
|
|
20
|
-
const res = await Promise.all(queries.map (
|
|
19
|
+
const res = await Promise.all(queries.map (query => {
|
|
20
|
+
if (query.INSERT) return this.onINSERT({query})
|
|
21
|
+
if (query.UPDATE) return this.onUPDATE({query})
|
|
22
|
+
if (query.DELETE) return this.onSIMPLE({query})
|
|
23
|
+
}))
|
|
21
24
|
return res[0] ?? 0 // TODO what todo with multiple result responses?
|
|
22
25
|
}
|
|
23
26
|
|
|
@@ -61,14 +64,22 @@ const getColumnsFromDataOrKeys = (data, target) => {
|
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
66
|
|
|
64
|
-
const _calculateExpandColumns = (
|
|
67
|
+
const _calculateExpandColumns = (target, data, expandColumns = [], elementMap = new Map()) => {
|
|
68
|
+
const compositions = target.compositions || {}
|
|
69
|
+
|
|
70
|
+
if(expandColumns.length === 0) {
|
|
71
|
+
// REVISIT: ensure that all keys are included in the expand columns
|
|
72
|
+
expandColumns.push(...getColumnsFromDataOrKeys(data, target))
|
|
73
|
+
}
|
|
74
|
+
|
|
65
75
|
for (const compName in compositions) {
|
|
66
76
|
let compositionData
|
|
67
|
-
if (data === null) {
|
|
77
|
+
if (data === null || (Array.isArray(data) && !data.length)) {
|
|
68
78
|
compositionData = null
|
|
69
79
|
} else {
|
|
70
80
|
compositionData = data[compName]
|
|
71
81
|
}
|
|
82
|
+
|
|
72
83
|
// ignore not provided compositions as nothing happens with them (expect deep delete)
|
|
73
84
|
if (compositionData === undefined) {
|
|
74
85
|
// fill columns in case
|
|
@@ -90,37 +101,30 @@ const _calculateExpandColumns = (compositions, data, expandColumns = [], element
|
|
|
90
101
|
ref: [composition.name],
|
|
91
102
|
expand: getColumnsFromDataOrKeys(compositionData, composition._target)
|
|
92
103
|
}
|
|
93
|
-
if (expandColumns.length === 0) {
|
|
94
|
-
expandColumns.push(...getColumnsFromDataOrKeys(data, composition.parent))
|
|
95
|
-
}
|
|
96
104
|
|
|
97
105
|
expandColumns.push(expandColumn)
|
|
98
106
|
}
|
|
99
107
|
|
|
100
|
-
const targetCompositions = composition._target.compositions
|
|
101
|
-
|
|
102
108
|
// expand deep
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
109
|
+
// Make a copy and do not share the same map among brother compositions
|
|
110
|
+
// as we're only interested in deep recursions, not wide recursions.
|
|
111
|
+
const newElementMap = new Map(elementMap)
|
|
112
|
+
newElementMap.set(fqn, (seen && seen + 1) || 1)
|
|
113
|
+
|
|
114
|
+
if (composition.is2many) {
|
|
115
|
+
// expandColumn.expand = getColumnsFromDataOrKeys(compositionData, composition._target)
|
|
116
|
+
if (compositionData === null || compositionData.length === 0) {
|
|
117
|
+
// deep delete, get all subitems until recursion depth
|
|
118
|
+
_calculateExpandColumns(composition._target, null, expandColumn.expand, newElementMap)
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
116
121
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
} else {
|
|
121
|
-
// to one
|
|
122
|
-
_calculateExpandColumns(targetCompositions, compositionData, expandColumn.expand, newElementMap)
|
|
122
|
+
for (const row of compositionData) {
|
|
123
|
+
_calculateExpandColumns(composition._target, row, expandColumn.expand, newElementMap)
|
|
123
124
|
}
|
|
125
|
+
} else {
|
|
126
|
+
// to one
|
|
127
|
+
_calculateExpandColumns(composition._target, compositionData, expandColumn.expand, newElementMap)
|
|
124
128
|
}
|
|
125
129
|
}
|
|
126
130
|
}
|
|
@@ -133,10 +137,8 @@ const getExpandForDeep = (query, target) => {
|
|
|
133
137
|
const cqn = SELECT.from(from)
|
|
134
138
|
if (where) cqn.SELECT.where = where
|
|
135
139
|
|
|
136
|
-
const compositions = target.compositions || {}
|
|
137
|
-
|
|
138
140
|
const columns = []
|
|
139
|
-
_calculateExpandColumns(
|
|
141
|
+
_calculateExpandColumns(target, data, columns)
|
|
140
142
|
cqn.columns(columns)
|
|
141
143
|
return cqn
|
|
142
144
|
}
|
|
@@ -175,16 +177,10 @@ const _getDeepQueries = (diff, target) => {
|
|
|
175
177
|
|
|
176
178
|
if (target.elements[prop] && _hasPersistenceSkip(target.elements[prop]._target)) {
|
|
177
179
|
delete diffEntry[prop]
|
|
178
|
-
} else if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
delete diffEntry[prop]
|
|
182
|
-
} else if (Array.isArray(propData)) {
|
|
183
|
-
// REVISIT can be arrayed too
|
|
184
|
-
propData.forEach(subEntry => {
|
|
185
|
-
if (subEntry._op) {
|
|
180
|
+
} else if (target.compositions?.[prop]) {
|
|
181
|
+
const arrayed = Array.isArray(propData) ? propData : [propData]
|
|
182
|
+
arrayed.forEach(subEntry => {
|
|
186
183
|
subQueries.push(..._getDeepQueries([subEntry], target.elements[prop]._target))
|
|
187
|
-
}
|
|
188
184
|
})
|
|
189
185
|
delete diffEntry[prop]
|
|
190
186
|
}
|
|
@@ -198,16 +194,17 @@ const _getDeepQueries = (diff, target) => {
|
|
|
198
194
|
delete diffEntry._old
|
|
199
195
|
}
|
|
200
196
|
|
|
201
|
-
// first calculate
|
|
197
|
+
// first calculate subqueries and rm their properties, then build root query
|
|
202
198
|
if (op === 'create') {
|
|
203
|
-
queries.push(INSERT.into(
|
|
199
|
+
queries.push(INSERT.into(target).entries(diffEntry))
|
|
204
200
|
} else if (op === 'delete') {
|
|
205
|
-
queries.push(DELETE.from(
|
|
201
|
+
queries.push(DELETE.from(target).where(diffEntry))
|
|
206
202
|
} else if (op === 'update') {
|
|
207
203
|
// TODO do we need the where here?
|
|
208
204
|
const keys = target.keys
|
|
209
|
-
const cqn = UPDATE(
|
|
205
|
+
const cqn = UPDATE(target).with(diffEntry)
|
|
210
206
|
for (const key in keys) {
|
|
207
|
+
if (keys[key].virtual) continue
|
|
211
208
|
if (!keys[key].isAssociation) {
|
|
212
209
|
cqn.where(key + '=', diffEntry[key])
|
|
213
210
|
}
|
package/lib/db/sql/func.js
CHANGED
|
@@ -4,14 +4,15 @@ const StandardFunctions = {
|
|
|
4
4
|
|
|
5
5
|
// String and Collection Functions
|
|
6
6
|
// length : (x) => `length(${x})`,
|
|
7
|
-
search: (ref, arg)
|
|
7
|
+
search: function(ref, arg) {
|
|
8
8
|
if (!('val' in arg)) throw `SQLite only supports single value arguments for $search`
|
|
9
9
|
const refs = ref.list || [ref], {toString} = ref
|
|
10
|
-
return '(' + refs.map(ref2 =>
|
|
10
|
+
return '(' + refs.map(ref2 => this.contains(this.tolower(toString(ref2)),this.tolower(arg))).join(' or ') + ')'
|
|
11
11
|
},
|
|
12
12
|
concat : (...args) => args.join('||'),
|
|
13
13
|
contains : (...args) => `ifnull(instr(${args}),0)`,
|
|
14
14
|
count : (x) => `count(${x||'*'})`,
|
|
15
|
+
countdistinct : (x) => `count(distinct ${x||'*'})`,
|
|
15
16
|
indexof : (x,y) => `instr(${x},${y}) - 1`, // sqlite instr is 1 indexed
|
|
16
17
|
startswith : (x,y) => `instr(${x},${y}) = 1`, // sqlite instr is 1 indexed
|
|
17
18
|
endswith : (x,y) => `instr(${x},${y}) = length(${x}) - length(${y}) +1`,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const { resolveView } = require('@sap/cds/libx/_runtime/common/utils/resolveView')
|
|
2
1
|
const cds = require('@sap/cds')
|
|
3
2
|
const propagateForeignKeys = require('@sap/cds/libx/_runtime/common/utils/propagateForeignKeys')
|
|
4
3
|
const { enrichDataWithKeysFromWhere } = require('@sap/cds/libx/_runtime/common/utils/keys')
|
|
@@ -38,7 +37,7 @@ const generateUUIDandPropagateKeys = (target, data, event) => {
|
|
|
38
37
|
|
|
39
38
|
const input = async function (req, next) {
|
|
40
39
|
// REVISIT dummy handler until we have input processing
|
|
41
|
-
if (!req.target || !this.model) return next()
|
|
40
|
+
if (!req.target || !this.model || req.target._unresolved) return next()
|
|
42
41
|
|
|
43
42
|
if (req.event === "UPDATE") {
|
|
44
43
|
// REVISIT for deep update we need to inject the keys first
|
|
@@ -56,15 +55,6 @@ const input = async function (req, next) {
|
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
const cmd = req.query.cmd || Object.keys(req.query)[0]
|
|
60
|
-
|
|
61
|
-
const resolved = resolveView(req.query, this.model, this)
|
|
62
|
-
|
|
63
|
-
if (resolved && resolved[cmd]._transitions?.[0].target) {
|
|
64
|
-
req.query = resolved || req.query
|
|
65
|
-
req.target = resolved?.[cmd]._transitions[0].target
|
|
66
|
-
}
|
|
67
|
-
|
|
68
58
|
return next()
|
|
69
59
|
}
|
|
70
60
|
|
|
@@ -96,6 +96,11 @@ class SQLiteService extends SQLService {
|
|
|
96
96
|
Double: expr => `nullif(quote(${expr}),'NULL')->'$'`,
|
|
97
97
|
struct: expr => `${expr}->'$'`, // Association + Composition inherits from struct
|
|
98
98
|
array: expr => `${expr}->'$'`,
|
|
99
|
+
// REVISIT: Timestamp should not loos precision
|
|
100
|
+
Date: e => `strftime('%Y-%m-%d',${e})`,
|
|
101
|
+
Time: e => `strftime('%H:%M:%S',${e})`,
|
|
102
|
+
DateTime: e => `strftime('%Y-%m-%dT%H:%M:%SZ',${e})`,
|
|
103
|
+
Timestamp: e => `strftime('%Y-%m-%dT%H:%M:%fZ',${e})`,
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
// Used for SQL function expressions
|
package/lib/ql/cds.infer.js
CHANGED
|
@@ -129,12 +129,14 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
129
129
|
* @param {object} arg the arg which shall be augmented
|
|
130
130
|
* @param {$refLink} $baseLink environment where the first `ref` step shall be resolved in.
|
|
131
131
|
* For infix filter / expand columns
|
|
132
|
-
* @param {boolean}
|
|
132
|
+
* @param {boolean} expandOrExists whether the `arg` is part of a `column.expand` /
|
|
133
|
+
* preceded by an `exists`.
|
|
134
|
+
* In those cases, unmanaged association paths are allowed .
|
|
133
135
|
*/
|
|
134
|
-
function attachRefLinksToArg(arg, $baseLink = null,
|
|
136
|
+
function attachRefLinksToArg(arg, $baseLink = null, expandOrExists = false) {
|
|
135
137
|
const { ref, xpr } = arg;
|
|
136
138
|
if(xpr)
|
|
137
|
-
xpr.forEach(t => attachRefLinksToArg(t, $baseLink,
|
|
139
|
+
xpr.forEach(t => attachRefLinksToArg(t, $baseLink, expandOrExists))
|
|
138
140
|
if (!ref)
|
|
139
141
|
return;
|
|
140
142
|
init$refLinks(arg);
|
|
@@ -150,10 +152,10 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
150
152
|
if (e.target) { // only fk access in infix filter
|
|
151
153
|
const nextStep = ref[1]?.id || ref[1];
|
|
152
154
|
// no unmanaged assoc in infix filter path
|
|
153
|
-
if (!
|
|
154
|
-
throw new Error(`"${ e.name }" in path "${ arg.ref.join('.') }" must not be an unmanaged association`);
|
|
155
|
+
if (!expandOrExists && e.on)
|
|
156
|
+
throw new Error(`"${ e.name }" in path "${ arg.ref.map(idOnly).join('.') }" must not be an unmanaged association`);
|
|
155
157
|
// no non-fk traversal in infix filter
|
|
156
|
-
if (!
|
|
158
|
+
if (!expandOrExists && nextStep && !(nextStep in e.foreignKeys))
|
|
157
159
|
throw new Error(`Only foreign keys of "${ e.name }" can be accessed in infix filter`);
|
|
158
160
|
}
|
|
159
161
|
arg.$refLinks.push({ definition: e, target: e._target || e });
|
|
@@ -179,8 +181,22 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
179
181
|
|
|
180
182
|
// link refs in where
|
|
181
183
|
if (step.where) { // REVISIT: why do we need to walk through these so early?
|
|
182
|
-
if (arg.$refLinks[i].definition.kind === 'entity' || arg.$refLinks[i].definition._target)
|
|
183
|
-
|
|
184
|
+
if (arg.$refLinks[i].definition.kind === 'entity' || arg.$refLinks[i].definition._target) {
|
|
185
|
+
let existsPredicate = false
|
|
186
|
+
const walkTokenStream = (token) => {
|
|
187
|
+
if (token === 'exists') { // no joins for infix filters along `exists <path>`
|
|
188
|
+
existsPredicate = true;
|
|
189
|
+
} else if(token.xpr) {
|
|
190
|
+
// don't miss an exists within an expression
|
|
191
|
+
token.xpr.forEach(walkTokenStream)
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
attachRefLinksToArg(token, arg.$refLinks[i], existsPredicate)
|
|
195
|
+
existsPredicate = false
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
step.where.forEach(walkTokenStream);
|
|
199
|
+
}
|
|
184
200
|
else
|
|
185
201
|
throw new Error('A filter can only be provided when navigating along associations');
|
|
186
202
|
}
|
|
@@ -314,7 +330,6 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
314
330
|
}
|
|
315
331
|
}
|
|
316
332
|
where.forEach(walkTokenStream);
|
|
317
|
-
|
|
318
333
|
}
|
|
319
334
|
if (groupBy) // link $refLinks
|
|
320
335
|
groupBy.forEach(token => inferQueryElement(token, false));
|
|
@@ -341,7 +356,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
341
356
|
* In some cases, no joins must be created for non-assoc path traversals:
|
|
342
357
|
* - for infix filters in `exists assoc[parent.foo='bar']` -> part of semi join
|
|
343
358
|
*/
|
|
344
|
-
function inferQueryElement(column, insertIntoQueryElements = true, $baseLink = null, inExists = false
|
|
359
|
+
function inferQueryElement(column, insertIntoQueryElements = true, $baseLink = null, inExists = false) {
|
|
345
360
|
if (column.param)
|
|
346
361
|
return; // parameter references are only resolved into values on execution e.g. :val, :1 or ?
|
|
347
362
|
if (column.args)
|
|
@@ -364,6 +379,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
364
379
|
const firstStepIsSelf
|
|
365
380
|
= !firstStepIsTableAlias && column.ref.length > 1 && [ '$self', '$projection' ].includes(column.ref[0]);
|
|
366
381
|
const nameSegments = [];
|
|
382
|
+
const skipNameStack = [];
|
|
367
383
|
let pseudoPath = false;
|
|
368
384
|
column.ref.forEach((step, i) => {
|
|
369
385
|
const id = step.id || step;
|
|
@@ -377,8 +393,6 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
377
393
|
const { definition, target } = $baseLink;
|
|
378
394
|
const elements = definition.elements || definition._target?.elements;
|
|
379
395
|
if (elements && id in elements) {
|
|
380
|
-
if (!inExists && isStepJoinRelevant(definition, id, inInline ? null : $baseLink))
|
|
381
|
-
Object.defineProperty(column, 'isJoinRelevant', { value: true });
|
|
382
396
|
column.$refLinks.push({ definition: elements[id], target });
|
|
383
397
|
}
|
|
384
398
|
else {
|
|
@@ -415,8 +429,6 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
415
429
|
const elements = definition.elements || definition._target?.elements;
|
|
416
430
|
if (elements && id in elements) {
|
|
417
431
|
const $refLink = { definition: elements[id], target: column.$refLinks[i - 1].target };
|
|
418
|
-
if (!inExists && isStepJoinRelevant(definition, id))
|
|
419
|
-
Object.defineProperty(column, 'isJoinRelevant', { value: true });
|
|
420
432
|
column.$refLinks.push($refLink);
|
|
421
433
|
}
|
|
422
434
|
else if (firstStepIsSelf) {
|
|
@@ -436,8 +448,19 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
436
448
|
stepNotFoundInPredecessor(id, notFoundIn);
|
|
437
449
|
}
|
|
438
450
|
const foreignKeyAlias = Array.isArray(definition.keys)
|
|
439
|
-
? definition.keys.find(k =>
|
|
440
|
-
|
|
451
|
+
? definition.keys.find(k =>{
|
|
452
|
+
if(k.ref[0] === id) {
|
|
453
|
+
skipNameStack.push(...k.ref.slice(1))
|
|
454
|
+
return true
|
|
455
|
+
}
|
|
456
|
+
return false
|
|
457
|
+
})?.as : null;
|
|
458
|
+
if(foreignKeyAlias)
|
|
459
|
+
nameSegments.push(foreignKeyAlias);
|
|
460
|
+
else if(skipNameStack[0] === id)
|
|
461
|
+
skipNameStack.shift()
|
|
462
|
+
else
|
|
463
|
+
nameSegments.push(id)
|
|
441
464
|
}
|
|
442
465
|
|
|
443
466
|
if (step.where) {
|
|
@@ -455,6 +478,9 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
455
478
|
else if (token.ref || token.xpr) {
|
|
456
479
|
inferQueryElement(token, false, column.$refLinks[i], skipJoinsForFilter);
|
|
457
480
|
}
|
|
481
|
+
else if (token.func) {
|
|
482
|
+
token.args?.forEach(arg => inferQueryElement(arg, false, column.$refLinks[i], skipJoinsForFilter));
|
|
483
|
+
}
|
|
458
484
|
});
|
|
459
485
|
}
|
|
460
486
|
|
|
@@ -503,9 +529,11 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
503
529
|
return;
|
|
504
530
|
}
|
|
505
531
|
}
|
|
506
|
-
|
|
507
|
-
if (!inExists && column
|
|
532
|
+
// check if we need to merg the column `ref` into the join tree of the query
|
|
533
|
+
if (!inExists && isColumnJoinRelevant(column)) {
|
|
534
|
+
Object.defineProperty(column, 'isJoinRelevant', { value: true });
|
|
508
535
|
joinTree.mergeColumn(column);
|
|
536
|
+
}
|
|
509
537
|
|
|
510
538
|
|
|
511
539
|
function resolveInline(col, namePrefix = col.as || col.flatName) {
|
|
@@ -623,10 +651,53 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
623
651
|
}
|
|
624
652
|
}
|
|
625
653
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
654
|
+
/**
|
|
655
|
+
* Checks whether or not the `ref` of the given column is join relevant.
|
|
656
|
+
* A `ref` is considered join relevant if it includes an association traversal and:
|
|
657
|
+
* - the association is unmanaged
|
|
658
|
+
* - a non-foreign key access is performed
|
|
659
|
+
* - an infix filter is applied at the association
|
|
660
|
+
*
|
|
661
|
+
* @param {object} column the column with the `ref` to check for join relevance
|
|
662
|
+
* @returns {boolean} true if the column ref needs to be merged into a join tree
|
|
663
|
+
*/
|
|
664
|
+
function isColumnJoinRelevant(column) {
|
|
665
|
+
let fkAccess = false;
|
|
666
|
+
let assoc = null
|
|
667
|
+
for(let i = 0; i<column.ref.length; i++) {
|
|
668
|
+
const ref = column.ref[i];
|
|
669
|
+
const link = column.$refLinks[i];
|
|
670
|
+
if(link.definition.on) {
|
|
671
|
+
if(!column.ref[i+1]) {
|
|
672
|
+
if(column.expand && assoc)
|
|
673
|
+
return true;
|
|
674
|
+
// if unmanaged assoc is exposed, ignore it
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
if(assoc && assoc.keys?.some(key => key.ref[0] === ref)) {
|
|
680
|
+
fkAccess = true
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
if(link.definition.target && link.definition.keys) {
|
|
684
|
+
if(column.ref[i+1] || assoc)
|
|
685
|
+
fkAccess = false;
|
|
686
|
+
else
|
|
687
|
+
fkAccess = true
|
|
688
|
+
assoc = link.definition;
|
|
689
|
+
if(ref.where) {
|
|
690
|
+
// always join relevant except for expand assoc
|
|
691
|
+
if(column.expand && !column.ref[i+1])
|
|
692
|
+
return false;
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if(!assoc) return false
|
|
699
|
+
if(fkAccess) return false
|
|
700
|
+
else return true
|
|
630
701
|
}
|
|
631
702
|
|
|
632
703
|
/**
|
package/lib/ql/join-tree.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/sqlite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CDS database service for SQLite",
|
|
5
5
|
"homepage": "https://cap.cloud.sap/",
|
|
6
6
|
"keywords": [
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
],
|
|
11
11
|
"author": "SAP SE (https://www.sap.com)",
|
|
12
12
|
"license": "SEE LICENSE",
|
|
13
|
-
"homepage": "https://cap.cloud.sap/",
|
|
14
13
|
"main": "index.js",
|
|
15
14
|
"files": [
|
|
16
15
|
"cds.js",
|
|
@@ -30,23 +29,27 @@
|
|
|
30
29
|
"pg:up": "docker-compose -f etc/pg-stack.yml up -d",
|
|
31
30
|
"prettier": "npx prettier --write .",
|
|
32
31
|
"test": "jest --silent",
|
|
32
|
+
"test:all": "npm run test:none && npm run test:sqlite && npm run test:pg",
|
|
33
|
+
"test:none": "DB=none npm run test",
|
|
34
|
+
"test:sqlite": "DB=sqlite npm run test",
|
|
35
|
+
"test:pg": "npm run pg:up && DB=pg npm run test",
|
|
33
36
|
"lint": "npx eslint . && npx prettier --check ."
|
|
34
37
|
},
|
|
35
38
|
"dependencies": {
|
|
36
39
|
"better-sqlite3": "^8"
|
|
37
40
|
},
|
|
38
41
|
"peerDependencies": {
|
|
39
|
-
"@sap/cds": "
|
|
42
|
+
"@sap/cds": ">=6.8.0"
|
|
40
43
|
},
|
|
41
44
|
"devDependencies": {
|
|
42
|
-
"@capire/sflight": "sap-samples/cap-sflight",
|
|
43
45
|
"@cap-js/sqlite": ".",
|
|
46
|
+
"@capire/sflight": "sap-samples/cap-sflight",
|
|
44
47
|
"axios": ">=1.3",
|
|
45
48
|
"chai": "^4.3.7",
|
|
46
49
|
"chai-as-promised": "^7.1.1",
|
|
47
50
|
"chai-subset": "^1.6.0",
|
|
48
51
|
"express": "^4",
|
|
49
|
-
"
|
|
50
|
-
"
|
|
52
|
+
"jest": "^29",
|
|
53
|
+
"pg": "^8"
|
|
51
54
|
}
|
|
52
55
|
}
|