@cap-js/postgres 1.10.0 → 1.10.2
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 +16 -0
- package/lib/PostgresService.js +96 -47
- package/lib/cql-functions.js +20 -0
- package/package.json +9 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@
|
|
|
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.10.2](https://github.com/cap-js/cds-dbs/compare/postgres-v1.10.1...postgres-v1.10.2) (2024-10-28)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
* properly support `default`, `cds.on.insert` and `cds.on.update` for `UPSERT` queries ([#425](https://github.com/cap-js/cds-dbs/issues/425)) ([338e9f5](https://github.com/cap-js/cds-dbs/commit/338e9f5de9109d36013208547fc648c17ce8c7b0))
|
|
13
|
+
|
|
14
|
+
## [1.10.1](https://github.com/cap-js/cds-dbs/compare/postgres-v1.10.0...postgres-v1.10.1) (2024-10-15)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
* add cds schema for postgres build plugin ([#843](https://github.com/cap-js/cds-dbs/issues/843)) ([6306d5c](https://github.com/cap-js/cds-dbs/commit/6306d5ce50c071b38a3d9f61b0820ea713a782d8))
|
|
20
|
+
* Improved behavioral consistency between the database services ([#837](https://github.com/cap-js/cds-dbs/issues/837)) ([b6f7187](https://github.com/cap-js/cds-dbs/commit/b6f718701e48dfb1c4c3d98ee016ec45930f8e7b))
|
|
21
|
+
* null as default value ([#845](https://github.com/cap-js/cds-dbs/issues/845)) ([0041ec0](https://github.com/cap-js/cds-dbs/commit/0041ec0a26c29b30f91470d93611b29acd837216))
|
|
22
|
+
|
|
7
23
|
## [1.10.0](https://github.com/cap-js/cds-dbs/compare/postgres-v1.9.1...postgres-v1.10.0) (2024-07-25)
|
|
8
24
|
|
|
9
25
|
|
package/lib/PostgresService.js
CHANGED
|
@@ -253,7 +253,7 @@ GROUP BY k
|
|
|
253
253
|
return this.dbc.query(sql)
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
onPlainSQL(req, next) {
|
|
256
|
+
async onPlainSQL(req, next) {
|
|
257
257
|
const query = req.query
|
|
258
258
|
if (this.options.independentDeploy) {
|
|
259
259
|
// REVISIT: Should not be needed when deployment supports all types or sends CQNs
|
|
@@ -289,8 +289,19 @@ GROUP BY k
|
|
|
289
289
|
// eslint-disable-next-line no-unused-vars
|
|
290
290
|
req.query = query.replace(/('|")(\1|[^\1]*?\1)|(\?)/g, (a, _b, _c, d, _e, _f, _g) => (d ? '$' + i++ : a))
|
|
291
291
|
}
|
|
292
|
-
|
|
293
|
-
|
|
292
|
+
try {
|
|
293
|
+
return await super.onPlainSQL(req, next)
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
if (err.code === '3F000') {
|
|
297
|
+
if (this.options?.credentials?.schema) {
|
|
298
|
+
cds.error`Failed to configure schema ("${this.options?.credentials?.schema}") before plainSQL call: ${req.query}`
|
|
299
|
+
} else {
|
|
300
|
+
cds.error`No schema was configure / detected before plainSQL call: ${req.query}`
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
throw err
|
|
304
|
+
}
|
|
294
305
|
}
|
|
295
306
|
|
|
296
307
|
async onSELECT({ query, data }) {
|
|
@@ -413,11 +424,32 @@ GROUP BY k
|
|
|
413
424
|
|
|
414
425
|
// REVISIT: this should probably be made a bit easier to adopt
|
|
415
426
|
return (this.sql = this.sql
|
|
416
|
-
// Adjusts json path expressions to be postgres specific
|
|
417
|
-
.replace(/->>'\$(?:(?:\."(.*?)")|(?:\[(\d*)\]))'/g, (a, b, c) => (b ? `->>'${b}'` : `->>${c}`))
|
|
418
427
|
// Adjusts json function to be postgres specific
|
|
419
428
|
.replace('json_each(?)', 'json_array_elements($1::json)')
|
|
420
|
-
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
UPSERT(q, isUpsert = false) {
|
|
433
|
+
super.UPSERT(q, isUpsert)
|
|
434
|
+
|
|
435
|
+
// REVISIT: this should probably be made a bit easier to adopt
|
|
436
|
+
return (this.sql = this.sql
|
|
437
|
+
// Adjusts json function to be postgres specific
|
|
438
|
+
.replace('json_each(?)', 'json_array_elements($1::json)')
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
managed_extract(name, element, converter) {
|
|
443
|
+
const { UPSERT, INSERT } = this.cqn
|
|
444
|
+
const extract = !(INSERT?.entries || UPSERT?.entries) && (INSERT?.rows || UPSERT?.rows)
|
|
445
|
+
? `value->>${this.columns.indexOf(name)}`
|
|
446
|
+
: `value->>'${name.replace(/'/g, "''")}'`
|
|
447
|
+
const sql = converter?.(extract) || extract
|
|
448
|
+
return { extract, sql }
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
managed_default(name, managed, src) {
|
|
452
|
+
return `(CASE WHEN json_typeof(value->${this.managed_extract(name).extract.slice(8)}) IS NULL THEN ${managed} ELSE ${src} END)`
|
|
421
453
|
}
|
|
422
454
|
|
|
423
455
|
param({ ref }) {
|
|
@@ -459,7 +491,7 @@ GROUP BY k
|
|
|
459
491
|
}
|
|
460
492
|
|
|
461
493
|
defaultValue(defaultValue = this.context.timestamp.toISOString()) {
|
|
462
|
-
return this.string(`${defaultValue}`)
|
|
494
|
+
return typeof defaultValue === 'string' ? this.string(`${defaultValue}`) : defaultValue
|
|
463
495
|
}
|
|
464
496
|
|
|
465
497
|
static Functions = { ...super.Functions, ...require('./cql-functions') }
|
|
@@ -492,27 +524,31 @@ GROUP BY k
|
|
|
492
524
|
// Used for INSERT statements
|
|
493
525
|
static InputConverters = {
|
|
494
526
|
...super.InputConverters,
|
|
495
|
-
// UUID:
|
|
496
|
-
boolean: e => `CASE ${e} WHEN 'true' THEN true WHEN 'false' THEN false END`,
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
527
|
+
// UUID: (e) => e[0] === '$' ? e : `CAST(${e} as UUID)`, // UUID is strict in formatting sflight does not comply
|
|
528
|
+
boolean: e => e[0] === '$' ? e : `CASE ${e} WHEN 'true' THEN true WHEN 'false' THEN false END`,
|
|
529
|
+
// REVISIT: Postgres and HANA round Decimal numbers differently therefore precision and scale are removed
|
|
530
|
+
// Float: (e, t) => e[0] === '$' ? e : `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
|
|
531
|
+
// Decimal: (e, t) => e[0] === '$' ? e : `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
|
|
532
|
+
Float: e => e[0] === '$' ? e : `CAST(${e} as decimal)`,
|
|
533
|
+
Decimal: e => e[0] === '$' ? e : `CAST(${e} as decimal)`,
|
|
534
|
+
Integer: e => e[0] === '$' ? e : `CAST(${e} as integer)`,
|
|
535
|
+
Int64: e => e[0] === '$' ? e : `CAST(${e} as bigint)`,
|
|
536
|
+
Date: e => e[0] === '$' ? e : `CAST(${e} as DATE)`,
|
|
537
|
+
Time: e => e[0] === '$' ? e : `CAST(${e} as TIME)`,
|
|
538
|
+
DateTime: e => e[0] === '$' ? e : `CAST(${e} as TIMESTAMP)`,
|
|
539
|
+
Timestamp: e => e[0] === '$' ? e : `CAST(${e} as TIMESTAMP)`,
|
|
505
540
|
// REVISIT: Remove that with upcomming fixes in cds.linked
|
|
506
|
-
Double: (e, t) => `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
|
|
507
|
-
DecimalFloat: (e, t) => `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
|
|
508
|
-
Binary: e => `DECODE(${e},'base64')`,
|
|
509
|
-
LargeBinary: e => `DECODE(${e},'base64')`,
|
|
541
|
+
Double: (e, t) => e[0] === '$' ? e : `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
|
|
542
|
+
DecimalFloat: (e, t) => e[0] === '$' ? e : `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
|
|
543
|
+
Binary: e => e[0] === '$' ? e : `DECODE(${e},'base64')`,
|
|
544
|
+
LargeBinary: e => e[0] === '$' ? e : `DECODE(${e},'base64')`,
|
|
510
545
|
|
|
511
546
|
// HANA Types
|
|
512
|
-
'cds.hana.CLOB': e => `DECODE(${e},'base64')`,
|
|
513
|
-
'cds.hana.BINARY': e => `DECODE(${e},'base64')`,
|
|
514
|
-
|
|
515
|
-
'cds.hana.
|
|
547
|
+
'cds.hana.CLOB': e => e[0] === '$' ? e : `DECODE(${e},'base64')`,
|
|
548
|
+
'cds.hana.BINARY': e => e[0] === '$' ? e : `DECODE(${e},'base64')`,
|
|
549
|
+
// REVISIT: have someone take a look at how this syntax exactly works in postgres with postgis
|
|
550
|
+
'cds.hana.ST_POINT': e => `(${e})::point`,
|
|
551
|
+
'cds.hana.ST_GEOMETRY': e => `(${e})::polygon`,
|
|
516
552
|
}
|
|
517
553
|
|
|
518
554
|
static OutputConverters = {
|
|
@@ -532,10 +568,14 @@ GROUP BY k
|
|
|
532
568
|
Int64: cds.env.features.ieee754compatible ? expr => `cast(${expr} as varchar)` : undefined,
|
|
533
569
|
// REVISIT: always cast to string in next major
|
|
534
570
|
// Reading decimal as string to not loose precision
|
|
535
|
-
Decimal: cds.env.features.ieee754compatible ?
|
|
571
|
+
Decimal: cds.env.features.ieee754compatible ? (expr, elem) => elem?.scale
|
|
572
|
+
? `to_char(${expr},'FM${'0'.padStart(elem.precision, '9')}${'D'.padEnd(elem.scale + 1, '0')}')`
|
|
573
|
+
: `cast(${expr} as varchar)`
|
|
574
|
+
: undefined,
|
|
536
575
|
|
|
537
|
-
// Convert
|
|
538
|
-
'cds.hana.ST_POINT': expr => `
|
|
576
|
+
// Convert ST types back to WKT format
|
|
577
|
+
'cds.hana.ST_POINT': expr => `ST_AsText(${expr})`,
|
|
578
|
+
'cds.hana.ST_POINT': expr => `ST_AsText(${expr})`,
|
|
539
579
|
}
|
|
540
580
|
}
|
|
541
581
|
|
|
@@ -567,14 +607,21 @@ GROUP BY k
|
|
|
567
607
|
`)
|
|
568
608
|
await this.exec(`CREATE DATABASE "${creds.database}" OWNER="${creds.user}" TEMPLATE=template0`)
|
|
569
609
|
} catch {
|
|
610
|
+
// Failed to connect to database
|
|
611
|
+
if (!this.dbc) {
|
|
612
|
+
return this.database({ database })
|
|
613
|
+
}
|
|
570
614
|
// Failed to reset database
|
|
571
615
|
} finally {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
616
|
+
// Only clean when successfully connected
|
|
617
|
+
if (this.dbc) {
|
|
618
|
+
await this.dbc.end()
|
|
619
|
+
delete this.dbc
|
|
620
|
+
|
|
621
|
+
// Update credentials to new Database owner
|
|
622
|
+
await this.disconnect()
|
|
623
|
+
this.options.credentials = Object.assign({}, system, creds)
|
|
624
|
+
}
|
|
578
625
|
}
|
|
579
626
|
}
|
|
580
627
|
|
|
@@ -589,18 +636,20 @@ GROUP BY k
|
|
|
589
636
|
|
|
590
637
|
try {
|
|
591
638
|
if (!clean) {
|
|
592
|
-
await
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
})
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
639
|
+
await cds
|
|
640
|
+
.run(`CREATE USER "${creds.user}" IN GROUP "${creds.usergroup}" PASSWORD '${creds.password}'`)
|
|
641
|
+
.catch(e => {
|
|
642
|
+
if (e.code === '42710') return
|
|
643
|
+
throw e
|
|
644
|
+
})
|
|
645
|
+
// Retry granting priviledges as this is being done by multiple instances
|
|
646
|
+
// Postgres just rejects when other connections are granting the same user
|
|
647
|
+
const grant = (i = 0) => cds.run(`GRANT CREATE, CONNECT ON DATABASE "${creds.database}" TO "${creds.user}";`)
|
|
648
|
+
.catch((err) => {
|
|
649
|
+
if (i > 100) throw err
|
|
650
|
+
return grant(i + 1)
|
|
651
|
+
})
|
|
652
|
+
await grant()
|
|
604
653
|
}
|
|
605
654
|
|
|
606
655
|
// Update credentials to new Schema owner
|
|
@@ -610,7 +659,7 @@ GROUP BY k
|
|
|
610
659
|
// Create new schema using schema owner
|
|
611
660
|
await this.tx(async tx => {
|
|
612
661
|
await tx.run(`DROP SCHEMA IF EXISTS "${creds.schema}" CASCADE`)
|
|
613
|
-
if (!clean) await tx.run(`CREATE SCHEMA "${creds.schema}" AUTHORIZATION "${creds.user}"`)
|
|
662
|
+
if (!clean) await tx.run(`CREATE SCHEMA "${creds.schema}" AUTHORIZATION "${creds.user}"`)
|
|
614
663
|
})
|
|
615
664
|
} finally {
|
|
616
665
|
await this.disconnect()
|
package/lib/cql-functions.js
CHANGED
|
@@ -24,6 +24,26 @@ const StandardFunctions = {
|
|
|
24
24
|
minute: x => `date_part('minute', ${castVal(x)})`,
|
|
25
25
|
second: x => `floor(date_part('second', ${castVal(x)}))`,
|
|
26
26
|
fractionalseconds: x => `CAST(date_part('second', ${castVal(x)}) - floor(date_part('second', ${castVal(x)})) AS DECIMAL)`,
|
|
27
|
+
totalseconds: x => `(
|
|
28
|
+
(
|
|
29
|
+
(
|
|
30
|
+
CAST(substring(${x},2,strpos(${x},'DT') - 2) AS INTEGER)
|
|
31
|
+
) + (
|
|
32
|
+
EXTRACT (EPOCH FROM
|
|
33
|
+
CAST(
|
|
34
|
+
replace(
|
|
35
|
+
replace(
|
|
36
|
+
replace(
|
|
37
|
+
substring(${x},strpos(${x},'DT') + 2),
|
|
38
|
+
'H',':'
|
|
39
|
+
),'M',':'
|
|
40
|
+
),'S','Z'
|
|
41
|
+
)
|
|
42
|
+
as TIME)
|
|
43
|
+
) - 0.5
|
|
44
|
+
)
|
|
45
|
+
) * 86400
|
|
46
|
+
)`,
|
|
27
47
|
now: function() {
|
|
28
48
|
return this.session_context({val: '$now'})
|
|
29
49
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/postgres",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.2",
|
|
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": {
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"CHANGELOG.md"
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
|
-
"test": "npm start &&
|
|
27
|
-
"start": "docker
|
|
26
|
+
"test": "npm start && cds-test $(../test/find)",
|
|
27
|
+
"start": "docker compose -f pg-stack.yml up -d"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@cap-js/db-service": "^1.9.0",
|
|
@@ -67,6 +67,12 @@
|
|
|
67
67
|
}
|
|
68
68
|
},
|
|
69
69
|
"db": "sql"
|
|
70
|
+
},
|
|
71
|
+
"schema": {
|
|
72
|
+
"buildTaskType": {
|
|
73
|
+
"name": "postgres",
|
|
74
|
+
"description": "Postgres database build plugin"
|
|
75
|
+
}
|
|
70
76
|
}
|
|
71
77
|
},
|
|
72
78
|
"license": "SEE LICENSE"
|