@cap-js/postgres 1.0.0 → 1.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 +30 -1
- package/README.md +149 -1
- package/lib/PostgresService.js +74 -34
- package/lib/func.js +0 -1
- package/package.json +22 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,36 @@
|
|
|
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.1.0 - 2023-08-01
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Connectivity to Azure PostgreSQL.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Order by collation waterfall:
|
|
16
|
+
1. ICU
|
|
17
|
+
2. best-effort mapping (`xx` -> `xx_YY`, where `xx_YY` is the first match)
|
|
18
|
+
3. without collation
|
|
19
|
+
- More stable configuration of `schema_evolution = 'auto'`.
|
|
20
|
+
- Log `hostname` preferrably during deployment.
|
|
21
|
+
- Allow overriding of pool configuration.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Session context variables are set as lower case instead of upper case.
|
|
26
|
+
|
|
27
|
+
## Version 1.0.1 - 2023-07-03
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- `pg` profile as mentioned in documentation
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Updated minimum required version of `@cap-js/db-service`
|
|
7
36
|
|
|
8
37
|
## Version 1.0.0 - 2023-06-23
|
|
9
38
|
|
|
10
|
-
- Initial Release
|
|
39
|
+
- Initial Release
|
package/README.md
CHANGED
|
@@ -1,5 +1,153 @@
|
|
|
1
1
|
# CDS database service for Postgres
|
|
2
2
|
|
|
3
3
|
Welcome to the new Postgres database service for [SAP Cloud Application Programming Model](https://cap.cloud.sap) Node.js, based on new, streamlined database architecture and [*pg* driver](https://www.npmjs.com/package/pg) .
|
|
4
|
+
Find full documentation at https://cap.cloud.sap/docs/guides/databases-postgres.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
## migration guide from `cds-pg` to `@cap-js/postgres`
|
|
7
|
+
|
|
8
|
+
`@cap-js/postgres` works as a drop-in replacement for `cds-pg`.
|
|
9
|
+
However, some preliminary checks and cleanups help:
|
|
10
|
+
|
|
11
|
+
- for using the BTP Postgres Hyperscaler as database,
|
|
12
|
+
- know that the credentials are picked up automatically by from the enviornment (`VCAP_SERVICES.postgres`)
|
|
13
|
+
- the service binding label is `postgresql-db`
|
|
14
|
+
- `cds-dbm` is replaced by a hand-crafted "db-deployer" app → see below
|
|
15
|
+
- your local `package.json`: you can safely remove the entry `cds.requires.postgres` previously mandatory for `cds-pg`
|
|
16
|
+
- recommendation: set the env var `DEBUG=sql` during local development to see DB-level output from PostgreSQL
|
|
17
|
+
|
|
18
|
+
### schema migration
|
|
19
|
+
|
|
20
|
+
`@cap-js/postgres` brings the same schema evolution capabilities to PostgreSQL known from HANA and SQLite.
|
|
21
|
+
Enabling schema migration in an existing `cds-pg`-based project consists of generating and deploying a "csn-snapshot" of your database structure.
|
|
22
|
+
|
|
23
|
+
#### local development
|
|
24
|
+
|
|
25
|
+
First, set a basis for the evolution
|
|
26
|
+
`$> cds deploy --model-only`
|
|
27
|
+
→ this will create the table `cds_model` laying the foundation for the schema migration
|
|
28
|
+
|
|
29
|
+
Subsequent deployments can then re-use the standard deploy mechanism via `$> cds deploy`
|
|
30
|
+
|
|
31
|
+
#### On BTP, Cloud Foundry environment
|
|
32
|
+
|
|
33
|
+
The above "csn-snapshots" can be implemented via the `mtar`-based approach. At the same time, the same `mtar` can be used for subsequent PostgreSQL deployments (with schema evolution).
|
|
34
|
+
|
|
35
|
+
Two major steps in addition to enabling the schema evolution are included in this `mtar`.
|
|
36
|
+
|
|
37
|
+
1. create local folder `deployer` (any name works)
|
|
38
|
+
2. in `deployer`, create a `package.json` containing
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
...
|
|
42
|
+
"//npm run migrate": "only one-time!",
|
|
43
|
+
"migrate": "cds deploy --model-only",
|
|
44
|
+
"//npm run deploy": "subsequent deployments",
|
|
45
|
+
"deploy": "cds deploy"
|
|
46
|
+
...
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
3. add a section to your `/mta.yaml` denoting the `deployer` directory as a standalone application that runs one-time
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
- name: pg-db-deployer
|
|
53
|
+
type: custom
|
|
54
|
+
path: deployer
|
|
55
|
+
parameters:
|
|
56
|
+
buildpacks: nodejs_buildpack
|
|
57
|
+
no-route: true
|
|
58
|
+
no-start: true
|
|
59
|
+
disk-quota: 2GB
|
|
60
|
+
memory: 512MB
|
|
61
|
+
tasks:
|
|
62
|
+
- name: migrate
|
|
63
|
+
command: npm run migrate
|
|
64
|
+
# # for subsequent deployments
|
|
65
|
+
# - name: deploy
|
|
66
|
+
# command: npm run deploy
|
|
67
|
+
disk-quota: 2GB
|
|
68
|
+
memory: 512MB
|
|
69
|
+
build-parameters:
|
|
70
|
+
before-all:
|
|
71
|
+
custom:
|
|
72
|
+
- npm i
|
|
73
|
+
# generate the "csn-snapshot" - only necessary for one-time migration,
|
|
74
|
+
# can be commented out on subsequent deployments
|
|
75
|
+
- cds compile '*' -2 json > deployer/schema.csn
|
|
76
|
+
ignore: ["node_modules/"]
|
|
77
|
+
requires:
|
|
78
|
+
- name: pg-database
|
|
79
|
+
|
|
80
|
+
resources:
|
|
81
|
+
- name: pg-database
|
|
82
|
+
parameters:
|
|
83
|
+
path: ./pg-options.json
|
|
84
|
+
service: postgresql-db
|
|
85
|
+
service-plan: trial # change to yours!
|
|
86
|
+
skip-service-updates:
|
|
87
|
+
parameters: true
|
|
88
|
+
service-tags:
|
|
89
|
+
- plain
|
|
90
|
+
type: org.cloudfoundry.managed-service
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## migration points to consider
|
|
94
|
+
|
|
95
|
+
### mixed-case identifiers
|
|
96
|
+
|
|
97
|
+
even though column names that are not double-quoted are folded to lowercase in PostgreSQL (`yourName` -> `yourname`, `"yourName"` -> `yourName`),
|
|
98
|
+
you can use the mixed case definitions from your `.cds` files to reference them.
|
|
99
|
+
|
|
100
|
+
example: `brewery_id` on DB level -> `brewery_ID` on CDS level
|
|
101
|
+
|
|
102
|
+
formerly w/ `cds-pg` you had to follow the DB level: `SELECT.from(Beers).columns('brewery_id').groupBy('brewery_id')`
|
|
103
|
+
now, re-use the CDS definitions: `SELECT.from(Beers).columns('brewery_ID').groupBy('brewery_ID')`
|
|
104
|
+
|
|
105
|
+
So please adjust your `CQL` statements accordingly.
|
|
106
|
+
|
|
107
|
+
### timezones (potential _**BREAKING CHANGE**_)
|
|
108
|
+
|
|
109
|
+
any date- + time-type will get stored in [`UTC`](https://en.wikipedia.org/wiki/Coordinated_Universal_Time) **without any timezone identifier in the actual data field**.
|
|
110
|
+
CAP's inbound- and outbound adapters take care of converting incoming and outgoing data from/to the desired time zones.
|
|
111
|
+
So when a `dateime` comes in being in [an ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) compatible format
|
|
112
|
+
`2009-01-01T15:00:00+01:00` (15:00:00 on January 1 2009 in Vienna (CEST))
|
|
113
|
+
will get stored as
|
|
114
|
+
`2009-01-01T13:00:00` (13:00:00 on January 1 2009 in UTC).
|
|
115
|
+
|
|
116
|
+
Please be aware of that concept and rely on the client to parse UTC in your desired timezone (format).
|
|
117
|
+
|
|
118
|
+
### `cds.DatabaseService` consumption
|
|
119
|
+
|
|
120
|
+
`InsertResult` now does only return the affected rows and their `ID`s.
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
const entries = [
|
|
124
|
+
{ name: 'Beer1', /* ... */ },
|
|
125
|
+
{ name: 'Beer2', /* ... */ },
|
|
126
|
+
{ name: 'Beer3', /* ... */ }
|
|
127
|
+
]
|
|
128
|
+
const insertResult = await cds.run(INSERT.into(Beers).entries(entries))
|
|
129
|
+
expect(insertResult.affectedRows).to.equal(3)
|
|
130
|
+
const beers = [...insertResult] //> this calls the [Symbol.iterator] method of the insert result
|
|
131
|
+
// beers:
|
|
132
|
+
// [
|
|
133
|
+
// { ID: "f81d7ee5-922b-48a1-a12a-a899b8594c99" },
|
|
134
|
+
// { ID: "ddda7f8e-e26b-430f-a80c-ac2c7df29510" },
|
|
135
|
+
// { ID: "7228c40f-0046-4f53-8a2b-3d55ad825f59" }
|
|
136
|
+
// ]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
In `cds-pg`, we additionally surfaced the entire inserted dataset.
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
// continuing after the insert of the above example:
|
|
143
|
+
// const insertResult = await cds.run(INSERT.into(Beers).entries(entries))
|
|
144
|
+
|
|
145
|
+
// this works NO MORE - see above
|
|
146
|
+
const beers = insertResult.results
|
|
147
|
+
expect(beers.length).toStrictEqual(3)
|
|
148
|
+
expect(beers[0].ID).toMatch(uuidRegex)
|
|
149
|
+
expect(beers[0].createdAt.toISOString()).toMatch(timestampRegex)
|
|
150
|
+
expect(beers[0].modifiedAt.toISOString()).toMatch(timestampRegex)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
So please adjust your runtime coding accordingly.
|
package/lib/PostgresService.js
CHANGED
|
@@ -17,11 +17,11 @@ class PostgresService extends SQLService {
|
|
|
17
17
|
get factory() {
|
|
18
18
|
return {
|
|
19
19
|
options: {
|
|
20
|
-
...this.options.pool,
|
|
21
20
|
min: 0,
|
|
22
21
|
testOnBorrow: true,
|
|
23
22
|
acquireTimeoutMillis: 1000,
|
|
24
23
|
destroyTimeoutMillis: 1000,
|
|
24
|
+
...this.options.pool,
|
|
25
25
|
},
|
|
26
26
|
create: async () => {
|
|
27
27
|
const cr = this.options.credentials || {}
|
|
@@ -36,10 +36,14 @@ class PostgresService extends SQLService {
|
|
|
36
36
|
database: cr.dbname || cr.database,
|
|
37
37
|
schema: cr.schema,
|
|
38
38
|
sslRequired: cr.sslrootcert && (cr.sslrootcert ?? true),
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
// from pg driver docs:
|
|
40
|
+
// passed directly to node.TLSSocket, supports all tls.connect options
|
|
41
|
+
ssl:
|
|
42
|
+
cr.ssl /* enable pg module setting to connect to Azure postgres */ ||
|
|
43
|
+
(cr.sslrootcert && {
|
|
44
|
+
rejectUnauthorized: false,
|
|
45
|
+
ca: cr.sslrootcert,
|
|
46
|
+
}),
|
|
43
47
|
}
|
|
44
48
|
const dbc = new Client(credentials)
|
|
45
49
|
await dbc.connect()
|
|
@@ -52,18 +56,18 @@ class PostgresService extends SQLService {
|
|
|
52
56
|
|
|
53
57
|
url4() {
|
|
54
58
|
// TODO: Maybe log which database and which user? Be more robust against missing properties?
|
|
55
|
-
let { host, port } = this.options?.credentials || this.options || {}
|
|
56
|
-
return host + ':' + (port || 5432)
|
|
59
|
+
let { host, hostname, port } = this.options?.credentials || this.options || {}
|
|
60
|
+
return (hostname || host) + ':' + (port || 5432)
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
async set(variables) {
|
|
60
64
|
// REVISIT: remove when all environment variables are aligned
|
|
61
65
|
// RESTRICTIONS: 'Custom parameter names must be two or more simple identifiers separated by dots.'
|
|
62
66
|
const nameMap = {
|
|
63
|
-
'$user.id': '
|
|
64
|
-
'$user.locale': '
|
|
65
|
-
'$valid.from': '
|
|
66
|
-
'$valid.to': '
|
|
67
|
+
'$user.id': 'cap.applicationuser',
|
|
68
|
+
'$user.locale': 'cap.locale',
|
|
69
|
+
'$valid.from': 'cap.valid_from',
|
|
70
|
+
'$valid.to': 'cap.valid_to',
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
const env = {}
|
|
@@ -79,22 +83,55 @@ class PostgresService extends SQLService {
|
|
|
79
83
|
? [this.exec(`SET search_path TO "${this.options?.credentials?.schema}";`)]
|
|
80
84
|
: []),
|
|
81
85
|
|
|
82
|
-
...(!this._initalCollateCheck
|
|
83
|
-
? [
|
|
84
|
-
(await this.prepare(`SELECT collname FROM pg_collation WHERE collname = 'en_US' OR collname ='en-x-icu';`))
|
|
85
|
-
.all([])
|
|
86
|
-
.then(resp => {
|
|
87
|
-
this._initalCollateCheck = true
|
|
88
|
-
if (resp.find(row => row.collname === 'en_US')) return
|
|
89
|
-
if (resp.find(row => row.collname === 'en-x-icu'))
|
|
90
|
-
this.class.CQN2SQL.prototype.orderBy = this.class.CQN2SQL.prototype.orderByICU
|
|
91
|
-
// REVISIT throw error when there is no collated libary found
|
|
92
|
-
}),
|
|
93
|
-
]
|
|
94
|
-
: []),
|
|
86
|
+
...(!this._initalCollateCheck ? [this._checkCollation()] : []),
|
|
95
87
|
])
|
|
96
88
|
}
|
|
97
89
|
|
|
90
|
+
async _checkCollation() {
|
|
91
|
+
this._initalCollateCheck = true
|
|
92
|
+
|
|
93
|
+
const icuPrep = await this.prepare(`SELECT collname FROM pg_collation WHERE collname = 'en-x-icu';`)
|
|
94
|
+
const icuResp = await icuPrep.all([])
|
|
95
|
+
|
|
96
|
+
if (icuResp.length > 0) {
|
|
97
|
+
this.class.CQN2SQL.prototype.orderBy = this.class.CQN2SQL.prototype.orderByICU
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Selects the first two characters of the collation name as key
|
|
103
|
+
* Select the smallest collation name as value (could also be max)
|
|
104
|
+
* Filter the collations by the provider c (libc)
|
|
105
|
+
* Filters the collation names by /.._../ Where '>' points at the '_' that is an actual '_'
|
|
106
|
+
* The group by is done by the key column to make sure that only one collation per key is returned
|
|
107
|
+
*/
|
|
108
|
+
const cSQL = `
|
|
109
|
+
SELECT
|
|
110
|
+
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 '>'
|
|
117
|
+
GROUP BY k
|
|
118
|
+
`
|
|
119
|
+
|
|
120
|
+
const cPrep = await this.prepare(cSQL)
|
|
121
|
+
const cResp = await cPrep.all([])
|
|
122
|
+
if (cResp.length > 0) {
|
|
123
|
+
const collationMap = (this.class.CQN2SQL.prototype.collationMap = cResp.reduce((ret, row) => {
|
|
124
|
+
ret[row.k] = row.v
|
|
125
|
+
return ret
|
|
126
|
+
}, {}))
|
|
127
|
+
collationMap.default = collationMap.en || collationMap[Object.keys(collationMap)[0]]
|
|
128
|
+
this.class.CQN2SQL.prototype.orderBy = this.class.CQN2SQL.prototype.orderByLIBC
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// REVISIT: print a warning when no collation is found
|
|
133
|
+
}
|
|
134
|
+
|
|
98
135
|
prepare(sql) {
|
|
99
136
|
const query = {
|
|
100
137
|
text: sql,
|
|
@@ -178,26 +215,29 @@ class PostgresService extends SQLService {
|
|
|
178
215
|
}
|
|
179
216
|
|
|
180
217
|
static CQN2SQL = class CQN2Postgres extends SQLService.CQN2SQL {
|
|
181
|
-
|
|
218
|
+
_orderBy(orderBy, localized, locale) {
|
|
182
219
|
return orderBy.map(
|
|
183
220
|
localized
|
|
184
221
|
? c =>
|
|
185
222
|
this.expr(c) +
|
|
186
|
-
(c.element?.[this.class._localized] ? ` COLLATE "${
|
|
223
|
+
(c.element?.[this.class._localized] ? ` COLLATE "${locale}"` : '') +
|
|
187
224
|
(c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
|
|
188
225
|
: c => this.expr(c) + (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC'),
|
|
189
226
|
)
|
|
190
227
|
}
|
|
191
228
|
|
|
229
|
+
orderBy(orderBy) {
|
|
230
|
+
return this._orderBy(orderBy)
|
|
231
|
+
}
|
|
232
|
+
|
|
192
233
|
orderByICU(orderBy, localized) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
)
|
|
234
|
+
const locale = `${this.context.locale.replace('_', '-')}-x-icu`
|
|
235
|
+
return this._orderBy(orderBy, localized, locale)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
orderByLIBC(orderBy, localized) {
|
|
239
|
+
const locale = this.collationMap[this.context.locale] || this.collationMap.default
|
|
240
|
+
return this._orderBy(orderBy, localized && locale, locale)
|
|
201
241
|
}
|
|
202
242
|
|
|
203
243
|
from(from) {
|
package/lib/func.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const StandardFunctions = {
|
|
2
2
|
countdistinct: x => `count(distinct ${x || '*'})`,
|
|
3
|
-
average: x => `avg(${x})`,
|
|
4
3
|
contains: (...args) => `(coalesce(strpos(${args}),0) > 0)`,
|
|
5
4
|
indexof: (x, y) => `strpos(${x},${y}) - 1`, // sqlite instr is 1 indexed
|
|
6
5
|
startswith: (x, y) => `strpos(${x},${y}) = 1`, // sqlite instr is 1 indexed
|
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/postgres",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "CDS database service for Postgres",
|
|
5
|
-
"homepage": "https://
|
|
5
|
+
"homepage": "https://github.com/cap-js/cds-dbs/tree/main/postgres#cds-database-service-for-postgres",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/cap-js/cds-dbs"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/cap-js/cds-dbs/issues"
|
|
12
|
+
},
|
|
6
13
|
"keywords": [
|
|
7
14
|
"CAP",
|
|
8
15
|
"CDS",
|
|
@@ -24,7 +31,7 @@
|
|
|
24
31
|
"test": "npm run setup && jest --silent"
|
|
25
32
|
},
|
|
26
33
|
"dependencies": {
|
|
27
|
-
"@cap-js/db-service": "^1",
|
|
34
|
+
"@cap-js/db-service": "^1.1.0",
|
|
28
35
|
"pg": "^8"
|
|
29
36
|
},
|
|
30
37
|
"peerDependencies": {
|
|
@@ -36,6 +43,16 @@
|
|
|
36
43
|
"sql": {
|
|
37
44
|
"[production]": {
|
|
38
45
|
"kind": "postgres"
|
|
46
|
+
},
|
|
47
|
+
"[pg!]": {
|
|
48
|
+
"kind": "postgres",
|
|
49
|
+
"credentials": {
|
|
50
|
+
"host": "localhost",
|
|
51
|
+
"port": 5432,
|
|
52
|
+
"user": "postgres",
|
|
53
|
+
"password": "postgres",
|
|
54
|
+
"database": "postgres"
|
|
55
|
+
}
|
|
39
56
|
}
|
|
40
57
|
},
|
|
41
58
|
"postgres": {
|
|
@@ -43,7 +60,8 @@
|
|
|
43
60
|
"dialect": "postgres",
|
|
44
61
|
"vcap": {
|
|
45
62
|
"label": "postgresql-db"
|
|
46
|
-
}
|
|
63
|
+
},
|
|
64
|
+
"schema_evolution": "auto"
|
|
47
65
|
}
|
|
48
66
|
},
|
|
49
67
|
"db": "sql"
|