@sap/cds-dk 10.0.3 → 10.0.4
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 +6 -0
- package/node_modules/@cap-js/db-service/CHANGELOG.md +768 -0
- package/node_modules/@cap-js/db-service/LICENSE +201 -0
- package/node_modules/@cap-js/db-service/README.md +26 -0
- package/node_modules/@cap-js/db-service/index.js +19 -0
- package/node_modules/@cap-js/db-service/lib/InsertResults.js +122 -0
- package/node_modules/@cap-js/db-service/lib/SQLService.js +581 -0
- package/node_modules/@cap-js/db-service/lib/common/DatabaseService.js +179 -0
- package/node_modules/@cap-js/db-service/lib/common/factory.d.ts +5 -0
- package/node_modules/@cap-js/db-service/lib/common/generic-pool.js +310 -0
- package/node_modules/@cap-js/db-service/lib/common/session-context.js +46 -0
- package/node_modules/@cap-js/db-service/lib/converters.d.ts +24 -0
- package/node_modules/@cap-js/db-service/lib/cql-functions.js +464 -0
- package/node_modules/@cap-js/db-service/lib/cqn2pql.js +116 -0
- package/node_modules/@cap-js/db-service/lib/cqn2sql.js +1612 -0
- package/node_modules/@cap-js/db-service/lib/cqn4sql.js +2895 -0
- package/node_modules/@cap-js/db-service/lib/deep-queries.js +290 -0
- package/node_modules/@cap-js/db-service/lib/fill-in-keys.js +75 -0
- package/node_modules/@cap-js/db-service/lib/infer/cqn.d.ts +42 -0
- package/node_modules/@cap-js/db-service/lib/infer/index.js +1290 -0
- package/node_modules/@cap-js/db-service/lib/infer/join-tree.js +278 -0
- package/node_modules/@cap-js/db-service/lib/infer/pseudos.js +24 -0
- package/node_modules/@cap-js/db-service/lib/search.js +220 -0
- package/node_modules/@cap-js/db-service/lib/utils.js +177 -0
- package/node_modules/@cap-js/db-service/package.json +36 -0
- package/node_modules/@cap-js/sqlite/CHANGELOG.md +404 -0
- package/node_modules/@cap-js/sqlite/LICENSE +201 -0
- package/node_modules/@cap-js/sqlite/README.md +34 -0
- package/node_modules/@cap-js/sqlite/cds-plugin.js +0 -0
- package/node_modules/@cap-js/sqlite/index.js +2 -0
- package/node_modules/@cap-js/sqlite/lib/SQLiteService.js +324 -0
- package/node_modules/@cap-js/sqlite/lib/cql-functions.js +160 -0
- package/node_modules/@cap-js/sqlite/lib/node-sqlite.js +36 -0
- package/node_modules/@cap-js/sqlite/lib/session.json +3 -0
- package/node_modules/@cap-js/sqlite/lib/sql.js.js +99 -0
- package/node_modules/@cap-js/sqlite/package.json +68 -0
- package/node_modules/iconv-lite/encodings/sbcs-data.js +2 -0
- package/node_modules/iconv-lite/encodings/utf32.js +10 -3
- package/node_modules/iconv-lite/package.json +2 -2
- package/node_modules/iconv-lite/types/encodings.d.ts +2 -0
- package/package.json +5 -3
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# CDS database service for SQLite
|
|
2
|
+
|
|
3
|
+
Welcome to the SQLite database service for [SAP Cloud Application Programming Model](https://cap.cloud.sap) Node.js, based on streamlined database architecture and [*better-sqlite* driver](https://www.npmjs.com/package/better-sqlite3).
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
If you want to use SQLite for development, all you need to do is to install the database package, as follows
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm add @cap-js/sqlite -D
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Learn more about setup and usage in the [respective database guide](https://cap.cloud.sap/docs/guides/databases/sqlite).
|
|
14
|
+
|
|
15
|
+
## Support
|
|
16
|
+
|
|
17
|
+
This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/cap-js/cds-dbs/issues).
|
|
18
|
+
|
|
19
|
+
## Contribution
|
|
20
|
+
|
|
21
|
+
Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](../CONTRIBUTING.md).
|
|
22
|
+
|
|
23
|
+
## Versioning
|
|
24
|
+
|
|
25
|
+
This library follows [Semantic Versioning](https://semver.org/).
|
|
26
|
+
All notable changes are documented in [CHANGELOG.md](CHANGELOG.md).
|
|
27
|
+
|
|
28
|
+
## Code of Conduct
|
|
29
|
+
|
|
30
|
+
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](../CODE_OF_CONDUCT.md) at all times.
|
|
31
|
+
|
|
32
|
+
## Licensing
|
|
33
|
+
|
|
34
|
+
Copyright 2024 SAP SE or an SAP affiliate company and cds-dbs contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/cap-js/cds-dbs).
|
|
File without changes
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
const { SQLService } = require('@cap-js/db-service')
|
|
2
|
+
const cds = require('@sap/cds/lib')
|
|
3
|
+
let sqlite // sqlite driver is loaded on connect
|
|
4
|
+
|
|
5
|
+
const $session = Symbol('dbc.session')
|
|
6
|
+
const sessionVariableMap = require('./session.json') // Adjust the path as necessary for your project
|
|
7
|
+
const convStrm = require('stream/consumers')
|
|
8
|
+
const { Readable } = require('stream')
|
|
9
|
+
|
|
10
|
+
const keywords = cds.compiler.to.sql.sqlite.keywords
|
|
11
|
+
// keywords come as array
|
|
12
|
+
const sqliteKeywords = keywords.reduce((prev, curr) => {
|
|
13
|
+
prev[curr] = 1
|
|
14
|
+
return prev
|
|
15
|
+
}, {})
|
|
16
|
+
|
|
17
|
+
// define date and time functions in js to allow for throwing errors
|
|
18
|
+
const isTime = /^\d{1,2}:\d{1,2}:\d{1,2}$/
|
|
19
|
+
const hasTimezone = /([+-]\d{1,2}:?\d{0,2}|Z)$/
|
|
20
|
+
const toDate = (d, allowTime = false) => {
|
|
21
|
+
const date = new Date(allowTime && isTime.test(d) ? `1970-01-01T${d}Z` : hasTimezone.test(d) ? d : d + 'Z')
|
|
22
|
+
if (Number.isNaN(date.getTime())) throw new Error(`Value does not contain a valid ${allowTime ? 'time' : 'date'} "${d}"`)
|
|
23
|
+
return date
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SQLiteService extends SQLService {
|
|
28
|
+
|
|
29
|
+
get factory() {
|
|
30
|
+
return {
|
|
31
|
+
options: this.options.pool || {},
|
|
32
|
+
create: async tenant => {
|
|
33
|
+
try {
|
|
34
|
+
if (!sqlite) loadSQLite(this.options.driver || this.options.credentials?.driver)
|
|
35
|
+
const database = this.url4(tenant)
|
|
36
|
+
const dbc = new sqlite(database, this.options.client || {})
|
|
37
|
+
await dbc.ready
|
|
38
|
+
|
|
39
|
+
const deterministic = { deterministic: true }
|
|
40
|
+
dbc.function('session_context', key => dbc[$session][key])
|
|
41
|
+
dbc.function('regexp', deterministic, (re, x) => (RegExp(re).test(x) ? 1 : 0))
|
|
42
|
+
dbc.function('ISO', deterministic, d => d && new Date(d).toISOString())
|
|
43
|
+
dbc.function('year', deterministic, d => d === null ? null : toDate(d).getUTCFullYear())
|
|
44
|
+
dbc.function('month', deterministic, d => d === null ? null : toDate(d).getUTCMonth() + 1)
|
|
45
|
+
dbc.function('day', deterministic, d => d === null ? null : toDate(d).getUTCDate())
|
|
46
|
+
dbc.function('hour', deterministic, d => d === null ? null : toDate(d, true).getUTCHours())
|
|
47
|
+
dbc.function('minute', deterministic, d => d === null ? null : toDate(d, true).getUTCMinutes())
|
|
48
|
+
dbc.function('second', deterministic, d => d === null ? null : toDate(d, true).getUTCSeconds())
|
|
49
|
+
if (database !== ':memory:') dbc.pragma?.('journal_mode = WAL') || dbc.exec('PRAGMA journal_mode = WAL')
|
|
50
|
+
return dbc
|
|
51
|
+
} catch (err) {
|
|
52
|
+
Promise.reject(err)
|
|
53
|
+
await new Promise(() => { })
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
destroy: dbc => dbc.close(),
|
|
57
|
+
validate: dbc => dbc.open,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
url4(tenant) {
|
|
62
|
+
let { url, database: db = url } = this.options.credentials || this.options || {}
|
|
63
|
+
if (!db || db === ':memory:') return ':memory:'
|
|
64
|
+
if (tenant) db = db.replace(/\.(db|sqlite)$/, `-${tenant}.$1`)
|
|
65
|
+
return cds.utils.path.resolve(cds.root, db)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
set(variables) {
|
|
69
|
+
const dbc = this.dbc || cds.error('Cannot set session context: No database connection')
|
|
70
|
+
|
|
71
|
+
// Enrich provided session context with aliases
|
|
72
|
+
for (const alias in sessionVariableMap) {
|
|
73
|
+
const name = sessionVariableMap[alias]
|
|
74
|
+
if (variables[name]) variables[alias] = variables[name]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!dbc[$session]) dbc[$session] = variables
|
|
78
|
+
else Object.assign(dbc[$session], variables)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
release() {
|
|
82
|
+
this.dbc[$session] = undefined
|
|
83
|
+
return super.release()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
prepare(sql) {
|
|
87
|
+
try {
|
|
88
|
+
const stmt = this.dbc.prepare(sql)
|
|
89
|
+
return {
|
|
90
|
+
run: (..._) => this._run(stmt, ..._),
|
|
91
|
+
get: (..._) => stmt.get(..._),
|
|
92
|
+
all: (..._) => stmt.all(..._),
|
|
93
|
+
stream: (..._) => this._allStream(stmt, ..._),
|
|
94
|
+
}
|
|
95
|
+
} catch (e) {
|
|
96
|
+
e.message += ' in:\n' + (e.query = sql)
|
|
97
|
+
throw e
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async _run(stmt, binding_params) {
|
|
102
|
+
for (let i = 0; i < binding_params.length; i++) {
|
|
103
|
+
const val = binding_params[i]
|
|
104
|
+
if (val instanceof Readable) {
|
|
105
|
+
binding_params[i] = await convStrm[val.type === 'json' ? 'text' : 'buffer'](val)
|
|
106
|
+
}
|
|
107
|
+
if (Buffer.isBuffer(val)) {
|
|
108
|
+
binding_params[i] = Buffer.from(val.toString('base64'))
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return stmt.run(binding_params)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async *_iteratorRaw(rs, one) {
|
|
115
|
+
const pageSize = (1 << 16)
|
|
116
|
+
// Allow for both array and iterator result sets
|
|
117
|
+
const first = Array.isArray(rs) ? { done: !rs[0], value: rs[0] } : rs.next()
|
|
118
|
+
if (first.done) {
|
|
119
|
+
yield one ? 'null' : '[]'
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
if (one) {
|
|
123
|
+
yield first.value[0]
|
|
124
|
+
// Close result set to release database connection
|
|
125
|
+
rs.return()
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let buffer = '[' + first.value[0]
|
|
130
|
+
// Print first value as stand alone to prevent comma check inside the loop
|
|
131
|
+
for (const row of rs) {
|
|
132
|
+
buffer += `,${row[0]}`
|
|
133
|
+
if (buffer.length > pageSize) {
|
|
134
|
+
yield buffer
|
|
135
|
+
buffer = ''
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
buffer += ']'
|
|
139
|
+
yield buffer
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async *_iteratorObjectMode(rs) {
|
|
143
|
+
for (const row of rs) {
|
|
144
|
+
yield JSON.parse(row[0])
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async _allStream(stmt, binding_params, one, objectMode) {
|
|
149
|
+
stmt = stmt.iterate ? stmt : stmt.__proto__
|
|
150
|
+
stmt.raw?.(true)
|
|
151
|
+
const rs = stmt.iterate(binding_params)
|
|
152
|
+
const stream = Readable.from(objectMode ? this._iteratorObjectMode(rs) : this._iteratorRaw(rs, one), { objectMode })
|
|
153
|
+
const close = () => rs.return() // finish result set when closed early
|
|
154
|
+
stream.on('error', close)
|
|
155
|
+
stream.on('close', close)
|
|
156
|
+
return stream
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
pragma(pragma, options) {
|
|
160
|
+
if (!this.dbc) return this.begin('pragma').then(tx => {
|
|
161
|
+
try { return tx.pragma(pragma, options) }
|
|
162
|
+
finally { tx.release() }
|
|
163
|
+
})
|
|
164
|
+
return this.dbc.pragma(pragma, options)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
exec(sql) {
|
|
169
|
+
return this.dbc.exec(sql)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_prepareStreams(values) {
|
|
173
|
+
let any
|
|
174
|
+
values.forEach((v, i) => {
|
|
175
|
+
if (v instanceof Readable) {
|
|
176
|
+
any = values[i] = convStrm.buffer(v)
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
return any ? Promise.all(values) : values
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async onSIMPLE({ query, data }) {
|
|
183
|
+
const { sql, values } = this.cqn2sql(query, data)
|
|
184
|
+
let ps = await this.prepare(sql)
|
|
185
|
+
const vals = await this._prepareStreams(values)
|
|
186
|
+
const { changes } = await ps.run(vals)
|
|
187
|
+
return this._return_affected (changes)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
onPlainSQL({ query, data }, next) {
|
|
191
|
+
if (typeof query === 'string') {
|
|
192
|
+
// REVISIT: this is a hack the target of $now might not be a timestamp or date time
|
|
193
|
+
// Add input converter to CURRENT_TIMESTAMP inside views using $now
|
|
194
|
+
if (/^CREATE VIEW.* CURRENT_TIMESTAMP[( ]/is.test(query)) {
|
|
195
|
+
query = query.replace(/CURRENT_TIMESTAMP/gi, "STRFTIME('%Y-%m-%dT%H:%M:%fZ','NOW')")
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return super.onPlainSQL({ query, data }, next)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
static CQN2SQL = class CQN2SQLite extends SQLService.CQN2SQL {
|
|
202
|
+
column_alias4(x, q) {
|
|
203
|
+
let alias = super.column_alias4(x, q)
|
|
204
|
+
if (alias) return alias
|
|
205
|
+
if (x.ref) {
|
|
206
|
+
let obm = q._orderByMap
|
|
207
|
+
if (!obm) {
|
|
208
|
+
Object.defineProperty(q, '_orderByMap', { value: (obm = {}) })
|
|
209
|
+
q.SELECT?.orderBy?.forEach(o => {
|
|
210
|
+
if (o.ref?.length === 1) obm[o.ref[0]] = o.ref[0]
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
return obm[x.ref.at(-1)]
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
val(v) {
|
|
218
|
+
if (typeof v.val === 'boolean') v.val = v.val ? 1 : 0
|
|
219
|
+
else if (Buffer.isBuffer(v.val)) v.val = v.val.toString('base64')
|
|
220
|
+
// intercept DateTime values and convert to Date objects to compare ISO Strings
|
|
221
|
+
else if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(.\d{1,9})?(Z|[+-]\d{2}(:?\d{2})?)$/.test(v.val)) {
|
|
222
|
+
const date = new Date(v.val)
|
|
223
|
+
if (!Number.isNaN(date.getTime())) {
|
|
224
|
+
v.val = date
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return super.val(v)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
forUpdate() {
|
|
231
|
+
return ''
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
forShareLock() {
|
|
235
|
+
return ''
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Used for INSERT statements
|
|
239
|
+
static InputConverters = {
|
|
240
|
+
...super.InputConverters,
|
|
241
|
+
// The following allows passing in ISO strings with non-zulu
|
|
242
|
+
// timezones and converts them into zulu dates and times
|
|
243
|
+
Date: e => e === '?' ? e : `strftime('%Y-%m-%d',${e})`,
|
|
244
|
+
Time: e => e === '?' ? e : `strftime('%H:%M:%S',${e})`,
|
|
245
|
+
// Both, DateTimes and Timestamps are canonicalized to ISO strings with
|
|
246
|
+
// ms precision to allow safe comparisons, also to query {val}s in where clauses
|
|
247
|
+
DateTime: e => e === '?' ? e : `ISO(${e})`,
|
|
248
|
+
Timestamp: e => e === '?' ? e : `ISO(${e})`,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
static OutputConverters = {
|
|
252
|
+
...super.OutputConverters,
|
|
253
|
+
// Structs and arrays are stored as JSON strings; the ->'$' unwraps them.
|
|
254
|
+
// Otherwise they would be added as strings to json_objects.
|
|
255
|
+
Association: expr => `${expr}->'$'`,
|
|
256
|
+
struct: expr => `${expr}->'$'`,
|
|
257
|
+
array: expr => `${expr}->'$'`,
|
|
258
|
+
// SQLite has no booleans so we need to convert 0 and 1
|
|
259
|
+
boolean:
|
|
260
|
+
cds.env.features.sql_simple_queries === 2
|
|
261
|
+
? undefined
|
|
262
|
+
: expr => `CASE ${expr} when 1 then 'true' when 0 then 'false' END ->'$'`,
|
|
263
|
+
// DateTimes are returned without ms added by InputConverters
|
|
264
|
+
DateTime: e => `substr(${e},0,20)||'Z'`,
|
|
265
|
+
// Timestamps are returned with ms, as written by InputConverters.
|
|
266
|
+
// And as cds.builtin.classes.Timestamp inherits from DateTime we need
|
|
267
|
+
// to override the DateTime converter above
|
|
268
|
+
Timestamp: undefined,
|
|
269
|
+
// int64 is stored as native int64 for best comparison
|
|
270
|
+
// Reading int64 as string to not loose precision
|
|
271
|
+
Int64: cds.env.features.ieee754compatible ? expr => `CAST(${expr} as TEXT)` : undefined,
|
|
272
|
+
// REVISIT: always cast to string in next major
|
|
273
|
+
// Reading decimal as string to not loose precision
|
|
274
|
+
Decimal: cds.env.features.ieee754compatible
|
|
275
|
+
? (expr, elem) =>
|
|
276
|
+
elem?.scale
|
|
277
|
+
? `CASE WHEN ${expr} IS NULL THEN NULL ELSE format('%.${elem.scale}f', ${expr}) END`
|
|
278
|
+
: `CASE WHEN ${expr} IS NULL THEN NULL ELSE rtrim(rtrim(format('%.999f', ${expr}), '0'), '.') END`
|
|
279
|
+
: undefined,
|
|
280
|
+
// Binary is not allowed in json objects
|
|
281
|
+
Binary: expr => `${expr} || ''`,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Used for SQL function expressions
|
|
285
|
+
static Functions = { ...super.Functions, ...require('./cql-functions') }
|
|
286
|
+
|
|
287
|
+
// Used for CREATE TABLE statements
|
|
288
|
+
static TypeMap = {
|
|
289
|
+
...super.TypeMap,
|
|
290
|
+
Binary: e => `BINARY_BLOB(${e.length || 5000})`,
|
|
291
|
+
Date: () => 'DATE_TEXT',
|
|
292
|
+
Time: () => 'TIME_TEXT',
|
|
293
|
+
DateTime: () => 'DATETIME_TEXT',
|
|
294
|
+
Timestamp: () => 'TIMESTAMP_TEXT',
|
|
295
|
+
Map: () => 'JSON_TEXT',
|
|
296
|
+
Decimal: cds.env.requires.db?.decimal_affinity?.match(/^real$/i) ? () => 'REAL_DECIMAL' : undefined,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
get is_distinct_from_() {
|
|
300
|
+
return 'is not'
|
|
301
|
+
}
|
|
302
|
+
get is_not_distinct_from_() {
|
|
303
|
+
return 'is'
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
static ReservedWords = { ...super.ReservedWords, ...sqliteKeywords }
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function loadSQLite(driver) {
|
|
311
|
+
const drivers = {
|
|
312
|
+
node: './node-sqlite.js',
|
|
313
|
+
'better-sqlite3': 'better-sqlite3',
|
|
314
|
+
'sql.js': './sql.js.js',
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (driver) {
|
|
318
|
+
sqlite = require(drivers[driver])
|
|
319
|
+
} else {
|
|
320
|
+
sqlite = require(drivers.node)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
module.exports = SQLiteService
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const StandardFunctions = {
|
|
4
|
+
// ==============================
|
|
5
|
+
// Date and Time Functions
|
|
6
|
+
// ==============================
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Ensures ISO strings are returned for current timestamp
|
|
10
|
+
* @returns {string} - SQL statement
|
|
11
|
+
*/
|
|
12
|
+
current_timestamp: () => 'ISO(current_timestamp)',
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* SQLite doesn't support arguments for current_date
|
|
16
|
+
* @returns {string} - SQL statement
|
|
17
|
+
*/
|
|
18
|
+
current_date: () => 'current_date',
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* SQLite doesn't support arguments for current_time
|
|
22
|
+
* @returns {string} - SQL statement
|
|
23
|
+
*/
|
|
24
|
+
current_time: () => 'current_time',
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generates SQL statement that produces the fractional seconds of a given timestamp
|
|
28
|
+
* @param {string} x - The timestamp input
|
|
29
|
+
* @returns {string} - SQL statement
|
|
30
|
+
*/
|
|
31
|
+
fractionalseconds: x => `cast(substr(strftime('%f', ${x}), length(strftime('%f', ${x})) - 3) as REAL)`,
|
|
32
|
+
|
|
33
|
+
// ==============================
|
|
34
|
+
// String Functions
|
|
35
|
+
// ==============================
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generates SQL statement that produces a boolean value indicating whether the first string contains the second string
|
|
39
|
+
* @param {...string} args - The strings to evaluate
|
|
40
|
+
* @returns {string} - SQL statement
|
|
41
|
+
*/
|
|
42
|
+
contains: (...args) => `(ifnull(instr(${args}),0) > 0)`,
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Generates SQL statement that produces the index of the first occurrence of the second string in the first string
|
|
46
|
+
* @param {string} x - The string to search
|
|
47
|
+
* @param {string} y - The substring to find
|
|
48
|
+
* @returns {string} - SQL statement
|
|
49
|
+
*/
|
|
50
|
+
indexof: (x, y) => `instr(${x},${y}) - 1`,
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generates SQL statement that produces a boolean value indicating whether the first string starts with the second string
|
|
54
|
+
* @param {string} x - The string to evaluate
|
|
55
|
+
* @param {string} y - The prefix to check
|
|
56
|
+
* @returns {string} - SQL statement
|
|
57
|
+
*/
|
|
58
|
+
startswith: (x, y) => `coalesce(instr(${x},${y}) = 1,false)`,
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generates SQL statement that produces a boolean value indicating whether the first string ends with the second string
|
|
62
|
+
* @param {string} x - The string to evaluate
|
|
63
|
+
* @param {string} y - The suffix to check
|
|
64
|
+
* @returns {string} - SQL statement
|
|
65
|
+
*/
|
|
66
|
+
endswith: (x, y) => `coalesce(substr(${x}, length(${x}) + 1 - length(${y})) = ${y},false)`,
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generates SQL statement that matches the given string against a regular expression
|
|
70
|
+
* @param {string} x - The string to match
|
|
71
|
+
* @param {string} y - The regular expression
|
|
72
|
+
* @returns {string} - SQL statement
|
|
73
|
+
*/
|
|
74
|
+
matchesPattern: (x, y) => `(${x} regexp ${y})`,
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Alias for matchesPattern
|
|
78
|
+
* @param {string} x - The string to match
|
|
79
|
+
* @param {string} y - The regular expression
|
|
80
|
+
* @returns {string} - SQL statement
|
|
81
|
+
*/
|
|
82
|
+
matchespattern: (x, y) => `(${x} regexp ${y})`,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const HANAFunctions = {
|
|
86
|
+
// ==============================
|
|
87
|
+
// Timestamp Difference Functions
|
|
88
|
+
// ==============================
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Generates SQL statement that calculates the difference in 100-nanoseconds between two timestamps
|
|
92
|
+
* @param {string} x - Left timestamp
|
|
93
|
+
* @param {string} y - Right timestamp
|
|
94
|
+
* @returns {string} - SQL statement
|
|
95
|
+
*/
|
|
96
|
+
nano100_between: (x, y) => `(julianday(${y}) - julianday(${x})) * 864000000000`,
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Generates SQL statement that calculates the difference in seconds between two timestamps
|
|
100
|
+
* @param {string} x - Left timestamp
|
|
101
|
+
* @param {string} y - Right timestamp
|
|
102
|
+
* @returns {string} - SQL statement
|
|
103
|
+
*/
|
|
104
|
+
seconds_between: (x, y) => `(julianday(${y}) - julianday(${x})) * 86400`,
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generates SQL statement that calculates the difference in days between two timestamps
|
|
108
|
+
* @param {string} x - Left timestamp
|
|
109
|
+
* @param {string} y - Right timestamp
|
|
110
|
+
* @returns {string} - SQL statement
|
|
111
|
+
*/
|
|
112
|
+
days_between: (x, y) => `(
|
|
113
|
+
cast(julianday(${y}) as Integer) - cast(julianday(${x}) as Integer)
|
|
114
|
+
) + (
|
|
115
|
+
case
|
|
116
|
+
when (julianday(${y}) < julianday(${x})) then
|
|
117
|
+
(cast(strftime('%H%M%S%f0000', ${y}) as Integer) < cast(strftime('%H%M%S%f0000', ${x}) as Integer))
|
|
118
|
+
else
|
|
119
|
+
(cast(strftime('%H%M%S%f0000', ${y}) as Integer) > cast(strftime('%H%M%S%f0000', ${x}) as Integer)) * -1
|
|
120
|
+
end
|
|
121
|
+
)`,
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Generates SQL statement that calculates the difference in months between two timestamps
|
|
125
|
+
* @param {string} x - Left timestamp
|
|
126
|
+
* @param {string} y - Right timestamp
|
|
127
|
+
* @returns {string} - SQL statement
|
|
128
|
+
*/
|
|
129
|
+
months_between: (x, y) => `
|
|
130
|
+
(
|
|
131
|
+
(
|
|
132
|
+
(cast(strftime('%Y', ${y}) as Integer) - cast(strftime('%Y', ${x}) as Integer)) * 12
|
|
133
|
+
) + (
|
|
134
|
+
cast(strftime('%m', ${y}) as Integer) - cast(strftime('%m', ${x}) as Integer)
|
|
135
|
+
) + (
|
|
136
|
+
(
|
|
137
|
+
case
|
|
138
|
+
when (cast(strftime('%Y%m', ${y}) as Integer) < cast(strftime('%Y%m', ${x}) as Integer)) then
|
|
139
|
+
(cast(strftime('%d%H%M%S%f0000', ${y}) as Integer) > cast(strftime('%d%H%M%S%f0000', ${x}) as Integer))
|
|
140
|
+
else
|
|
141
|
+
(cast(strftime('%d%H%M%S%f0000', ${y}) as Integer) < cast(strftime('%d%H%M%S%f0000', ${x}) as Integer)) * -1
|
|
142
|
+
end
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
)`,
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Generates SQL statement that calculates the difference in years between two timestamps
|
|
149
|
+
* @param {string} x - Left timestamp
|
|
150
|
+
* @param {string} y - Right timestamp
|
|
151
|
+
* @returns {string} - SQL statement
|
|
152
|
+
*/
|
|
153
|
+
years_between(x, y) {
|
|
154
|
+
return `floor(${this.expr({ func: 'months_between', args: [x, y] })} / 12)`
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (let each in HANAFunctions) HANAFunctions[each.toUpperCase()] = HANAFunctions[each]
|
|
159
|
+
|
|
160
|
+
module.exports = { ...StandardFunctions, ...HANAFunctions }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
2
|
+
|
|
3
|
+
class NodeSqlite extends DatabaseSync {
|
|
4
|
+
prepare(sql) {
|
|
5
|
+
const stmt = super.prepare(sql)
|
|
6
|
+
const ret = {
|
|
7
|
+
run(params) {
|
|
8
|
+
try {
|
|
9
|
+
params = Array.isArray(params) ? params : [params]
|
|
10
|
+
return stmt.run(...params)
|
|
11
|
+
} catch (err) {
|
|
12
|
+
if (err.message.indexOf('NOT NULL constraint failed:') === 0) {
|
|
13
|
+
err.code = 'SQLITE_CONSTRAINT_NOTNULL'
|
|
14
|
+
}
|
|
15
|
+
throw err
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
get(params) {
|
|
19
|
+
params = Array.isArray(params) ? params : [params]
|
|
20
|
+
return stmt.get(...params)
|
|
21
|
+
},
|
|
22
|
+
all(params) {
|
|
23
|
+
params = Array.isArray(params) ? params : [params]
|
|
24
|
+
return stmt.all(...params)
|
|
25
|
+
},
|
|
26
|
+
iterate(params) {
|
|
27
|
+
stmt.setReturnArrays(true)
|
|
28
|
+
params = Array.isArray(params) ? params : [params]
|
|
29
|
+
return stmt.iterate(...params)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return ret
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = NodeSqlite
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const initSqlJs = require('sql.js');
|
|
2
|
+
|
|
3
|
+
const init = initSqlJs({})
|
|
4
|
+
|
|
5
|
+
class WasmSqlite {
|
|
6
|
+
constructor(/*database*/) {
|
|
7
|
+
// TODO: load / store database file contents
|
|
8
|
+
this.ready = init
|
|
9
|
+
.then(SQL => {
|
|
10
|
+
this.db = new SQL.Database()
|
|
11
|
+
// polyfill for missing or mismatched default sqlite3 math functions
|
|
12
|
+
this.db.create_function('ln', x => Math.log(x))
|
|
13
|
+
this.db.create_function('log', (x) => Math.log10(x))
|
|
14
|
+
this.db.create_function('log', (x, y) => Math.log(y) / Math.log(x))
|
|
15
|
+
this.db.create_function('mod', (x, y) => x % y)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
this.memory = true
|
|
19
|
+
this.gc = new FinalizationRegistry(stmt => { stmt.free() })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
prepare(sql) {
|
|
23
|
+
const stmt = this.db.prepare(sql)
|
|
24
|
+
const ret = {
|
|
25
|
+
run(params) {
|
|
26
|
+
try {
|
|
27
|
+
stmt.bind(params)
|
|
28
|
+
stmt.step()
|
|
29
|
+
const changes = stmt.db.getRowsModified(stmt)
|
|
30
|
+
const lastInsertRowid = stmt.db.exec('SELECT last_insert_rowid()')[0]?.values[0][0]
|
|
31
|
+
return { changes, lastInsertRowid }
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (err.message.indexOf('NOT NULL constraint failed:') === 0) {
|
|
34
|
+
err.code = 'SQLITE_CONSTRAINT_NOTNULL'
|
|
35
|
+
}
|
|
36
|
+
throw err
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
get(params) {
|
|
40
|
+
const columns = stmt.getColumnNames()
|
|
41
|
+
stmt.bind(params)
|
|
42
|
+
stmt.step()
|
|
43
|
+
const row = stmt.get()
|
|
44
|
+
const ret = {}
|
|
45
|
+
for (let i = 0; i < columns.length; i++) {
|
|
46
|
+
ret[columns[i]] = row[i]
|
|
47
|
+
}
|
|
48
|
+
return ret
|
|
49
|
+
},
|
|
50
|
+
all(params) {
|
|
51
|
+
const columns = stmt.getColumnNames()
|
|
52
|
+
const ret = []
|
|
53
|
+
stmt.bind(params)
|
|
54
|
+
while (stmt.step()) {
|
|
55
|
+
const row = stmt.get()
|
|
56
|
+
const obj = {}
|
|
57
|
+
for (let i = 0; i < columns.length; i++) {
|
|
58
|
+
obj[columns[i]] = row[i]
|
|
59
|
+
}
|
|
60
|
+
ret.push(obj)
|
|
61
|
+
}
|
|
62
|
+
return ret
|
|
63
|
+
},
|
|
64
|
+
*iterate(params) {
|
|
65
|
+
stmt.bind(params)
|
|
66
|
+
while (stmt.step()) {
|
|
67
|
+
yield stmt.get()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
this.gc.register(ret, stmt)
|
|
72
|
+
return ret
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
exec(sql) {
|
|
76
|
+
try {
|
|
77
|
+
const { columns, values } = this.db.exec(sql)
|
|
78
|
+
return !Array.isArray(values) ? values : values.map(val => {
|
|
79
|
+
const ret = {}
|
|
80
|
+
for (let i = 0; i < columns.length; i++) {
|
|
81
|
+
ret[columns[i]] = val[i]
|
|
82
|
+
}
|
|
83
|
+
return ret
|
|
84
|
+
})
|
|
85
|
+
} catch (err) {
|
|
86
|
+
// REVISIT: address transaction errors
|
|
87
|
+
if (sql === 'BEGIN' || sql === 'ROLLBACK') { return }
|
|
88
|
+
throw err
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function(name, config, func) {
|
|
93
|
+
this.db.create_function(name, func || config)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
close() { this.db.close() }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = WasmSqlite
|