@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 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
@@ -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,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
- 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
- const alias = this.column_name(x)
72
- // Check whether the column alias should be added
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
- operator(x, i, xpr) {
80
- if (x === '=' && xpr[i + 1]?.val === null) return 'is'
81
- if (x === '!=') return 'is not'
82
- 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)
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
- DateTime: e => `strftime('%Y-%m-%dT%H:%M:%SZ',${fixTimeZone(e)})`,
91
- 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})`,
92
162
  }
93
163
 
94
164
  static OutputConverters = {
95
165
  ...super.OutputConverters,
96
- boolean: expr => `CASE ${expr} when 1 then 'true' when 0 then 'false' END ->'$'`, // REVIEW: ist that correct?
97
- Int64: expr => `CAST(${expr} as TEXT)`, // REVISIT: As discussed: please put that on a list of things to revisit later on
98
- Decimal: expr => `nullif(quote(${expr}),'NULL')->'$'`, // REVISIT: what is that ->'$' doing?
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
- // REVISIT: Timestamp should not loos precision
104
- Date: e => `strftime('%Y-%m-%d',${e})`,
105
- Time: e => `strftime('%H:%M:%S',${e})`,
106
- DateTime: e => `strftime('%Y-%m-%dT%H:%M:%SZ',${fixTimeZone(e)})`,
107
- 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} || ''`,
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: () => 'TIMESTAMP_TEXT',
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.1",
3
+ "version": "1.2.0",
4
4
  "description": "CDS database service for SQLite",
5
- "homepage": "https://cap.cloud.sap/",
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.1",
33
+ "@cap-js/db-service": "^1.2.0",
27
34
  "better-sqlite3": "^8"
28
35
  },
29
36
  "peerDependencies": {