@cap-js/sqlite 1.0.1 → 1.2.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 +12 -0
- package/lib/SQLiteService.js +138 -99
- package/package.json +10 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
- The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
5
5
|
- This project adheres to [Semantic Versioning](http://semver.org/).
|
|
6
6
|
|
|
7
|
+
## Version 1.2.0 - 2023-09-06
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `cds.Decimal` and `cds.Float` return numbers instead of strings
|
|
12
|
+
|
|
13
|
+
## Version 1.1.0 - 2023-08-01
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Updated minimum required version of `@cap-js/db-service`.
|
|
18
|
+
|
|
7
19
|
## Version 1.0.1 - 2023-07-03
|
|
8
20
|
|
|
9
21
|
### Changed
|
package/lib/SQLiteService.js
CHANGED
|
@@ -12,8 +12,9 @@ class SQLiteService extends SQLService {
|
|
|
12
12
|
create: tenant => {
|
|
13
13
|
const database = this.url4(tenant)
|
|
14
14
|
const dbc = new sqlite(database)
|
|
15
|
-
dbc.function('
|
|
16
|
-
dbc.function('
|
|
15
|
+
dbc.function('session_context', key => dbc[$session][key])
|
|
16
|
+
dbc.function('regexp', { deterministic: true }, (re, x) => (RegExp(re).test(x) ? 1 : 0))
|
|
17
|
+
dbc.function('ISO', { deterministic: true }, d => d && new Date(d).toISOString())
|
|
17
18
|
if (!dbc.memory) dbc.pragma('journal_mode = WAL')
|
|
18
19
|
return dbc
|
|
19
20
|
},
|
|
@@ -31,84 +32,170 @@ class SQLiteService extends SQLService {
|
|
|
31
32
|
|
|
32
33
|
set(variables) {
|
|
33
34
|
const dbc = this.dbc || cds.error('Cannot set session context: No database connection')
|
|
34
|
-
if (!dbc[$session])
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
} else Object.assign(dbc[$session], variables) // subsequent uses from custom code
|
|
35
|
+
if (!dbc[$session]) dbc[$session] = variables
|
|
36
|
+
else Object.assign(dbc[$session], variables)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
release() {
|
|
40
|
+
this.dbc[$session] = undefined
|
|
41
|
+
return super.release()
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
prepare(sql) {
|
|
46
45
|
try {
|
|
47
|
-
|
|
46
|
+
const stmt = this.dbc.prepare(sql)
|
|
47
|
+
return {
|
|
48
|
+
run: (..._) => this._run(stmt, ..._),
|
|
49
|
+
get: (..._) => stmt.get(..._),
|
|
50
|
+
all: (..._) => stmt.all(..._),
|
|
51
|
+
stream: (..._) => this._stream(stmt, ..._),
|
|
52
|
+
}
|
|
48
53
|
} catch (e) {
|
|
49
54
|
e.message += ' in:\n' + (e.sql = sql)
|
|
50
55
|
throw e
|
|
51
56
|
}
|
|
52
57
|
}
|
|
53
58
|
|
|
59
|
+
async _run(stmt, binding_params) {
|
|
60
|
+
for (let i = 0; i < binding_params.length; i++) {
|
|
61
|
+
const val = binding_params[i]
|
|
62
|
+
if (Buffer.isBuffer(val)) {
|
|
63
|
+
binding_params[i] = Buffer.from(val.base64Slice())
|
|
64
|
+
} else if (typeof val === 'object' && val && val.pipe) {
|
|
65
|
+
// REVISIT: stream.setEncoding('base64') sometimes misses the last bytes
|
|
66
|
+
// if (val.type === 'binary') val.setEncoding('base64')
|
|
67
|
+
binding_params[i] = await convStrm.buffer(val)
|
|
68
|
+
if (val.type === 'binary') binding_params[i] = Buffer.from(binding_params[i].toString('base64'))
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return stmt.run(binding_params)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async *_iterator(rs, one) {
|
|
75
|
+
// Allow for both array and iterator result sets
|
|
76
|
+
const first = Array.isArray(rs) ? { done: !rs[0], value: rs[0] } : rs.next()
|
|
77
|
+
if (first.done) return
|
|
78
|
+
if (one) {
|
|
79
|
+
yield first.value[0]
|
|
80
|
+
// Close result set to release database connection
|
|
81
|
+
rs.return()
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
yield '['
|
|
86
|
+
// Print first value as stand alone to prevent comma check inside the loop
|
|
87
|
+
yield first.value[0]
|
|
88
|
+
for (const row of rs) {
|
|
89
|
+
yield `,${row[0]}`
|
|
90
|
+
}
|
|
91
|
+
yield ']'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async _stream(stmt, binding_params, one) {
|
|
95
|
+
const columns = stmt.columns()
|
|
96
|
+
// Stream single blob column
|
|
97
|
+
if (columns.length === 1 && columns[0].name !== '_json_') {
|
|
98
|
+
// Setting result set to raw to keep better-sqlite from doing additional processing
|
|
99
|
+
stmt.raw(true)
|
|
100
|
+
const rows = stmt.all(binding_params)
|
|
101
|
+
// REVISIT: return undefined when no rows are found
|
|
102
|
+
if (rows.length === 0) return undefined
|
|
103
|
+
if (rows[0][0] === null) return null
|
|
104
|
+
// Buffer.from only applies encoding when the input is a string
|
|
105
|
+
let raw = Buffer.from(rows[0][0].toString(), 'base64')
|
|
106
|
+
stmt.raw(false)
|
|
107
|
+
return new Readable({
|
|
108
|
+
read(size) {
|
|
109
|
+
if (raw.length === 0) return this.push(null)
|
|
110
|
+
const chunk = raw.slice(0, size)
|
|
111
|
+
raw = raw.slice(size)
|
|
112
|
+
this.push(chunk)
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
stmt.raw(true)
|
|
118
|
+
const rs = stmt.iterate(binding_params)
|
|
119
|
+
return Readable.from(this._iterator(rs, one))
|
|
120
|
+
}
|
|
121
|
+
|
|
54
122
|
exec(sql) {
|
|
55
123
|
return this.dbc.exec(sql)
|
|
56
124
|
}
|
|
57
125
|
|
|
58
126
|
static CQN2SQL = class CQN2SQLite extends SQLService.CQN2SQL {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const xpr = this.column_expr(x)
|
|
74
|
-
const needsAlias = (typeof x.as === 'string' && x.as) || orderByMap[alias]
|
|
75
|
-
return `${xpr}${needsAlias ? ` as ${this.quote(alias)}` : ''}`
|
|
76
|
-
})
|
|
127
|
+
|
|
128
|
+
column_alias4(x, q) {
|
|
129
|
+
let alias = super.column_alias4(x, q)
|
|
130
|
+
if (alias) return alias
|
|
131
|
+
if (x.ref) {
|
|
132
|
+
let obm = q._orderByMap
|
|
133
|
+
if (!obm) {
|
|
134
|
+
Object.defineProperty(q, '_orderByMap', { value: (obm = {}) })
|
|
135
|
+
q.SELECT?.orderBy?.forEach(o => {
|
|
136
|
+
if (o.ref?.length === 1) obm[o.ref[0]] = o.ref[0]
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
return obm[x.ref.at(-1)]
|
|
140
|
+
}
|
|
77
141
|
}
|
|
78
142
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
|
|
143
|
+
val(v) {
|
|
144
|
+
// intercept DateTime values and convert to Date objects to compare ISO Strings
|
|
145
|
+
if (/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[Z+-]/.test(v.val)) v.val = new Date(v.val)
|
|
146
|
+
return super.val(v)
|
|
83
147
|
}
|
|
84
148
|
|
|
85
149
|
// Used for INSERT statements
|
|
86
150
|
static InputConverters = {
|
|
87
151
|
...super.InputConverters,
|
|
152
|
+
|
|
153
|
+
// The following allows passing in ISO strings with non-zulu
|
|
154
|
+
// timezones and converts them into zulu dates and times
|
|
88
155
|
Date: e => `strftime('%Y-%m-%d',${e})`,
|
|
89
156
|
Time: e => `strftime('%H:%M:%S',${e})`,
|
|
90
|
-
|
|
91
|
-
|
|
157
|
+
|
|
158
|
+
// Both, DateTimes and Timestamps are canonicalized to ISO strings with
|
|
159
|
+
// ms precision to allow safe comparisons, also to query {val}s in where clauses
|
|
160
|
+
DateTime: e => `ISO(${e})`,
|
|
161
|
+
Timestamp: e => `ISO(${e})`,
|
|
92
162
|
}
|
|
93
163
|
|
|
94
164
|
static OutputConverters = {
|
|
95
165
|
...super.OutputConverters,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
Float: expr => `nullif(quote(${expr}),'NULL')->'$'`,
|
|
100
|
-
Double: expr => `nullif(quote(${expr}),'NULL')->'$'`,
|
|
166
|
+
|
|
167
|
+
// Structs and arrays are stored as JSON strings; the ->'$' unwraps them.
|
|
168
|
+
// Otherwise they would be added as strings to json_objects.
|
|
101
169
|
struct: expr => `${expr}->'$'`, // Association + Composition inherits from struct
|
|
102
170
|
array: expr => `${expr}->'$'`,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
171
|
+
|
|
172
|
+
// SQLite has no booleans so we need to convert 0 and 1
|
|
173
|
+
boolean: expr => `CASE ${expr} when 1 then 'true' when 0 then 'false' END ->'$'`,
|
|
174
|
+
|
|
175
|
+
// DateTimes are returned without ms added by InputConverters
|
|
176
|
+
DateTime: e => `substr(${e},0,20)||'Z'`,
|
|
177
|
+
|
|
178
|
+
// Timestamps are returned with ms, as written by InputConverters.
|
|
179
|
+
// And as cds.builtin.classes.Timestamp inherits from DateTime we need
|
|
180
|
+
// to override the DateTime converter above
|
|
181
|
+
Timestamp: undefined,
|
|
182
|
+
|
|
183
|
+
// int64 is stored as native int64 for best comparison
|
|
184
|
+
// Reading int64 as string to not loose precision
|
|
185
|
+
Int64: expr => `CAST(${expr} as TEXT)`,
|
|
186
|
+
|
|
187
|
+
// Binary is not allowed in json objects
|
|
188
|
+
Binary: expr => `${expr} || ''`,
|
|
108
189
|
}
|
|
109
190
|
|
|
110
191
|
// Used for SQL function expressions
|
|
111
|
-
static Functions = { ...super.Functions
|
|
192
|
+
static Functions = { ...super.Functions,
|
|
193
|
+
// Ensure ISO strings are returned for date/time functions
|
|
194
|
+
current_timestamp: () => 'ISO(current_timestamp)',
|
|
195
|
+
// SQLite doesn't support arguments for current_date and current_time
|
|
196
|
+
current_date: () => 'current_date',
|
|
197
|
+
current_time: () => 'current_time',
|
|
198
|
+
}
|
|
112
199
|
|
|
113
200
|
// Used for CREATE TABLE statements
|
|
114
201
|
static TypeMap = {
|
|
@@ -116,10 +203,13 @@ class SQLiteService extends SQLService {
|
|
|
116
203
|
Binary: e => `BINARY_BLOB(${e.length || 5000})`,
|
|
117
204
|
Date: () => 'DATE_TEXT',
|
|
118
205
|
Time: () => 'TIME_TEXT',
|
|
119
|
-
DateTime: () => '
|
|
206
|
+
DateTime: () => 'DATETIME_TEXT',
|
|
120
207
|
Timestamp: () => 'TIMESTAMP_TEXT',
|
|
121
208
|
}
|
|
122
209
|
|
|
210
|
+
get is_distinct_from_() { return 'is not' }
|
|
211
|
+
get is_not_distinct_from_() { return 'is' }
|
|
212
|
+
|
|
123
213
|
static ReservedWords = { ...super.ReservedWords, ...require('./ReservedWords.json') }
|
|
124
214
|
}
|
|
125
215
|
|
|
@@ -142,31 +232,6 @@ class SQLiteService extends SQLService {
|
|
|
142
232
|
throw _not_unique(err, 'UNIQUE_CONSTRAINT_VIOLATION') || err
|
|
143
233
|
}
|
|
144
234
|
}
|
|
145
|
-
|
|
146
|
-
// overrides generic onSTREAM
|
|
147
|
-
// SQLite doesn't support streaming, the whole data is read from/written into the database
|
|
148
|
-
async onSTREAM(req) {
|
|
149
|
-
const { sql, values, entries } = this.cqn2sql(req.query)
|
|
150
|
-
// writing stream
|
|
151
|
-
if (req.query.STREAM.into) {
|
|
152
|
-
const stream = entries[0]
|
|
153
|
-
stream.on('error', () => stream.removeAllListeners('error'))
|
|
154
|
-
values.unshift((await convStrm.buffer(stream)).toString('base64'))
|
|
155
|
-
const ps = await this.prepare(sql)
|
|
156
|
-
return (await ps.run(values)).changes
|
|
157
|
-
}
|
|
158
|
-
// reading stream
|
|
159
|
-
const ps = await this.prepare(sql)
|
|
160
|
-
let result = await ps.all(values)
|
|
161
|
-
if (result.length === 0) return
|
|
162
|
-
|
|
163
|
-
const val = Object.values(result[0])[0]
|
|
164
|
-
if (val === null) return val
|
|
165
|
-
const stream_ = new Readable()
|
|
166
|
-
stream_.push(Buffer.from(val, 'base64'))
|
|
167
|
-
stream_.push(null)
|
|
168
|
-
return stream_
|
|
169
|
-
}
|
|
170
235
|
}
|
|
171
236
|
|
|
172
237
|
// function _not_null (err) {
|
|
@@ -186,30 +251,4 @@ function _not_unique(err, code) {
|
|
|
186
251
|
})
|
|
187
252
|
}
|
|
188
253
|
|
|
189
|
-
/**
|
|
190
|
-
* Generates SQL statement that allows SQLite to support most of the ISO 8601 timezone syntaxes
|
|
191
|
-
* @example
|
|
192
|
-
* '1970-01-01T00:00:00+0200' -> '1970-01-01T00:00:00+02:00'
|
|
193
|
-
* @example
|
|
194
|
-
* '1970-01-01T00:00:00-02' -> '1970-01-01T00:00:00-02:00'
|
|
195
|
-
* @example
|
|
196
|
-
* '1970-01-01T00:00:00Z' -> '1970-01-01T00:00:00Z'
|
|
197
|
-
* @param {String} e value SQL expression
|
|
198
|
-
* @returns {String} SQL statement that ensures that the value has the valid ISO timezone for SQLite
|
|
199
|
-
*/
|
|
200
|
-
const fixTimeZone = e =>
|
|
201
|
-
`(
|
|
202
|
-
SELECT
|
|
203
|
-
CASE
|
|
204
|
-
WHEN substr(T,length(T),1) = 'Z' THEN
|
|
205
|
-
T
|
|
206
|
-
WHEN substr(T,length(T) - 4,1) = '-' OR substr(T,length(T) - 4,1) = '+' THEN
|
|
207
|
-
substr(T,0,length(T) - 1) || ':' || substr(T,length(T) - 1)
|
|
208
|
-
WHEN substr(T,length(T) - 2,1) = '-' OR substr(T,length(T) - 2,1) = '+' THEN
|
|
209
|
-
T || ':' || '00'
|
|
210
|
-
ELSE T
|
|
211
|
-
END AS T
|
|
212
|
-
FROM (SELECT (${e}) AS T)
|
|
213
|
-
)`.replace(/\s*\n\s*/g, ' ')
|
|
214
|
-
|
|
215
254
|
module.exports = SQLiteService
|
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/sqlite",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "CDS database service for SQLite",
|
|
5
|
-
"homepage": "https://
|
|
5
|
+
"homepage": "https://github.com/cap-js/cds-dbs/tree/main/sqlite#cds-database-service-for-sqlite",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/cap-js/cds-dbs"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/cap-js/cds-dbs/issues"
|
|
12
|
+
},
|
|
6
13
|
"keywords": [
|
|
7
14
|
"CAP",
|
|
8
15
|
"CDS",
|
|
@@ -23,7 +30,7 @@
|
|
|
23
30
|
"test": "jest --silent"
|
|
24
31
|
},
|
|
25
32
|
"dependencies": {
|
|
26
|
-
"@cap-js/db-service": "^1.0
|
|
33
|
+
"@cap-js/db-service": "^1.2.0",
|
|
27
34
|
"better-sqlite3": "^8"
|
|
28
35
|
},
|
|
29
36
|
"peerDependencies": {
|