@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 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
@@ -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('SESSION_CONTEXT', key => dbc[$session][key])
16
- dbc.function('REGEXP', { deterministic: true }, (re, x) => (RegExp(re).test(x) ? 1 : 0))
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
- dbc[$session] = variables // initial call from within this.begin()
36
- const $super = this._release
37
- this._release = function (dbc) {
38
- // reset session on release
39
- delete dbc[$session]
40
- return $super.call(this, dbc)
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
- return this.dbc.prepare(sql)
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
- SELECT_columns({ SELECT }) {
60
- if (!SELECT.columns) return '*'
61
- const { orderBy } = SELECT
62
- const orderByMap = {}
63
- // Collect all orderBy columns that should be taken from the SELECT.columns
64
- if (Array.isArray(orderBy))
65
- orderBy?.forEach(o => {
66
- if (o.ref?.length === 1) {
67
- orderByMap[o.ref[0]] = true
68
- }
69
- })
70
- return SELECT.columns.map(x => {
71
- if (x === '*') return x
72
- const alias = this.column_name(x)
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
- operator(x, i, xpr) {
81
- if (x === '=' && xpr[i + 1]?.val === null) return 'is'
82
- if (x === '!=') return 'is not'
83
- else return x
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
- DateTime: e => `strftime('%Y-%m-%dT%H:%M:%SZ',${fixTimeZone(e)})`,
92
- Timestamp: e => `strftime('%Y-%m-%dT%H:%M:%fZ',${fixTimeZone(e)})`,
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
- boolean: expr => `CASE ${expr} when 1 then 'true' when 0 then 'false' END ->'$'`, // REVIEW: ist that correct?
98
- Int64: expr => `CAST(${expr} as TEXT)`, // REVISIT: As discussed: please put that on a list of things to revisit later on
99
- Decimal: expr => `nullif(quote(${expr}),'NULL')->'$'`, // REVISIT: what is that ->'$' doing?
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
- // REVISIT: Timestamp should not loos precision
105
- Date: e => `strftime('%Y-%m-%d',${e})`,
106
- Time: e => `strftime('%H:%M:%S',${e})`,
107
- DateTime: e => `strftime('%Y-%m-%dT%H:%M:%SZ',${fixTimeZone(e)})`,
108
- Timestamp: e => `strftime('%Y-%m-%dT%H:%M:%fZ',${fixTimeZone(e)})`,
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: () => 'TIMESTAMP_TEXT',
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.0",
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.0",
33
+ "@cap-js/db-service": "^1.2.1",
34
34
  "better-sqlite3": "^8"
35
35
  },
36
36
  "peerDependencies": {