@cap-js/db-service 1.0.1 → 1.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 +29 -2
- package/index.js +16 -2
- package/lib/InsertResults.js +22 -4
- package/lib/SQLService.js +169 -73
- package/lib/common/DatabaseService.js +93 -88
- package/lib/common/factory.d.ts +5 -0
- package/lib/common/generic-pool.js +34 -0
- package/lib/common/session-context.js +32 -0
- package/lib/converters.d.ts +24 -0
- package/lib/cql-functions.js +205 -5
- package/lib/cqn2sql.js +463 -154
- package/lib/cqn4sql.js +176 -71
- package/lib/deep-queries.js +31 -3
- package/lib/fill-in-keys.js +15 -4
- package/lib/infer/cqn.d.ts +45 -0
- package/lib/infer/index.js +128 -31
- package/lib/infer/join-tree.js +64 -19
- package/package.json +17 -8
package/lib/cqn2sql.js
CHANGED
|
@@ -2,6 +2,8 @@ const cds = require('@sap/cds/lib')
|
|
|
2
2
|
const cds_infer = require('./infer')
|
|
3
3
|
const cqn4sql = require('./cqn4sql')
|
|
4
4
|
|
|
5
|
+
const { Readable } = require('stream')
|
|
6
|
+
|
|
5
7
|
const DEBUG = (() => {
|
|
6
8
|
let DEBUG = cds.debug('sql-json')
|
|
7
9
|
if (DEBUG) return DEBUG
|
|
@@ -14,32 +16,58 @@ const DEBUG = (() => {
|
|
|
14
16
|
})()
|
|
15
17
|
|
|
16
18
|
class CQN2SQLRenderer {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Creates a new CQN2SQL instance for processing a query
|
|
21
|
+
* @constructor
|
|
22
|
+
* @param {import('@sap/cds/apis/services').ContextProperties} context the cds.context of the request
|
|
23
|
+
*/
|
|
24
|
+
constructor(srv) {
|
|
25
|
+
this.context = srv?.context || cds.context // Using srv.context is required due to stakeholders doing unmanaged txs without cds.context being set
|
|
19
26
|
this.class = new.target // for IntelliSense
|
|
20
27
|
this.class._init() // is a noop for subsequent calls
|
|
21
28
|
}
|
|
29
|
+
|
|
30
|
+
static _add_mixins (aspect, mixins) {
|
|
31
|
+
const fqn = this.name + aspect
|
|
32
|
+
const types = cds.builtin.types
|
|
33
|
+
for (let each in mixins) {
|
|
34
|
+
const def = types[each]
|
|
35
|
+
if (!def) continue
|
|
36
|
+
Object.defineProperty(def, fqn, { value: mixins[each] })
|
|
37
|
+
}
|
|
38
|
+
return fqn
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Initializes the class one first creation to link types to data converters
|
|
43
|
+
*/
|
|
22
44
|
static _init() {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
this._localized = this._add_mixins(':localized', this.localized)
|
|
46
|
+
this._convertInput = this._add_mixins(':convertInput', this.InputConverters)
|
|
47
|
+
this._convertOutput = this._add_mixins(':convertOutput', this.OutputConverters)
|
|
48
|
+
this._sqlType = this._add_mixins(':sqlType', this.TypeMap)
|
|
49
|
+
// Have all-uppercase all-lowercase, and capitalized keywords to speed up lookups
|
|
50
|
+
for (let each in this.ReservedWords) {
|
|
51
|
+
// ORDER
|
|
52
|
+
this.ReservedWords[each[0] + each.slice(1).toLowerCase()] = 1 // Order
|
|
53
|
+
this.ReservedWords[each.toLowerCase()] = 1 // order
|
|
32
54
|
}
|
|
33
|
-
this._localized = _add_mixins(':localized', this.localized)
|
|
34
|
-
this._convertInput = _add_mixins(':convertInput', this.InputConverters)
|
|
35
|
-
this._convertOutput = _add_mixins(':convertOutput', this.OutputConverters)
|
|
36
|
-
this._sqlType = _add_mixins(':sqlType', this.TypeMap)
|
|
37
55
|
this._init = () => {} // makes this a noop for subsequent calls
|
|
38
56
|
}
|
|
39
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Renders incoming query into SQL and generates binding values
|
|
60
|
+
* @param {import('./infer/cqn').Query} q CQN query to be rendered
|
|
61
|
+
* @param {unknown[]|undefined} vars Values to be used for params
|
|
62
|
+
* @returns {CQN2SQLRenderer|unknown}
|
|
63
|
+
*/
|
|
40
64
|
render(q, vars) {
|
|
41
65
|
const cmd = q.cmd || Object.keys(q)[0] // SELECT, INSERT, ...
|
|
66
|
+
/**
|
|
67
|
+
* @type {string} the rendered SQL string
|
|
68
|
+
*/
|
|
42
69
|
this.sql = '' // to have it as first property for debugging
|
|
70
|
+
/** @type {unknown[]} */
|
|
43
71
|
this.values = [] // prepare values, filled in by subroutines
|
|
44
72
|
this[cmd]((this.cqn = q)) // actual sql rendering happens here
|
|
45
73
|
if (vars?.length && !this.values.length) this.values = vars
|
|
@@ -51,12 +79,21 @@ class CQN2SQLRenderer {
|
|
|
51
79
|
return this
|
|
52
80
|
}
|
|
53
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Links the incoming query with the current service model
|
|
84
|
+
* @param {import('./infer/cqn').Query} q
|
|
85
|
+
* @returns {import('./infer/cqn').Query}
|
|
86
|
+
*/
|
|
54
87
|
infer(q) {
|
|
55
88
|
return q.target ? q : cds_infer(q)
|
|
56
89
|
}
|
|
57
90
|
|
|
58
91
|
// CREATE Statements ------------------------------------------------
|
|
59
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Renders a CREATE query into generic SQL
|
|
95
|
+
* @param {import('./infer/cqn').CREATE} q
|
|
96
|
+
*/
|
|
60
97
|
CREATE(q) {
|
|
61
98
|
const { target } = q,
|
|
62
99
|
{ query } = target
|
|
@@ -71,6 +108,11 @@ class CQN2SQLRenderer {
|
|
|
71
108
|
return
|
|
72
109
|
}
|
|
73
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Renders a column clause for the given elements
|
|
113
|
+
* @param {import('./infer/cqn').elements} elements
|
|
114
|
+
* @returns {string} SQL
|
|
115
|
+
*/
|
|
74
116
|
CREATE_elements(elements) {
|
|
75
117
|
let sql = ''
|
|
76
118
|
for (let e in elements) {
|
|
@@ -82,11 +124,21 @@ class CQN2SQLRenderer {
|
|
|
82
124
|
return sql.slice(0, -2)
|
|
83
125
|
}
|
|
84
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Renders a column definition for the given element
|
|
129
|
+
* @param {import('./infer/cqn').element} element
|
|
130
|
+
* @returns {string} SQL
|
|
131
|
+
*/
|
|
85
132
|
CREATE_element(element) {
|
|
86
133
|
const type = this.type4(element)
|
|
87
134
|
if (type) return this.quote(element.name) + ' ' + type
|
|
88
135
|
}
|
|
89
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Renders the SQL type definition for the given element
|
|
139
|
+
* @param {import('./infer/cqn').element} element
|
|
140
|
+
* @returns {string}
|
|
141
|
+
*/
|
|
90
142
|
type4(element) {
|
|
91
143
|
if (!element._type) element = cds.builtin.types[element.type] || element
|
|
92
144
|
const fn = element[this.class._sqlType]
|
|
@@ -95,6 +147,9 @@ class CQN2SQLRenderer {
|
|
|
95
147
|
)
|
|
96
148
|
}
|
|
97
149
|
|
|
150
|
+
/** @callback converter */
|
|
151
|
+
|
|
152
|
+
/** @type {Object<string,import('@sap/cds/apis/csn').Definition>} */
|
|
98
153
|
static TypeMap = {
|
|
99
154
|
// Utilizing cds.linked inheritance
|
|
100
155
|
String: e => `NVARCHAR(${e.length || 5000})`,
|
|
@@ -120,6 +175,10 @@ class CQN2SQLRenderer {
|
|
|
120
175
|
|
|
121
176
|
// DROP Statements ------------------------------------------------
|
|
122
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Renders a DROP query into generic SQL
|
|
180
|
+
* @param {import('./infer/cqn').DROP} q
|
|
181
|
+
*/
|
|
123
182
|
DROP(q) {
|
|
124
183
|
const { target } = q
|
|
125
184
|
const isView = target.query || target.projection
|
|
@@ -128,98 +187,146 @@ class CQN2SQLRenderer {
|
|
|
128
187
|
|
|
129
188
|
// SELECT Statements ------------------------------------------------
|
|
130
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Renders a SELECT statement into generic SQL
|
|
192
|
+
* @param {import('./infer/cqn').SELECT} q
|
|
193
|
+
*/
|
|
131
194
|
SELECT(q) {
|
|
132
195
|
let { from, expand, where, groupBy, having, orderBy, limit, one, distinct, localized } = q.SELECT
|
|
133
|
-
if (!expand) expand = q.SELECT.expand = has_expands(q) || has_arrays(q)
|
|
134
196
|
// REVISIT: When selecting from an entity that is not in the model the from.where are not normalized (as cqn4sql is skipped)
|
|
135
197
|
if (!where && from?.ref?.length === 1 && from.ref[0]?.where) where = from.ref[0]?.where
|
|
136
198
|
let columns = this.SELECT_columns(q)
|
|
137
|
-
let
|
|
138
|
-
sql = `SELECT`
|
|
199
|
+
let sql = `SELECT`
|
|
139
200
|
if (distinct) sql += ` DISTINCT`
|
|
140
|
-
if (!_empty(
|
|
141
|
-
if (!_empty(
|
|
142
|
-
if (!_empty(
|
|
143
|
-
if (!_empty(
|
|
144
|
-
if (!_empty(
|
|
145
|
-
if (!_empty(
|
|
146
|
-
if (one)
|
|
147
|
-
|
|
148
|
-
|
|
201
|
+
if (!_empty(columns)) sql += ` ${columns}`
|
|
202
|
+
if (!_empty(from)) sql += ` FROM ${this.from(from)}`
|
|
203
|
+
if (!_empty(where)) sql += ` WHERE ${this.where(where)}`
|
|
204
|
+
if (!_empty(groupBy)) sql += ` GROUP BY ${this.groupBy(groupBy)}`
|
|
205
|
+
if (!_empty(having)) sql += ` HAVING ${this.having(having)}`
|
|
206
|
+
if (!_empty(orderBy)) sql += ` ORDER BY ${this.orderBy(orderBy, localized)}`
|
|
207
|
+
if (one) limit = Object.assign({}, limit, { rows: { val: 1 } })
|
|
208
|
+
if (limit) sql += ` LIMIT ${this.limit(limit)}`
|
|
209
|
+
// Expand cannot work without an inferred query
|
|
210
|
+
if (expand) {
|
|
211
|
+
// REVISIT: Why don't we handle that as an error in SELECT_expand?
|
|
212
|
+
if (!q.elements) cds.error`Query was not inferred and includes expand. For which the metadata is missing.`
|
|
213
|
+
sql = this.SELECT_expand(q, sql)
|
|
214
|
+
}
|
|
149
215
|
return (this.sql = sql)
|
|
150
216
|
}
|
|
151
217
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
218
|
+
/**
|
|
219
|
+
* Renders a column clause into generic SQL
|
|
220
|
+
* @param {import('./infer/cqn').SELECT} param0
|
|
221
|
+
* @returns {string} SQL
|
|
222
|
+
*/
|
|
223
|
+
SELECT_columns(q) {
|
|
224
|
+
return (q.SELECT.columns ?? ['*']).map(x => this.column_expr(x, q))
|
|
156
225
|
}
|
|
157
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Renders a JSON select around the provided SQL statement
|
|
229
|
+
* @param {import('./infer/cqn').SELECT} param0
|
|
230
|
+
* @param {string} sql
|
|
231
|
+
* @returns {string} SQL
|
|
232
|
+
*/
|
|
158
233
|
SELECT_expand({ SELECT, elements }, sql) {
|
|
159
234
|
if (!SELECT.columns) return sql
|
|
160
|
-
if (!elements) return sql
|
|
161
|
-
let cols =
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const qc = cds.ql.clone(x, { columns: [{ func: 'count' }], one: 1, limit: 0, orderBy: 0 })
|
|
172
|
-
col += `, '$."${name}@odata.count"',${this.expr(qc)}`
|
|
173
|
-
}
|
|
174
|
-
return col
|
|
175
|
-
})
|
|
235
|
+
if (!elements) return sql // REVISIT: Above we say this is an error condition, but here we say it's ok?
|
|
236
|
+
let cols = SELECT.columns.map(x => {
|
|
237
|
+
const name = this.column_name(x)
|
|
238
|
+
let col = `'$."${name}"',${this.output_converter4(x.element, this.quote(name))}`
|
|
239
|
+
if (x.SELECT?.count) {
|
|
240
|
+
// Return both the sub select and the count for @odata.count
|
|
241
|
+
const qc = cds.ql.clone(x, { columns: [{ func: 'count' }], one: 1, limit: 0, orderBy: 0 })
|
|
242
|
+
col += `, '$."${name}@odata.count"',${this.expr(qc)}`
|
|
243
|
+
}
|
|
244
|
+
return col
|
|
245
|
+
})
|
|
176
246
|
|
|
177
247
|
// Prevent SQLite from hitting function argument limit of 100
|
|
178
|
-
let colsLength = cols.length
|
|
179
248
|
let obj = "'{}'"
|
|
180
|
-
for (let i = 0; i <
|
|
249
|
+
for (let i = 0; i < cols.length; i += 48) {
|
|
181
250
|
obj = `json_insert(${obj},${cols.slice(i, i + 48)})`
|
|
182
251
|
}
|
|
183
252
|
return `SELECT ${SELECT.one || SELECT.expand === 'root' ? obj : `json_group_array(${obj})`} as _json_ FROM (${sql})`
|
|
184
253
|
}
|
|
185
254
|
|
|
186
|
-
|
|
187
|
-
|
|
255
|
+
/**
|
|
256
|
+
* Renders a SELECT column expression into generic SQL
|
|
257
|
+
* @param {import('./infer/cqn').col} x
|
|
258
|
+
* @returns {string} SQL
|
|
259
|
+
*/
|
|
260
|
+
column_expr(x, q) {
|
|
261
|
+
if (x === '*') return '*'
|
|
262
|
+
///////////////////////////////////////////////////////////////////////////////////////
|
|
263
|
+
// REVISIT: that should move out of here!
|
|
188
264
|
if (x?.element?.['@cds.extension']) {
|
|
189
|
-
x.as
|
|
190
|
-
return `extensions__->${this.string('$."' + x.element.name + '"')}`
|
|
265
|
+
return `extensions__->${this.string('$."' + x.element.name + '"')} as ${x.as || x.element.name}`
|
|
191
266
|
}
|
|
267
|
+
///////////////////////////////////////////////////////////////////////////////////////
|
|
192
268
|
let sql = this.expr(x)
|
|
269
|
+
let alias = this.column_alias4(x, q)
|
|
270
|
+
if (alias) sql += ' as ' + this.quote(alias)
|
|
193
271
|
return sql
|
|
194
272
|
}
|
|
195
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Extracts the column alias from a SELECT column expression
|
|
276
|
+
* @param {import('./infer/cqn').col} x
|
|
277
|
+
* @returns {string}
|
|
278
|
+
*/
|
|
279
|
+
column_alias4(x) {
|
|
280
|
+
return typeof x.as === 'string' ? x.as : x.func
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Renders a FROM clause into generic SQL
|
|
285
|
+
* @param {import('./infer/cqn').source} from
|
|
286
|
+
* @returns {string} SQL
|
|
287
|
+
*/
|
|
196
288
|
from(from) {
|
|
197
|
-
const { ref, as } = from
|
|
198
|
-
|
|
289
|
+
const { ref, as } = from
|
|
290
|
+
const _aliased = as ? s => s + ` as ${this.quote(as)}` : s => s
|
|
199
291
|
if (ref) return _aliased(this.quote(this.name(ref[0])))
|
|
200
292
|
if (from.SELECT) return _aliased(`(${this.SELECT(from)})`)
|
|
201
|
-
if (from.join)
|
|
202
|
-
|
|
203
|
-
join,
|
|
204
|
-
args: [left, right],
|
|
205
|
-
on,
|
|
206
|
-
} = from
|
|
207
|
-
return `${this.from(left)} ${join} JOIN ${this.from(right)} ON ${this.xpr({ xpr: on })}`
|
|
208
|
-
}
|
|
293
|
+
if (from.join)
|
|
294
|
+
return `${this.from(from.args[0])} ${from.join} JOIN ${this.from(from.args[1])} ON ${this.where(from.on)}`
|
|
209
295
|
}
|
|
210
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Renders a WHERE clause into generic SQL
|
|
299
|
+
* @param {import('./infer/cqn').predicate} xpr
|
|
300
|
+
* @returns {string} SQL
|
|
301
|
+
*/
|
|
211
302
|
where(xpr) {
|
|
212
303
|
return this.xpr({ xpr })
|
|
213
304
|
}
|
|
214
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Renders a HAVING clause into generic SQL
|
|
308
|
+
* @param {import('./infer/cqn').predicate} xpr
|
|
309
|
+
* @returns {string} SQL
|
|
310
|
+
*/
|
|
215
311
|
having(xpr) {
|
|
216
312
|
return this.xpr({ xpr })
|
|
217
313
|
}
|
|
218
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Renders a groupBy clause into generic SQL
|
|
317
|
+
* @param {import('./infer/cqn').expr[]} clause
|
|
318
|
+
* @returns {string[] | string} SQL
|
|
319
|
+
*/
|
|
219
320
|
groupBy(clause) {
|
|
220
321
|
return clause.map(c => this.expr(c))
|
|
221
322
|
}
|
|
222
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Renders an orderBy clause into generic SQL
|
|
326
|
+
* @param {import('./infer/cqn').ordering_term[]} orderBy
|
|
327
|
+
* @param {boolean | undefined} localized
|
|
328
|
+
* @returns {string[] | string} SQL
|
|
329
|
+
*/
|
|
223
330
|
orderBy(orderBy, localized) {
|
|
224
331
|
return orderBy.map(
|
|
225
332
|
localized
|
|
@@ -231,6 +338,12 @@ class CQN2SQLRenderer {
|
|
|
231
338
|
)
|
|
232
339
|
}
|
|
233
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Renders an limit clause into generic SQL
|
|
343
|
+
* @param {import('./infer/cqn').limit} param0
|
|
344
|
+
* @returns {string} SQL
|
|
345
|
+
* @throws {Error} When no rows are defined
|
|
346
|
+
*/
|
|
234
347
|
limit({ rows, offset }) {
|
|
235
348
|
if (!rows) throw new Error('Rows parameter is missing in SELECT.limit(rows, offset)')
|
|
236
349
|
return !offset ? rows.val : `${rows.val} OFFSET ${offset.val}`
|
|
@@ -238,6 +351,11 @@ class CQN2SQLRenderer {
|
|
|
238
351
|
|
|
239
352
|
// INSERT Statements ------------------------------------------------
|
|
240
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Renders an INSERT query into generic SQL
|
|
356
|
+
* @param {import('./infer/cqn').INSERT} q
|
|
357
|
+
* @returns {string} SQL
|
|
358
|
+
*/
|
|
241
359
|
INSERT(q) {
|
|
242
360
|
const { INSERT } = q
|
|
243
361
|
return INSERT.entries
|
|
@@ -251,6 +369,11 @@ class CQN2SQLRenderer {
|
|
|
251
369
|
: cds.error`Missing .entries, .rows, or .values in ${q}`
|
|
252
370
|
}
|
|
253
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Renders an INSERT query with entries property
|
|
374
|
+
* @param {import('./infer/cqn').INSERT} q
|
|
375
|
+
* @returns {string} SQL
|
|
376
|
+
*/
|
|
254
377
|
INSERT_entries(q) {
|
|
255
378
|
const { INSERT } = q
|
|
256
379
|
const entity = this.name(q.target?.name || INSERT.into.ref[0])
|
|
@@ -262,6 +385,8 @@ class CQN2SQLRenderer {
|
|
|
262
385
|
const columns = elements
|
|
263
386
|
? ObjectKeys(elements).filter(c => c in elements && !elements[c].virtual && !elements[c].isAssociation)
|
|
264
387
|
: ObjectKeys(INSERT.entries[0])
|
|
388
|
+
|
|
389
|
+
/** @type {string[]} */
|
|
265
390
|
this.columns = columns.filter(elements ? c => !elements[c]?.['@cds.extension'] : () => true).map(c => this.quote(c))
|
|
266
391
|
|
|
267
392
|
const extractions = this.managed(
|
|
@@ -288,12 +413,19 @@ class CQN2SQLRenderer {
|
|
|
288
413
|
.filter(a => a)
|
|
289
414
|
.map(c => c.sql)
|
|
290
415
|
|
|
291
|
-
this.
|
|
292
|
-
|
|
416
|
+
// Include this.values for placeholders
|
|
417
|
+
/** @type {unknown[][]} */
|
|
418
|
+
this.entries = [[...this.values, JSON.stringify(INSERT.entries)]]
|
|
419
|
+
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${
|
|
293
420
|
this.columns
|
|
294
421
|
}) SELECT ${extraction} FROM json_each(?)`)
|
|
295
422
|
}
|
|
296
423
|
|
|
424
|
+
/**
|
|
425
|
+
* Renders an INSERT query with rows property
|
|
426
|
+
* @param {import('./infer/cqn').INSERT} q
|
|
427
|
+
* @returns {string} SQL
|
|
428
|
+
*/
|
|
297
429
|
INSERT_rows(q) {
|
|
298
430
|
const { INSERT } = q
|
|
299
431
|
const entity = this.name(q.target?.name || INSERT.into.ref[0])
|
|
@@ -302,14 +434,11 @@ class CQN2SQLRenderer {
|
|
|
302
434
|
if (!INSERT.columns && !elements) {
|
|
303
435
|
throw cds.error`Cannot insert rows without columns or elements`
|
|
304
436
|
}
|
|
305
|
-
let columns = INSERT.columns || (elements && ObjectKeys(elements))
|
|
306
|
-
if (elements) {
|
|
307
|
-
columns = columns.filter(c => c in elements && !elements[c].virtual && !elements[c].isAssociation)
|
|
308
|
-
}
|
|
437
|
+
let columns = INSERT.columns || (elements && ObjectKeys(elements).filter(c => !elements[c].virtual && !elements[c].isAssociation))
|
|
309
438
|
this.columns = columns.map(c => this.quote(c))
|
|
310
439
|
|
|
311
440
|
const inputConverterKey = this.class._convertInput
|
|
312
|
-
const extraction = columns.map((c,
|
|
441
|
+
const extraction = columns.map((c,i) => {
|
|
313
442
|
const element = elements?.[c] || {}
|
|
314
443
|
const extract = `value->>'$[${i}]'`
|
|
315
444
|
const converter = element[inputConverterKey] || (e => e)
|
|
@@ -317,16 +446,26 @@ class CQN2SQLRenderer {
|
|
|
317
446
|
})
|
|
318
447
|
|
|
319
448
|
this.entries = [[JSON.stringify(INSERT.rows)]]
|
|
320
|
-
return (this.sql = `INSERT INTO ${entity}${alias ? ' as ' + this.quote(alias) : ''} (${
|
|
449
|
+
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${
|
|
321
450
|
this.columns
|
|
322
451
|
}) SELECT ${extraction} FROM json_each(?)`)
|
|
323
452
|
}
|
|
324
453
|
|
|
454
|
+
/**
|
|
455
|
+
* Renders an INSERT query with values property
|
|
456
|
+
* @param {import('./infer/cqn').INSERT} q
|
|
457
|
+
* @returns {string} SQL
|
|
458
|
+
*/
|
|
325
459
|
INSERT_values(q) {
|
|
326
460
|
let { columns, values } = q.INSERT
|
|
327
461
|
return this.INSERT_rows({ __proto__: q, INSERT: { __proto__: q.INSERT, columns, rows: [values] } })
|
|
328
462
|
}
|
|
329
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Renders an INSERT query from SELECT query
|
|
466
|
+
* @param {import('./infer/cqn').INSERT} q
|
|
467
|
+
* @returns {string} SQL
|
|
468
|
+
*/
|
|
330
469
|
INSERT_select(q) {
|
|
331
470
|
const { INSERT } = q
|
|
332
471
|
const entity = this.name(q.target.name)
|
|
@@ -342,19 +481,32 @@ class CQN2SQLRenderer {
|
|
|
342
481
|
return this.sql
|
|
343
482
|
}
|
|
344
483
|
|
|
484
|
+
/**
|
|
485
|
+
* Wraps the provided SQL expression for output processing
|
|
486
|
+
* @param {import('./infer/cqn').element} element
|
|
487
|
+
* @param {string} expr
|
|
488
|
+
* @returns {string} SQL
|
|
489
|
+
*/
|
|
345
490
|
output_converter4(element, expr) {
|
|
346
491
|
const fn = element?.[this.class._convertOutput]
|
|
347
492
|
return fn?.(expr, element) || expr
|
|
348
493
|
}
|
|
349
494
|
|
|
495
|
+
/** @type {import('./converters').Converters} */
|
|
350
496
|
static InputConverters = {} // subclasses to override
|
|
351
497
|
|
|
498
|
+
/** @type {import('./converters').Converters} */
|
|
352
499
|
static OutputConverters = {} // subclasses to override
|
|
353
500
|
|
|
354
501
|
static localized = { String: true, UUID: false }
|
|
355
502
|
|
|
356
503
|
// UPSERT Statements ------------------------------------------------
|
|
357
504
|
|
|
505
|
+
/**
|
|
506
|
+
* Renders an UPSERT query into generic SQL
|
|
507
|
+
* @param {import('./infer/cqn').UPDATE} q
|
|
508
|
+
* @returns {string} SQL
|
|
509
|
+
*/
|
|
358
510
|
UPSERT(q) {
|
|
359
511
|
let { UPSERT } = q,
|
|
360
512
|
sql = this.INSERT({ __proto__: q, INSERT: UPSERT })
|
|
@@ -376,31 +528,32 @@ class CQN2SQLRenderer {
|
|
|
376
528
|
|
|
377
529
|
// UPDATE Statements ------------------------------------------------
|
|
378
530
|
|
|
531
|
+
/**
|
|
532
|
+
* Renders an UPDATE query into generic SQL
|
|
533
|
+
* @param {import('./infer/cqn').UPDATE} q
|
|
534
|
+
* @returns {string} SQL
|
|
535
|
+
*/
|
|
379
536
|
UPDATE(q) {
|
|
380
|
-
const {
|
|
381
|
-
|
|
382
|
-
} = q,
|
|
383
|
-
elements = q.target?.elements
|
|
537
|
+
const { entity, with: _with, data, where } = q.UPDATE
|
|
538
|
+
const elements = q.target?.elements
|
|
384
539
|
let sql = `UPDATE ${this.name(entity.ref?.[0] || entity)}`
|
|
385
540
|
if (entity.as) sql += ` AS ${entity.as}`
|
|
541
|
+
|
|
386
542
|
let columns = []
|
|
387
|
-
if (data)
|
|
388
|
-
|
|
543
|
+
if (data) _add (data, val => this.val({val}))
|
|
544
|
+
if (_with) _add (_with, x => this.expr(x))
|
|
545
|
+
function _add (data, sql4) {
|
|
546
|
+
for (let c in data) {
|
|
389
547
|
if (!elements || (c in elements && !elements[c].virtual)) {
|
|
390
|
-
columns.push({ name: c, sql:
|
|
391
|
-
}
|
|
392
|
-
if (_with)
|
|
393
|
-
for (let c in _with)
|
|
394
|
-
if (!elements || (c in elements && !elements[c].virtual)) {
|
|
395
|
-
columns.push({ name: c, sql: this.expr(_with[c]) })
|
|
548
|
+
columns.push({ name: c, sql: sql4(data[c]) })
|
|
396
549
|
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
397
552
|
|
|
398
553
|
columns = columns.map(c => {
|
|
399
|
-
if (q.elements?.[c.name]?.['@cds.extension']) {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
sql: `json_set(extensions__,${this.string('$."' + c.name + '"')},${c.sql})`,
|
|
403
|
-
}
|
|
554
|
+
if (q.elements?.[c.name]?.['@cds.extension']) return {
|
|
555
|
+
name: 'extensions__',
|
|
556
|
+
sql: `json_set(extensions__,${this.string('$."' + c.name + '"')},${c.sql})`,
|
|
404
557
|
}
|
|
405
558
|
return c
|
|
406
559
|
})
|
|
@@ -414,6 +567,11 @@ class CQN2SQLRenderer {
|
|
|
414
567
|
|
|
415
568
|
// DELETE Statements ------------------------------------------------
|
|
416
569
|
|
|
570
|
+
/**
|
|
571
|
+
* Renders a DELETE query into generic SQL
|
|
572
|
+
* @param {import('./infer/cqn').DELETE} param0
|
|
573
|
+
* @returns {string} SQL
|
|
574
|
+
*/
|
|
417
575
|
DELETE({ DELETE: { from, where } }) {
|
|
418
576
|
let sql = `DELETE FROM ${this.from(from)}`
|
|
419
577
|
if (where) sql += ` WHERE ${this.where(where)}`
|
|
@@ -422,27 +580,79 @@ class CQN2SQLRenderer {
|
|
|
422
580
|
|
|
423
581
|
// STREAM Statement -------------------------------------------------
|
|
424
582
|
|
|
583
|
+
/**
|
|
584
|
+
* Renders a STREAM query into generic SQL
|
|
585
|
+
* @param {import('./infer/cqn').STREAM} q
|
|
586
|
+
* @returns {string} SQL
|
|
587
|
+
*/
|
|
425
588
|
STREAM(q) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
589
|
+
const { STREAM } = q
|
|
590
|
+
return STREAM.from
|
|
591
|
+
? this.STREAM_from(q)
|
|
592
|
+
: STREAM.into
|
|
593
|
+
? this.STREAM_into(q)
|
|
594
|
+
: cds.error`Missing .form or .into in ${q}`
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Renders a STREAM.into query into generic SQL
|
|
599
|
+
* @param {import('./infer/cqn').STREAM} q
|
|
600
|
+
* @returns {string} SQL
|
|
601
|
+
*/
|
|
602
|
+
STREAM_into(q) {
|
|
603
|
+
const { into, column, where, data } = q.STREAM
|
|
604
|
+
|
|
605
|
+
let sql
|
|
606
|
+
if (!_empty(column)) {
|
|
607
|
+
data.type = 'binary'
|
|
608
|
+
const update = UPDATE(into)
|
|
609
|
+
.with({ [column]: data })
|
|
610
|
+
.where(where)
|
|
611
|
+
Object.defineProperty(update, 'target', { value: q.target })
|
|
612
|
+
sql = this.UPDATE(update)
|
|
433
613
|
} else {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
sql =
|
|
437
|
-
this.
|
|
614
|
+
data.type = 'json'
|
|
615
|
+
// REVISIT: decide whether dataset streams should behave like INSERT or UPSERT
|
|
616
|
+
sql = this.UPSERT(UPSERT([{}]).into(into).forSQL())
|
|
617
|
+
this.values = [data]
|
|
438
618
|
}
|
|
439
|
-
|
|
440
|
-
if (from) sql += ` LIMIT ${this.limit({ rows: { val: 1 } })}`
|
|
619
|
+
|
|
441
620
|
return (this.sql = sql)
|
|
442
621
|
}
|
|
443
622
|
|
|
623
|
+
/**
|
|
624
|
+
* Renders a STREAM.from query into generic SQL
|
|
625
|
+
* @param {import('./infer/cqn').STREAM} q
|
|
626
|
+
* @returns {string} SQL
|
|
627
|
+
*/
|
|
628
|
+
STREAM_from(q) {
|
|
629
|
+
const { column, from, where, columns } = q.STREAM
|
|
630
|
+
|
|
631
|
+
const select = cds.ql
|
|
632
|
+
.SELECT(column ? [column] : columns)
|
|
633
|
+
.where(where)
|
|
634
|
+
.limit(column ? 1 : undefined)
|
|
635
|
+
|
|
636
|
+
// SELECT.from() does not accept joins
|
|
637
|
+
select.SELECT.from = from
|
|
638
|
+
|
|
639
|
+
if (column) {
|
|
640
|
+
this.one = true
|
|
641
|
+
} else {
|
|
642
|
+
select.SELECT.expand = 'root'
|
|
643
|
+
this.one = !!from.SELECT?.one
|
|
644
|
+
}
|
|
645
|
+
return this.SELECT(select.forSQL())
|
|
646
|
+
}
|
|
647
|
+
|
|
444
648
|
// Expression Clauses ---------------------------------------------
|
|
445
649
|
|
|
650
|
+
/**
|
|
651
|
+
* Renders an expression object into generic SQL
|
|
652
|
+
* @param {import('./infer/cqn').expr} x
|
|
653
|
+
* @returns {string} SQL
|
|
654
|
+
* @throws {Error} When an unknown un supported expression is provided
|
|
655
|
+
*/
|
|
446
656
|
expr(x) {
|
|
447
657
|
const wrap = x.cast ? sql => `cast(${sql} as ${this.type4(x.cast)})` : sql => sql
|
|
448
658
|
if (typeof x === 'string') throw cds.error`Unsupported expr: ${x}`
|
|
@@ -456,6 +666,11 @@ class CQN2SQLRenderer {
|
|
|
456
666
|
else throw cds.error`Unsupported expr: ${x}`
|
|
457
667
|
}
|
|
458
668
|
|
|
669
|
+
/**
|
|
670
|
+
* Renders an list of expression objects into generic SQL
|
|
671
|
+
* @param {import('./infer/cqn').xpr} param0
|
|
672
|
+
* @returns {string} SQL
|
|
673
|
+
*/
|
|
459
674
|
xpr({ xpr }) {
|
|
460
675
|
return xpr
|
|
461
676
|
.map((x, i) => {
|
|
@@ -467,125 +682,209 @@ class CQN2SQLRenderer {
|
|
|
467
682
|
.join(' ')
|
|
468
683
|
}
|
|
469
684
|
|
|
685
|
+
/**
|
|
686
|
+
* Renders an operation into generic SQL
|
|
687
|
+
* @param {string} x The current operator string
|
|
688
|
+
* @param {Number} i Current index of the operator inside the xpr
|
|
689
|
+
* @param {import('./infer/cqn').predicate[]} xpr The parent xpr in which the operator is used
|
|
690
|
+
* @returns {string} The correct operator string
|
|
691
|
+
*/
|
|
470
692
|
operator(x, i, xpr) {
|
|
471
|
-
|
|
472
|
-
|
|
693
|
+
|
|
694
|
+
// Translate = to IS NULL for rhs operand being NULL literal
|
|
695
|
+
if (x === '=') return xpr[i+1]?.val === null ? 'is' : '='
|
|
696
|
+
|
|
697
|
+
// Translate == to IS NOT NULL for rhs operand being NULL literal, otherwise ...
|
|
698
|
+
// Translate == to IS NOT DISTINCT FROM, unless both operands cannot be NULL
|
|
699
|
+
if (x === '==') return xpr[i+1]?.val === null ? 'is' : _not_null(i-1) && _not_null(i+1) ? '=' : this.is_not_distinct_from_
|
|
700
|
+
|
|
701
|
+
// Translate != to IS NULL for rhs operand being NULL literal, otherwise...
|
|
702
|
+
// Translate != to IS DISTINCT FROM, unless both operands cannot be NULL
|
|
703
|
+
if (x === '!=') return xpr[i+1]?.val === null ? 'is not' : _not_null(i-1) && _not_null(i+1) ? '<>' : this.is_distinct_from_
|
|
704
|
+
|
|
473
705
|
else return x
|
|
706
|
+
|
|
707
|
+
/** Checks if the operand at xpr[i+-1] can be NULL. @returns true if not */
|
|
708
|
+
function _not_null(i) {
|
|
709
|
+
const operand = xpr[i]
|
|
710
|
+
if (!operand) return false
|
|
711
|
+
if (operand.val != null) return true // non-null values are not null
|
|
712
|
+
let element = operand.element
|
|
713
|
+
if (!element) return false
|
|
714
|
+
if (element.key) return true // primary keys usually should not be null
|
|
715
|
+
if (element.notNull) return true // not null elements cannot be null
|
|
716
|
+
}
|
|
474
717
|
}
|
|
475
718
|
|
|
719
|
+
get is_distinct_from_() { return 'is distinct from' }
|
|
720
|
+
get is_not_distinct_from_() { return 'is not distinct from' }
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Renders an argument place holder into the SQL for prepared statements
|
|
724
|
+
* @param {import('./infer/cqn').ref} param0
|
|
725
|
+
* @returns {string} SQL
|
|
726
|
+
* @throws {Error} When an unsupported ref definition is provided
|
|
727
|
+
*/
|
|
476
728
|
param({ ref }) {
|
|
477
729
|
if (ref.length > 1) throw cds.error`Unsupported nested ref parameter: ${ref}`
|
|
478
730
|
return ref[0] === '?' ? '?' : `:${ref}`
|
|
479
731
|
}
|
|
480
732
|
|
|
733
|
+
/**
|
|
734
|
+
* Renders a ref into generic SQL
|
|
735
|
+
* @param {import('./infer/cqn').ref} param0
|
|
736
|
+
* @returns {string} SQL
|
|
737
|
+
*/
|
|
481
738
|
ref({ ref }) {
|
|
482
|
-
|
|
739
|
+
switch (ref[0]) {
|
|
740
|
+
case '$now': return this.func({ func: 'session_context', args: [{ val: '$now' }]})
|
|
741
|
+
case '$user':
|
|
742
|
+
case '$user.id': return this.func({ func: 'session_context', args: [{ val: '$user.id' }]})
|
|
743
|
+
default: return ref.map(r => this.quote(r)).join('.')
|
|
744
|
+
}
|
|
483
745
|
}
|
|
484
746
|
|
|
747
|
+
/**
|
|
748
|
+
* Renders a value into the correct SQL syntax of a placeholder for a prepared statement
|
|
749
|
+
* @param {import('./infer/cqn').val} param0
|
|
750
|
+
* @returns {string} SQL
|
|
751
|
+
*/
|
|
485
752
|
val({ val }) {
|
|
486
753
|
switch (typeof val) {
|
|
487
|
-
case 'function':
|
|
488
|
-
|
|
489
|
-
case '
|
|
490
|
-
|
|
491
|
-
case 'boolean':
|
|
492
|
-
return val
|
|
493
|
-
case 'number':
|
|
494
|
-
return val // REVISIT for HANA
|
|
754
|
+
case 'function': throw new Error('Function values not supported.')
|
|
755
|
+
case 'undefined': return 'NULL'
|
|
756
|
+
case 'boolean': return `${val}`
|
|
757
|
+
case 'number': return `${val}` // REVISIT for HANA
|
|
495
758
|
case 'object':
|
|
496
759
|
if (val === null) return 'NULL'
|
|
497
760
|
if (val instanceof Date) return `'${val.toISOString()}'`
|
|
498
|
-
if (
|
|
499
|
-
else
|
|
761
|
+
if (val instanceof Readable) ; // go on with default below
|
|
762
|
+
else if (Buffer.isBuffer(val)) val = val.toString('base64')
|
|
763
|
+
else if (is_regexp(val)) val = val.source
|
|
764
|
+
else val = JSON.stringify(val)
|
|
765
|
+
case 'string': // eslint-disable-line no-fallthrough
|
|
500
766
|
}
|
|
501
767
|
if (!this.values) return this.string(val)
|
|
502
|
-
this.values.push(val)
|
|
768
|
+
else this.values.push(val)
|
|
503
769
|
return '?'
|
|
504
770
|
}
|
|
505
771
|
|
|
506
772
|
static Functions = require('./cql-functions')
|
|
773
|
+
/**
|
|
774
|
+
* Renders a function call into mapped SQL definitions from the Functions definition
|
|
775
|
+
* @param {import('./infer/cqn').func} param0
|
|
776
|
+
* @returns {string} SQL
|
|
777
|
+
*/
|
|
507
778
|
func({ func, args }) {
|
|
508
779
|
args = (args || []).map(e => (e === '*' ? e : { __proto__: e, toString: (x = e) => this.expr(x) }))
|
|
509
780
|
return this.class.Functions[func]?.apply(this.class.Functions, args) || `${func}(${args})`
|
|
510
781
|
}
|
|
511
782
|
|
|
783
|
+
/**
|
|
784
|
+
* Renders a list into generic SQL
|
|
785
|
+
* @param {import('./infer/cqn').list} param0
|
|
786
|
+
* @returns {string} SQL
|
|
787
|
+
*/
|
|
512
788
|
list({ list }) {
|
|
513
789
|
return `(${list.map(e => this.expr(e))})`
|
|
514
790
|
}
|
|
515
791
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
return JSON.stringify(o)
|
|
522
|
-
}
|
|
523
|
-
|
|
792
|
+
/**
|
|
793
|
+
* Renders a javascript string into a SQL string literal
|
|
794
|
+
* @param {string} s
|
|
795
|
+
* @returns {string} SQL
|
|
796
|
+
*/
|
|
524
797
|
string(s) {
|
|
525
798
|
return `'${s.replace(/'/g, "''")}'`
|
|
526
799
|
}
|
|
527
800
|
|
|
801
|
+
/**
|
|
802
|
+
* Calculates the effect column name
|
|
803
|
+
* @param {import('./infer/cqn').col} col
|
|
804
|
+
* @returns {string} explicit/implicit column alias
|
|
805
|
+
*/
|
|
528
806
|
column_name(col) {
|
|
529
|
-
|
|
807
|
+
if (col === '*')
|
|
808
|
+
// REVISIT: When could this ever happen? I think this is only about that irrealistic test whech uses column_name to implement SELECT_columns. We should eliminate column_name as its only used and designed for use in SELECT_expand, isn't it?
|
|
809
|
+
cds.error`Query was not inferred and includes '*' in the columns. For which there is no column name available.`
|
|
810
|
+
return (typeof col.as === 'string' && col.as) || ('val' in col && col.val + '') || col.func || col.ref.at(-1)
|
|
530
811
|
}
|
|
531
812
|
|
|
813
|
+
/**
|
|
814
|
+
* Calculates the Database name of the given name
|
|
815
|
+
* @param {string|import('./infer/cqn').ref} name
|
|
816
|
+
* @returns {string} Database name
|
|
817
|
+
*/
|
|
532
818
|
name(name) {
|
|
533
819
|
return (name.id || name).replace(/\./g, '_')
|
|
534
820
|
}
|
|
535
821
|
|
|
822
|
+
/** @type {unknown} */
|
|
536
823
|
static ReservedWords = {}
|
|
824
|
+
/**
|
|
825
|
+
* Ensures that the given identifier is properly quoted when required by the database
|
|
826
|
+
* @param {string} s
|
|
827
|
+
* @returns {string} SQL
|
|
828
|
+
*/
|
|
537
829
|
quote(s) {
|
|
538
830
|
if (typeof s !== 'string') return '"' + s + '"'
|
|
539
831
|
if (s.includes('"')) return '"' + s.replace(/"/g, '""') + '"'
|
|
540
|
-
|
|
832
|
+
// Column names like "Order" clash with "ORDER" keyword so toUpperCase is required
|
|
833
|
+
if (s in this.class.ReservedWords || /^\d|[$' ?@./\\]/.test(s)) return '"' + s + '"'
|
|
541
834
|
return s
|
|
542
835
|
}
|
|
543
836
|
|
|
837
|
+
/**
|
|
838
|
+
* Convers the columns array into an array of SQL expressions that extract the correct value from inserted JSON data
|
|
839
|
+
* @param {object[]} columns
|
|
840
|
+
* @param {import('./infer/cqn').elements} elements
|
|
841
|
+
* @param {Boolean} isUpdate
|
|
842
|
+
* @returns {string[]} Array of SQL expressions for processing input JSON data
|
|
843
|
+
*/
|
|
544
844
|
managed(columns, elements, isUpdate = false) {
|
|
545
845
|
const annotation = isUpdate ? '@cds.on.update' : '@cds.on.insert'
|
|
546
|
-
const
|
|
846
|
+
const { _convertInput } = this.class
|
|
547
847
|
// Ensure that missing managed columns are added
|
|
548
848
|
const requiredColumns = !elements
|
|
549
849
|
? []
|
|
550
850
|
: Object.keys(elements)
|
|
551
851
|
.filter(
|
|
552
852
|
e =>
|
|
553
|
-
(elements[e]?.[annotation] || (!isUpdate && elements[e]?.default && !elements[e].virtual)) &&
|
|
853
|
+
(elements[e]?.[annotation] || (!isUpdate && elements[e]?.default && !elements[e].virtual && !elements[e].isAssociation)) &&
|
|
554
854
|
!columns.find(c => c.name === e),
|
|
555
855
|
)
|
|
556
856
|
.map(name => ({ name, sql: 'NULL' }))
|
|
557
857
|
|
|
558
858
|
return [...columns, ...requiredColumns].map(({ name, sql }) => {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
let
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
managed = this.string(this.context.timestamp.toISOString())
|
|
570
|
-
break
|
|
571
|
-
default:
|
|
572
|
-
managed = undefined
|
|
573
|
-
}
|
|
574
|
-
if (!isUpdate) {
|
|
859
|
+
let element = elements?.[name] || {}
|
|
860
|
+
if (!sql) sql = `value->>'$."${name}"'`
|
|
861
|
+
|
|
862
|
+
let converter = element[_convertInput]
|
|
863
|
+
if (converter && sql[0] !== '$') sql = converter(sql, element)
|
|
864
|
+
|
|
865
|
+
let val = _managed[element[annotation]?.['=']]
|
|
866
|
+
if (val) sql = `coalesce(${sql}, ${this.func({ func: 'session_context', args: [{ val }] })})`
|
|
867
|
+
|
|
868
|
+
else if (!isUpdate && element.default) {
|
|
575
869
|
const d = element.default
|
|
576
|
-
if (d
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
870
|
+
if (d.val !== undefined || d.ref?.[0] === '$now') {
|
|
871
|
+
// REVISIT: d.ref is not used afterwards
|
|
872
|
+
sql = `(CASE WHEN json_type(value,'$."${name}"') IS NULL THEN ${
|
|
873
|
+
this.defaultValue(d.val) // REVISIT: this.defaultValue is a strange function
|
|
874
|
+
} ELSE ${sql} END)`
|
|
580
875
|
}
|
|
581
876
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
sql: converter(managed === undefined ? extract : `coalesce(${extract}, ${managed})`, element),
|
|
585
|
-
}
|
|
877
|
+
|
|
878
|
+
return { name, sql }
|
|
586
879
|
})
|
|
587
880
|
}
|
|
588
881
|
|
|
882
|
+
/**
|
|
883
|
+
* Returns the default value
|
|
884
|
+
* @param {string} defaultValue
|
|
885
|
+
* @returns {string}
|
|
886
|
+
*/
|
|
887
|
+
// REVISIT: This is a strange method, also overridden inconsistently in postgres
|
|
589
888
|
defaultValue(defaultValue = this.context.timestamp.toISOString()) {
|
|
590
889
|
return typeof defaultValue === 'string' ? this.string(defaultValue) : defaultValue
|
|
591
890
|
}
|
|
@@ -597,9 +896,19 @@ Buffer.prototype.toJSON = function () {
|
|
|
597
896
|
}
|
|
598
897
|
|
|
599
898
|
const ObjectKeys = o => (o && [...ObjectKeys(o.__proto__), ...Object.keys(o)]) || []
|
|
600
|
-
const
|
|
601
|
-
|
|
899
|
+
const _managed = {
|
|
900
|
+
'$user.id': '$user.id',
|
|
901
|
+
$user: '$user.id',
|
|
902
|
+
$now: '$now',
|
|
903
|
+
}
|
|
602
904
|
|
|
603
905
|
const is_regexp = x => x?.constructor?.name === 'RegExp' // NOTE: x instanceof RegExp doesn't work in repl
|
|
604
906
|
const _empty = a => !a || a.length === 0
|
|
605
|
-
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* @param {import('@sap/cds/apis/cqn').Query} q
|
|
910
|
+
* @param {import('@sap/cds/apis/csn').CSN} m
|
|
911
|
+
*/
|
|
912
|
+
module.exports = (q, m) => new CQN2SQLRenderer().render(cqn4sql(q, m), m)
|
|
913
|
+
module.exports.class = CQN2SQLRenderer
|
|
914
|
+
module.exports.classDefinition = CQN2SQLRenderer // class is a reserved typescript word
|