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