@cap-js/sqlite 0.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 +9 -0
- package/LICENSE +201 -0
- package/README.md +189 -0
- package/cds.js +39 -0
- package/index.js +1 -0
- package/lib/db/DatabaseService.js +101 -0
- package/lib/db/sql/InsertResults.js +87 -0
- package/lib/db/sql/SQLService.js +223 -0
- package/lib/db/sql/copy.js +17 -0
- package/lib/db/sql/cqn2sql.js +515 -0
- package/lib/db/sql/cqn4sql.js +1461 -0
- package/lib/db/sql/deep.js +233 -0
- package/lib/db/sql/func.js +146 -0
- package/lib/db/sql/structuralComparisonOps.js +16 -0
- package/lib/db/sql/utils.js +22 -0
- package/lib/db/sql/workarounds.js +73 -0
- package/lib/db/sqlite/ReservedWords.json +149 -0
- package/lib/db/sqlite/SQLiteService.js +170 -0
- package/lib/ql/cds.infer.js +786 -0
- package/lib/ql/join-tree.js +167 -0
- package/lib/ql/pseudos.js +23 -0
- package/package.json +52 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
const cds = require('../../../cds')
|
|
2
|
+
const cds_infer = require('../../ql/cds.infer')
|
|
3
|
+
const cqn4sql = require('./cqn4sql')
|
|
4
|
+
|
|
5
|
+
const DEBUG = (()=>{
|
|
6
|
+
let DEBUG = cds.debug('sql-json'); if (DEBUG) return DEBUG
|
|
7
|
+
else DEBUG = cds.debug('sql|sqlite'); if (DEBUG) {
|
|
8
|
+
return DEBUG
|
|
9
|
+
// (sql, ...more) => DEBUG (sql.replace(/(?:SELECT[\n\r\s]+(json_group_array\()?[\n\r\s]*json_insert\((\n|\r|.)*?\)[\n\r\s]*\)?[\n\r\s]+as[\n\r\s]+_json_[\n\r\s]+FROM[\n\r\s]*\(|\)[\n\r\s]*(\)[\n\r\s]+AS )|\)$)/gim,(a,b,c,d) => d || ''), ...more)
|
|
10
|
+
// FIXME: looses closing ) on INSERT queries
|
|
11
|
+
}
|
|
12
|
+
})()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CQN2SQLRenderer {
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
this.class = new.target // for IntelliSense
|
|
19
|
+
this.class._init() // is a noop for subsequent calls
|
|
20
|
+
}
|
|
21
|
+
static _init() {
|
|
22
|
+
const _add_mixins = (aspect, mixins) => {
|
|
23
|
+
const fqn = this.name + aspect
|
|
24
|
+
const types = cds.builtin.types
|
|
25
|
+
for (let each in mixins) {
|
|
26
|
+
const def = types[each]; if (!def) continue
|
|
27
|
+
Object.defineProperty (def, fqn, { value: mixins[each] })
|
|
28
|
+
}
|
|
29
|
+
return fqn
|
|
30
|
+
}
|
|
31
|
+
this._convertInput = _add_mixins (':convertInput', this.InputConverters)
|
|
32
|
+
this._convertOutput = _add_mixins (':convertOutput', this.OutputConverters)
|
|
33
|
+
this._sqlType = _add_mixins (':sqlType', this.TypeMap)
|
|
34
|
+
this._init = ()=>{} // makes this a noop for subsequent calls
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
render (q, vars) {
|
|
38
|
+
const cmd = q.cmd || Object.keys(q)[0] // SELECT, INSERT, ...
|
|
39
|
+
this.sql = '' // to have it as first property for debugging
|
|
40
|
+
this.values = [] // prepare values, filled in by subroutines
|
|
41
|
+
this[cmd](this.cqn = q) // actual sql rendering happens here
|
|
42
|
+
if (vars?.length && !this.values.length) this.values = vars
|
|
43
|
+
DEBUG?.(this.sql, this.entries || this.values)
|
|
44
|
+
return this
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
infer(q) {
|
|
48
|
+
return q.target ? q : cds_infer(q)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
// CREATE Statements ------------------------------------------------
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
CREATE(q) {
|
|
56
|
+
const { target } = q, { query } = target
|
|
57
|
+
const name = this.name(target.name)
|
|
58
|
+
return this.sql = !query
|
|
59
|
+
? `CREATE TABLE ${name} ( ${this.CREATE_elements(target.elements)} )`
|
|
60
|
+
: `CREATE VIEW ${name} AS ${this.SELECT(cqn4sql(query))}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
CREATE_elements(elements) {
|
|
64
|
+
let sql = ''
|
|
65
|
+
for (let e in elements) {
|
|
66
|
+
const s = this.CREATE_element(elements[e])
|
|
67
|
+
if (s) sql += `${s}, `
|
|
68
|
+
}
|
|
69
|
+
return sql.slice(0, -2)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
CREATE_element(element) {
|
|
73
|
+
const type = this.type4(element)
|
|
74
|
+
if (type) return element.name + ' ' + type
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type4 (element) {
|
|
78
|
+
if (!element._type) element = cds.builtin.types[element.type] || element
|
|
79
|
+
const fn = element[this.class._sqlType]
|
|
80
|
+
return fn?.(element) || (element._type)?.replace('cds.','').toUpperCase()
|
|
81
|
+
|| cds.error`Unsupported type: ${element.type}`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static TypeMap = { // Utilizing cds.linked inheritance
|
|
85
|
+
String: e => `NVARCHAR(${e.length || 5000})`,
|
|
86
|
+
Binary: e => `VARBINARY(${e.length || 5000})`,
|
|
87
|
+
Int64: () => 'BIGINT',
|
|
88
|
+
Int32: () => 'INTEGER',
|
|
89
|
+
Int16: () => 'SMALLINT',
|
|
90
|
+
UInt8: () => 'SMALLINT',
|
|
91
|
+
Integer64: () => 'BIGINT',
|
|
92
|
+
LargeString: () => 'NCLOB',
|
|
93
|
+
LargeBinary: () => 'BLOB',
|
|
94
|
+
Association: () => false,
|
|
95
|
+
Composition: () => false,
|
|
96
|
+
array: () => 'NCLOB',
|
|
97
|
+
// HANA types
|
|
98
|
+
'cds.hana.REAL': () => 'REAL',
|
|
99
|
+
'cds.hana.CHAR': e => `CHAR(${e.length || 1})`,
|
|
100
|
+
'cds.hana.ST_POINT': () => 'CHAR',
|
|
101
|
+
'cds.hana.ST_GEOMETRY': () => 'CHAR',
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
// DROP Statements ------------------------------------------------
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
DROP(q) {
|
|
109
|
+
const { target } = q
|
|
110
|
+
const isView = target.query || target.projection
|
|
111
|
+
return this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} IF EXISTS ${this.name(target.name)}`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
// SELECT Statements ------------------------------------------------
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
SELECT(q) {
|
|
119
|
+
let { from, expand, where, groupBy, having, orderBy, limit, one, distinct } = q.SELECT
|
|
120
|
+
if (!expand) expand = q.SELECT.expand = has_expands(q) || has_arrays(q)
|
|
121
|
+
// REVISIT: When selecting from an entity that is not in the model the from.where are not normalized (as cqn4sql is skipped)
|
|
122
|
+
if (!where && from?.ref?.length === 1 && from.ref[0]?.where) where = from.ref[0]?.where
|
|
123
|
+
let columns = this.SELECT_columns(q)
|
|
124
|
+
let x, sql = `SELECT`
|
|
125
|
+
if (distinct) sql += ` DISTINCT`
|
|
126
|
+
if (!_empty(x = columns)) sql += ` ${x}`
|
|
127
|
+
if (!_empty(x = from)) sql += ` FROM ${this.from(x)}`
|
|
128
|
+
if (!_empty(x = where)) sql += ` WHERE ${this.where(x)}`
|
|
129
|
+
if (!_empty(x = groupBy)) sql += ` GROUP BY ${this.groupBy(x)}`
|
|
130
|
+
if (!_empty(x = having)) sql += ` HAVING ${this.having(x)}`
|
|
131
|
+
if (!_empty(x = orderBy)) sql += ` ORDER BY ${this.orderBy(x)}`
|
|
132
|
+
if (one) sql += ` LIMIT ${this.limit({rows:{val:1}})}`
|
|
133
|
+
else if ((x = limit)) sql += ` LIMIT ${this.limit(x)}`
|
|
134
|
+
if (expand) sql = this.SELECT_expand(q, sql)
|
|
135
|
+
return this.sql = sql
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
SELECT_columns({SELECT}) {
|
|
139
|
+
// REVISIT: We don't have to run x.as through this.column_name(), do we?
|
|
140
|
+
if (!SELECT.columns) return '*'
|
|
141
|
+
return SELECT.columns.map(x => this.column_expr(x) + (typeof x.as === 'string' ? ' as '+ this.quote(x.as) : ''))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
SELECT_expand({ SELECT, elements }, sql) {
|
|
145
|
+
if (!SELECT.columns) return sql
|
|
146
|
+
if (!elements) return sql
|
|
147
|
+
let cols = !SELECT.columns ? ['*'] : SELECT.columns.map(x => {
|
|
148
|
+
const name = this.column_name(x)
|
|
149
|
+
// REVISIT: can be removed when alias handling is resolved properly
|
|
150
|
+
const d = elements[name] || elements[name.substring(1,name.length - 1)]
|
|
151
|
+
let col = `'$.${name}',${this.output_converter4(d,this.quote(name))}`
|
|
152
|
+
|
|
153
|
+
if (x.SELECT?.count) {
|
|
154
|
+
// Return both the sub select and the count for @odata.count
|
|
155
|
+
const qc = cds.ql.clone(x, { columns: [{ func: 'count' }], one: 1, limit: 0, orderBy: 0 })
|
|
156
|
+
col += `, '$.${name}@odata.count',${this.expr(qc)}`
|
|
157
|
+
}
|
|
158
|
+
return col
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// Prevent SQLite from hitting function argument limit of 100
|
|
162
|
+
let colsLength = cols.length
|
|
163
|
+
let obj = "'{}'"
|
|
164
|
+
for(let i = 0; i < colsLength; i+= 48) {
|
|
165
|
+
obj = `json_insert(${obj},${cols.slice(i,i + 48)})`
|
|
166
|
+
}
|
|
167
|
+
return `SELECT ${SELECT.one || SELECT.expand === 'root' ? obj : `json_group_array(${obj})`} as _json_ FROM (${sql})`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
column_expr(x) {
|
|
171
|
+
if (x.func && !x.as) x.as = x.func
|
|
172
|
+
if (x?.element?.['@cds.extension']) {
|
|
173
|
+
x.as = x.as || x.element.name
|
|
174
|
+
return `extensions__->${this.string('$.'+x.element.name)}`
|
|
175
|
+
}
|
|
176
|
+
let sql = this.expr(x)
|
|
177
|
+
return sql
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
from (from) {
|
|
181
|
+
const { ref, as } = from, _aliased = as ? s => s + ` as ${this.quote(as)}` : s => s
|
|
182
|
+
if (ref) return _aliased(this.quote(this.name(ref[0])))
|
|
183
|
+
if (from.SELECT) return _aliased(`(${this.SELECT(from)})`)
|
|
184
|
+
if (from.join) {
|
|
185
|
+
const { join, args: [left, right], on } = from
|
|
186
|
+
return `${this.from(left)} ${join} JOIN ${this.from(right)} ON ${this.xpr({ xpr: on })}`
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
where(xpr) {
|
|
191
|
+
return this.xpr({ xpr })
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
having(xpr) {
|
|
195
|
+
return this.xpr({ xpr })
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
groupBy(clause) {
|
|
199
|
+
return clause.map(c => this.expr(c))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
orderBy(orderBy) {
|
|
203
|
+
return orderBy.map(c => this.expr(c) + (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC'))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
limit({ rows, offset }) {
|
|
207
|
+
if(!rows) throw new Error('Rows parameter is missing in SELECT.limit(rows, offset)')
|
|
208
|
+
return !offset ? rows.val : `${rows.val} OFFSET ${offset.val}`
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
// INSERT Statements ------------------------------------------------
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
INSERT(q) {
|
|
218
|
+
const { INSERT } = q
|
|
219
|
+
return INSERT.entries ? this.INSERT_entries(q)
|
|
220
|
+
: INSERT.rows ? this.INSERT_rows(q)
|
|
221
|
+
: INSERT.values ? this.INSERT_values(q)
|
|
222
|
+
: INSERT.as ? this.INSERT_select(q)
|
|
223
|
+
: cds.error`Missing .entries, .rows, or .values in ${q}`
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
INSERT_entries(q) {
|
|
227
|
+
const { INSERT } = q
|
|
228
|
+
const entity = this.name(q.target?.name || INSERT.into.ref[0])
|
|
229
|
+
const alias = INSERT.into.as
|
|
230
|
+
const elements = q.elements || q.target?.elements
|
|
231
|
+
if(!elements && !INSERT.entries?.length) {
|
|
232
|
+
return // REVISIT: mtx sends an insert statement without entries and no reference entity
|
|
233
|
+
}
|
|
234
|
+
const columns = (
|
|
235
|
+
elements ? ObjectKeys(elements).filter(c => c in elements && !elements[c].virtual && !elements[c].isAssociation)
|
|
236
|
+
: ObjectKeys(INSERT.entries[0])
|
|
237
|
+
)
|
|
238
|
+
this.columns = columns.filter(elements ? c => !elements[c]?.['@cds.extension'] : ()=>true).map(c => this.quote(c))
|
|
239
|
+
|
|
240
|
+
const extractions = this.managed(columns.map(c => ({name:c})), elements, !!q.UPSERT)
|
|
241
|
+
const extraction = extractions.map(c => {
|
|
242
|
+
const element = elements?.[c.name]
|
|
243
|
+
if(element?.['@cds.extension']) {
|
|
244
|
+
return false
|
|
245
|
+
}
|
|
246
|
+
if(c.name === 'extensions__') {
|
|
247
|
+
const merges = extractions.filter(c => elements?.[c.name]?.['@cds.extension'])
|
|
248
|
+
if(merges.length) {
|
|
249
|
+
c.sql = `json_set(ifnull(${c.sql},'{}'),${merges.map(c => this.string('$.'+c.name)+','+c.sql)})`
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return c
|
|
253
|
+
})
|
|
254
|
+
.filter(a => a)
|
|
255
|
+
.map(c => c.sql)
|
|
256
|
+
|
|
257
|
+
this.entries = [[JSON.stringify(INSERT.entries)]]
|
|
258
|
+
return (this.sql = `INSERT INTO ${entity}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns}) SELECT ${extraction} FROM json_each(?)`)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
INSERT_rows(q) {
|
|
262
|
+
const { INSERT } = q
|
|
263
|
+
const entity = this.name(q.target?.name || INSERT.into.ref[0])
|
|
264
|
+
const alias = INSERT.into.as
|
|
265
|
+
const elements = q.elements || q.target?.elements
|
|
266
|
+
if(!INSERT.columns && !elements) {
|
|
267
|
+
throw cds.error`Cannot insert rows without columns or elements`
|
|
268
|
+
}
|
|
269
|
+
let columns = (INSERT.columns || (elements && ObjectKeys(elements)))
|
|
270
|
+
if(elements){
|
|
271
|
+
columns = columns.filter(c => c in elements && !elements[c].virtual && !elements[c].isAssociation)
|
|
272
|
+
}
|
|
273
|
+
this.columns = columns.map(c => this.quote(c))
|
|
274
|
+
|
|
275
|
+
const inputConverterKey = this.class._convertInput
|
|
276
|
+
const extraction = columns.map((c, i) => {
|
|
277
|
+
const element = elements?.[c] || {}
|
|
278
|
+
const extract = `value->>'$[${i}]'`
|
|
279
|
+
const converter = element[inputConverterKey] || (e => e)
|
|
280
|
+
return converter(extract, element)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
this.entries = [[JSON.stringify(INSERT.rows)]]
|
|
284
|
+
return (this.sql = `INSERT INTO ${entity}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns}) SELECT ${extraction} FROM json_each(?)`)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
INSERT_values (q){
|
|
288
|
+
let { columns, values } = q.INSERT
|
|
289
|
+
return this.INSERT_rows ({__proto__:q, INSERT:{__proto__: q.INSERT, columns, rows:[values] }})
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
INSERT_select(q) {
|
|
293
|
+
const { INSERT } = q
|
|
294
|
+
const entity = this.name(q.target.name)
|
|
295
|
+
const alias = INSERT.into.as
|
|
296
|
+
const elements = q.elements || q.target?.elements || {}
|
|
297
|
+
const columns = this.columns = (
|
|
298
|
+
INSERT.columns || ObjectKeys(elements)).filter(c => c in elements && !elements[c].virtual && !elements[c].isAssociation
|
|
299
|
+
)
|
|
300
|
+
return this.sql = `INSERT INTO ${entity}${alias ? ' as ' + this.quote(alias) : ''} (${columns}) ${this.SELECT(cqn4sql(INSERT.as))}`
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
output_converter4(element,expr) {
|
|
304
|
+
const fn = element?.[this.class._convertOutput]
|
|
305
|
+
return fn?.(expr,element) || expr
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
static InputConverters = {} // subclasses to override
|
|
309
|
+
|
|
310
|
+
static OutputConverters = {} // subclasses to override
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
// UPSERT Statements ------------------------------------------------
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
UPSERT(q) {
|
|
317
|
+
let { UPSERT } = q, sql = this.INSERT ({__proto__:q, INSERT:UPSERT })
|
|
318
|
+
let keys = q.target?.keys; if (!keys) return this.sql = sql // REVISIT: We should converge q.target and q._target
|
|
319
|
+
const updates = this.columns.filter(c => !(c in keys)) .map (c => `${c} = excluded.${c}`)
|
|
320
|
+
const conflict = updates.length
|
|
321
|
+
? ` ON CONFLICT(${ Object.keys(keys) }) DO UPDATE SET ` + updates
|
|
322
|
+
: ` ON CONFLICT(${ Object.keys(keys) }) DO NOTHING`
|
|
323
|
+
return this.sql = `${sql} WHERE true ${conflict}` // REVISIT: Why do we have that "WHERE true" in here?
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
// UPDATE Statements ------------------------------------------------
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
UPDATE(q) {
|
|
331
|
+
const { UPDATE: { entity, with:_with, data, where } } = q, elements = q.target?.elements
|
|
332
|
+
let sql = `UPDATE ${this.name(entity.ref?.[0] || entity)}`
|
|
333
|
+
if (entity.as) sql += ` AS ${entity.as}`
|
|
334
|
+
let columns = []
|
|
335
|
+
if (data) for (let c in data) if (!elements || c in elements && !elements[c].virtual) {
|
|
336
|
+
columns.push({ name:c, sql: this.val({val:data[c]}) })
|
|
337
|
+
}
|
|
338
|
+
if (_with) for (let c in _with) if (!elements || c in elements && !elements[c].virtual) {
|
|
339
|
+
columns.push({ name:c, sql: this.expr(_with[c]) })
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
columns = columns.map(c => {
|
|
343
|
+
if(q.elements?.[c.name]?.['@cds.extension']){
|
|
344
|
+
return {
|
|
345
|
+
name: 'extensions__',
|
|
346
|
+
sql: `json_set(extensions__,${this.string('$.' + c.name)},${c.sql})`
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return c
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
const extraction = this.managed(columns, q.elements, true).map(c => `${this.quote(c.name)}=${c.sql}`)
|
|
353
|
+
|
|
354
|
+
sql += ` SET ${extraction}`
|
|
355
|
+
if (where) sql += ` WHERE ${this.where(where)}`
|
|
356
|
+
return this.sql = sql
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
// DELETE Statements ------------------------------------------------
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
DELETE({ DELETE: { from, where } }) {
|
|
364
|
+
let sql = `DELETE FROM ${this.from(from)}`
|
|
365
|
+
if (where) sql += ` WHERE ${this.where(where)}`
|
|
366
|
+
return this.sql = sql
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
// Expression Clauses ---------------------------------------------
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
expr(x) {
|
|
374
|
+
const wrap = x.cast ? sql => `cast(${sql} as ${this.type4(x.cast)})` : sql => sql
|
|
375
|
+
if (typeof x === 'string') throw cds.error`Unsupported expr: ${x}`
|
|
376
|
+
if ('param' in x) return wrap(this.param(x))
|
|
377
|
+
if ('ref' in x) return wrap(this.ref(x))
|
|
378
|
+
if ('val' in x) return wrap(this.val(x))
|
|
379
|
+
if ('xpr' in x) return wrap(this.xpr(x))
|
|
380
|
+
if ('func' in x) return wrap(this.func(x))
|
|
381
|
+
if ('list' in x) return wrap(this.list(x))
|
|
382
|
+
if ('SELECT' in x) return wrap(`(${this.SELECT(x)})`)
|
|
383
|
+
else throw cds.error`Unsupported expr: ${x}`
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
xpr({ xpr }) {
|
|
387
|
+
return xpr.map((x,i) => {
|
|
388
|
+
if (x in {LIKE:1,like:1} && is_regexp(xpr[i+1]?.val)) return 'regexp'
|
|
389
|
+
if (typeof x === 'string') return this.operator(x,i,xpr)
|
|
390
|
+
if (x.xpr) return `(${this.xpr(x)})`
|
|
391
|
+
else return this.expr(x)
|
|
392
|
+
}).join(' ')
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
operator (x,i,xpr) {
|
|
396
|
+
if (x === '=' && xpr[i+1]?.val === null) return 'is'
|
|
397
|
+
if (x === '!=') return 'is not'
|
|
398
|
+
else return x
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
param({ ref }) {
|
|
402
|
+
if (ref.length > 1) throw cds.error `Unsupported nested ref parameter: ${ref}`
|
|
403
|
+
return ref[0] === '?' ? '?' : `:${ref}`
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
ref({ ref }) {
|
|
407
|
+
return ref.map(r => this.quote(r)).join('.')
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
val({ val }) {
|
|
411
|
+
switch (typeof val) {
|
|
412
|
+
case 'undefined': return 'NULL'
|
|
413
|
+
case 'boolean': return val
|
|
414
|
+
case 'number': return val // REVISIT for HANA
|
|
415
|
+
case 'object':
|
|
416
|
+
if (val === null) return 'NULL'
|
|
417
|
+
if (val instanceof Date) return `'${val.toISOString()}'`
|
|
418
|
+
if (Buffer.isBuffer(val)) val = val.toString('base64')
|
|
419
|
+
else val = this.regex(val) || this.json(val)
|
|
420
|
+
}
|
|
421
|
+
this.values.push(val)
|
|
422
|
+
return '?'
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
static Functions = require('./func')
|
|
426
|
+
func({ func, args }) {
|
|
427
|
+
args = (args||[]).map(e => e === '*' ? e : { __proto__:e, toString:(x=e)=>this.expr(x) })
|
|
428
|
+
return this.class.Functions[func]?.(...args) || `${func}(${args})`
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
list({ list }) {
|
|
432
|
+
return `(${list.map(e => this.expr(e))})`
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
regex(o) {
|
|
436
|
+
if (is_regexp(o)) return o.source
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
json(o) {
|
|
440
|
+
return this.string(JSON.stringify(o))
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
string(s) {
|
|
444
|
+
return `'${s.replace(/'/g, "''")}'`
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
column_name(col) {
|
|
448
|
+
return (typeof col.as === 'string' && col.as) || ('val' in col && col.val + '') || col.ref[col.ref.length - 1]
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
name(name) {
|
|
452
|
+
return (name.id || name).replace(/\./g, '_')
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
static ReservedWords = {}
|
|
456
|
+
quote(s) {
|
|
457
|
+
if (typeof s !== 'string') return '"'+s+'"'
|
|
458
|
+
if (s.includes('"')) return '"'+s.replace(/"/g,'""')+'"'
|
|
459
|
+
if (s.toUpperCase() in this.class.ReservedWords || /^\d|[$' /\\]/.test(s)) return '"'+s+'"'
|
|
460
|
+
return s
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
managed(columns, elements, isUpdate = false) {
|
|
464
|
+
const annotation = isUpdate ? '@cds.on.update' : '@cds.on.insert'
|
|
465
|
+
const inputConverterKey = this.class._convertInput
|
|
466
|
+
// Ensure that missing managed columns are added
|
|
467
|
+
const requiredColumns = !elements ? [] : Object.keys(elements)
|
|
468
|
+
.filter(e => (elements[e]?.[annotation] || (!isUpdate && elements[e]?.default)) && !columns.find(c => c.name === e))
|
|
469
|
+
.map(name => ({name,sql:'NULL'}))
|
|
470
|
+
|
|
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
|
+
return [...columns,...requiredColumns].map(({name,sql}) => {
|
|
475
|
+
const element = elements?.[name] || {}
|
|
476
|
+
let extract = sql ?? `value->>'$.${name}'`
|
|
477
|
+
const converter = element[inputConverterKey] || (e => e)
|
|
478
|
+
let managed = element[annotation]?.['=']
|
|
479
|
+
switch (managed) {
|
|
480
|
+
case '$user.id':
|
|
481
|
+
case '$user':
|
|
482
|
+
managed = this.string(ctx.user.id)
|
|
483
|
+
break
|
|
484
|
+
case '$now':
|
|
485
|
+
// REVISIT fix for date precision
|
|
486
|
+
managed = this.string(ctx.timestamp.toISOString())
|
|
487
|
+
break
|
|
488
|
+
default:
|
|
489
|
+
managed = undefined
|
|
490
|
+
}
|
|
491
|
+
if(!isUpdate) {
|
|
492
|
+
const d = element.default
|
|
493
|
+
if(d && (d.val !== undefined || d.ref?.[0] === '$now')) {
|
|
494
|
+
const defaultValue = d.val ?? (cds.context?.timestamp || new Date()).toISOString()
|
|
495
|
+
extract = `(CASE WHEN json_type(value,'$.${name}') IS NULL THEN ${typeof defaultValue === 'string' ? this.string(defaultValue) : defaultValue} ELSE ${extract} END)`
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
name,
|
|
500
|
+
sql:converter(managed === undefined ? extract : `coalesce(${extract}, ${managed})`, element)
|
|
501
|
+
}
|
|
502
|
+
})
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// REVISIT: Workaround for JSON.stringify to work with buffers
|
|
507
|
+
Buffer.prototype.toJSON = function() { return this.toString('base64') }
|
|
508
|
+
|
|
509
|
+
const ObjectKeys = o => (o && [...ObjectKeys(o.__proto__), ...Object.keys(o)] || [])
|
|
510
|
+
const has_expands = q => q.SELECT.columns?.some(c => c.SELECT?.expand)
|
|
511
|
+
const has_arrays = q => q.elements && Object.values(q.elements).some(e => e.items)
|
|
512
|
+
|
|
513
|
+
const is_regexp = x => x?.constructor?.name === 'RegExp' // NOTE: x instanceof RegExp doesn't work in repl
|
|
514
|
+
const _empty = a => !a || a.length === 0
|
|
515
|
+
module.exports = Object.assign((q,m) => (new CQN2SQLRenderer).render(cqn4sql(q,m),m), { class: CQN2SQLRenderer })
|