@cap-js/sqlite 0.1.0 → 1.0.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.
@@ -1,515 +0,0 @@
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 })