@cap-js/postgres 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 +17 -0
- package/lib/PostgresService.js +327 -57
- package/lib/ReservedWords.json +99 -1
- package/lib/func.js +7 -0
- package/lib/session.json +7 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@
|
|
|
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
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Bump minimum required version of `@cap-js/db-service`
|
|
12
|
+
|
|
13
|
+
## Version 1.2.0 - 2023-09-06
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Reduced the usage of `is not distinct [not] from`. #157
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- [Reserved words](https://www.postgresql.org/docs/current/sql-keywords-appendix.html) are now used to automatically escape reserved words which are used as identifier. #178
|
|
22
|
+
- Remove column count limitation. #150
|
|
23
|
+
|
|
7
24
|
## Version 1.1.0 - 2023-08-01
|
|
8
25
|
|
|
9
26
|
### Added
|
package/lib/PostgresService.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
const { SQLService } = require('@cap-js/db-service')
|
|
2
|
-
const { Client } = require('pg')
|
|
2
|
+
const { Client, Query } = require('pg')
|
|
3
3
|
const cds = require('@sap/cds/lib')
|
|
4
4
|
const crypto = require('crypto')
|
|
5
|
+
const { Writable, Readable } = require('stream')
|
|
6
|
+
const sessionVariableMap = require('./session.json')
|
|
5
7
|
|
|
6
8
|
class PostgresService extends SQLService {
|
|
7
9
|
init() {
|
|
@@ -61,18 +63,18 @@ class PostgresService extends SQLService {
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
async set(variables) {
|
|
64
|
-
// REVISIT: remove when all environment variables are aligned
|
|
65
66
|
// RESTRICTIONS: 'Custom parameter names must be two or more simple identifiers separated by dots.'
|
|
66
|
-
const nameMap = {
|
|
67
|
-
'$user.id': 'cap.applicationuser',
|
|
68
|
-
'$user.locale': 'cap.locale',
|
|
69
|
-
'$valid.from': 'cap.valid_from',
|
|
70
|
-
'$valid.to': 'cap.valid_to',
|
|
71
|
-
}
|
|
72
|
-
|
|
73
67
|
const env = {}
|
|
68
|
+
|
|
69
|
+
// Check all properties on the variables object
|
|
74
70
|
for (let name in variables) {
|
|
75
|
-
env[
|
|
71
|
+
env[sessionVariableMap[name] || name] = variables[name]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Explicitly check for the default session variable properties
|
|
75
|
+
// As they are getters and not own properties of the object
|
|
76
|
+
for (let name in sessionVariableMap) {
|
|
77
|
+
if (variables[name]) env[sessionVariableMap[name]] = variables[name]
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
return Promise.all([
|
|
@@ -106,14 +108,14 @@ class PostgresService extends SQLService {
|
|
|
106
108
|
* The group by is done by the key column to make sure that only one collation per key is returned
|
|
107
109
|
*/
|
|
108
110
|
const cSQL = `
|
|
109
|
-
SELECT
|
|
111
|
+
SELECT
|
|
110
112
|
SUBSTRING(collname, 1, 2) AS K,
|
|
111
|
-
MIN(collname) AS V
|
|
112
|
-
FROM
|
|
113
|
-
pg_collation
|
|
114
|
-
WHERE
|
|
115
|
-
collprovider = 'c' AND
|
|
116
|
-
collname LIKE '__>___' ESCAPE '>'
|
|
113
|
+
MIN(collname) AS V
|
|
114
|
+
FROM
|
|
115
|
+
pg_collation
|
|
116
|
+
WHERE
|
|
117
|
+
collprovider = 'c' AND
|
|
118
|
+
collname LIKE '__>___' ESCAPE '>'
|
|
117
119
|
GROUP BY k
|
|
118
120
|
`
|
|
119
121
|
|
|
@@ -134,6 +136,7 @@ GROUP BY k
|
|
|
134
136
|
|
|
135
137
|
prepare(sql) {
|
|
136
138
|
const query = {
|
|
139
|
+
_streams: 0,
|
|
137
140
|
text: sql,
|
|
138
141
|
// Track queries name for postgres referencing prepare statements
|
|
139
142
|
// sha1 as it needs to be less then 63 characters
|
|
@@ -142,7 +145,9 @@ GROUP BY k
|
|
|
142
145
|
return {
|
|
143
146
|
run: async values => {
|
|
144
147
|
// REVISIT: SQLService provides empty values as {} for plain SQL statements - PostgreSQL driver expects array or nothing - see issue #78
|
|
145
|
-
|
|
148
|
+
let newQuery = this._prepareStreams(query, values)
|
|
149
|
+
if (typeof newQuery.then === 'function') newQuery = await newQuery
|
|
150
|
+
const result = await this.dbc.query(newQuery)
|
|
146
151
|
return { changes: result.rowCount }
|
|
147
152
|
},
|
|
148
153
|
get: async values => {
|
|
@@ -159,6 +164,14 @@ GROUP BY k
|
|
|
159
164
|
throw Object.assign(e, { sql: sql + '\n' + new Array(e.position).fill(' ').join('') + '^' })
|
|
160
165
|
}
|
|
161
166
|
},
|
|
167
|
+
stream: async (values, one) => {
|
|
168
|
+
try {
|
|
169
|
+
const streamQuery = new QueryStream({ ...query, values: this._getValues(values) }, one)
|
|
170
|
+
return await this.dbc.query(streamQuery)
|
|
171
|
+
} catch (e) {
|
|
172
|
+
throw Object.assign(e, { sql: sql + '\n' + new Array(e.position).fill(' ').join('') + '^' })
|
|
173
|
+
}
|
|
174
|
+
},
|
|
162
175
|
}
|
|
163
176
|
}
|
|
164
177
|
|
|
@@ -170,6 +183,60 @@ GROUP BY k
|
|
|
170
183
|
return values
|
|
171
184
|
}
|
|
172
185
|
|
|
186
|
+
_prepareStreams(query, values) {
|
|
187
|
+
values = this._getValues(values)
|
|
188
|
+
if (!values) return query
|
|
189
|
+
|
|
190
|
+
const streams = []
|
|
191
|
+
const newValues = []
|
|
192
|
+
let sql = query.text
|
|
193
|
+
if (Array.isArray(values)) {
|
|
194
|
+
values.forEach((value, i) => {
|
|
195
|
+
if (value instanceof Readable) {
|
|
196
|
+
const streamID = query._streams++
|
|
197
|
+
const isBinary = value.type === 'binary'
|
|
198
|
+
const paramStream = new ParameterStream(query.name, streamID)
|
|
199
|
+
if (isBinary) value.setEncoding('base64')
|
|
200
|
+
value.pipe(paramStream)
|
|
201
|
+
value.on('error', err => paramStream.emit('error', err))
|
|
202
|
+
streams[i] = paramStream
|
|
203
|
+
newValues[i] = streamID
|
|
204
|
+
sql = sql.replace(
|
|
205
|
+
new RegExp(`\\$${i + 1}`, 'g'),
|
|
206
|
+
// Don't ask about the dollar signs
|
|
207
|
+
`(SELECT ${isBinary ? `DECODE(PARAM,'base64')` : 'PARAM'} FROM "$$$$PARAMETER_BUFFER$$$$" WHERE NAME='${
|
|
208
|
+
query.name
|
|
209
|
+
}' AND ID=$${i + 1})`,
|
|
210
|
+
)
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
newValues[i] = value
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (streams.length > 0) {
|
|
218
|
+
return (async () => {
|
|
219
|
+
const newQuery = {
|
|
220
|
+
text: sql,
|
|
221
|
+
// Even with the changed SQL it might be common to call this statement with the same parameters as streams
|
|
222
|
+
// As the streams are selected with their ID as prepared statement parameter, the sql is the same
|
|
223
|
+
name: crypto.createHash('sha1').update(sql).digest('hex'),
|
|
224
|
+
values: newValues,
|
|
225
|
+
}
|
|
226
|
+
await this.dbc.query({
|
|
227
|
+
text: 'CREATE TEMP TABLE IF NOT EXISTS "$$PARAMETER_BUFFER$$" (PARAM TEXT, NAME TEXT, ID INT) ON COMMIT DROP',
|
|
228
|
+
})
|
|
229
|
+
const proms = []
|
|
230
|
+
for (const stream of streams) {
|
|
231
|
+
proms.push(this.dbc.query(stream))
|
|
232
|
+
}
|
|
233
|
+
await Promise.all(proms)
|
|
234
|
+
return newQuery
|
|
235
|
+
})()
|
|
236
|
+
}
|
|
237
|
+
return { ...query, values }
|
|
238
|
+
}
|
|
239
|
+
|
|
173
240
|
async exec(sql) {
|
|
174
241
|
return this.dbc.query(sql)
|
|
175
242
|
}
|
|
@@ -249,18 +316,9 @@ GROUP BY k
|
|
|
249
316
|
return super.from(from)
|
|
250
317
|
}
|
|
251
318
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (!SELECT.columns) return '*'
|
|
256
|
-
const unique = {}
|
|
257
|
-
return SELECT.columns
|
|
258
|
-
.map(x => `${this.column_expr(x)} as ${this.quote(this.column_name(x))}`)
|
|
259
|
-
.filter(x => {
|
|
260
|
-
if (unique[x]) return false
|
|
261
|
-
unique[x] = true
|
|
262
|
-
return true
|
|
263
|
-
})
|
|
319
|
+
column_alias4(x, q) {
|
|
320
|
+
if (!x.as && 'val' in x) return String(x.val)
|
|
321
|
+
return super.column_alias4(x, q)
|
|
264
322
|
}
|
|
265
323
|
|
|
266
324
|
SELECT_expand({ SELECT }, sql) {
|
|
@@ -268,19 +326,25 @@ GROUP BY k
|
|
|
268
326
|
const queryAlias = this.quote(SELECT.from?.as || (SELECT.expand === 'root' && 'root'))
|
|
269
327
|
const cols = SELECT.columns.map(x => {
|
|
270
328
|
const name = this.column_name(x)
|
|
271
|
-
|
|
329
|
+
const outputConverter = this.output_converter4(x.element, `${queryAlias}.${this.quote(name)}`)
|
|
330
|
+
let col = `${outputConverter} as ${this.doubleQuote(name)}`
|
|
272
331
|
|
|
273
332
|
if (x.SELECT?.count) {
|
|
274
333
|
// Return both the sub select and the count for @odata.count
|
|
275
334
|
const qc = cds.ql.clone(x, { columns: [{ func: 'count' }], one: 1, limit: 0, orderBy: 0 })
|
|
276
|
-
col +=
|
|
335
|
+
col += `,${this.expr(qc)} as ${this.doubleQuote(`${name}@odata.count`)}`
|
|
277
336
|
}
|
|
278
337
|
return col
|
|
279
338
|
})
|
|
280
|
-
|
|
339
|
+
// REVISIT: Remove SELECT ${cols} by adjusting SELECT_columns
|
|
340
|
+
let obj = `row_to_json(${queryAlias}.*)`
|
|
281
341
|
return `SELECT ${
|
|
282
342
|
SELECT.one || SELECT.expand === 'root' ? obj : `coalesce(json_agg(${obj}),'[]'::json)`
|
|
283
|
-
} as _json_ FROM (${sql}) as ${queryAlias}`
|
|
343
|
+
} as _json_ FROM (SELECT ${cols} FROM (${sql}) as ${queryAlias}) as ${queryAlias}`
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
doubleQuote(name) {
|
|
347
|
+
return `"${name.replace(/"/g, '""')}"`
|
|
284
348
|
}
|
|
285
349
|
|
|
286
350
|
INSERT(q, isUpsert = false) {
|
|
@@ -291,20 +355,24 @@ GROUP BY k
|
|
|
291
355
|
// Adjusts json path expressions to be postgres specific
|
|
292
356
|
.replace(/->>'\$(?:(?:\."(.*?)")|(?:\[(\d*)\]))'/g, (a, b, c) => (b ? `->>'${b}'` : `->>${c}`))
|
|
293
357
|
// Adjusts json function to be postgres specific
|
|
294
|
-
.replace('json_each(?)', 'json_array_elements($1)')
|
|
358
|
+
.replace('json_each(?)', 'json_array_elements($1::JSON)')
|
|
295
359
|
.replace(/json_type\((\w+),'\$\."(\w+)"'\)/g, (_a, b, c) => `json_typeof(${b}->'${c}')`))
|
|
296
360
|
}
|
|
297
361
|
|
|
362
|
+
param({ ref }) {
|
|
363
|
+
this._paramCount = this._paramCount || 1
|
|
364
|
+
if (ref.length > 1) throw cds.error`Unsupported nested ref parameter: ${ref}`
|
|
365
|
+
return ref[0] === '?' ? `$${this._paramCount++}` : `:${ref}`
|
|
366
|
+
}
|
|
367
|
+
|
|
298
368
|
val(val) {
|
|
299
369
|
const ret = super.val(val)
|
|
300
370
|
return ret === '?' ? `$${this.values.length}` : ret
|
|
301
371
|
}
|
|
302
372
|
|
|
303
|
-
operator(x) {
|
|
373
|
+
operator(x, i, xpr) {
|
|
304
374
|
if (x === 'regexp') return '~'
|
|
305
|
-
|
|
306
|
-
if (x === '!=') return 'is distinct from'
|
|
307
|
-
else return x
|
|
375
|
+
else return super.operator(x, i, xpr)
|
|
308
376
|
}
|
|
309
377
|
|
|
310
378
|
defaultValue(defaultValue = this.context.timestamp.toISOString()) {
|
|
@@ -346,14 +414,14 @@ GROUP BY k
|
|
|
346
414
|
// REVISIT: Remove that with upcomming fixes in cds.linked
|
|
347
415
|
Double: (e, t) => `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
|
|
348
416
|
DecimalFloat: (e, t) => `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
|
|
349
|
-
Binary: e => `
|
|
350
|
-
LargeBinary: e => `
|
|
417
|
+
Binary: e => `DECODE(${e},'base64')`,
|
|
418
|
+
LargeBinary: e => `DECODE(${e},'base64')`,
|
|
351
419
|
}
|
|
352
420
|
|
|
353
421
|
static OutputConverters = {
|
|
354
422
|
...super.OutputConverters,
|
|
355
|
-
Binary: e => e,
|
|
356
|
-
LargeBinary: e => e,
|
|
423
|
+
Binary: e => `ENCODE(${e},'base64')`,
|
|
424
|
+
LargeBinary: e => `ENCODE(${e},'base64')`,
|
|
357
425
|
Date: e => `to_char(${e}, 'YYYY-MM-DD')`,
|
|
358
426
|
Time: e => `to_char(${e}, 'HH24:MI:SS')`,
|
|
359
427
|
DateTime: e => `to_char(${e}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
|
|
@@ -403,7 +471,7 @@ GROUP BY k
|
|
|
403
471
|
}
|
|
404
472
|
}
|
|
405
473
|
|
|
406
|
-
async tenant({ database, tenant }) {
|
|
474
|
+
async tenant({ database, tenant }, clean = false) {
|
|
407
475
|
const creds = {
|
|
408
476
|
database: database,
|
|
409
477
|
usergroup: `${database}_USERS`,
|
|
@@ -413,18 +481,20 @@ GROUP BY k
|
|
|
413
481
|
creds.password = creds.user
|
|
414
482
|
|
|
415
483
|
try {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
await tx
|
|
427
|
-
|
|
484
|
+
if (!clean) {
|
|
485
|
+
await this.tx(async tx => {
|
|
486
|
+
// await tx.run(`DROP USER IF EXISTS "${creds.user}"`)
|
|
487
|
+
await tx
|
|
488
|
+
.run(`CREATE USER "${creds.user}" IN GROUP "${creds.usergroup}" PASSWORD '${creds.password}'`)
|
|
489
|
+
.catch(e => {
|
|
490
|
+
if (e.code === '42710') return
|
|
491
|
+
throw e
|
|
492
|
+
})
|
|
493
|
+
})
|
|
494
|
+
await this.tx(async tx => {
|
|
495
|
+
await tx.run(`GRANT CREATE, CONNECT ON DATABASE "${creds.database}" TO "${creds.user}";`)
|
|
496
|
+
})
|
|
497
|
+
}
|
|
428
498
|
|
|
429
499
|
// Update credentials to new Schema owner
|
|
430
500
|
await this.disconnect()
|
|
@@ -433,7 +503,7 @@ GROUP BY k
|
|
|
433
503
|
// Create new schema using schema owner
|
|
434
504
|
await this.tx(async tx => {
|
|
435
505
|
await tx.run(`DROP SCHEMA IF EXISTS "${creds.schema}" CASCADE`)
|
|
436
|
-
await tx.run(`CREATE SCHEMA "${creds.schema}" AUTHORIZATION "${creds.user}"`).catch(() => {})
|
|
506
|
+
if (!clean) await tx.run(`CREATE SCHEMA "${creds.schema}" AUTHORIZATION "${creds.user}"`).catch(() => {})
|
|
437
507
|
})
|
|
438
508
|
} finally {
|
|
439
509
|
await this.disconnect()
|
|
@@ -441,4 +511,204 @@ GROUP BY k
|
|
|
441
511
|
}
|
|
442
512
|
}
|
|
443
513
|
|
|
514
|
+
class QueryStream extends Query {
|
|
515
|
+
constructor(config, one) {
|
|
516
|
+
// REVISIT: currently when setting the row chunk size
|
|
517
|
+
// it results in an inconsistent connection state
|
|
518
|
+
// if (!one) config.rows = 1000
|
|
519
|
+
super(config)
|
|
520
|
+
|
|
521
|
+
this._one = one || config.one
|
|
522
|
+
|
|
523
|
+
this.stream = new Readable({
|
|
524
|
+
read: this.rows
|
|
525
|
+
? () => {
|
|
526
|
+
this.stream.pause()
|
|
527
|
+
// Request more rows
|
|
528
|
+
this.connection.execute({
|
|
529
|
+
portal: this.portal,
|
|
530
|
+
rows: this.rows,
|
|
531
|
+
})
|
|
532
|
+
this.connection.flush()
|
|
533
|
+
}
|
|
534
|
+
: () => {},
|
|
535
|
+
})
|
|
536
|
+
this.push = this.stream.push.bind(this.stream)
|
|
537
|
+
|
|
538
|
+
this._prom = new Promise((resolve, reject) => {
|
|
539
|
+
this.once('error', reject)
|
|
540
|
+
this.once('end', () => {
|
|
541
|
+
if (!this._one) this.push(this.constructor.close)
|
|
542
|
+
this.push(null)
|
|
543
|
+
if (this.stream.isPaused()) this.stream.resume()
|
|
544
|
+
resolve(null)
|
|
545
|
+
})
|
|
546
|
+
this.once('row', row => {
|
|
547
|
+
if (row == null) return resolve(null)
|
|
548
|
+
resolve(this.stream)
|
|
549
|
+
})
|
|
550
|
+
})
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
static sep = Buffer.from(',')
|
|
554
|
+
static open = Buffer.from('[')
|
|
555
|
+
static close = Buffer.from(']')
|
|
556
|
+
|
|
557
|
+
// Trigger query initialization
|
|
558
|
+
_getRows(connection) {
|
|
559
|
+
this.connection = connection
|
|
560
|
+
connection.execute({
|
|
561
|
+
portal: this.portal,
|
|
562
|
+
rows: this.rows ? 1 : undefined,
|
|
563
|
+
})
|
|
564
|
+
if (this.rows) {
|
|
565
|
+
connection.flush()
|
|
566
|
+
} else {
|
|
567
|
+
connection.sync()
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Delay requesting more rows until next is called
|
|
572
|
+
handlePortalSuspended() {
|
|
573
|
+
this.stream.resume()
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Provides metadata information from the database
|
|
577
|
+
handleRowDescription(msg) {
|
|
578
|
+
// Use default parser for binary results
|
|
579
|
+
if (msg.fields.length === 1 && msg.fields[0].dataTypeID === 17) {
|
|
580
|
+
this.handleDataRow = this.handleBinaryRow
|
|
581
|
+
} else {
|
|
582
|
+
this.handleDataRow = msg => {
|
|
583
|
+
const val = msg.fields[0]
|
|
584
|
+
if (!this._one && val !== null) this.push(this.constructor.open)
|
|
585
|
+
this.emit('row', val)
|
|
586
|
+
this.push(val)
|
|
587
|
+
delete this.handleDataRow
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return super.handleRowDescription(msg)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Called when a new row is received
|
|
594
|
+
handleDataRow(msg) {
|
|
595
|
+
this.push(this.constructor.sep)
|
|
596
|
+
this.push(msg.fields[0])
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Called when a new binary row is received
|
|
600
|
+
handleBinaryRow(msg) {
|
|
601
|
+
const val = msg.fields[0] === null ? null : this._result._parsers[0](msg.fields[0])
|
|
602
|
+
this.push(val)
|
|
603
|
+
this.emit('row', val)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
then(resolve, reject) {
|
|
607
|
+
return this._prom.then(resolve, reject)
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
class ParameterStream extends Writable {
|
|
612
|
+
constructor(queryName, id) {
|
|
613
|
+
super({})
|
|
614
|
+
this.queryName = queryName
|
|
615
|
+
this.id = id
|
|
616
|
+
this.text = `COPY "$$PARAMETER_BUFFER$$"(param,name,id) FROM STDIN DELIMITER ',' QUOTE '${this.constructor.sep}' CSV`
|
|
617
|
+
this.lengthBuffer = Buffer.from([0x64, 0, 0, 0, 0])
|
|
618
|
+
|
|
619
|
+
// Flush quote character before input stream
|
|
620
|
+
this.flushChunk = chunk => {
|
|
621
|
+
delete this.flushChunk
|
|
622
|
+
|
|
623
|
+
this.lengthBuffer.writeUInt32BE(chunk.length + 5, 1)
|
|
624
|
+
this.connection.stream.write(this.lengthBuffer)
|
|
625
|
+
this.connection.stream.write(Buffer.from(this.constructor.sep))
|
|
626
|
+
return this.connection.stream.write(chunk)
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
static sep = String.fromCharCode(31) // Separator One
|
|
631
|
+
static done = Buffer.from([0x63, 0, 0, 0, 4])
|
|
632
|
+
|
|
633
|
+
then(resolve, reject) {
|
|
634
|
+
this.on('error', reject)
|
|
635
|
+
this.on('finish', resolve)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Indicates that the query was started by the connection
|
|
640
|
+
* @param {Object} connection
|
|
641
|
+
*/
|
|
642
|
+
submit(connection) {
|
|
643
|
+
this.connection = connection
|
|
644
|
+
// Initialize query to be executed
|
|
645
|
+
connection.query(this.text)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Used by the client to handle timeouts
|
|
649
|
+
callback() {}
|
|
650
|
+
|
|
651
|
+
_write(chunk, enc, cb) {
|
|
652
|
+
return this.flush(chunk, cb)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
_construct(cb) {
|
|
656
|
+
this.handleCopyInResponse = () => cb()
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
_destroy(err, cb) {
|
|
660
|
+
this.handleError = () => {
|
|
661
|
+
this.callback()
|
|
662
|
+
this.connection = null
|
|
663
|
+
cb(err)
|
|
664
|
+
}
|
|
665
|
+
this.connection.sendCopyFail(err ? err.message : 'ParameterStream early destroy')
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
_final(cb) {
|
|
669
|
+
const sep = this.constructor.sep
|
|
670
|
+
this.flush(Buffer.from(`${sep},${this.queryName},${this.id}`), err => {
|
|
671
|
+
if (err) return cb(err)
|
|
672
|
+
this._finish = () => {
|
|
673
|
+
this.emit('finish')
|
|
674
|
+
cb()
|
|
675
|
+
}
|
|
676
|
+
this._destroy = (err, cb) => cb(err)
|
|
677
|
+
this.connection.stream.write(this.constructor.done)
|
|
678
|
+
})
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
flush(chunk, callback) {
|
|
682
|
+
if (this.flushChunk(chunk)) {
|
|
683
|
+
return callback()
|
|
684
|
+
}
|
|
685
|
+
this.connection.stream.once('drain', callback)
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
flushChunk(chunk) {
|
|
689
|
+
this.lengthBuffer.writeUInt32BE(chunk.length + 4, 1)
|
|
690
|
+
this.connection.stream.write(this.lengthBuffer)
|
|
691
|
+
return this.connection.stream.write(chunk)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
handleError(e) {
|
|
695
|
+
this.callback()
|
|
696
|
+
this.emit('error', e)
|
|
697
|
+
this.connection = null
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
handleCommandComplete(msg) {
|
|
701
|
+
const match = /COPY (\d+)/.exec((msg || {}).text)
|
|
702
|
+
if (match) {
|
|
703
|
+
this.rowCount = parseInt(match[1], 10)
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
handleReadyForQuery() {
|
|
708
|
+
this.callback()
|
|
709
|
+
this._finish()
|
|
710
|
+
this.connection = null
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
444
714
|
module.exports = PostgresService
|
package/lib/ReservedWords.json
CHANGED
|
@@ -1,4 +1,102 @@
|
|
|
1
1
|
{
|
|
2
|
+
"ALL": 1,
|
|
3
|
+
"ANALYSE": 1,
|
|
4
|
+
"ANALYZE": 1,
|
|
5
|
+
"AND": 1,
|
|
6
|
+
"ANY": 1,
|
|
7
|
+
"ARRAY": 1,
|
|
8
|
+
"AS": 1,
|
|
9
|
+
"ASC": 1,
|
|
10
|
+
"ASYMMETRIC": 1,
|
|
11
|
+
"AUTHORIZATION": 1,
|
|
2
12
|
"BINARY": 1,
|
|
3
|
-
"
|
|
13
|
+
"BOTH": 1,
|
|
14
|
+
"CASE": 1,
|
|
15
|
+
"CAST": 1,
|
|
16
|
+
"CHECK": 1,
|
|
17
|
+
"COLLATE": 1,
|
|
18
|
+
"COLLATION": 1,
|
|
19
|
+
"COLUMN": 1,
|
|
20
|
+
"CONCURRENTLY": 1,
|
|
21
|
+
"CONSTRAINT": 1,
|
|
22
|
+
"CREATE": 1,
|
|
23
|
+
"CROSS": 1,
|
|
24
|
+
"CURRENT_CATALOG": 1,
|
|
25
|
+
"CURRENT_DATE": 1,
|
|
26
|
+
"CURRENT_ROLE": 1,
|
|
27
|
+
"CURRENT_SCHEMA": 1,
|
|
28
|
+
"CURRENT_TIME": 1,
|
|
29
|
+
"CURRENT_TIMESTAMP": 1,
|
|
30
|
+
"CURRENT_USER": 1,
|
|
31
|
+
"DEFAULT": 1,
|
|
32
|
+
"DEFERRABLE": 1,
|
|
33
|
+
"DESC": 1,
|
|
34
|
+
"DISTINCT": 1,
|
|
35
|
+
"DO": 1,
|
|
36
|
+
"ELSE": 1,
|
|
37
|
+
"END": 1,
|
|
38
|
+
"EXCEPT": 1,
|
|
39
|
+
"FALSE": 1,
|
|
40
|
+
"FETCH": 1,
|
|
41
|
+
"FOR": 1,
|
|
42
|
+
"FOREIGN": 1,
|
|
43
|
+
"FREEZE": 1,
|
|
44
|
+
"FROM": 1,
|
|
45
|
+
"FULL": 1,
|
|
46
|
+
"GRANT": 1,
|
|
47
|
+
"GROUP": 1,
|
|
48
|
+
"HAVING": 1,
|
|
49
|
+
"ILIKE": 1,
|
|
50
|
+
"IN": 1,
|
|
51
|
+
"INITIALLY": 1,
|
|
52
|
+
"INNER": 1,
|
|
53
|
+
"INTERSECT": 1,
|
|
54
|
+
"INTO": 1,
|
|
55
|
+
"IS": 1,
|
|
56
|
+
"ISNULL": 1,
|
|
57
|
+
"JOIN": 1,
|
|
58
|
+
"LATERAL": 1,
|
|
59
|
+
"LEADING": 1,
|
|
60
|
+
"LEFT": 1,
|
|
61
|
+
"LIKE": 1,
|
|
62
|
+
"LIMIT": 1,
|
|
63
|
+
"LOCALTIME": 1,
|
|
64
|
+
"LOCALTIMESTAMP": 1,
|
|
65
|
+
"NATURAL": 1,
|
|
66
|
+
"NOT": 1,
|
|
67
|
+
"NOTNULL": 1,
|
|
68
|
+
"NULL": 1,
|
|
69
|
+
"OFFSET": 1,
|
|
70
|
+
"ON": 1,
|
|
71
|
+
"ONLY": 1,
|
|
72
|
+
"OR": 1,
|
|
73
|
+
"ORDER": 1,
|
|
74
|
+
"OUTER": 1,
|
|
75
|
+
"OVERLAPS": 1,
|
|
76
|
+
"PLACING": 1,
|
|
77
|
+
"PRIMARY": 1,
|
|
78
|
+
"REFERENCES": 1,
|
|
79
|
+
"RETURNING": 1,
|
|
80
|
+
"RIGHT": 1,
|
|
81
|
+
"SELECT": 1,
|
|
82
|
+
"SESSION_USER": 1,
|
|
83
|
+
"SIMILAR": 1,
|
|
84
|
+
"SOME": 1,
|
|
85
|
+
"SYMMETRIC": 1,
|
|
86
|
+
"TABLE": 1,
|
|
87
|
+
"TABLESAMPLE": 1,
|
|
88
|
+
"THEN": 1,
|
|
89
|
+
"TO": 1,
|
|
90
|
+
"TRAILING": 1,
|
|
91
|
+
"TRUE": 1,
|
|
92
|
+
"UNION": 1,
|
|
93
|
+
"UNIQUE": 1,
|
|
94
|
+
"USER": 1,
|
|
95
|
+
"USING": 1,
|
|
96
|
+
"VARIADIC": 1,
|
|
97
|
+
"VERBOSE": 1,
|
|
98
|
+
"WHEN": 1,
|
|
99
|
+
"WHERE": 1,
|
|
100
|
+
"WINDOW": 1,
|
|
101
|
+
"WITH": 1
|
|
4
102
|
}
|
package/lib/func.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
const session = require('./session.json')
|
|
2
|
+
|
|
1
3
|
const StandardFunctions = {
|
|
4
|
+
session_context: x => {
|
|
5
|
+
let sql = `current_setting('${ session[x.val] || x.val }')`
|
|
6
|
+
if (x.val === '$now') sql += '::timestamp'
|
|
7
|
+
return sql
|
|
8
|
+
},
|
|
2
9
|
countdistinct: x => `count(distinct ${x || '*'})`,
|
|
3
10
|
contains: (...args) => `(coalesce(strpos(${args}),0) > 0)`,
|
|
4
11
|
indexof: (x, y) => `strpos(${x},${y}) - 1`, // sqlite instr is 1 indexed
|
package/lib/session.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/postgres",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "CDS database service for Postgres",
|
|
5
5
|
"homepage": "https://github.com/cap-js/cds-dbs/tree/main/postgres#cds-database-service-for-postgres",
|
|
6
6
|
"repository": {
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"npm": ">=8"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
|
-
"
|
|
31
|
-
"
|
|
30
|
+
"test": "npm start && jest --silent",
|
|
31
|
+
"start": "docker-compose -f pg-stack.yml up -d"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@cap-js/db-service": "^1.1
|
|
34
|
+
"@cap-js/db-service": "^1.2.1",
|
|
35
35
|
"pg": "^8"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|