@cap-js/sqlite 0.1.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/LICENSE +201 -0
- package/README.md +189 -0
- package/cds.js +39 -0
- package/index.js +1 -0
- package/lib/db/DatabaseService.js +101 -0
- package/lib/db/sql/InsertResults.js +87 -0
- package/lib/db/sql/SQLService.js +223 -0
- package/lib/db/sql/copy.js +17 -0
- package/lib/db/sql/cqn2sql.js +515 -0
- package/lib/db/sql/cqn4sql.js +1461 -0
- package/lib/db/sql/deep.js +233 -0
- package/lib/db/sql/func.js +146 -0
- package/lib/db/sql/structuralComparisonOps.js +16 -0
- package/lib/db/sql/utils.js +22 -0
- package/lib/db/sql/workarounds.js +73 -0
- package/lib/db/sqlite/ReservedWords.json +149 -0
- package/lib/db/sqlite/SQLiteService.js +170 -0
- package/lib/ql/cds.infer.js +786 -0
- package/lib/ql/join-tree.js +167 -0
- package/lib/ql/pseudos.js +23 -0
- package/package.json +52 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
const cds = require('../../../cds'), DEBUG = cds.debug('sql|db')
|
|
2
|
+
const DatabaseService = require('../DatabaseService')
|
|
3
|
+
const cqn4sql = require('./cqn4sql')
|
|
4
|
+
const { target_name4 } = require('./utils')
|
|
5
|
+
const { PassThrough, pipeline } = require('stream')
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SQLService extends DatabaseService {
|
|
9
|
+
|
|
10
|
+
init() {
|
|
11
|
+
this.on([ 'INSERT', 'UPSERT', 'UPDATE', 'DELETE' ], require('./workarounds').input) // REVISIT should be replaced by correct input processing eventually
|
|
12
|
+
this.on([ 'INSERT', 'UPSERT', 'UPDATE', 'DELETE' ], require('./deep').onDeep)
|
|
13
|
+
this.on([ 'SELECT' ], this.onSELECT)
|
|
14
|
+
this.on([ 'INSERT', 'UPSERT' ], this.onINSERT)
|
|
15
|
+
this.on([ 'UPDATE' ], this.onUPDATE)
|
|
16
|
+
this.on([ 'DELETE', 'CREATE ENTITY', 'DROP ENTITY' ], this.onSIMPLE)
|
|
17
|
+
this.on([ 'BEGIN', 'COMMIT', 'ROLLBACK' ], this.onEVENT)
|
|
18
|
+
this.on([ '*' ], this.onPlainSQL)
|
|
19
|
+
return super.init()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Handler for SELECT */
|
|
23
|
+
async onSELECT ({ query, data }) {
|
|
24
|
+
// REVISIT: disable this for queries like (SELECT 1)
|
|
25
|
+
// Will return multiple rows with objects inside
|
|
26
|
+
// REVISIT: streaming: if we need custom app and db handlers with app stream and cds.stream
|
|
27
|
+
if (query._streaming) return this.onStream(query) // TODO: implemented on HANA
|
|
28
|
+
query.SELECT.expand = 'root'
|
|
29
|
+
const { sql, values, cqn } = this.cqn2sql (query, data)
|
|
30
|
+
let ps = await this.prepare(sql)
|
|
31
|
+
let rows = await ps.all(values)
|
|
32
|
+
if (rows.length)
|
|
33
|
+
if (cqn.SELECT.expand) rows = rows.map(r => typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r)
|
|
34
|
+
if (cqn.SELECT.count) rows.$count = await this.count(query, rows)
|
|
35
|
+
return cqn.SELECT.one || query.SELECT.from.ref?.[0].cardinality?.max === 1 ? rows[0] || null : rows
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Handler for INSERT & UPSERT, which support bulk queries */
|
|
39
|
+
async onINSERT ({ query, data }) {
|
|
40
|
+
const { sql, entries, cqn } = this.cqn2sql (query, data)
|
|
41
|
+
if(!sql) return // Do nothing when there is nothing to be done
|
|
42
|
+
const ps = await this.prepare(sql)
|
|
43
|
+
const results = entries ? await Promise.all(entries.map(e => ps.run(e))) : await ps.run()
|
|
44
|
+
return new this.class.InsertResults (cqn, results)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Handler for UPDATE */
|
|
48
|
+
async onUPDATE (req) {
|
|
49
|
+
return this.onSIMPLE(req)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Handler for CREATE, DROP, UPDATE, DELETE, with simple CQN */
|
|
53
|
+
async onSIMPLE ({ query, data }) {
|
|
54
|
+
const { sql, values } = this.cqn2sql (query, data)
|
|
55
|
+
let ps = await this.prepare(sql)
|
|
56
|
+
return (await ps.run(values)).changes
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Handler for BEGIN, COMMIT, ROLLBACK, which don't have any CQN */
|
|
60
|
+
async onEVENT ({ event }) {
|
|
61
|
+
DEBUG?.(event) // in the other cases above DEBUG happens in cqn2sql
|
|
62
|
+
return await this.exec(event)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Handler for SQL statements which don't have any CQN */
|
|
66
|
+
async onPlainSQL ({ query, data }, next) {
|
|
67
|
+
if (typeof query === 'string') {
|
|
68
|
+
DEBUG?.(query)
|
|
69
|
+
const ps = await this.prepare(query)
|
|
70
|
+
const exec = this.hasResults(query) ? d => ps.all(d) : d => ps.run(d)
|
|
71
|
+
if (Array.isArray(data) && typeof data[0] === 'object')
|
|
72
|
+
return await Promise.all(data.map(exec))
|
|
73
|
+
else return exec(data)
|
|
74
|
+
}
|
|
75
|
+
else return next()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Override in subclasses to detect more statements to be called with ps.all() */
|
|
79
|
+
hasResults(sql) {
|
|
80
|
+
return /^(SELECT|WITH|CALL|PRAGMA table_info)/.test(sql)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
/** Derives and executes a query to fill in `$count` for given query */
|
|
85
|
+
async count (query, ret) {
|
|
86
|
+
if (ret) {
|
|
87
|
+
const { one, limit:_ } = query.SELECT, n = ret.length
|
|
88
|
+
const [ max, offset=0 ] = one ? [1] : _ ? [ _.rows?.val, _.offset?.val ] : []
|
|
89
|
+
if (max === undefined || n < max && (n || !offset)) return n + offset
|
|
90
|
+
}
|
|
91
|
+
const cq = cds.ql.clone (query, {
|
|
92
|
+
columns: [{func:'count'}],
|
|
93
|
+
localized: false,
|
|
94
|
+
expand: false,
|
|
95
|
+
limit: 0,
|
|
96
|
+
orderBy: 0
|
|
97
|
+
})
|
|
98
|
+
const { sql, values } = this.cqn2sql(cq)
|
|
99
|
+
const ps = await this.prepare(sql)
|
|
100
|
+
const { count } = await ps.get(values)
|
|
101
|
+
return count
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Streaming
|
|
106
|
+
* Returns either a readable stream for sync calls or a readable stream promise for async calls
|
|
107
|
+
*/
|
|
108
|
+
stream(q) {
|
|
109
|
+
return typeof q === 'object'
|
|
110
|
+
// aynchronous API: cds.stream(query)
|
|
111
|
+
? this.run(Object.assign(q, { _streaming: true }))
|
|
112
|
+
// synchronous API: cds.stream('column').from(entity).where(...)
|
|
113
|
+
: new StreamCQN(q, this)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
static InsertResults = require('./InsertResults')
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Helper class implementing {@link SQLService#cqn2sql}.
|
|
121
|
+
* Subclasses commonly override this.
|
|
122
|
+
*/
|
|
123
|
+
static CQN2SQL = require('./cqn2sql').class
|
|
124
|
+
constructor() {
|
|
125
|
+
super(...arguments)
|
|
126
|
+
this.class = new.target // for IntelliSense
|
|
127
|
+
}
|
|
128
|
+
cqn2sql(q,values) {
|
|
129
|
+
const cqn = this.cqn4sql(q)
|
|
130
|
+
return (new this.class.CQN2SQL) .render (cqn, values)
|
|
131
|
+
}
|
|
132
|
+
cqn4sql(q) {
|
|
133
|
+
if (!this.model?.definitions[target_name4(q)]) return _unquirked(q)
|
|
134
|
+
return cqn4sql (q, this.model)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Returns a Promise which resolves to a prepared statement object with
|
|
139
|
+
* `{run,get,all}` signature as specified in {@link PreparedStatement}.
|
|
140
|
+
* @returns {PreparedStatement}
|
|
141
|
+
*/
|
|
142
|
+
// eslint-disable-next-line no-unused-vars
|
|
143
|
+
async prepare (sql) { throw '2b overridden by subclass' }
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Used to execute simple SQL statement like BEGIN, COMMIT, ROLLBACK
|
|
147
|
+
*/
|
|
148
|
+
// eslint-disable-next-line no-unused-vars
|
|
149
|
+
async exec (sql) { throw '2b overridden by subclass' }
|
|
150
|
+
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
/** Interface of prepared statement objects as returned by {@link SQLService#prepare} */
|
|
155
|
+
class PreparedStatement { // eslint-disable-line no-unused-vars
|
|
156
|
+
/**
|
|
157
|
+
* Executes a prepared DML query, i.e., INSERT, UPDATE, DELETE, CREATE, DROP
|
|
158
|
+
* @param {[]|{}} binding_params
|
|
159
|
+
*/
|
|
160
|
+
async run (binding_params) {} // eslint-disable-line no-unused-vars
|
|
161
|
+
/**
|
|
162
|
+
* Executes a prepared SELECT query and returns a single/first row only
|
|
163
|
+
* @param {[]|{}} binding_params
|
|
164
|
+
*/
|
|
165
|
+
async get (binding_params) { return {} } // eslint-disable-line no-unused-vars
|
|
166
|
+
/**
|
|
167
|
+
* Executes a prepared SELECT query and returns an array of all rows
|
|
168
|
+
* @param {[]|{}} binding_params
|
|
169
|
+
*/
|
|
170
|
+
async all (binding_params) { return [{}] } // eslint-disable-line no-unused-vars
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Class that builds and runs stream CQN
|
|
175
|
+
*/
|
|
176
|
+
class StreamCQN {
|
|
177
|
+
constructor (column, srv) {
|
|
178
|
+
this.column = column
|
|
179
|
+
this.srv = srv
|
|
180
|
+
this.result = new PassThrough()
|
|
181
|
+
}
|
|
182
|
+
/** synchronous streaming API: returns readable stream or class instance for chaining */
|
|
183
|
+
from (...args) {
|
|
184
|
+
this.sq = SELECT.from(...args)
|
|
185
|
+
this.sq._streaming = true
|
|
186
|
+
if (this.column) this.sq.columns([this.column])
|
|
187
|
+
const ref = this.sq.SELECT.from.ref
|
|
188
|
+
if (!ref?.[ref.length - 1].where) return this
|
|
189
|
+
this._runStream()
|
|
190
|
+
return this.result
|
|
191
|
+
}
|
|
192
|
+
/** synchronous streaming API: returns readable stream */
|
|
193
|
+
where (...args) {
|
|
194
|
+
this.sq.where(...args)
|
|
195
|
+
this._runStream()
|
|
196
|
+
return this.result
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async _runStream() {
|
|
200
|
+
try {
|
|
201
|
+
const stream = await this.srv.run(this.sq)
|
|
202
|
+
// In case of streaming error while streaming from stream to this.result
|
|
203
|
+
// the error is emitted to both streams. After this the output stream this.result is destroyed.
|
|
204
|
+
// No explicit closing of this.result is needed.
|
|
205
|
+
// In (theoretical) case if for some error this.result is not destroyed the code like below can be used
|
|
206
|
+
// as callback: err => err && this.result.push(null)
|
|
207
|
+
stream ? pipeline(stream, this.result, () => {}) : this.result.push(null)
|
|
208
|
+
}
|
|
209
|
+
catch (err) { this.result.emit('error', err); this.result.push(null) }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const _unquirked = q => {
|
|
214
|
+
if (typeof q.INSERT?.into === 'string') q.INSERT.into = {ref:[q.INSERT.into]}
|
|
215
|
+
if (typeof q.UPSERT?.into === 'string') q.UPSERT.into = {ref:[q.UPSERT.into]}
|
|
216
|
+
if (typeof q.UPDATE?.entity === 'string') q.UPDATE.entity = {ref:[q.UPDATE.entity]}
|
|
217
|
+
if (typeof q.DELETE?.from === 'string') q.DELETE.from = {ref:[q.DELETE.from]}
|
|
218
|
+
if (typeof q.CREATE?.entity === 'string') q.CREATE.entity = {ref:[q.CREATE.entity]}
|
|
219
|
+
if (typeof q.DROP?.entity === 'string') q.DROP.entity = {ref:[q.DROP.entity]}
|
|
220
|
+
return q
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = SQLService
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module.exports.copy = function (obj) {
|
|
2
|
+
const walk = function (par, prop) {
|
|
3
|
+
const val = prop ? par[prop] : par
|
|
4
|
+
|
|
5
|
+
// If value is native return
|
|
6
|
+
if (typeof val !== 'object' || val == null || val instanceof RegExp || val instanceof Date || val instanceof Buffer)
|
|
7
|
+
return val
|
|
8
|
+
|
|
9
|
+
const ret = Array.isArray(val) ? [] : {}
|
|
10
|
+
Object.keys(val).forEach(k => {
|
|
11
|
+
ret[k] = walk(val, k)
|
|
12
|
+
})
|
|
13
|
+
return ret
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return walk(obj)
|
|
17
|
+
}
|