@cap-js/sqlite 0.1.0 → 1.0.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 +9 -0
- package/README.md +1 -184
- package/cds-plugin.js +5 -0
- package/index.js +1 -1
- package/lib/ReservedWords.json +149 -0
- package/lib/SQLiteService.js +215 -0
- package/package.json +23 -27
- package/cds.js +0 -39
- package/lib/db/DatabaseService.js +0 -101
- package/lib/db/sql/InsertResults.js +0 -87
- package/lib/db/sql/SQLService.js +0 -223
- package/lib/db/sql/copy.js +0 -17
- package/lib/db/sql/cqn2sql.js +0 -515
- package/lib/db/sql/cqn4sql.js +0 -1461
- package/lib/db/sql/deep.js +0 -233
- package/lib/db/sql/func.js +0 -146
- package/lib/db/sql/structuralComparisonOps.js +0 -16
- package/lib/db/sql/utils.js +0 -22
- package/lib/db/sql/workarounds.js +0 -73
- package/lib/db/sqlite/ReservedWords.json +0 -149
- package/lib/db/sqlite/SQLiteService.js +0 -170
- package/lib/ql/cds.infer.js +0 -786
- package/lib/ql/join-tree.js +0 -167
- package/lib/ql/pseudos.js +0 -23
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
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
|
+
|
|
8
|
+
## Version 1.0.0 - 2023-06-23
|
|
9
|
+
|
|
10
|
+
- First official release
|
|
11
|
+
|
|
12
|
+
## Version 0.2.0 - 2023-05-03
|
|
13
|
+
|
|
14
|
+
- Continuous improvements
|
|
15
|
+
|
|
7
16
|
## Version 0.1.0 - 2023-04-04
|
|
8
17
|
|
|
9
18
|
- Initial release
|
package/README.md
CHANGED
|
@@ -2,188 +2,5 @@
|
|
|
2
2
|
|
|
3
3
|
Welcome to the new SQLite database service for [SAP Cloud Application Programming Model](https://cap.cloud.sap) Node.js, based on new, streamlined database architecture and [*better-sqlite* driver](https://www.npmjs.com/package/better-sqlite3) .
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
>
|
|
5
|
+
Find documentation at https://cap.cloud.sap/docs/guides/databases-sqlite.
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
## Installing
|
|
10
|
-
|
|
11
|
-
### Add `@cap-js/sqlite` as package dependency
|
|
12
|
-
|
|
13
|
-
As SQLite is commonly used during development or tests only, add it as a dev dependency like so:
|
|
14
|
-
|
|
15
|
-
```sh
|
|
16
|
-
npm add @cap-js/sqlite -D
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
> **Note:** There's no need to add the [*better-sqlite*](https://www.npmjs.com/package/better-sqlite3) driver, and it's also not recommended anymore to do so, as that is done transiently.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
### Remove `sqlite3` package dependency
|
|
23
|
-
|
|
24
|
-
If migrating an existing project you may want to remove the old [*sqlite3* driver](https://www.npmjs.com/package/sqlite3) :
|
|
25
|
-
|
|
26
|
-
```sh
|
|
27
|
-
npm rm sqlite3
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
## Usage
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
### Via `@cap-js/sqlite` in package dependencies
|
|
35
|
-
|
|
36
|
-
Adding `@cap-js/sqlite` to your package dependencies as described above is all you need to do. We'll automatically use the new service if that dependency is there in
|
|
37
|
-
`dependencies` or in `devDependencies`. Your configuration can stay as is, e.g.:
|
|
38
|
-
|
|
39
|
-
```jsonc
|
|
40
|
-
{ ...,
|
|
41
|
-
"cds": {
|
|
42
|
-
"requires": {
|
|
43
|
-
"db": "sql"
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
> **Note:** This automatically also enables the new, lean draft implementation that's required for the new database services, i.e., `cds.fiori.lean_draft` will be automatically set to `true`.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
### Via `better-sqlite` config profile
|
|
53
|
-
|
|
54
|
-
Alternatively, if you don't have `@cap-js/sqlite` in your package dependencies, but installed in an outer monorepo like in *[cap/samples](https://github.com/sap-samples/cloud-cap-samples)*, you can occasionally run or test your apps with the `better-sqlite` profile using one of these options to specify the profile:
|
|
55
|
-
|
|
56
|
-
```sh
|
|
57
|
-
cds watch bookshop --profile better-sqlite
|
|
58
|
-
```
|
|
59
|
-
```sh
|
|
60
|
-
CDS_ENV=better-sqlite cds watch bookshop
|
|
61
|
-
```
|
|
62
|
-
```sh
|
|
63
|
-
CDS_ENV=better-sqlite jest --silent
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
## New Features & Improvements
|
|
69
|
-
|
|
70
|
-
### Full Support for Path Expressions
|
|
71
|
-
|
|
72
|
-
The new SQLite service provides full support for all kinds of [path expressions](https://cap.cloud.sap/docs/cds/cql#path-expressions), including [infix filters](https://cap.cloud.sap/docs/cds/cql#with-infix-filters), and [exists predicates](https://cap.cloud.sap/docs/cds/cql#exists-predicate). For example, you can try this out with *[cap/samples](https://github.com/sap-samples/cloud-cap-samples)* as follows:
|
|
73
|
-
|
|
74
|
-
```sh
|
|
75
|
-
cds repl --profile better-sqlite
|
|
76
|
-
var { server } = await cds.test('bookshop')
|
|
77
|
-
var { Books, Authors } = cds.entities
|
|
78
|
-
await INSERT.into (Books) .entries ({ title: 'Unwritten Book' })
|
|
79
|
-
await INSERT.into (Authors) .entries ({ name: 'Upcomming Author' })
|
|
80
|
-
await SELECT `from ${Books} { title as book, author.name as author, genre.name as genre }`
|
|
81
|
-
await SELECT `from ${Authors} { books.title as book, name as author, books.genre.name as genre }`
|
|
82
|
-
await SELECT `from ${Books} { title as book, author[ID<170].name as author, genre.name as genre }`
|
|
83
|
-
await SELECT `from ${Books} { title as book, author.name as author, genre.name as genre }` .where ({'author.name':{like:'Ed%'},or:{'author.ID':170}})
|
|
84
|
-
await SELECT `from ${Books} { title as book, author.name as author, genre.name as genre } where author.name like 'Ed%' or author.ID=170`
|
|
85
|
-
await SELECT `from ${Books}:author[name like 'Ed%' or ID=170] { books.title as book, name as author, books.genre.name as genre }`
|
|
86
|
-
await SELECT `from ${Books}:author[150] { books.title as book, name as author, books.genre.name as genre }`
|
|
87
|
-
await SELECT `from ${Authors} { ID, name, books { ID, title }}`
|
|
88
|
-
await SELECT `from ${Authors} { ID, name, books { ID, title, genre { ID, name }}}`
|
|
89
|
-
await SELECT `from ${Authors} { ID, name, books.genre { ID, name }}`
|
|
90
|
-
await SELECT `from ${Authors} { ID, name, books as some_books { ID, title, genre.name as genre }}`
|
|
91
|
-
await SELECT `from ${Authors} { ID, name, books[genre.ID=11] as dramatic_books { ID, title, genre.name as genre }}`
|
|
92
|
-
await SELECT `from ${Authors} { ID, name, books.genre[name!='Drama'] as no_drama_books_count { count(*) as sum }}`
|
|
93
|
-
await SELECT `from ${Authors} { books.genre.ID }`
|
|
94
|
-
await SELECT `from ${Authors} { books.genre }`
|
|
95
|
-
await SELECT `from ${Authors} { books.genre.name }`
|
|
96
|
-
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
### Specified Standard Functions
|
|
102
|
-
|
|
103
|
-
A specified set of standard functions is now supported and translated to database-specific variants. These functions are by and large the same as specified in OData:
|
|
104
|
-
|
|
105
|
-
* `concat`, `indexof`, `length`
|
|
106
|
-
* `contains`, `startswith`, `endswith`, `substring`, `matchesPattern`
|
|
107
|
-
* `tolower`, `toupper`
|
|
108
|
-
* `ceiling`
|
|
109
|
-
* `year`, `month`, `day`, `hour`, `minute`, `second`
|
|
110
|
-
|
|
111
|
-
The db service implementation translates these to the best-possible native SQL functions, thus enhancing the extend of **portable** queries.
|
|
112
|
-
|
|
113
|
-
> **Note** that usage is **case-sensitive**, which means you have to write these functions exactly as given above; all-uppercase usages are not supported.
|
|
114
|
-
|
|
115
|
-
### Support for Common HANA Functions
|
|
116
|
-
|
|
117
|
-
In addition to the standard functions, which all new database services will support, the new SQLite service also supports these common HANA functions, to further increase the scope for portable testing:
|
|
118
|
-
|
|
119
|
-
- `years_between`
|
|
120
|
-
- `months_between`
|
|
121
|
-
- `days_between`
|
|
122
|
-
- `seconds_between`
|
|
123
|
-
- `nano100_between`
|
|
124
|
-
|
|
125
|
-
> Both usages are allowed here: all-lowercase as given above, as well as all-uppercase.
|
|
126
|
-
|
|
127
|
-
### Support for Session Context Variables
|
|
128
|
-
|
|
129
|
-
The new SQLite service can leverage [*better-sqlite*](https://www.npmjs.com/package/better-sqlite3)'s user-defined functions to support *session context* variables. In particular, the pseudo variables `$user.id`, `$user.locale`, `$valid.from`, and `$valid.to` are available in native SQL queries like so:
|
|
130
|
-
|
|
131
|
-
```sql
|
|
132
|
-
SELECT session_context('$user.id')
|
|
133
|
-
SELECT session_context('$user.locale')
|
|
134
|
-
SELECT session_context('$valid.from')
|
|
135
|
-
SELECT session_context('$valid.to')
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
Amongst other, this allows us to get rid of static helper views for localized data like `localized_de_sap_capire_Books`.
|
|
139
|
-
|
|
140
|
-
### Deep Reads via Single Queries
|
|
141
|
-
|
|
142
|
-
The old database service implementation(s) translated deep reads, i.e., SELECTs with expands, into several database queries and collected the individual results into deep result structures. The new service uses `json_object` functions and alike to instead do that in one single query, with sub selects, which greatly improves performance.
|
|
143
|
-
|
|
144
|
-
### New `SELECT.localized` Queries
|
|
145
|
-
|
|
146
|
-
With the old implementation when running queries like `SELECT.from(Books)` would always return localized data, without being able to easily read the non-localized data. The new service does only what you asked for, offering new `SELECT.localized` options:
|
|
147
|
-
|
|
148
|
-
```js
|
|
149
|
-
let books = await SELECT.from(Books) //> non-localized data
|
|
150
|
-
let lbooks = await SELECT.localized(Books) //> localized data
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
Usage variants include:
|
|
154
|
-
|
|
155
|
-
```js
|
|
156
|
-
SELECT.localized(Books)
|
|
157
|
-
SELECT.from.localized(Books)
|
|
158
|
-
SELECT.one.localized(Books)
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
> **Note:** Queries executed through generic application service handlers continue to serve localized data as before.
|
|
162
|
-
|
|
163
|
-
### Using Lean Draft Implementation
|
|
164
|
-
|
|
165
|
-
The old implementation was overly polluted with draft handling. But as draft is actually a Fiori UI concept, that should not be the case. Hence, we eliminated all draft handling from the new database service implementations, and implemented draft in a modular, non-intrusive way — called *'Lean Draft'*. The most important change is that we don't do expensive UNIONs anymore but work with single cheap selects.
|
|
166
|
-
|
|
167
|
-
When using the new SQLite service the new `cds.fiori.lean_draft` mode is automatically switched on. You may additionally switch on `cds.fiori_draft_compat` in case you run into problems.
|
|
168
|
-
|
|
169
|
-
More detailed documentation for that will follow soon.
|
|
170
|
-
|
|
171
|
-
### Performance Improvements
|
|
172
|
-
|
|
173
|
-
The combination of the above-mentioned improvements commonly leads to significant performance improvements. For example displaying the list page of Travels in [cap/sflight](https://github.com/SAP-samples/cap-sflight) took **>250ms** in the past, and **~15ms** now.
|
|
174
|
-
|
|
175
|
-
## Known Limitations & Changes
|
|
176
|
-
|
|
177
|
-
- Node v14 is no longer supported → will be dropped anyways with upcomming cds7.
|
|
178
|
-
- JOINs and UNIONs by CQN are no longer supported → use plain SQL instead.
|
|
179
|
-
* CQNs with subqueries require table aliases to refer to elements of outer queries.
|
|
180
|
-
* CQNs with an empty columns array now throws an error.
|
|
181
|
-
* Search: only single values are allowed as search expression.
|
|
182
|
-
* CSV input: column names like `author.ID` are disallowed → use `author_ID` instead.
|
|
183
|
-
* No `default` values are returned anymore for `virtual` elements.
|
|
184
|
-
* `SELECT.from(...)` queries on database level don't return localized data anymore → use `SELECT.localized(...)`
|
|
185
|
-
* Standard functions in CQN are case-sensitive → don't uppercase them.
|
|
186
|
-
* For `@cds.on.insert/update`annotations only `$now` and `$user.id` are supported.
|
|
187
|
-
* The `cds/db.stream()` methods are not implemented yet → will come soon.
|
|
188
|
-
|
|
189
|
-
_The list of important changes is not final and will be constantly updated._
|
package/cds-plugin.js
ADDED
package/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
module.exports = require('./lib/
|
|
1
|
+
module.exports = require('./lib/SQLiteService.js')
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ABORT": 1,
|
|
3
|
+
"ACTION": 1,
|
|
4
|
+
"ADD": 1,
|
|
5
|
+
"AFTER": 1,
|
|
6
|
+
"ALL": 1,
|
|
7
|
+
"ALTER": 1,
|
|
8
|
+
"ALWAYS": 1,
|
|
9
|
+
"ANALYZE": 1,
|
|
10
|
+
"AND": 1,
|
|
11
|
+
"AS": 1,
|
|
12
|
+
"ASC": 1,
|
|
13
|
+
"ATTACH": 1,
|
|
14
|
+
"AUTOINCREMENT": 1,
|
|
15
|
+
"BEFORE": 1,
|
|
16
|
+
"BEGIN": 1,
|
|
17
|
+
"BETWEEN": 1,
|
|
18
|
+
"BY": 1,
|
|
19
|
+
"CASCADE": 1,
|
|
20
|
+
"CASE": 1,
|
|
21
|
+
"CAST": 1,
|
|
22
|
+
"CHECK": 1,
|
|
23
|
+
"COLLATE": 1,
|
|
24
|
+
"COLUMN": 1,
|
|
25
|
+
"COMMIT": 1,
|
|
26
|
+
"CONFLICT": 1,
|
|
27
|
+
"CONSTRAINT": 1,
|
|
28
|
+
"CREATE": 1,
|
|
29
|
+
"CROSS": 1,
|
|
30
|
+
"CURRENT": 1,
|
|
31
|
+
"CURRENT_DATE": 1,
|
|
32
|
+
"CURRENT_TIME": 1,
|
|
33
|
+
"CURRENT_TIMESTAMP": 1,
|
|
34
|
+
"DATABASE": 1,
|
|
35
|
+
"DEFAULT": 1,
|
|
36
|
+
"DEFERRABLE": 1,
|
|
37
|
+
"DEFERRED": 1,
|
|
38
|
+
"DELETE": 1,
|
|
39
|
+
"DESC": 1,
|
|
40
|
+
"DETACH": 1,
|
|
41
|
+
"DISTINCT": 1,
|
|
42
|
+
"DO": 1,
|
|
43
|
+
"DROP": 1,
|
|
44
|
+
"EACH": 1,
|
|
45
|
+
"ELSE": 1,
|
|
46
|
+
"END": 1,
|
|
47
|
+
"ESCAPE": 1,
|
|
48
|
+
"EXCEPT": 1,
|
|
49
|
+
"EXCLUDE": 1,
|
|
50
|
+
"EXCLUSIVE": 1,
|
|
51
|
+
"EXISTS": 1,
|
|
52
|
+
"EXPLAIN": 1,
|
|
53
|
+
"FAIL": 1,
|
|
54
|
+
"FILTER": 1,
|
|
55
|
+
"FIRST": 1,
|
|
56
|
+
"FOLLOWING": 1,
|
|
57
|
+
"FOR": 1,
|
|
58
|
+
"FOREIGN": 1,
|
|
59
|
+
"FROM": 1,
|
|
60
|
+
"FULL": 1,
|
|
61
|
+
"GENERATED": 1,
|
|
62
|
+
"GLOB": 1,
|
|
63
|
+
"GROUP": 1,
|
|
64
|
+
"GROUPS": 1,
|
|
65
|
+
"HAVING": 1,
|
|
66
|
+
"IF": 1,
|
|
67
|
+
"IGNORE": 1,
|
|
68
|
+
"IMMEDIATE": 1,
|
|
69
|
+
"IN": 1,
|
|
70
|
+
"INDEX": 1,
|
|
71
|
+
"INDEXED": 1,
|
|
72
|
+
"INITIALLY": 1,
|
|
73
|
+
"INNER": 1,
|
|
74
|
+
"INSERT": 1,
|
|
75
|
+
"INSTEAD": 1,
|
|
76
|
+
"INTERSECT": 1,
|
|
77
|
+
"INTO": 1,
|
|
78
|
+
"IS": 1,
|
|
79
|
+
"ISNULL": 1,
|
|
80
|
+
"JOIN": 1,
|
|
81
|
+
"KEY": 1,
|
|
82
|
+
"LAST": 1,
|
|
83
|
+
"LEFT": 1,
|
|
84
|
+
"LIKE": 1,
|
|
85
|
+
"LIMIT": 1,
|
|
86
|
+
"MATCH": 1,
|
|
87
|
+
"MATERIALIZED": 1,
|
|
88
|
+
"NATURAL": 1,
|
|
89
|
+
"NO": 1,
|
|
90
|
+
"NOT": 1,
|
|
91
|
+
"NOTHING": 1,
|
|
92
|
+
"NOTNULL": 1,
|
|
93
|
+
"NULL": 1,
|
|
94
|
+
"NULLS": 1,
|
|
95
|
+
"OF": 1,
|
|
96
|
+
"OFFSET": 1,
|
|
97
|
+
"ON": 1,
|
|
98
|
+
"OR": 1,
|
|
99
|
+
"ORDER": 1,
|
|
100
|
+
"OTHERS": 1,
|
|
101
|
+
"OUTER": 1,
|
|
102
|
+
"OVER": 1,
|
|
103
|
+
"PARTITION": 1,
|
|
104
|
+
"PLAN": 1,
|
|
105
|
+
"PRAGMA": 1,
|
|
106
|
+
"PRECEDING": 1,
|
|
107
|
+
"PRIMARY": 1,
|
|
108
|
+
"QUERY": 1,
|
|
109
|
+
"RAISE": 1,
|
|
110
|
+
"RANGE": 1,
|
|
111
|
+
"RECURSIVE": 1,
|
|
112
|
+
"REFERENCES": 1,
|
|
113
|
+
"REGEXP": 1,
|
|
114
|
+
"REINDEX": 1,
|
|
115
|
+
"RELEASE": 1,
|
|
116
|
+
"RENAME": 1,
|
|
117
|
+
"REPLACE": 1,
|
|
118
|
+
"RESTRICT": 1,
|
|
119
|
+
"RETURNING": 1,
|
|
120
|
+
"RIGHT": 1,
|
|
121
|
+
"ROLLBACK": 1,
|
|
122
|
+
"ROW": 1,
|
|
123
|
+
"ROWS": 1,
|
|
124
|
+
"SAVEPOINT": 1,
|
|
125
|
+
"SELECT": 1,
|
|
126
|
+
"SET": 1,
|
|
127
|
+
"TABLE": 1,
|
|
128
|
+
"TEMP": 1,
|
|
129
|
+
"TEMPORARY": 1,
|
|
130
|
+
"THEN": 1,
|
|
131
|
+
"TIES": 1,
|
|
132
|
+
"TO": 1,
|
|
133
|
+
"TRANSACTION": 1,
|
|
134
|
+
"TRIGGER": 1,
|
|
135
|
+
"UNBOUNDED": 1,
|
|
136
|
+
"UNION": 1,
|
|
137
|
+
"UNIQUE": 1,
|
|
138
|
+
"UPDATE": 1,
|
|
139
|
+
"USING": 1,
|
|
140
|
+
"VACUUM": 1,
|
|
141
|
+
"VALUES": 1,
|
|
142
|
+
"VIEW": 1,
|
|
143
|
+
"VIRTUAL": 1,
|
|
144
|
+
"WHEN": 1,
|
|
145
|
+
"WHERE": 1,
|
|
146
|
+
"WINDOW": 1,
|
|
147
|
+
"WITH": 1,
|
|
148
|
+
"WITHOUT": 1
|
|
149
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
const { SQLService } = require('@cap-js/db-service')
|
|
2
|
+
const { Readable } = require('stream')
|
|
3
|
+
const cds = require('@sap/cds/lib')
|
|
4
|
+
const sqlite = require('better-sqlite3')
|
|
5
|
+
const $session = Symbol('dbc.session')
|
|
6
|
+
const convStrm = require('stream/consumers')
|
|
7
|
+
|
|
8
|
+
class SQLiteService extends SQLService {
|
|
9
|
+
get factory() {
|
|
10
|
+
return {
|
|
11
|
+
options: { max: 1, ...this.options.pool },
|
|
12
|
+
create: tenant => {
|
|
13
|
+
const database = this.url4(tenant)
|
|
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))
|
|
17
|
+
if (!dbc.memory) dbc.pragma('journal_mode = WAL')
|
|
18
|
+
return dbc
|
|
19
|
+
},
|
|
20
|
+
destroy: dbc => dbc.close(),
|
|
21
|
+
validate: dbc => dbc.open,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
url4(tenant) {
|
|
26
|
+
let { url, database: db = url } = this.options.credentials || this.options || {}
|
|
27
|
+
if (!db || db === ':memory:') return ':memory:'
|
|
28
|
+
if (tenant) db = db.replace(/\.(db|sqlite)$/, `-${tenant}.$1`)
|
|
29
|
+
return cds.utils.path.resolve(cds.root, db)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
set(variables) {
|
|
33
|
+
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
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
prepare(sql) {
|
|
46
|
+
try {
|
|
47
|
+
return this.dbc.prepare(sql)
|
|
48
|
+
} catch (e) {
|
|
49
|
+
e.message += ' in:\n' + (e.sql = sql)
|
|
50
|
+
throw e
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
exec(sql) {
|
|
55
|
+
return this.dbc.exec(sql)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
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
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
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
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Used for INSERT statements
|
|
86
|
+
static InputConverters = {
|
|
87
|
+
...super.InputConverters,
|
|
88
|
+
Date: e => `strftime('%Y-%m-%d',${e})`,
|
|
89
|
+
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)})`,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static OutputConverters = {
|
|
95
|
+
...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')->'$'`,
|
|
101
|
+
struct: expr => `${expr}->'$'`, // Association + Composition inherits from struct
|
|
102
|
+
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)})`,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Used for SQL function expressions
|
|
111
|
+
static Functions = { ...super.Functions }
|
|
112
|
+
|
|
113
|
+
// Used for CREATE TABLE statements
|
|
114
|
+
static TypeMap = {
|
|
115
|
+
...super.TypeMap,
|
|
116
|
+
Binary: e => `BINARY_BLOB(${e.length || 5000})`,
|
|
117
|
+
Date: () => 'DATE_TEXT',
|
|
118
|
+
Time: () => 'TIME_TEXT',
|
|
119
|
+
DateTime: () => 'TIMESTAMP_TEXT',
|
|
120
|
+
Timestamp: () => 'TIMESTAMP_TEXT',
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
static ReservedWords = { ...super.ReservedWords, ...require('./ReservedWords.json') }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// REALLY REVISIT: Here we are doing error handling which we probably never should have started.
|
|
127
|
+
// And worst of all, we handed out this as APIs without documenting it, so stakeholder tests rely
|
|
128
|
+
// on that? -> we urgently need to review these stakeholder tests.
|
|
129
|
+
// And we'd also need this to be implemented by each db service, and therefore documented, correct?
|
|
130
|
+
async onINSERT(req) {
|
|
131
|
+
try {
|
|
132
|
+
return await super.onINSERT(req)
|
|
133
|
+
} catch (err) {
|
|
134
|
+
throw _not_unique(err, 'ENTITY_ALREADY_EXISTS') || err
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async onUPDATE(req) {
|
|
139
|
+
try {
|
|
140
|
+
return await super.onUPDATE(req)
|
|
141
|
+
} catch (err) {
|
|
142
|
+
throw _not_unique(err, 'UNIQUE_CONSTRAINT_VIOLATION') || err
|
|
143
|
+
}
|
|
144
|
+
}
|
|
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
|
+
}
|
|
171
|
+
|
|
172
|
+
// function _not_null (err) {
|
|
173
|
+
// if (err.code === "SQLITE_CONSTRAINT_NOTNULL") return Object.assign ({
|
|
174
|
+
// code: 'MUST_NOT_BE_NULL',
|
|
175
|
+
// target: /\.(.*?)$/.exec(err.message)[1], // here we are even constructing OData responses, with .target
|
|
176
|
+
// message: 'Value is required',
|
|
177
|
+
// })
|
|
178
|
+
// }
|
|
179
|
+
|
|
180
|
+
function _not_unique(err, code) {
|
|
181
|
+
if (err.message.match(/unique constraint/i))
|
|
182
|
+
return Object.assign({
|
|
183
|
+
originalMessage: err.message, // FIXME: required because of next line
|
|
184
|
+
message: code, // FIXME: misusing message as code
|
|
185
|
+
code: 400, // FIXME: misusing code as (http) status
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
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
|
+
module.exports = SQLiteService
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/sqlite",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "CDS database service for SQLite",
|
|
5
5
|
"homepage": "https://cap.cloud.sap/",
|
|
6
6
|
"keywords": [
|
|
@@ -9,44 +9,40 @@
|
|
|
9
9
|
"SQLite"
|
|
10
10
|
],
|
|
11
11
|
"author": "SAP SE (https://www.sap.com)",
|
|
12
|
-
"license": "SEE LICENSE",
|
|
13
|
-
"homepage": "https://cap.cloud.sap/",
|
|
14
12
|
"main": "index.js",
|
|
15
13
|
"files": [
|
|
16
|
-
"cds.js",
|
|
17
|
-
"lib
|
|
18
|
-
"lib/db/sqlite",
|
|
19
|
-
"lib/db/sql",
|
|
20
|
-
"lib/ql",
|
|
21
|
-
"LICENSE",
|
|
14
|
+
"cds-plugin.js",
|
|
15
|
+
"lib",
|
|
22
16
|
"CHANGELOG.md"
|
|
23
17
|
],
|
|
24
18
|
"engines": {
|
|
25
|
-
"node": ">=
|
|
19
|
+
"node": ">=16",
|
|
26
20
|
"npm": ">=8"
|
|
27
21
|
},
|
|
28
22
|
"scripts": {
|
|
29
|
-
"
|
|
30
|
-
"pg:up": "docker-compose -f etc/pg-stack.yml up -d",
|
|
31
|
-
"prettier": "npx prettier --write .",
|
|
32
|
-
"test": "jest --silent",
|
|
33
|
-
"lint": "npx eslint . && npx prettier --check ."
|
|
23
|
+
"test": "jest --silent"
|
|
34
24
|
},
|
|
35
25
|
"dependencies": {
|
|
26
|
+
"@cap-js/db-service": "^1",
|
|
36
27
|
"better-sqlite3": "^8"
|
|
37
28
|
},
|
|
38
29
|
"peerDependencies": {
|
|
39
|
-
"@sap/cds": "
|
|
30
|
+
"@sap/cds": ">=7"
|
|
31
|
+
},
|
|
32
|
+
"cds": {
|
|
33
|
+
"requires": {
|
|
34
|
+
"kinds": {
|
|
35
|
+
"sql": {
|
|
36
|
+
"[development]": {
|
|
37
|
+
"kind": "sqlite"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"sqlite": {
|
|
41
|
+
"impl": "@cap-js/sqlite"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"db": "sql"
|
|
45
|
+
}
|
|
40
46
|
},
|
|
41
|
-
"
|
|
42
|
-
"@capire/sflight": "sap-samples/cap-sflight",
|
|
43
|
-
"@cap-js/sqlite": ".",
|
|
44
|
-
"axios": ">=1.3",
|
|
45
|
-
"chai": "^4.3.7",
|
|
46
|
-
"chai-as-promised": "^7.1.1",
|
|
47
|
-
"chai-subset": "^1.6.0",
|
|
48
|
-
"express": "^4",
|
|
49
|
-
"pg": "^8",
|
|
50
|
-
"jest": "^29"
|
|
51
|
-
}
|
|
47
|
+
"license": "SEE LICENSE"
|
|
52
48
|
}
|