@cap-js/db-service 1.0.0 → 1.1.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 +19 -1
- package/index.js +16 -2
- package/lib/InsertResults.js +20 -3
- package/lib/SQLService.js +112 -28
- package/lib/common/DatabaseService.js +55 -4
- package/lib/common/factory.d.ts +5 -0
- package/lib/converters.d.ts +24 -0
- package/lib/cql-functions.js +192 -4
- package/lib/cqn2sql.js +270 -5
- package/lib/cqn4sql.js +172 -38
- package/lib/deep-queries.js +27 -0
- package/lib/fill-in-keys.js +10 -0
- package/lib/infer/cqn.d.ts +45 -0
- package/lib/infer/index.js +178 -43
- package/lib/infer/join-tree.js +62 -17
- package/package.json +18 -6
package/lib/cqn2sql.js
CHANGED
|
@@ -14,11 +14,25 @@ const DEBUG = (() => {
|
|
|
14
14
|
})()
|
|
15
15
|
|
|
16
16
|
class CQN2SQLRenderer {
|
|
17
|
+
/**
|
|
18
|
+
* Creates a new CQN2SQL instance for processing a query
|
|
19
|
+
* @constructor
|
|
20
|
+
* @param {import('@sap/cds/apis/services').ContextProperties} context the cds.context of the request
|
|
21
|
+
*/
|
|
17
22
|
constructor(context) {
|
|
23
|
+
/**
|
|
24
|
+
* @type {import('@sap/cds/apis/services').ContextProperties}
|
|
25
|
+
*/
|
|
18
26
|
this.context = cds.context || context
|
|
27
|
+
// REVISIT: find a way to make CQN2SQLRenderer work in SQLService as well
|
|
28
|
+
/** @type {CQN2SQLRenderer|unknown} */
|
|
19
29
|
this.class = new.target // for IntelliSense
|
|
20
30
|
this.class._init() // is a noop for subsequent calls
|
|
21
31
|
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Initializes the class one first creation to link types to data converters
|
|
35
|
+
*/
|
|
22
36
|
static _init() {
|
|
23
37
|
const _add_mixins = (aspect, mixins) => {
|
|
24
38
|
const fqn = this.name + aspect
|
|
@@ -37,9 +51,19 @@ class CQN2SQLRenderer {
|
|
|
37
51
|
this._init = () => {} // makes this a noop for subsequent calls
|
|
38
52
|
}
|
|
39
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Renders incoming query into SQL and generates binding values
|
|
56
|
+
* @param {import('./infer/cqn').Query} q CQN query to be rendered
|
|
57
|
+
* @param {unknown[]|undefined} vars Values to be used for params
|
|
58
|
+
* @returns {CQN2SQLRenderer|unknown}
|
|
59
|
+
*/
|
|
40
60
|
render(q, vars) {
|
|
41
61
|
const cmd = q.cmd || Object.keys(q)[0] // SELECT, INSERT, ...
|
|
62
|
+
/**
|
|
63
|
+
* @type {string} the rendered SQL string
|
|
64
|
+
*/
|
|
42
65
|
this.sql = '' // to have it as first property for debugging
|
|
66
|
+
/** @type {unknown[]} */
|
|
43
67
|
this.values = [] // prepare values, filled in by subroutines
|
|
44
68
|
this[cmd]((this.cqn = q)) // actual sql rendering happens here
|
|
45
69
|
if (vars?.length && !this.values.length) this.values = vars
|
|
@@ -51,12 +75,21 @@ class CQN2SQLRenderer {
|
|
|
51
75
|
return this
|
|
52
76
|
}
|
|
53
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Links the incoming query with the current service model
|
|
80
|
+
* @param {import('./infer/cqn').Query} q
|
|
81
|
+
* @returns {import('./infer/cqn').Query}
|
|
82
|
+
*/
|
|
54
83
|
infer(q) {
|
|
55
84
|
return q.target ? q : cds_infer(q)
|
|
56
85
|
}
|
|
57
86
|
|
|
58
87
|
// CREATE Statements ------------------------------------------------
|
|
59
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Renders a CREATE query into generic SQL
|
|
91
|
+
* @param {import('./infer/cqn').CREATE} q
|
|
92
|
+
*/
|
|
60
93
|
CREATE(q) {
|
|
61
94
|
const { target } = q,
|
|
62
95
|
{ query } = target
|
|
@@ -71,6 +104,11 @@ class CQN2SQLRenderer {
|
|
|
71
104
|
return
|
|
72
105
|
}
|
|
73
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Renders a column clause for the given elements
|
|
109
|
+
* @param {import('./infer/cqn').elements} elements
|
|
110
|
+
* @returns {string} SQL
|
|
111
|
+
*/
|
|
74
112
|
CREATE_elements(elements) {
|
|
75
113
|
let sql = ''
|
|
76
114
|
for (let e in elements) {
|
|
@@ -82,11 +120,21 @@ class CQN2SQLRenderer {
|
|
|
82
120
|
return sql.slice(0, -2)
|
|
83
121
|
}
|
|
84
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Renders a column definition for the given element
|
|
125
|
+
* @param {import('./infer/cqn').element} element
|
|
126
|
+
* @returns {string} SQL
|
|
127
|
+
*/
|
|
85
128
|
CREATE_element(element) {
|
|
86
129
|
const type = this.type4(element)
|
|
87
130
|
if (type) return this.quote(element.name) + ' ' + type
|
|
88
131
|
}
|
|
89
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Renders the SQL type definition for the given element
|
|
135
|
+
* @param {import('./infer/cqn').element} element
|
|
136
|
+
* @returns {string}
|
|
137
|
+
*/
|
|
90
138
|
type4(element) {
|
|
91
139
|
if (!element._type) element = cds.builtin.types[element.type] || element
|
|
92
140
|
const fn = element[this.class._sqlType]
|
|
@@ -95,6 +143,9 @@ class CQN2SQLRenderer {
|
|
|
95
143
|
)
|
|
96
144
|
}
|
|
97
145
|
|
|
146
|
+
/** @callback converter */
|
|
147
|
+
|
|
148
|
+
/** @type {Object<string,import('@sap/cds/apis/csn').Definition>} */
|
|
98
149
|
static TypeMap = {
|
|
99
150
|
// Utilizing cds.linked inheritance
|
|
100
151
|
String: e => `NVARCHAR(${e.length || 5000})`,
|
|
@@ -120,6 +171,10 @@ class CQN2SQLRenderer {
|
|
|
120
171
|
|
|
121
172
|
// DROP Statements ------------------------------------------------
|
|
122
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Renders a DROP query into generic SQL
|
|
176
|
+
* @param {import('./infer/cqn').DROP} q
|
|
177
|
+
*/
|
|
123
178
|
DROP(q) {
|
|
124
179
|
const { target } = q
|
|
125
180
|
const isView = target.query || target.projection
|
|
@@ -128,6 +183,10 @@ class CQN2SQLRenderer {
|
|
|
128
183
|
|
|
129
184
|
// SELECT Statements ------------------------------------------------
|
|
130
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Renders a SELECT statement into generic SQL
|
|
188
|
+
* @param {import('./infer/cqn').SELECT} q
|
|
189
|
+
*/
|
|
131
190
|
SELECT(q) {
|
|
132
191
|
let { from, expand, where, groupBy, having, orderBy, limit, one, distinct, localized } = q.SELECT
|
|
133
192
|
if (!expand) expand = q.SELECT.expand = has_expands(q) || has_arrays(q)
|
|
@@ -145,16 +204,34 @@ class CQN2SQLRenderer {
|
|
|
145
204
|
if (!_empty((x = orderBy))) sql += ` ORDER BY ${this.orderBy(x, localized)}`
|
|
146
205
|
if (one) sql += ` LIMIT ${this.limit({ rows: { val: 1 } })}`
|
|
147
206
|
else if ((x = limit)) sql += ` LIMIT ${this.limit(x)}`
|
|
148
|
-
|
|
207
|
+
// Expand cannot work without an inferred query
|
|
208
|
+
if (expand) {
|
|
209
|
+
if (!q.elements) cds.error`Query was not inferred and includes expand. For which the metadata is missing.`
|
|
210
|
+
sql = this.SELECT_expand(q, sql)
|
|
211
|
+
}
|
|
149
212
|
return (this.sql = sql)
|
|
150
213
|
}
|
|
151
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Renders a column clause into generic SQL
|
|
217
|
+
* @param {import('./infer/cqn').SELECT} param0
|
|
218
|
+
* @returns {string} SQL
|
|
219
|
+
*/
|
|
152
220
|
SELECT_columns({ SELECT }) {
|
|
153
221
|
// REVISIT: We don't have to run x.as through this.column_name(), do we?
|
|
154
222
|
if (!SELECT.columns) return '*'
|
|
155
|
-
return SELECT.columns.map(x =>
|
|
223
|
+
return SELECT.columns.map(x => {
|
|
224
|
+
if (x === '*') return x
|
|
225
|
+
return this.column_expr(x) + (typeof x.as === 'string' ? ' as ' + this.quote(x.as) : '')
|
|
226
|
+
})
|
|
156
227
|
}
|
|
157
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Renders a JSON select around the provided SQL statement
|
|
231
|
+
* @param {import('./infer/cqn').SELECT} param0
|
|
232
|
+
* @param {string} sql
|
|
233
|
+
* @returns {string} SQL
|
|
234
|
+
*/
|
|
158
235
|
SELECT_expand({ SELECT, elements }, sql) {
|
|
159
236
|
if (!SELECT.columns) return sql
|
|
160
237
|
if (!elements) return sql
|
|
@@ -183,6 +260,11 @@ class CQN2SQLRenderer {
|
|
|
183
260
|
return `SELECT ${SELECT.one || SELECT.expand === 'root' ? obj : `json_group_array(${obj})`} as _json_ FROM (${sql})`
|
|
184
261
|
}
|
|
185
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Renders a SELECT column expression into generic SQL
|
|
265
|
+
* @param {import('./infer/cqn').col} x
|
|
266
|
+
* @returns {string} SQL
|
|
267
|
+
*/
|
|
186
268
|
column_expr(x) {
|
|
187
269
|
if (x.func && !x.as) x.as = x.func
|
|
188
270
|
if (x?.element?.['@cds.extension']) {
|
|
@@ -193,6 +275,11 @@ class CQN2SQLRenderer {
|
|
|
193
275
|
return sql
|
|
194
276
|
}
|
|
195
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Renders a FROM clause into generic SQL
|
|
280
|
+
* @param {import('./infer/cqn').source} from
|
|
281
|
+
* @returns {string} SQL
|
|
282
|
+
*/
|
|
196
283
|
from(from) {
|
|
197
284
|
const { ref, as } = from,
|
|
198
285
|
_aliased = as ? s => s + ` as ${this.quote(as)}` : s => s
|
|
@@ -208,18 +295,39 @@ class CQN2SQLRenderer {
|
|
|
208
295
|
}
|
|
209
296
|
}
|
|
210
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Renders a WHERE clause into generic SQL
|
|
300
|
+
* @param {import('./infer/cqn').predicate} xpr
|
|
301
|
+
* @returns {string} SQL
|
|
302
|
+
*/
|
|
211
303
|
where(xpr) {
|
|
212
304
|
return this.xpr({ xpr })
|
|
213
305
|
}
|
|
214
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Renders a HAVING clause into generic SQL
|
|
309
|
+
* @param {import('./infer/cqn').predicate} xpr
|
|
310
|
+
* @returns {string} SQL
|
|
311
|
+
*/
|
|
215
312
|
having(xpr) {
|
|
216
313
|
return this.xpr({ xpr })
|
|
217
314
|
}
|
|
218
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Renders a groupBy clause into generic SQL
|
|
318
|
+
* @param {import('./infer/cqn').expr[]} clause
|
|
319
|
+
* @returns {string[] | string} SQL
|
|
320
|
+
*/
|
|
219
321
|
groupBy(clause) {
|
|
220
322
|
return clause.map(c => this.expr(c))
|
|
221
323
|
}
|
|
222
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Renders an orderBy clause into generic SQL
|
|
327
|
+
* @param {import('./infer/cqn').ordering_term[]} orderBy
|
|
328
|
+
* @param {boolean | undefined} localized
|
|
329
|
+
* @returns {string[] | string} SQL
|
|
330
|
+
*/
|
|
223
331
|
orderBy(orderBy, localized) {
|
|
224
332
|
return orderBy.map(
|
|
225
333
|
localized
|
|
@@ -231,6 +339,12 @@ class CQN2SQLRenderer {
|
|
|
231
339
|
)
|
|
232
340
|
}
|
|
233
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Renders an limit clause into generic SQL
|
|
344
|
+
* @param {import('./infer/cqn').limit} param0
|
|
345
|
+
* @returns {string} SQL
|
|
346
|
+
* @throws {Error} When no rows are defined
|
|
347
|
+
*/
|
|
234
348
|
limit({ rows, offset }) {
|
|
235
349
|
if (!rows) throw new Error('Rows parameter is missing in SELECT.limit(rows, offset)')
|
|
236
350
|
return !offset ? rows.val : `${rows.val} OFFSET ${offset.val}`
|
|
@@ -238,6 +352,11 @@ class CQN2SQLRenderer {
|
|
|
238
352
|
|
|
239
353
|
// INSERT Statements ------------------------------------------------
|
|
240
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Renders an INSERT query into generic SQL
|
|
357
|
+
* @param {import('./infer/cqn').INSERT} q
|
|
358
|
+
* @returns {string} SQL
|
|
359
|
+
*/
|
|
241
360
|
INSERT(q) {
|
|
242
361
|
const { INSERT } = q
|
|
243
362
|
return INSERT.entries
|
|
@@ -251,6 +370,11 @@ class CQN2SQLRenderer {
|
|
|
251
370
|
: cds.error`Missing .entries, .rows, or .values in ${q}`
|
|
252
371
|
}
|
|
253
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Renders an INSERT query with entries property
|
|
375
|
+
* @param {import('./infer/cqn').INSERT} q
|
|
376
|
+
* @returns {string} SQL
|
|
377
|
+
*/
|
|
254
378
|
INSERT_entries(q) {
|
|
255
379
|
const { INSERT } = q
|
|
256
380
|
const entity = this.name(q.target?.name || INSERT.into.ref[0])
|
|
@@ -262,6 +386,8 @@ class CQN2SQLRenderer {
|
|
|
262
386
|
const columns = elements
|
|
263
387
|
? ObjectKeys(elements).filter(c => c in elements && !elements[c].virtual && !elements[c].isAssociation)
|
|
264
388
|
: ObjectKeys(INSERT.entries[0])
|
|
389
|
+
|
|
390
|
+
/** @type {string[]} */
|
|
265
391
|
this.columns = columns.filter(elements ? c => !elements[c]?.['@cds.extension'] : () => true).map(c => this.quote(c))
|
|
266
392
|
|
|
267
393
|
const extractions = this.managed(
|
|
@@ -294,6 +420,11 @@ class CQN2SQLRenderer {
|
|
|
294
420
|
}) SELECT ${extraction} FROM json_each(?)`)
|
|
295
421
|
}
|
|
296
422
|
|
|
423
|
+
/**
|
|
424
|
+
* Renders an INSERT query with rows property
|
|
425
|
+
* @param {import('./infer/cqn').INSERT} q
|
|
426
|
+
* @returns {string} SQL
|
|
427
|
+
*/
|
|
297
428
|
INSERT_rows(q) {
|
|
298
429
|
const { INSERT } = q
|
|
299
430
|
const entity = this.name(q.target?.name || INSERT.into.ref[0])
|
|
@@ -322,11 +453,21 @@ class CQN2SQLRenderer {
|
|
|
322
453
|
}) SELECT ${extraction} FROM json_each(?)`)
|
|
323
454
|
}
|
|
324
455
|
|
|
456
|
+
/**
|
|
457
|
+
* Renders an INSERT query with values property
|
|
458
|
+
* @param {import('./infer/cqn').INSERT} q
|
|
459
|
+
* @returns {string} SQL
|
|
460
|
+
*/
|
|
325
461
|
INSERT_values(q) {
|
|
326
462
|
let { columns, values } = q.INSERT
|
|
327
463
|
return this.INSERT_rows({ __proto__: q, INSERT: { __proto__: q.INSERT, columns, rows: [values] } })
|
|
328
464
|
}
|
|
329
465
|
|
|
466
|
+
/**
|
|
467
|
+
* Renders an INSERT query from SELECT query
|
|
468
|
+
* @param {import('./infer/cqn').INSERT} q
|
|
469
|
+
* @returns {string} SQL
|
|
470
|
+
*/
|
|
330
471
|
INSERT_select(q) {
|
|
331
472
|
const { INSERT } = q
|
|
332
473
|
const entity = this.name(q.target.name)
|
|
@@ -342,19 +483,32 @@ class CQN2SQLRenderer {
|
|
|
342
483
|
return this.sql
|
|
343
484
|
}
|
|
344
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Wraps the provided SQL expression for output processing
|
|
488
|
+
* @param {import('./infer/cqn').element} element
|
|
489
|
+
* @param {string} expr
|
|
490
|
+
* @returns {string} SQL
|
|
491
|
+
*/
|
|
345
492
|
output_converter4(element, expr) {
|
|
346
493
|
const fn = element?.[this.class._convertOutput]
|
|
347
494
|
return fn?.(expr, element) || expr
|
|
348
495
|
}
|
|
349
496
|
|
|
497
|
+
/** @type {import('./converters').Converters} */
|
|
350
498
|
static InputConverters = {} // subclasses to override
|
|
351
499
|
|
|
500
|
+
/** @type {import('./converters').Converters} */
|
|
352
501
|
static OutputConverters = {} // subclasses to override
|
|
353
502
|
|
|
354
503
|
static localized = { String: true, UUID: false }
|
|
355
504
|
|
|
356
505
|
// UPSERT Statements ------------------------------------------------
|
|
357
506
|
|
|
507
|
+
/**
|
|
508
|
+
* Renders an UPSERT query into generic SQL
|
|
509
|
+
* @param {import('./infer/cqn').UPDATE} q
|
|
510
|
+
* @returns {string} SQL
|
|
511
|
+
*/
|
|
358
512
|
UPSERT(q) {
|
|
359
513
|
let { UPSERT } = q,
|
|
360
514
|
sql = this.INSERT({ __proto__: q, INSERT: UPSERT })
|
|
@@ -376,6 +530,11 @@ class CQN2SQLRenderer {
|
|
|
376
530
|
|
|
377
531
|
// UPDATE Statements ------------------------------------------------
|
|
378
532
|
|
|
533
|
+
/**
|
|
534
|
+
* Renders an UPDATE query into generic SQL
|
|
535
|
+
* @param {import('./infer/cqn').UPDATE} q
|
|
536
|
+
* @returns {string} SQL
|
|
537
|
+
*/
|
|
379
538
|
UPDATE(q) {
|
|
380
539
|
const {
|
|
381
540
|
UPDATE: { entity, with: _with, data, where },
|
|
@@ -414,6 +573,11 @@ class CQN2SQLRenderer {
|
|
|
414
573
|
|
|
415
574
|
// DELETE Statements ------------------------------------------------
|
|
416
575
|
|
|
576
|
+
/**
|
|
577
|
+
* Renders a DELETE query into generic SQL
|
|
578
|
+
* @param {import('./infer/cqn').DELETE} param0
|
|
579
|
+
* @returns {string} SQL
|
|
580
|
+
*/
|
|
417
581
|
DELETE({ DELETE: { from, where } }) {
|
|
418
582
|
let sql = `DELETE FROM ${this.from(from)}`
|
|
419
583
|
if (where) sql += ` WHERE ${this.where(where)}`
|
|
@@ -422,6 +586,11 @@ class CQN2SQLRenderer {
|
|
|
422
586
|
|
|
423
587
|
// STREAM Statement -------------------------------------------------
|
|
424
588
|
|
|
589
|
+
/**
|
|
590
|
+
* Renders a STREAM query into generic SQL
|
|
591
|
+
* @param {import('./infer/cqn').STREAM} q
|
|
592
|
+
* @returns {string} SQL
|
|
593
|
+
*/
|
|
425
594
|
STREAM(q) {
|
|
426
595
|
let { from, into, where, column, data } = q.STREAM
|
|
427
596
|
let x, sql
|
|
@@ -443,6 +612,12 @@ class CQN2SQLRenderer {
|
|
|
443
612
|
|
|
444
613
|
// Expression Clauses ---------------------------------------------
|
|
445
614
|
|
|
615
|
+
/**
|
|
616
|
+
* Renders an expression object into generic SQL
|
|
617
|
+
* @param {import('./infer/cqn').expr} x
|
|
618
|
+
* @returns {string} SQL
|
|
619
|
+
* @throws {Error} When an unknown un supported expression is provided
|
|
620
|
+
*/
|
|
446
621
|
expr(x) {
|
|
447
622
|
const wrap = x.cast ? sql => `cast(${sql} as ${this.type4(x.cast)})` : sql => sql
|
|
448
623
|
if (typeof x === 'string') throw cds.error`Unsupported expr: ${x}`
|
|
@@ -456,6 +631,11 @@ class CQN2SQLRenderer {
|
|
|
456
631
|
else throw cds.error`Unsupported expr: ${x}`
|
|
457
632
|
}
|
|
458
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Renders an list of expression objects into generic SQL
|
|
636
|
+
* @param {import('./infer/cqn').xpr} param0
|
|
637
|
+
* @returns {string} SQL
|
|
638
|
+
*/
|
|
459
639
|
xpr({ xpr }) {
|
|
460
640
|
return xpr
|
|
461
641
|
.map((x, i) => {
|
|
@@ -467,21 +647,44 @@ class CQN2SQLRenderer {
|
|
|
467
647
|
.join(' ')
|
|
468
648
|
}
|
|
469
649
|
|
|
650
|
+
/**
|
|
651
|
+
* Renders an operation into generic SQL
|
|
652
|
+
* @param {string} x The current operator string
|
|
653
|
+
* @param {Number} i Current index of the operator inside the xpr
|
|
654
|
+
* @param {import('./infer/cqn').predicate[]} xpr The parent xpr in which the operator is used
|
|
655
|
+
* @returns {string} The correct operator string
|
|
656
|
+
*/
|
|
470
657
|
operator(x, i, xpr) {
|
|
471
658
|
if (x === '=' && xpr[i + 1]?.val === null) return 'is'
|
|
472
659
|
if (x === '!=') return 'is not'
|
|
473
660
|
else return x
|
|
474
661
|
}
|
|
475
662
|
|
|
663
|
+
/**
|
|
664
|
+
* Renders an argument place holder into the SQL for prepared statements
|
|
665
|
+
* @param {import('./infer/cqn').ref} param0
|
|
666
|
+
* @returns {string} SQL
|
|
667
|
+
* @throws {Error} When an unsupported ref definition is provided
|
|
668
|
+
*/
|
|
476
669
|
param({ ref }) {
|
|
477
670
|
if (ref.length > 1) throw cds.error`Unsupported nested ref parameter: ${ref}`
|
|
478
671
|
return ref[0] === '?' ? '?' : `:${ref}`
|
|
479
672
|
}
|
|
480
673
|
|
|
674
|
+
/**
|
|
675
|
+
* Renders a ref into generic SQL
|
|
676
|
+
* @param {import('./infer/cqn').ref} param0
|
|
677
|
+
* @returns {string} SQL
|
|
678
|
+
*/
|
|
481
679
|
ref({ ref }) {
|
|
482
680
|
return ref.map(r => this.quote(r)).join('.')
|
|
483
681
|
}
|
|
484
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Renders a value into the correct SQL syntax of a placeholder for a prepared statement
|
|
685
|
+
* @param {import('./infer/cqn').val} param0
|
|
686
|
+
* @returns {string} SQL
|
|
687
|
+
*/
|
|
485
688
|
val({ val }) {
|
|
486
689
|
switch (typeof val) {
|
|
487
690
|
case 'function':
|
|
@@ -504,43 +707,93 @@ class CQN2SQLRenderer {
|
|
|
504
707
|
}
|
|
505
708
|
|
|
506
709
|
static Functions = require('./cql-functions')
|
|
710
|
+
/**
|
|
711
|
+
* Renders a function call into mapped SQL definitions from the Functions definition
|
|
712
|
+
* @param {import('./infer/cqn').func} param0
|
|
713
|
+
* @returns {string} SQL
|
|
714
|
+
*/
|
|
507
715
|
func({ func, args }) {
|
|
508
716
|
args = (args || []).map(e => (e === '*' ? e : { __proto__: e, toString: (x = e) => this.expr(x) }))
|
|
509
717
|
return this.class.Functions[func]?.apply(this.class.Functions, args) || `${func}(${args})`
|
|
510
718
|
}
|
|
511
719
|
|
|
720
|
+
/**
|
|
721
|
+
* Renders a list into generic SQL
|
|
722
|
+
* @param {import('./infer/cqn').list} param0
|
|
723
|
+
* @returns {string} SQL
|
|
724
|
+
*/
|
|
512
725
|
list({ list }) {
|
|
513
726
|
return `(${list.map(e => this.expr(e))})`
|
|
514
727
|
}
|
|
515
728
|
|
|
729
|
+
/**
|
|
730
|
+
* Renders a Regular Expression into its string representation
|
|
731
|
+
* @param {RegExp} o
|
|
732
|
+
* @returns {string} SQL
|
|
733
|
+
*/
|
|
516
734
|
regex(o) {
|
|
517
735
|
if (is_regexp(o)) return o.source
|
|
518
736
|
}
|
|
519
737
|
|
|
738
|
+
/**
|
|
739
|
+
* Renders the object as a JSON string in generic SQL
|
|
740
|
+
* @param {object} o
|
|
741
|
+
* @returns {string} SQL
|
|
742
|
+
*/
|
|
520
743
|
json(o) {
|
|
521
|
-
return JSON.stringify(o)
|
|
744
|
+
return this.string(JSON.stringify(o))
|
|
522
745
|
}
|
|
523
746
|
|
|
747
|
+
/**
|
|
748
|
+
* Renders a javascript string into a generic SQL string
|
|
749
|
+
* @param {string} s
|
|
750
|
+
* @returns {string} SQL
|
|
751
|
+
*/
|
|
524
752
|
string(s) {
|
|
525
753
|
return `'${s.replace(/'/g, "''")}'`
|
|
526
754
|
}
|
|
527
755
|
|
|
756
|
+
/**
|
|
757
|
+
* Calculates the effect column name
|
|
758
|
+
* @param {import('./infer/cqn').col} col
|
|
759
|
+
* @returns {string} explicit/implicit column alias
|
|
760
|
+
*/
|
|
528
761
|
column_name(col) {
|
|
762
|
+
if (col === '*')
|
|
763
|
+
cds.error`Query was not inferred and includes '*' in the columns. For which there is no column name available.`
|
|
529
764
|
return (typeof col.as === 'string' && col.as) || ('val' in col && col.val + '') || col.ref[col.ref.length - 1]
|
|
530
765
|
}
|
|
531
766
|
|
|
767
|
+
/**
|
|
768
|
+
* Calculates the Database name of the given name
|
|
769
|
+
* @param {string|import('./infer/cqn').ref} name
|
|
770
|
+
* @returns {string} Database name
|
|
771
|
+
*/
|
|
532
772
|
name(name) {
|
|
533
773
|
return (name.id || name).replace(/\./g, '_')
|
|
534
774
|
}
|
|
535
775
|
|
|
776
|
+
/** @type {unknown} */
|
|
536
777
|
static ReservedWords = {}
|
|
778
|
+
/**
|
|
779
|
+
* Ensures that the given identifier is properly quoted when required by the database
|
|
780
|
+
* @param {string} s
|
|
781
|
+
* @returns {string} SQL
|
|
782
|
+
*/
|
|
537
783
|
quote(s) {
|
|
538
784
|
if (typeof s !== 'string') return '"' + s + '"'
|
|
539
785
|
if (s.includes('"')) return '"' + s.replace(/"/g, '""') + '"'
|
|
540
|
-
if (s.toUpperCase() in this.class.ReservedWords || /^\d|[$'
|
|
786
|
+
if (s.toUpperCase() in this.class.ReservedWords || /^\d|[$' ?@./\\]/.test(s)) return '"' + s + '"'
|
|
541
787
|
return s
|
|
542
788
|
}
|
|
543
789
|
|
|
790
|
+
/**
|
|
791
|
+
* Convers the columns array into an array of SQL expressions that extract the correct value from inserted JSON data
|
|
792
|
+
* @param {object[]} columns
|
|
793
|
+
* @param {import('./infer/cqn').elements} elements
|
|
794
|
+
* @param {Boolean} isUpdate
|
|
795
|
+
* @returns {string[]} Array of SQL expressions for processing input JSON data
|
|
796
|
+
*/
|
|
544
797
|
managed(columns, elements, isUpdate = false) {
|
|
545
798
|
const annotation = isUpdate ? '@cds.on.update' : '@cds.on.insert'
|
|
546
799
|
const inputConverterKey = this.class._convertInput
|
|
@@ -586,6 +839,11 @@ class CQN2SQLRenderer {
|
|
|
586
839
|
})
|
|
587
840
|
}
|
|
588
841
|
|
|
842
|
+
/**
|
|
843
|
+
* Returns the default value
|
|
844
|
+
* @param {string} defaultValue
|
|
845
|
+
* @returns {string}
|
|
846
|
+
*/
|
|
589
847
|
defaultValue(defaultValue = this.context.timestamp.toISOString()) {
|
|
590
848
|
return typeof defaultValue === 'string' ? this.string(defaultValue) : defaultValue
|
|
591
849
|
}
|
|
@@ -602,4 +860,11 @@ const has_arrays = q => q.elements && Object.values(q.elements).some(e => e.item
|
|
|
602
860
|
|
|
603
861
|
const is_regexp = x => x?.constructor?.name === 'RegExp' // NOTE: x instanceof RegExp doesn't work in repl
|
|
604
862
|
const _empty = a => !a || a.length === 0
|
|
605
|
-
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* @param {import('@sap/cds/apis/cqn').Query} q
|
|
866
|
+
* @param {import('@sap/cds/apis/csn').CSN} m
|
|
867
|
+
*/
|
|
868
|
+
module.exports = (q, m) => new CQN2SQLRenderer().render(cqn4sql(q, m), m)
|
|
869
|
+
module.exports.class = CQN2SQLRenderer
|
|
870
|
+
module.exports.classDefinition = CQN2SQLRenderer // class is a reserved typescript word
|