@cap-js/postgres 1.7.0 → 1.9.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 +29 -0
- package/README.md +1 -1
- package/lib/PostgresService.js +82 -33
- package/lib/{func.js → cql-functions.js} +4 -3
- package/package.json +2 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,35 @@
|
|
|
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
|
+
## [1.9.1](https://github.com/cap-js/cds-dbs/compare/postgres-v1.9.0...postgres-v1.9.1) (2024-07-09)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
* expand reach of `cds.features.ieee754compatible` to `int64` ([#722](https://github.com/cap-js/cds-dbs/issues/722)) ([7eef5e9](https://github.com/cap-js/cds-dbs/commit/7eef5e9c5ec286285b2552abd1e673175c59fdc1))
|
|
13
|
+
|
|
14
|
+
## [1.9.0](https://github.com/cap-js/cds-dbs/compare/postgres-v1.8.0...postgres-v1.9.0) (2024-05-29)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
* Add simple queries feature flag ([#660](https://github.com/cap-js/cds-dbs/issues/660)) ([3335202](https://github.com/cap-js/cds-dbs/commit/33352024201a96cc6bdfa30a0fe3fff4227dee10))
|
|
20
|
+
|
|
21
|
+
## [1.8.0](https://github.com/cap-js/cds-dbs/compare/postgres-v1.7.0...postgres-v1.8.0) (2024-05-08)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
* select decimals as strings if cds.env.features.string_decimals ([#616](https://github.com/cap-js/cds-dbs/issues/616)) ([39addbf](https://github.com/cap-js/cds-dbs/commit/39addbfe01da757d86a4d65e62eda86e59fc9b87))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
* Align all quote functions with @sap/cds-compiler ([#619](https://github.com/cap-js/cds-dbs/issues/619)) ([42e9828](https://github.com/cap-js/cds-dbs/commit/42e9828baf11ec55281ea634ce56ce93e6741b91))
|
|
32
|
+
* Change `sql` property to `query` for errors ([#611](https://github.com/cap-js/cds-dbs/issues/611)) ([585577a](https://github.com/cap-js/cds-dbs/commit/585577a9817e7749fb71958c26c4bfa20981c663))
|
|
33
|
+
* Improved placeholders and limit clause ([#567](https://github.com/cap-js/cds-dbs/issues/567)) ([d5d5dbb](https://github.com/cap-js/cds-dbs/commit/d5d5dbb7219bcef6134440715cf756fdd439f076))
|
|
34
|
+
* Use json datatype for `INSERT` ([#582](https://github.com/cap-js/cds-dbs/issues/582)) ([f1c9c89](https://github.com/cap-js/cds-dbs/commit/f1c9c89036a7f8e4709c67d713d06926630aa36d))
|
|
35
|
+
|
|
7
36
|
## [1.7.0](https://github.com/cap-js/cds-dbs/compare/postgres-v1.6.0...postgres-v1.7.0) (2024-04-12)
|
|
8
37
|
|
|
9
38
|
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ However, some preliminary checks and cleanups help:
|
|
|
47
47
|
|
|
48
48
|
### schema migration
|
|
49
49
|
|
|
50
|
-
`@cap-js/postgres` brings the same schema evolution capabilities to PostgreSQL known from HANA and SQLite.
|
|
50
|
+
`@cap-js/postgres` brings the same schema evolution capabilities to PostgreSQL known from SAP HANA and SQLite.
|
|
51
51
|
Enabling schema migration in an existing `cds-pg`-based project consists of generating and deploying a "csn-snapshot" of your database structure.
|
|
52
52
|
|
|
53
53
|
#### local development
|
package/lib/PostgresService.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { SQLService } = require('@cap-js/db-service')
|
|
2
2
|
const { Client, Query } = require('pg')
|
|
3
|
-
const cds = require('@sap/cds
|
|
3
|
+
const cds = require('@sap/cds')
|
|
4
4
|
const crypto = require('crypto')
|
|
5
5
|
const { Writable, Readable } = require('stream')
|
|
6
6
|
const sessionVariableMap = require('./session.json')
|
|
@@ -144,18 +144,29 @@ GROUP BY k
|
|
|
144
144
|
text: sql,
|
|
145
145
|
name: sha,
|
|
146
146
|
}
|
|
147
|
+
|
|
148
|
+
const enhanceError = (err, sql) => Object.assign(err, { query: sql + '\n' + new Array(err.position).fill(' ').join('') + '^' })
|
|
149
|
+
|
|
147
150
|
return {
|
|
148
151
|
run: async values => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
try {
|
|
153
|
+
// REVISIT: SQLService provides empty values as {} for plain SQL statements - PostgreSQL driver expects array or nothing - see issue #78
|
|
154
|
+
let newQuery = this._prepareStreams(query, values)
|
|
155
|
+
if (typeof newQuery.then === 'function') newQuery = await newQuery
|
|
156
|
+
const result = await this.dbc.query(newQuery)
|
|
157
|
+
return { changes: result.rowCount }
|
|
158
|
+
} catch (e) {
|
|
159
|
+
throw enhanceError(e, sql)
|
|
160
|
+
}
|
|
154
161
|
},
|
|
155
162
|
get: async values => {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
163
|
+
try {
|
|
164
|
+
// REVISIT: SQLService provides empty values as {} for plain SQL statements - PostgreSQL driver expects array or nothing - see issue #78
|
|
165
|
+
const result = await this.dbc.query({ ...query, values: this._getValues(values) })
|
|
166
|
+
return result.rows[0]
|
|
167
|
+
} catch (e) {
|
|
168
|
+
throw enhanceError(e, sql)
|
|
169
|
+
}
|
|
159
170
|
},
|
|
160
171
|
all: async values => {
|
|
161
172
|
// REVISIT: SQLService provides empty values as {} for plain SQL statements - PostgreSQL driver expects array or nothing - see issue #78
|
|
@@ -163,7 +174,7 @@ GROUP BY k
|
|
|
163
174
|
const result = await this.dbc.query({ ...query, values: this._getValues(values) })
|
|
164
175
|
return result.rows
|
|
165
176
|
} catch (e) {
|
|
166
|
-
throw
|
|
177
|
+
throw enhanceError(e, sql)
|
|
167
178
|
}
|
|
168
179
|
},
|
|
169
180
|
stream: async (values, one) => {
|
|
@@ -171,7 +182,7 @@ GROUP BY k
|
|
|
171
182
|
const streamQuery = new QueryStream({ ...query, values: this._getValues(values) }, one)
|
|
172
183
|
return await this.dbc.query(streamQuery)
|
|
173
184
|
} catch (e) {
|
|
174
|
-
throw
|
|
185
|
+
throw enhanceError(e, sql)
|
|
175
186
|
}
|
|
176
187
|
},
|
|
177
188
|
}
|
|
@@ -206,8 +217,7 @@ GROUP BY k
|
|
|
206
217
|
sql = sql.replace(
|
|
207
218
|
new RegExp(`\\$${i + 1}`, 'g'),
|
|
208
219
|
// Don't ask about the dollar signs
|
|
209
|
-
`(SELECT ${isBinary ? `DECODE(PARAM,'base64')` : 'PARAM'} FROM "$$$$PARAMETER_BUFFER$$$$" WHERE NAME='${
|
|
210
|
-
query.name
|
|
220
|
+
`(SELECT ${isBinary ? `DECODE(PARAM,'base64')` : 'PARAM'} FROM "$$$$PARAMETER_BUFFER$$$$" WHERE NAME='${query.name
|
|
211
221
|
}' AND ID=$${i + 1})`,
|
|
212
222
|
)
|
|
213
223
|
return
|
|
@@ -288,21 +298,17 @@ GROUP BY k
|
|
|
288
298
|
if (query.SELECT?.columns?.find(col => col.as === '$mediaContentType')) {
|
|
289
299
|
const columns = query.SELECT.columns
|
|
290
300
|
const index = columns.findIndex(col => query.elements[col.ref?.[col.ref.length - 1]].type === 'cds.LargeBinary')
|
|
291
|
-
const binary = columns
|
|
301
|
+
const binary = columns.splice(index, 1)
|
|
292
302
|
// SELECT without binary column
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
let ps = this.prepare(sql)
|
|
296
|
-
let res = await ps.all(values)
|
|
297
|
-
if (res.length === 0) return
|
|
298
|
-
res = res.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))[0]
|
|
303
|
+
let res = await super.onSELECT({ query, data })
|
|
304
|
+
if (!res) return res
|
|
299
305
|
// SELECT only binary column
|
|
300
|
-
query.SELECT.columns =
|
|
306
|
+
query.SELECT.columns = binary
|
|
301
307
|
const { sql: streamSql, values: valuesStream } = this.cqn2sql(query, data)
|
|
302
|
-
ps = this.prepare(streamSql)
|
|
308
|
+
const ps = this.prepare(streamSql)
|
|
303
309
|
const stream = await ps.stream(valuesStream, true)
|
|
304
310
|
// merge results
|
|
305
|
-
res[
|
|
311
|
+
res[this.class.CQN2SQL.prototype.column_name(binary[0])] = stream
|
|
306
312
|
return res
|
|
307
313
|
}
|
|
308
314
|
return super.onSELECT({ query, data })
|
|
@@ -364,7 +370,8 @@ GROUP BY k
|
|
|
364
370
|
return super.column_alias4(x, q)
|
|
365
371
|
}
|
|
366
372
|
|
|
367
|
-
SELECT_expand(
|
|
373
|
+
SELECT_expand(q, sql) {
|
|
374
|
+
const { SELECT } = q
|
|
368
375
|
if (!SELECT.columns) return sql
|
|
369
376
|
const queryAlias = this.quote(SELECT.from?.as || (SELECT.expand === 'root' && 'root'))
|
|
370
377
|
const cols = SELECT.columns.map(x => {
|
|
@@ -379,10 +386,22 @@ GROUP BY k
|
|
|
379
386
|
}
|
|
380
387
|
return col
|
|
381
388
|
})
|
|
389
|
+
const isRoot = SELECT.expand === 'root'
|
|
390
|
+
const isSimple = cds.env.features.sql_simple_queries &&
|
|
391
|
+
isRoot && // Simple queries are only allowed to have a root
|
|
392
|
+
!Object.keys(q.elements).some(e =>
|
|
393
|
+
q.elements[e].isAssociation || // Indicates columns contains an expand
|
|
394
|
+
q.elements[e].$assocExpand || // REVISIT: sometimes associations are structs
|
|
395
|
+
q.elements[e].items // Array types require to be inlined with a json result
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
const subQuery = `SELECT ${cols} FROM (${sql}) as ${queryAlias}`
|
|
399
|
+
if (isSimple) return subQuery
|
|
400
|
+
|
|
382
401
|
// REVISIT: Remove SELECT ${cols} by adjusting SELECT_columns
|
|
383
402
|
let obj = `to_jsonb(${queryAlias}.*)`
|
|
384
|
-
return `SELECT ${SELECT.one ||
|
|
385
|
-
} as _json_ FROM (
|
|
403
|
+
return `SELECT ${SELECT.one || isRoot ? obj : `coalesce(jsonb_agg (${obj}),'[]'::jsonb)`
|
|
404
|
+
} as _json_ FROM (${subQuery}) as ${queryAlias}`
|
|
386
405
|
}
|
|
387
406
|
|
|
388
407
|
doubleQuote(name) {
|
|
@@ -397,8 +416,8 @@ GROUP BY k
|
|
|
397
416
|
// Adjusts json path expressions to be postgres specific
|
|
398
417
|
.replace(/->>'\$(?:(?:\."(.*?)")|(?:\[(\d*)\]))'/g, (a, b, c) => (b ? `->>'${b}'` : `->>${c}`))
|
|
399
418
|
// Adjusts json function to be postgres specific
|
|
400
|
-
.replace('json_each(?)', '
|
|
401
|
-
.replace(/json_type\((\w+),'\$\."(\w+)"'\)/g, (_a, b, c) => `
|
|
419
|
+
.replace('json_each(?)', 'json_array_elements($1::json)')
|
|
420
|
+
.replace(/json_type\((\w+),'\$\."(\w+)"'\)/g, (_a, b, c) => `json_typeof(${b}->'${c}')`))
|
|
402
421
|
}
|
|
403
422
|
|
|
404
423
|
param({ ref }) {
|
|
@@ -431,11 +450,19 @@ GROUP BY k
|
|
|
431
450
|
return 'FOR SHARE'
|
|
432
451
|
}
|
|
433
452
|
|
|
453
|
+
// Postgres requires own quote function, becuase the effective name is lower case
|
|
454
|
+
quote(s) {
|
|
455
|
+
if (typeof s !== 'string') return '"' + s + '"'
|
|
456
|
+
if (s.includes('"')) return '"' + s.replace(/"/g, '""').toLowerCase() + '"'
|
|
457
|
+
if (s in this.class.ReservedWords || !/^[A-Za-z_][A-Za-z_$0-9]*$/.test(s)) return '"' + s.toLowerCase() + '"'
|
|
458
|
+
return s
|
|
459
|
+
}
|
|
460
|
+
|
|
434
461
|
defaultValue(defaultValue = this.context.timestamp.toISOString()) {
|
|
435
462
|
return this.string(`${defaultValue}`)
|
|
436
463
|
}
|
|
437
464
|
|
|
438
|
-
static Functions = { ...super.Functions, ...require('./
|
|
465
|
+
static Functions = { ...super.Functions, ...require('./cql-functions') }
|
|
439
466
|
|
|
440
467
|
static ReservedWords = { ...super.ReservedWords, ...require('./ReservedWords.json') }
|
|
441
468
|
|
|
@@ -443,6 +470,7 @@ GROUP BY k
|
|
|
443
470
|
...super.TypeMap,
|
|
444
471
|
// REVISIT: check whether we should use native UUID support
|
|
445
472
|
UUID: () => `VARCHAR(36)`,
|
|
473
|
+
UInt8: () => `INT`,
|
|
446
474
|
String: e => `VARCHAR(${e.length || 5000})`,
|
|
447
475
|
Binary: () => `BYTEA`,
|
|
448
476
|
Double: () => 'FLOAT8',
|
|
@@ -452,6 +480,13 @@ GROUP BY k
|
|
|
452
480
|
Time: () => 'TIME',
|
|
453
481
|
DateTime: () => 'TIMESTAMP',
|
|
454
482
|
Timestamp: () => 'TIMESTAMP',
|
|
483
|
+
|
|
484
|
+
// HANA Types
|
|
485
|
+
'cds.hana.CLOB': () => 'BYTEA',
|
|
486
|
+
'cds.hana.BINARY': () => 'BYTEA',
|
|
487
|
+
'cds.hana.TINYINT': () => 'SMALLINT',
|
|
488
|
+
'cds.hana.ST_POINT': () => 'POINT',
|
|
489
|
+
'cds.hana.ST_GEOMETRY': () => 'POLYGON',
|
|
455
490
|
}
|
|
456
491
|
|
|
457
492
|
// Used for INSERT statements
|
|
@@ -472,6 +507,12 @@ GROUP BY k
|
|
|
472
507
|
DecimalFloat: (e, t) => `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
|
|
473
508
|
Binary: e => `DECODE(${e},'base64')`,
|
|
474
509
|
LargeBinary: e => `DECODE(${e},'base64')`,
|
|
510
|
+
|
|
511
|
+
// HANA Types
|
|
512
|
+
'cds.hana.CLOB': e => `DECODE(${e},'base64')`,
|
|
513
|
+
'cds.hana.BINARY': e => `DECODE(${e},'base64')`,
|
|
514
|
+
'cds.hana.ST_POINT': e => `POINT(((${e})::json->>'x')::float, ((${e})::json->>'y')::float)`,
|
|
515
|
+
'cds.hana.ST_GEOMETRY': e => `POLYGON(${e})`,
|
|
475
516
|
}
|
|
476
517
|
|
|
477
518
|
static OutputConverters = {
|
|
@@ -487,6 +528,14 @@ GROUP BY k
|
|
|
487
528
|
Association: e => `jsonb(${e})`,
|
|
488
529
|
struct: e => `jsonb(${e})`,
|
|
489
530
|
array: e => `jsonb(${e})`,
|
|
531
|
+
// Reading int64 as string to not loose precision
|
|
532
|
+
Int64: cds.env.features.ieee754compatible ? expr => `cast(${expr} as varchar)` : undefined,
|
|
533
|
+
// REVISIT: always cast to string in next major
|
|
534
|
+
// Reading decimal as string to not loose precision
|
|
535
|
+
Decimal: cds.env.features.ieee754compatible ? expr => `cast(${expr} as varchar)` : undefined,
|
|
536
|
+
|
|
537
|
+
// Convert point back to json format
|
|
538
|
+
'cds.hana.ST_POINT': expr => `CASE WHEN (${expr}) IS NOT NULL THEN json_object('x':(${expr})[0],'y':(${expr})[1])::varchar END`,
|
|
490
539
|
}
|
|
491
540
|
}
|
|
492
541
|
|
|
@@ -517,7 +566,7 @@ GROUP BY k
|
|
|
517
566
|
GRANT "${creds.usergroup}" TO "${creds.user}" WITH ADMIN OPTION;
|
|
518
567
|
`)
|
|
519
568
|
await this.exec(`CREATE DATABASE "${creds.database}" OWNER="${creds.user}" TEMPLATE=template0`)
|
|
520
|
-
} catch
|
|
569
|
+
} catch {
|
|
521
570
|
// Failed to reset database
|
|
522
571
|
} finally {
|
|
523
572
|
await this.dbc.end()
|
|
@@ -561,7 +610,7 @@ GROUP BY k
|
|
|
561
610
|
// Create new schema using schema owner
|
|
562
611
|
await this.tx(async tx => {
|
|
563
612
|
await tx.run(`DROP SCHEMA IF EXISTS "${creds.schema}" CASCADE`)
|
|
564
|
-
if (!clean) await tx.run(`CREATE SCHEMA "${creds.schema}" AUTHORIZATION "${creds.user}"`).catch(() => {})
|
|
613
|
+
if (!clean) await tx.run(`CREATE SCHEMA "${creds.schema}" AUTHORIZATION "${creds.user}"`).catch(() => { })
|
|
565
614
|
})
|
|
566
615
|
} finally {
|
|
567
616
|
await this.disconnect()
|
|
@@ -589,7 +638,7 @@ class QueryStream extends Query {
|
|
|
589
638
|
})
|
|
590
639
|
this.connection.flush()
|
|
591
640
|
}
|
|
592
|
-
: () => {},
|
|
641
|
+
: () => { },
|
|
593
642
|
})
|
|
594
643
|
this.push = this.stream.push.bind(this.stream)
|
|
595
644
|
|
|
@@ -704,7 +753,7 @@ class ParameterStream extends Writable {
|
|
|
704
753
|
}
|
|
705
754
|
|
|
706
755
|
// Used by the client to handle timeouts
|
|
707
|
-
callback() {}
|
|
756
|
+
callback() { }
|
|
708
757
|
|
|
709
758
|
_write(chunk, enc, cb) {
|
|
710
759
|
return this.flush(chunk, cb)
|
|
@@ -6,10 +6,11 @@ const StandardFunctions = {
|
|
|
6
6
|
if (x.val === '$now') sql += '::timestamp'
|
|
7
7
|
return sql
|
|
8
8
|
},
|
|
9
|
-
|
|
9
|
+
count: x => `count(${x?.val || x || '*'})`,
|
|
10
|
+
countdistinct: x => `count(distinct ${x.val || x || '*'})`,
|
|
10
11
|
contains: (...args) => `(coalesce(strpos(${args}),0) > 0)`,
|
|
11
|
-
indexof: (x, y) => `strpos(${x},${y}) - 1`, //
|
|
12
|
-
startswith: (x, y) => `strpos(${x},${y}) = 1`, //
|
|
12
|
+
indexof: (x, y) => `strpos(${x},${y}) - 1`, // strpos is 1 indexed
|
|
13
|
+
startswith: (x, y) => `strpos(${x},${y}) = 1`, // strpos is 1 indexed
|
|
13
14
|
endswith: (x, y) => `substr(${x},length(${x}) + 1 - length(${y})) = ${y}`,
|
|
14
15
|
matchesPattern: (x, y) => `regexp_like(${x}, ${y})`,
|
|
15
16
|
matchespattern: (x, y) => `regexp_like(${x}, ${y})`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/postgres",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.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": {
|
|
@@ -22,16 +22,12 @@
|
|
|
22
22
|
"lib",
|
|
23
23
|
"CHANGELOG.md"
|
|
24
24
|
],
|
|
25
|
-
"engines": {
|
|
26
|
-
"node": ">=16",
|
|
27
|
-
"npm": ">=8"
|
|
28
|
-
},
|
|
29
25
|
"scripts": {
|
|
30
26
|
"test": "npm start && jest --silent",
|
|
31
27
|
"start": "docker-compose -f pg-stack.yml up -d"
|
|
32
28
|
},
|
|
33
29
|
"dependencies": {
|
|
34
|
-
"@cap-js/db-service": "^1.
|
|
30
|
+
"@cap-js/db-service": "^1.9.0",
|
|
35
31
|
"pg": "^8"
|
|
36
32
|
},
|
|
37
33
|
"peerDependencies": {
|