@cap-js/postgres 1.0.1 → 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 CHANGED
@@ -4,6 +4,26 @@
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
+
7
27
  ## Version 1.0.1 - 2023-07-03
8
28
 
9
29
  ### Added
@@ -16,4 +36,4 @@
16
36
 
17
37
  ## Version 1.0.0 - 2023-06-23
18
38
 
19
- - 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
- Find documentation at https://cap.cloud.sap/docs/guides/databases-postgres.
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.
@@ -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
- ssl: cr.sslrootcert && {
40
- rejectUnauthorized: false,
41
- ca: cr.sslrootcert,
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': 'CAP.APPLICATIONUSER',
64
- '$user.locale': 'CAP.LOCALE',
65
- '$valid.from': 'CAP.VALID_FROM',
66
- '$valid.to': 'CAP.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
- orderBy(orderBy, localized) {
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 "${this.context.locale}"` : '') +
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
- return orderBy.map(
194
- localized
195
- ? c =>
196
- this.expr(c) +
197
- (c.element?.[this.class._localized] ? ` COLLATE "${this.context.locale.replace('_', '-')}-x-icu"` : '') +
198
- (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
199
- : c => this.expr(c) + (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC'),
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/package.json CHANGED
@@ -1,8 +1,15 @@
1
1
  {
2
2
  "name": "@cap-js/postgres",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "CDS database service for Postgres",
5
- "homepage": "https://cap.cloud.sap/",
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.0.1",
34
+ "@cap-js/db-service": "^1.1.0",
28
35
  "pg": "^8"
29
36
  },
30
37
  "peerDependencies": {
@@ -53,7 +60,8 @@
53
60
  "dialect": "postgres",
54
61
  "vcap": {
55
62
  "label": "postgresql-db"
56
- }
63
+ },
64
+ "schema_evolution": "auto"
57
65
  }
58
66
  },
59
67
  "db": "sql"