@sap/cds-dk 10.0.3 → 10.0.5
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 +28 -0
- package/bin/debug.js +6 -2
- package/bin/up.js +1 -1
- package/bin/watch.js +1 -0
- package/lib/build/plugins/helm/files/templates/_deployment_helpers.tpl +2 -0
- package/lib/build/plugins/nodejs/index.js +6 -3
- package/lib/build/util.js +1 -1
- package/lib/init/template/ams/files/containerize.yaml.hbs +1 -0
- package/lib/init/template/approuter/files/values.yaml.hbs +9 -1
- package/lib/init/template/data/as-csv.js +4 -2
- package/lib/init/template/data/as-json.js +4 -1
- package/lib/init/template/github-actions/files/workflows/cf.yaml.hbs +6 -6
- package/lib/init/template/ias/files/values.yaml.hbs +4 -4
- package/lib/init/template/portal/files/values.yaml.hbs +12 -0
- package/lib/init/template/sample/files/app/browse/fiori-service.cds +2 -0
- package/lib/init/template/typer/files/xsconfig.json +1 -1
- package/lib/init/template/typescript/files/package.json +1 -1
- package/lib/lint/eslintHelp.json +1 -1
- package/lib/watch/esm-resolve-registration.js +27 -0
- package/lib/watch/vite-watch.js +1 -1
- package/node_modules/@cap-js/db-service/CHANGELOG.md +768 -0
- package/node_modules/@cap-js/db-service/LICENSE +201 -0
- package/node_modules/@cap-js/db-service/README.md +26 -0
- package/node_modules/@cap-js/db-service/index.js +19 -0
- package/node_modules/@cap-js/db-service/lib/InsertResults.js +122 -0
- package/node_modules/@cap-js/db-service/lib/SQLService.js +581 -0
- package/node_modules/@cap-js/db-service/lib/common/DatabaseService.js +179 -0
- package/node_modules/@cap-js/db-service/lib/common/factory.d.ts +5 -0
- package/node_modules/@cap-js/db-service/lib/common/generic-pool.js +310 -0
- package/node_modules/@cap-js/db-service/lib/common/session-context.js +46 -0
- package/node_modules/@cap-js/db-service/lib/converters.d.ts +24 -0
- package/node_modules/@cap-js/db-service/lib/cql-functions.js +464 -0
- package/node_modules/@cap-js/db-service/lib/cqn2pql.js +116 -0
- package/node_modules/@cap-js/db-service/lib/cqn2sql.js +1612 -0
- package/node_modules/@cap-js/db-service/lib/cqn4sql.js +2895 -0
- package/node_modules/@cap-js/db-service/lib/deep-queries.js +290 -0
- package/node_modules/@cap-js/db-service/lib/fill-in-keys.js +75 -0
- package/node_modules/@cap-js/db-service/lib/infer/cqn.d.ts +42 -0
- package/node_modules/@cap-js/db-service/lib/infer/index.js +1290 -0
- package/node_modules/@cap-js/db-service/lib/infer/join-tree.js +278 -0
- package/node_modules/@cap-js/db-service/lib/infer/pseudos.js +24 -0
- package/node_modules/@cap-js/db-service/lib/search.js +220 -0
- package/node_modules/@cap-js/db-service/lib/utils.js +177 -0
- package/node_modules/@cap-js/db-service/package.json +36 -0
- package/node_modules/@cap-js/sqlite/CHANGELOG.md +404 -0
- package/node_modules/@cap-js/sqlite/LICENSE +201 -0
- package/node_modules/@cap-js/sqlite/README.md +34 -0
- package/node_modules/@cap-js/sqlite/cds-plugin.js +0 -0
- package/node_modules/@cap-js/sqlite/index.js +2 -0
- package/node_modules/@cap-js/sqlite/lib/SQLiteService.js +324 -0
- package/node_modules/@cap-js/sqlite/lib/cql-functions.js +160 -0
- package/node_modules/@cap-js/sqlite/lib/node-sqlite.js +36 -0
- package/node_modules/@cap-js/sqlite/lib/session.json +3 -0
- package/node_modules/@cap-js/sqlite/lib/sql.js.js +99 -0
- package/node_modules/@cap-js/sqlite/package.json +68 -0
- package/node_modules/@sap/cds-compiler/CHANGELOG.md +14 -0
- package/node_modules/@sap/cds-compiler/bin/cdsse.js +1 -1
- package/node_modules/@sap/cds-compiler/bin/cdsv2m.js +1 -1
- package/node_modules/@sap/cds-compiler/doc/IncompatibleChanges_v7.md +20 -13
- package/node_modules/@sap/cds-compiler/lib/base/builtins.js +4 -1
- package/node_modules/@sap/cds-compiler/lib/base/message-registry.js +15 -2
- package/node_modules/@sap/cds-compiler/lib/checks/defaultValues.js +0 -1
- package/node_modules/@sap/cds-compiler/lib/compiler/resolve.js +6 -5
- package/node_modules/@sap/cds-compiler/lib/compiler/tweak-assocs.js +27 -6
- package/node_modules/@sap/cds-compiler/lib/edm/annotations/edmJson.js +5 -0
- package/node_modules/@sap/cds-compiler/lib/json/from-csn.js +3 -3
- package/node_modules/@sap/cds-compiler/lib/json/to-csn.js +5 -4
- package/node_modules/@sap/cds-compiler/lib/parsers/AstBuildingParser.js +14 -3
- package/node_modules/@sap/cds-compiler/lib/tool-lib/enrichCsn.js +1 -1
- package/node_modules/@sap/cds-compiler/lib/transform/db/cdsPersistence.js +3 -2
- package/node_modules/@sap/cds-compiler/lib/transform/db/views.js +2 -2
- package/node_modules/@sap/cds-compiler/lib/transform/transformUtils.js +1 -2
- package/node_modules/@sap/cds-compiler/package.json +1 -1
- package/node_modules/iconv-lite/encodings/sbcs-data.js +2 -0
- package/node_modules/iconv-lite/encodings/utf32.js +10 -3
- package/node_modules/iconv-lite/package.json +2 -2
- package/node_modules/iconv-lite/types/encodings.d.ts +2 -0
- package/node_modules/ws/lib/receiver.js +15 -32
- package/node_modules/ws/lib/websocket-server.js +4 -4
- package/node_modules/ws/lib/websocket.js +4 -4
- package/node_modules/ws/package.json +5 -1
- package/package.json +5 -3
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
|
+
const DEBUG = cds.log('sql|db')
|
|
3
|
+
const { Readable, Transform } = require('stream')
|
|
4
|
+
const { pipeline } = require('stream/promises')
|
|
5
|
+
const DatabaseService = require('./common/DatabaseService')
|
|
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' }
|
|
12
|
+
|
|
13
|
+
const BINARY_TYPES = {
|
|
14
|
+
'cds.Binary': 1,
|
|
15
|
+
'cds.hana.BINARY': 1
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Checks if parameter is an object that at least contains one property.
|
|
20
|
+
*
|
|
21
|
+
* @param {*} obj
|
|
22
|
+
* @returns Boolean
|
|
23
|
+
*/
|
|
24
|
+
const _hasProps = (obj) => {
|
|
25
|
+
if (!obj) return false
|
|
26
|
+
for (const p in obj) {
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @typedef {import('@sap/cds/apis/services').Request} Request */
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @callback Handler
|
|
36
|
+
* @param {Request} req
|
|
37
|
+
* @param {(err? : Error) => {Promise<unknown>}} next
|
|
38
|
+
* @returns {Promise<unknown>}
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
class SQLService extends DatabaseService {
|
|
42
|
+
init() {
|
|
43
|
+
this.on(['INSERT', 'UPSERT', 'UPDATE'], require('./fill-in-keys')) // REVISIT should be replaced by correct input processing eventually
|
|
44
|
+
this.on(['INSERT', 'UPSERT', 'UPDATE'], require('./deep-queries').onDeep)
|
|
45
|
+
if (cds.env.features.db_strict) {
|
|
46
|
+
this.before(['INSERT', 'UPSERT', 'UPDATE'], ({ query }) => {
|
|
47
|
+
const elements = query._target?.elements
|
|
48
|
+
if (!elements) return
|
|
49
|
+
const kind = query.kind || Object.keys(query)[0]
|
|
50
|
+
const operation = query[kind]
|
|
51
|
+
if (!operation.columns && !operation.entries && !operation.data) return
|
|
52
|
+
const columns =
|
|
53
|
+
operation.columns ||
|
|
54
|
+
Object.keys(
|
|
55
|
+
operation.data ||
|
|
56
|
+
operation.entries?.reduce((acc, obj) => {
|
|
57
|
+
return Object.assign(acc, obj)
|
|
58
|
+
}, {}),
|
|
59
|
+
)
|
|
60
|
+
const invalidColumns = columns.filter(c => !(c in elements))
|
|
61
|
+
|
|
62
|
+
if (invalidColumns.length > 0) {
|
|
63
|
+
cds.error(`STRICT MODE: Trying to ${kind} non existent columns (${invalidColumns})`)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
this.on(['SELECT'], this.onSELECT)
|
|
68
|
+
this.on(['INSERT'], this.onINSERT)
|
|
69
|
+
this.on(['UPSERT'], this.onUPSERT)
|
|
70
|
+
this.on(['UPDATE'], this.onUPDATE)
|
|
71
|
+
this.on(['DELETE'], this.onDELETE)
|
|
72
|
+
this.on(['CREATE ENTITY', 'DROP ENTITY'], this.onSIMPLE)
|
|
73
|
+
this.on(['BEGIN', 'COMMIT', 'ROLLBACK'], this.onEVENT)
|
|
74
|
+
this.on(['*'], this.onPlainSQL)
|
|
75
|
+
return super.init()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_changeToStreams(columns, rows, one) {
|
|
79
|
+
if (!rows || !columns) return
|
|
80
|
+
if (!Array.isArray(rows)) rows = [rows]
|
|
81
|
+
if (!rows.length || !Object.keys(rows[0]).length) return
|
|
82
|
+
|
|
83
|
+
let changes = false
|
|
84
|
+
for (let col of columns) {
|
|
85
|
+
const name = col.as || col.ref?.[col.ref.length - 1] || (typeof col === 'string' && col)
|
|
86
|
+
if (col.element?.isAssociation) {
|
|
87
|
+
if (one) this._changeToStreams(col.SELECT.columns, rows[0][name], false)
|
|
88
|
+
else
|
|
89
|
+
changes = rows.some(row => !this._changeToStreams(col.SELECT.columns, row[name], false))
|
|
90
|
+
} else if (col.element?.type === 'cds.LargeBinary') {
|
|
91
|
+
changes = true
|
|
92
|
+
if (one) rows[0][name] = this._stream(rows[0][name])
|
|
93
|
+
else
|
|
94
|
+
rows.forEach(row => {
|
|
95
|
+
row[name] = this._stream(row[name])
|
|
96
|
+
})
|
|
97
|
+
} else if (col.element?.type in BINARY_TYPES) {
|
|
98
|
+
changes = true
|
|
99
|
+
if (one) rows[0][name] = this._buffer(rows[0][name])
|
|
100
|
+
else
|
|
101
|
+
rows.forEach(row => {
|
|
102
|
+
row[name] = this._buffer(row[name])
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return changes
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_stream(val) {
|
|
110
|
+
if (val === null) return null
|
|
111
|
+
if (val instanceof Readable) return val
|
|
112
|
+
// Buffer.from only applies encoding when the input is a string
|
|
113
|
+
let raw = typeof val === 'string' ? Buffer.from(val.toString(), 'base64') : val
|
|
114
|
+
return new Readable({
|
|
115
|
+
read(size) {
|
|
116
|
+
if (raw.length === 0) return this.push(null)
|
|
117
|
+
const chunk = raw.slice(0, size)
|
|
118
|
+
raw = raw.slice(size)
|
|
119
|
+
this.push(chunk)
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_buffer(val) {
|
|
125
|
+
if (val === null) return null
|
|
126
|
+
return Buffer.from(val, 'base64')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Handler for SELECT
|
|
131
|
+
* @type {Handler}
|
|
132
|
+
*/
|
|
133
|
+
async onSELECT({ query, data, iterator, objectMode }) {
|
|
134
|
+
// REVISIT: for custom joins, infer is called twice, which is bad
|
|
135
|
+
// --> make cds.infer properly work with custom joins and remove this
|
|
136
|
+
if (!(query._target instanceof cds.entity)) {
|
|
137
|
+
try { this.infer(query) } catch { /**/ }
|
|
138
|
+
}
|
|
139
|
+
if (!query._target?._unresolved) { // REVISIT: use query._target instead
|
|
140
|
+
// Will return multiple rows with objects inside
|
|
141
|
+
query.SELECT.expand = 'root'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const { sql, values, cqn } = this.cqn2sql(query, data)
|
|
145
|
+
const expand = query.SELECT.expand
|
|
146
|
+
delete query.SELECT.expand
|
|
147
|
+
const isOne = cqn.SELECT.one || query.SELECT.from?.ref?.[0].cardinality?.max === 1
|
|
148
|
+
|
|
149
|
+
let ps = await this.prepare(sql)
|
|
150
|
+
let rows = iterator ? await ps.stream(values, isOne, objectMode) : await ps.all(values)
|
|
151
|
+
try {
|
|
152
|
+
if (rows.length)
|
|
153
|
+
if (expand) rows = rows.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))
|
|
154
|
+
|
|
155
|
+
if (!iterator) {
|
|
156
|
+
this._changeToStreams(cqn.SELECT.columns, rows, query.SELECT.one)
|
|
157
|
+
} else if (objectMode) {
|
|
158
|
+
const converter = (row) => this._changeToStreams(cqn.SELECT.columns, row, true)
|
|
159
|
+
const changeToStreams = new Transform({
|
|
160
|
+
objectMode: true,
|
|
161
|
+
transform(row, enc, cb) {
|
|
162
|
+
converter(row)
|
|
163
|
+
cb(null, row)
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
pipeline(rows, changeToStreams)
|
|
167
|
+
rows = changeToStreams
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (cqn.SELECT.count) {
|
|
171
|
+
// REVISIT: the runtime always expects that the count is preserved with .map, required for renaming in mocks
|
|
172
|
+
return SQLService._arrayWithCount(rows, await this.count(query, rows))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return !iterator && isOne ? rows[0] : rows
|
|
176
|
+
} catch (err) {
|
|
177
|
+
// Ensure that iterators receive pre stream errors
|
|
178
|
+
if (iterator) rows.emit('error', err)
|
|
179
|
+
throw err
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Handler for INSERT
|
|
185
|
+
* @type {Handler}
|
|
186
|
+
*/
|
|
187
|
+
async onINSERT({ query, data }) {
|
|
188
|
+
const { sql, entries, cqn } = this.cqn2sql(query, data)
|
|
189
|
+
if (!sql) return // Do nothing when there is nothing to be done // REVISIT: fix within mtxs
|
|
190
|
+
const ps = await this.prepare(sql)
|
|
191
|
+
const results = entries ? await Promise.all(entries.map(e => ps.run(e))) : await ps.run()
|
|
192
|
+
return new this.class.InsertResults(cqn, results)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Handler for UPSERT
|
|
197
|
+
* @type {Handler}
|
|
198
|
+
*/
|
|
199
|
+
async onUPSERT({ query, data }) {
|
|
200
|
+
const { sql, entries } = this.cqn2sql(query, data)
|
|
201
|
+
if (!sql) return // Do nothing when there is nothing to be done // REVISIT: When does this happen?
|
|
202
|
+
let ps = await this.prepare(sql)
|
|
203
|
+
let results = entries ? await Promise.all(entries.map(e => ps.run(e))) : await ps.run()
|
|
204
|
+
let changes = results.reduce?.((total,r) => total + r.changes, 0) ?? results.changes
|
|
205
|
+
return this._return_affected(changes)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Handler for UPDATE
|
|
210
|
+
* @type {Handler}
|
|
211
|
+
*/
|
|
212
|
+
async onUPDATE(req) {
|
|
213
|
+
// noop if not a touch for @cds.on.update
|
|
214
|
+
if (
|
|
215
|
+
!_hasProps(req.query.UPDATE.data) &&
|
|
216
|
+
!_hasProps(req.query.UPDATE.with) &&
|
|
217
|
+
!Object.values(req.target?.elements || {}).some(e => e['@cds.on.update'])
|
|
218
|
+
)
|
|
219
|
+
return this._return_affected(0)
|
|
220
|
+
return this.onSIMPLE(req)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Handler for CREATE, DROP, UPDATE, DELETE, with simple CQN
|
|
225
|
+
* @type {Handler}
|
|
226
|
+
*/
|
|
227
|
+
async onSIMPLE({ query, data }) {
|
|
228
|
+
const { sql, values } = this.cqn2sql(query, data)
|
|
229
|
+
let ps = await this.prepare(sql)
|
|
230
|
+
let { changes } = await ps.run(values)
|
|
231
|
+
return this._return_affected(changes)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
get onDELETE() {
|
|
235
|
+
// REVISIT: It's not yet 100 % clear under which circumstances we can rely on db constraints
|
|
236
|
+
return (super.onDELETE = /* cds.env.features.assert_integrity === 'db' ? this.onSIMPLE : */ deep_delete)
|
|
237
|
+
async function deep_delete(/** @type {Request} */ req) {
|
|
238
|
+
const resolve = this.resolve
|
|
239
|
+
const transitions = resolve.transitions(req.query)
|
|
240
|
+
if (transitions.target !== transitions.queryTarget) {
|
|
241
|
+
const keys = []
|
|
242
|
+
const transitionsTarget = transitions.queryTarget.keys || transitions.queryTarget.elements
|
|
243
|
+
for (const key in transitionsTarget) {
|
|
244
|
+
const exists = e => e && !e.virtual && !e.value && !e.isAssociation
|
|
245
|
+
if (exists(transitionsTarget[key])) keys.push(key)
|
|
246
|
+
}
|
|
247
|
+
const matchedKeys = keys.filter(key => transitions.mapping.has(key)).map(k => ({ ref: [k] }))
|
|
248
|
+
const query = DELETE.from({
|
|
249
|
+
ref: [
|
|
250
|
+
{
|
|
251
|
+
id: transitions.target.name,
|
|
252
|
+
where: [
|
|
253
|
+
{ list: matchedKeys.map(k => transitions.mapping.get(k.ref[0])) },
|
|
254
|
+
'in',
|
|
255
|
+
SELECT.from(req.query.DELETE.from).columns(matchedKeys).where(req.query.DELETE.where),
|
|
256
|
+
],
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
})
|
|
260
|
+
return this.onDELETE({ query, target: transitions.target })
|
|
261
|
+
}
|
|
262
|
+
const table = resolveTable(req.target)
|
|
263
|
+
const { compositions } = table
|
|
264
|
+
if (compositions) {
|
|
265
|
+
// Transform CQL`DELETE from Foo[p1] WHERE p2` into CQL`DELETE from Foo[p1 and p2]`
|
|
266
|
+
let { from, where } = req.query.DELETE
|
|
267
|
+
if (typeof from === 'string') from = { ref: [from] }
|
|
268
|
+
if (where) {
|
|
269
|
+
let last = from.ref.at(-1)
|
|
270
|
+
if (last.where) [last, where] = [last.id, [{ xpr: last.where }, 'and', { xpr: where }]]
|
|
271
|
+
from = { ref: [...from.ref.slice(0, -1), { id: last, where }] }
|
|
272
|
+
}
|
|
273
|
+
// Process child compositions depth-first
|
|
274
|
+
let { depth = 0, visited = [] } = req
|
|
275
|
+
visited.push(req.target.name)
|
|
276
|
+
await Promise.all(
|
|
277
|
+
Object.values(compositions).map(c => {
|
|
278
|
+
if (c._target['@cds.persistence.skip'] === true) return
|
|
279
|
+
if (c._target === req.target) {
|
|
280
|
+
// the Genre.children case
|
|
281
|
+
if (++depth > (c['@depth'] || 3)) return
|
|
282
|
+
} else if (visited.includes(c._target.name))
|
|
283
|
+
throw new Error(
|
|
284
|
+
`Transitive circular composition detected: \n\n` +
|
|
285
|
+
` ${visited.join(' > ')} > ${c._target.name} \n\n` +
|
|
286
|
+
`These are not supported by deep delete.`,
|
|
287
|
+
)
|
|
288
|
+
// Prepare and run deep query, à la CQL`DELETE from Foo[pred]:comp1.comp2...`
|
|
289
|
+
const query = DELETE.from({ ref: [...from.ref, c.name] })
|
|
290
|
+
query._target = c._target
|
|
291
|
+
return this.onDELETE({ query, depth, visited: [...visited], target: c._target })
|
|
292
|
+
}),
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
return this.onSIMPLE(req)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Handler for BEGIN, COMMIT, ROLLBACK, which don't have any CQN
|
|
301
|
+
* @type {Handler}
|
|
302
|
+
*/
|
|
303
|
+
async onEVENT({ event }) {
|
|
304
|
+
if (DEBUG._debug) DEBUG.debug(event) // in the other cases above DEBUG happens in cqn2sql
|
|
305
|
+
return await this.exec(event)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Handler for SQL statements which don't have any CQN
|
|
310
|
+
* @type {Handler}
|
|
311
|
+
*/
|
|
312
|
+
async onPlainSQL({ query, data }, next) {
|
|
313
|
+
if (typeof query === 'string') {
|
|
314
|
+
if (DEBUG._debug) DEBUG.debug(query, data)
|
|
315
|
+
const ps = await this.prepare(query)
|
|
316
|
+
const exec = this.hasResults(query) ? d => ps.all(d) : d => ps.run(d)
|
|
317
|
+
if (Array.isArray(data) && Array.isArray(data[0])) return await Promise.all(data.map(exec))
|
|
318
|
+
else return exec(data)
|
|
319
|
+
} else return next()
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Override in subclasses to detect more statements to be called with ps.all()
|
|
324
|
+
* @param {string} sql
|
|
325
|
+
*/
|
|
326
|
+
hasResults(sql) {
|
|
327
|
+
return /^\s*(SELECT|WITH|CALL|PRAGMA table_info)/i.test(sql)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Derives and executes a query to fill in `$count` for given query
|
|
332
|
+
* @param {import('@sap/cds/apis/cqn').SELECT} query - SELECT CQN
|
|
333
|
+
* @param {unknown[]} ret - Results of the original query
|
|
334
|
+
* @returns {Promise<number|string>}
|
|
335
|
+
*/
|
|
336
|
+
async count(query, ret) {
|
|
337
|
+
if (ret?.length) {
|
|
338
|
+
const { one, limit: _ } = query.SELECT,
|
|
339
|
+
n = ret.length
|
|
340
|
+
const [max, offset = 0] = one ? [1] : _ ? [_.rows?.val, _.offset?.val] : []
|
|
341
|
+
if (max === undefined || (n < max && (n || !offset))) return _count_as_string ? `${n + offset}` : n + offset
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Keep original query columns when potentially used insde conditions
|
|
345
|
+
const { having, groupBy } = query.SELECT
|
|
346
|
+
let columns = []
|
|
347
|
+
if (having?.length || groupBy?.length) {
|
|
348
|
+
columns = query.SELECT.columns.filter(c => !c.expand)
|
|
349
|
+
}
|
|
350
|
+
if (columns.length === 0) columns.push({ val: 1 })
|
|
351
|
+
const cq = SELECT.one([_count]).from(
|
|
352
|
+
cds.ql.clone(query, {
|
|
353
|
+
columns,
|
|
354
|
+
localized: false,
|
|
355
|
+
expand: false,
|
|
356
|
+
limit: undefined,
|
|
357
|
+
orderBy: undefined,
|
|
358
|
+
}),
|
|
359
|
+
)
|
|
360
|
+
const { count } = await this.onSELECT({ query: cq })
|
|
361
|
+
return count
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Streaming API variant of .run().
|
|
366
|
+
* @param {import('@sap/cds/apis/cqn').SELECT} query - SELECT CQN
|
|
367
|
+
* @param {function} callback - Function to be invoked for each row
|
|
368
|
+
*/
|
|
369
|
+
foreach(query, callback) {
|
|
370
|
+
return query.foreach(callback)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Helper class for results of INSERTs.
|
|
375
|
+
* Subclasses may override this.
|
|
376
|
+
*/
|
|
377
|
+
static InsertResults = require('./InsertResults')
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Helper class implementing {@link SQLService#cqn2sql}.
|
|
381
|
+
* Subclasses commonly override this.
|
|
382
|
+
*/
|
|
383
|
+
static CQN2SQL = require('./cqn2sql').class
|
|
384
|
+
|
|
385
|
+
// REVISIT: There must be a better way!
|
|
386
|
+
// preserves $count for .map calls on array
|
|
387
|
+
static _arrayWithCount = function (a, count) {
|
|
388
|
+
const _map = a.map
|
|
389
|
+
|
|
390
|
+
const map = function (..._) {
|
|
391
|
+
return SQLService._arrayWithCount(_map.call(a, ..._), count)
|
|
392
|
+
}
|
|
393
|
+
return Object.defineProperties(a, {
|
|
394
|
+
$count: { value: count, enumerable: false, configurable: true, writable: true },
|
|
395
|
+
map: { value: map, enumerable: false, configurable: true, writable: true },
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
constructor(...args) {
|
|
400
|
+
super(...args)
|
|
401
|
+
this.class = new.target // for IntelliSense
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* @param {import('@sap/cds/apis/cqn').Query} query
|
|
406
|
+
* @returns {typeof SQLService.CQN2SQL}
|
|
407
|
+
*/
|
|
408
|
+
cqn2sql(query, values) {
|
|
409
|
+
const cqn2sql = new this.class.CQN2SQL(this)
|
|
410
|
+
const q = this.cqn4sql(query)
|
|
411
|
+
const sql = cqn2sql.render(q, values)
|
|
412
|
+
return sql
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* @param {import('@sap/cds/apis/cqn').Query} q
|
|
417
|
+
* @returns {import('./infer/cqn').Query}
|
|
418
|
+
*/
|
|
419
|
+
cqn4sql(q, useTechnicalAlias = true) {
|
|
420
|
+
if (
|
|
421
|
+
!cds.env.features.db_strict &&
|
|
422
|
+
!q.SELECT?.from?.join &&
|
|
423
|
+
!q.SELECT?.from?.SELECT &&
|
|
424
|
+
!this.model?.definitions[_target_name4(q)]
|
|
425
|
+
) return q
|
|
426
|
+
else return cqn4sql(q, this.model, useTechnicalAlias)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Returns a Promise which resolves to a prepared statement object with
|
|
431
|
+
* `{run,get,all}` signature as specified in {@link PreparedStatement}.
|
|
432
|
+
* @abstract
|
|
433
|
+
* @param {string} sql The SQL String to be prepared
|
|
434
|
+
* @returns {PreparedStatement}
|
|
435
|
+
*/
|
|
436
|
+
async prepare(sql) {
|
|
437
|
+
sql
|
|
438
|
+
throw '2b overridden by subclass'
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Used to execute simple SQL statement like BEGIN, COMMIT, ROLLBACK
|
|
443
|
+
* @param {string} sql
|
|
444
|
+
* @returns {Promise<unknown>} The result of the query
|
|
445
|
+
*/
|
|
446
|
+
async exec(sql) {
|
|
447
|
+
sql
|
|
448
|
+
throw '2b overridden by subclass'
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Interface of prepared statement objects as returned by {@link SQLService#prepare}
|
|
454
|
+
* @class
|
|
455
|
+
* @interface
|
|
456
|
+
*/
|
|
457
|
+
class PreparedStatement {
|
|
458
|
+
/**
|
|
459
|
+
* Executes a prepared DML query, i.e., INSERT, UPDATE, DELETE, CREATE, DROP
|
|
460
|
+
* @abstract
|
|
461
|
+
* @param {unknown|unknown[]} binding_params
|
|
462
|
+
*/
|
|
463
|
+
async run(binding_params) {
|
|
464
|
+
binding_params
|
|
465
|
+
return 0
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Executes a prepared SELECT query and returns a single/first row only
|
|
469
|
+
* @abstract
|
|
470
|
+
* @param {unknown|unknown[]} binding_params
|
|
471
|
+
* @returns {Promise<unknown>}
|
|
472
|
+
*/
|
|
473
|
+
async get(binding_params) {
|
|
474
|
+
binding_params
|
|
475
|
+
return {}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Executes a prepared SELECT query and returns an array of all rows
|
|
479
|
+
* @abstract
|
|
480
|
+
* @param {unknown|unknown[]} binding_params
|
|
481
|
+
* @returns {Promise<unknown[]>}
|
|
482
|
+
*/
|
|
483
|
+
async all(binding_params) {
|
|
484
|
+
binding_params
|
|
485
|
+
return [{}]
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Executes a prepared SELECT query and returns a stream of the result
|
|
489
|
+
* @abstract
|
|
490
|
+
* @param {unknown|unknown[]} binding_params
|
|
491
|
+
* @returns {ReadableStream<string|Buffer>} A stream of the result
|
|
492
|
+
*/
|
|
493
|
+
async stream(binding_params) {
|
|
494
|
+
binding_params
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
SQLService.prototype.PreparedStatement = PreparedStatement
|
|
498
|
+
|
|
499
|
+
/** @param {import('@sap/cds').ql.Query} q */
|
|
500
|
+
const _target_name4 = q => {
|
|
501
|
+
const target = q._subject
|
|
502
|
+
|| q.SELECT?.from
|
|
503
|
+
|| q.INSERT?.into
|
|
504
|
+
|| q.UPSERT?.into
|
|
505
|
+
|| q.UPDATE?.entity
|
|
506
|
+
|| q.DELETE?.from
|
|
507
|
+
|| q.CREATE?.entity
|
|
508
|
+
|| q.DROP?.entity
|
|
509
|
+
if (target?.SET?.op === 'union') throw new cds.error('UNION-based queries are not supported')
|
|
510
|
+
if (!target?.ref) return target
|
|
511
|
+
const [first] = target.ref
|
|
512
|
+
return first.id || first
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
// Add support for cqn2pql if debug logging for pql is enabled, or if running in the REPL.
|
|
517
|
+
const DEBUG_PQL = cds.log('pql')
|
|
518
|
+
if (DEBUG_PQL._debug || cds.repl) {
|
|
519
|
+
|
|
520
|
+
// Add helper method to convert CQN to PQL, used below...
|
|
521
|
+
SQLService.prototype.cqn2pql = function cqn2pql(query, values) {
|
|
522
|
+
const CQN2PQL = cqn2pql.renderer ??= require('./cqn2pql')
|
|
523
|
+
return new CQN2PQL(this).render(query, values)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Add support for logging generated PQL if debug logging for pql is enabled.
|
|
527
|
+
if (DEBUG_PQL._debug) {
|
|
528
|
+
const $super = SQLService.prototype.cqn2sql
|
|
529
|
+
SQLService.prototype.cqn2sql = function (query, values) {
|
|
530
|
+
const q2 = this.cqn4sql(query, false) // FIXME: calling cqn4sql twice per query is utterly expensive, isn't it ?!?
|
|
531
|
+
const pql = this.cqn2pql(q2, values)
|
|
532
|
+
DEBUG_PQL.debug(pql.sql, pql.values ?? '')
|
|
533
|
+
return $super.call(this, query, values)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// If running in the REPL, extend cds.ql.Query with helpers to inspect queries.
|
|
538
|
+
if (cds.repl) {
|
|
539
|
+
|
|
540
|
+
cds.extend(cds.ql.Query).with(
|
|
541
|
+
class {
|
|
542
|
+
forSQL() {
|
|
543
|
+
const cqn = db.srv.cqn4sql(this)
|
|
544
|
+
return this.flat(cqn)
|
|
545
|
+
}
|
|
546
|
+
forSql() { return this.forSQL() }
|
|
547
|
+
toSQL() {
|
|
548
|
+
if (this.SELECT) this.SELECT.expand = 'root' // Enforces using json functions always for top-level SELECTS
|
|
549
|
+
const { sql, values } = db.srv.cqn2sql(this)
|
|
550
|
+
return { sql, values } // skipping .cqn property
|
|
551
|
+
}
|
|
552
|
+
toSql() {
|
|
553
|
+
const { sql } = this.toSQL()
|
|
554
|
+
return sql
|
|
555
|
+
}
|
|
556
|
+
toPQL() {
|
|
557
|
+
const { sql, values } = db.srv.cqn2pql(this)
|
|
558
|
+
return { sql, values } // skipping .cqn property
|
|
559
|
+
}
|
|
560
|
+
toPql() {
|
|
561
|
+
const { sql } = this.toPQL()
|
|
562
|
+
return sql
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Dummy SQL service used in extensions to cds.ql above,
|
|
569
|
+
* if no real SQL service is available yet through cds.db.
|
|
570
|
+
*/
|
|
571
|
+
class db extends SQLService {
|
|
572
|
+
/** @returns {SQLService} */
|
|
573
|
+
static get srv() { return cds.db || (this.singleton ??= new this) }
|
|
574
|
+
get factory() { return null }
|
|
575
|
+
get model() { return cds.model }
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
Object.assign(SQLService, { _target_name4 })
|
|
581
|
+
module.exports = SQLService
|