@cap-js/sqlite 1.1.0 → 1.2.1
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 -100
- package/package.json +2 -2
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.1 - 2023-09-08
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Adapt implementation to comply with implication of SQLite version 3.43 which is included in `better-sqlite3@8.6.0`. #210
|
|
12
|
+
|
|
13
|
+
## Version 1.2.0 - 2023-09-06
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `cds.Decimal` and `cds.Float` return numbers instead of strings
|
|
18
|
+
|
|
7
19
|
## Version 1.1.0 - 2023-08-01
|
|
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,85 +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
|
-
// Check whether the column alias should be added
|
|
74
|
-
const xpr = this.column_expr(x)
|
|
75
|
-
const needsAlias = (typeof x.as === 'string' && x.as) || orderByMap[alias]
|
|
76
|
-
return `${xpr}${needsAlias ? ` as ${this.quote(alias)}` : ''}`
|
|
77
|
-
})
|
|
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
|
+
}
|
|
78
141
|
}
|
|
79
142
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
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)
|
|
84
147
|
}
|
|
85
148
|
|
|
86
149
|
// Used for INSERT statements
|
|
87
150
|
static InputConverters = {
|
|
88
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
|
|
89
155
|
Date: e => `strftime('%Y-%m-%d',${e})`,
|
|
90
156
|
Time: e => `strftime('%H:%M:%S',${e})`,
|
|
91
|
-
|
|
92
|
-
|
|
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})`,
|
|
93
162
|
}
|
|
94
163
|
|
|
95
164
|
static OutputConverters = {
|
|
96
165
|
...super.OutputConverters,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
Float: expr => `nullif(quote(${expr}),'NULL')->'$'`,
|
|
101
|
-
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.
|
|
102
169
|
struct: expr => `${expr}->'$'`, // Association + Composition inherits from struct
|
|
103
170
|
array: expr => `${expr}->'$'`,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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} || ''`,
|
|
109
189
|
}
|
|
110
190
|
|
|
111
191
|
// Used for SQL function expressions
|
|
112
|
-
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
|
+
}
|
|
113
199
|
|
|
114
200
|
// Used for CREATE TABLE statements
|
|
115
201
|
static TypeMap = {
|
|
@@ -117,10 +203,13 @@ class SQLiteService extends SQLService {
|
|
|
117
203
|
Binary: e => `BINARY_BLOB(${e.length || 5000})`,
|
|
118
204
|
Date: () => 'DATE_TEXT',
|
|
119
205
|
Time: () => 'TIME_TEXT',
|
|
120
|
-
DateTime: () => '
|
|
206
|
+
DateTime: () => 'DATETIME_TEXT',
|
|
121
207
|
Timestamp: () => 'TIMESTAMP_TEXT',
|
|
122
208
|
}
|
|
123
209
|
|
|
210
|
+
get is_distinct_from_() { return 'is not' }
|
|
211
|
+
get is_not_distinct_from_() { return 'is' }
|
|
212
|
+
|
|
124
213
|
static ReservedWords = { ...super.ReservedWords, ...require('./ReservedWords.json') }
|
|
125
214
|
}
|
|
126
215
|
|
|
@@ -143,31 +232,6 @@ class SQLiteService extends SQLService {
|
|
|
143
232
|
throw _not_unique(err, 'UNIQUE_CONSTRAINT_VIOLATION') || err
|
|
144
233
|
}
|
|
145
234
|
}
|
|
146
|
-
|
|
147
|
-
// overrides generic onSTREAM
|
|
148
|
-
// SQLite doesn't support streaming, the whole data is read from/written into the database
|
|
149
|
-
async onSTREAM(req) {
|
|
150
|
-
const { sql, values, entries } = this.cqn2sql(req.query)
|
|
151
|
-
// writing stream
|
|
152
|
-
if (req.query.STREAM.into) {
|
|
153
|
-
const stream = entries[0]
|
|
154
|
-
stream.on('error', () => stream.removeAllListeners('error'))
|
|
155
|
-
values.unshift((await convStrm.buffer(stream)).toString('base64'))
|
|
156
|
-
const ps = await this.prepare(sql)
|
|
157
|
-
return (await ps.run(values)).changes
|
|
158
|
-
}
|
|
159
|
-
// reading stream
|
|
160
|
-
const ps = await this.prepare(sql)
|
|
161
|
-
let result = await ps.all(values)
|
|
162
|
-
if (result.length === 0) return
|
|
163
|
-
|
|
164
|
-
const val = Object.values(result[0])[0]
|
|
165
|
-
if (val === null) return val
|
|
166
|
-
const stream_ = new Readable()
|
|
167
|
-
stream_.push(Buffer.from(val, 'base64'))
|
|
168
|
-
stream_.push(null)
|
|
169
|
-
return stream_
|
|
170
|
-
}
|
|
171
235
|
}
|
|
172
236
|
|
|
173
237
|
// function _not_null (err) {
|
|
@@ -187,30 +251,4 @@ function _not_unique(err, code) {
|
|
|
187
251
|
})
|
|
188
252
|
}
|
|
189
253
|
|
|
190
|
-
/**
|
|
191
|
-
* Generates SQL statement that allows SQLite to support most of the ISO 8601 timezone syntaxes
|
|
192
|
-
* @example
|
|
193
|
-
* '1970-01-01T00:00:00+0200' -> '1970-01-01T00:00:00+02:00'
|
|
194
|
-
* @example
|
|
195
|
-
* '1970-01-01T00:00:00-02' -> '1970-01-01T00:00:00-02:00'
|
|
196
|
-
* @example
|
|
197
|
-
* '1970-01-01T00:00:00Z' -> '1970-01-01T00:00:00Z'
|
|
198
|
-
* @param {String} e value SQL expression
|
|
199
|
-
* @returns {String} SQL statement that ensures that the value has the valid ISO timezone for SQLite
|
|
200
|
-
*/
|
|
201
|
-
const fixTimeZone = e =>
|
|
202
|
-
`(
|
|
203
|
-
SELECT
|
|
204
|
-
CASE
|
|
205
|
-
WHEN substr(T,length(T),1) = 'Z' THEN
|
|
206
|
-
T
|
|
207
|
-
WHEN substr(T,length(T) - 4,1) = '-' OR substr(T,length(T) - 4,1) = '+' THEN
|
|
208
|
-
substr(T,0,length(T) - 1) || ':' || substr(T,length(T) - 1)
|
|
209
|
-
WHEN substr(T,length(T) - 2,1) = '-' OR substr(T,length(T) - 2,1) = '+' THEN
|
|
210
|
-
T || ':' || '00'
|
|
211
|
-
ELSE T
|
|
212
|
-
END AS T
|
|
213
|
-
FROM (SELECT (${e}) AS T)
|
|
214
|
-
)`.replace(/\s*\n\s*/g, ' ')
|
|
215
|
-
|
|
216
254
|
module.exports = SQLiteService
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/sqlite",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "CDS database service for SQLite",
|
|
5
5
|
"homepage": "https://github.com/cap-js/cds-dbs/tree/main/sqlite#cds-database-service-for-sqlite",
|
|
6
6
|
"repository": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"test": "jest --silent"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@cap-js/db-service": "^1.1
|
|
33
|
+
"@cap-js/db-service": "^1.2.1",
|
|
34
34
|
"better-sqlite3": "^8"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|