@cap-js/postgres 1.0.1 → 1.2.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 +32 -1
- package/README.md +149 -1
- package/lib/PostgresService.js +390 -80
- package/lib/ReservedWords.json +99 -1
- package/lib/func.js +7 -0
- package/lib/session.json +7 -0
- package/package.json +14 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,37 @@
|
|
|
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.0 - 2023-09-06
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Reduced the usage of `is not distinct [not] from`. #157
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- [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
|
|
16
|
+
- Remove column count limitation. #150
|
|
17
|
+
|
|
18
|
+
## Version 1.1.0 - 2023-08-01
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Connectivity to Azure PostgreSQL.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Order by collation waterfall:
|
|
27
|
+
1. ICU
|
|
28
|
+
2. best-effort mapping (`xx` -> `xx_YY`, where `xx_YY` is the first match)
|
|
29
|
+
3. without collation
|
|
30
|
+
- More stable configuration of `schema_evolution = 'auto'`.
|
|
31
|
+
- Log `hostname` preferrably during deployment.
|
|
32
|
+
- Allow overriding of pool configuration.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- Session context variables are set as lower case instead of upper case.
|
|
37
|
+
|
|
7
38
|
## Version 1.0.1 - 2023-07-03
|
|
8
39
|
|
|
9
40
|
### Added
|
|
@@ -16,4 +47,4 @@
|
|
|
16
47
|
|
|
17
48
|
## Version 1.0.0 - 2023-06-23
|
|
18
49
|
|
|
19
|
-
- Initial Release
|
|
50
|
+
- 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
|
@@ -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() {
|
|
@@ -17,11 +19,11 @@ class PostgresService extends SQLService {
|
|
|
17
19
|
get factory() {
|
|
18
20
|
return {
|
|
19
21
|
options: {
|
|
20
|
-
...this.options.pool,
|
|
21
22
|
min: 0,
|
|
22
23
|
testOnBorrow: true,
|
|
23
24
|
acquireTimeoutMillis: 1000,
|
|
24
25
|
destroyTimeoutMillis: 1000,
|
|
26
|
+
...this.options.pool,
|
|
25
27
|
},
|
|
26
28
|
create: async () => {
|
|
27
29
|
const cr = this.options.credentials || {}
|
|
@@ -36,10 +38,14 @@ class PostgresService extends SQLService {
|
|
|
36
38
|
database: cr.dbname || cr.database,
|
|
37
39
|
schema: cr.schema,
|
|
38
40
|
sslRequired: cr.sslrootcert && (cr.sslrootcert ?? true),
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// from pg driver docs:
|
|
42
|
+
// passed directly to node.TLSSocket, supports all tls.connect options
|
|
43
|
+
ssl:
|
|
44
|
+
cr.ssl /* enable pg module setting to connect to Azure postgres */ ||
|
|
45
|
+
(cr.sslrootcert && {
|
|
46
|
+
rejectUnauthorized: false,
|
|
47
|
+
ca: cr.sslrootcert,
|
|
48
|
+
}),
|
|
43
49
|
}
|
|
44
50
|
const dbc = new Client(credentials)
|
|
45
51
|
await dbc.connect()
|
|
@@ -52,23 +58,23 @@ class PostgresService extends SQLService {
|
|
|
52
58
|
|
|
53
59
|
url4() {
|
|
54
60
|
// 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)
|
|
61
|
+
let { host, hostname, port } = this.options?.credentials || this.options || {}
|
|
62
|
+
return (hostname || host) + ':' + (port || 5432)
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
async set(variables) {
|
|
60
|
-
// REVISIT: remove when all environment variables are aligned
|
|
61
66
|
// RESTRICTIONS: 'Custom parameter names must be two or more simple identifiers separated by dots.'
|
|
62
|
-
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
|
-
}
|
|
68
|
-
|
|
69
67
|
const env = {}
|
|
68
|
+
|
|
69
|
+
// Check all properties on the variables object
|
|
70
70
|
for (let name in variables) {
|
|
71
|
-
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]
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
return Promise.all([
|
|
@@ -79,24 +85,58 @@ class PostgresService extends SQLService {
|
|
|
79
85
|
? [this.exec(`SET search_path TO "${this.options?.credentials?.schema}";`)]
|
|
80
86
|
: []),
|
|
81
87
|
|
|
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
|
-
: []),
|
|
88
|
+
...(!this._initalCollateCheck ? [this._checkCollation()] : []),
|
|
95
89
|
])
|
|
96
90
|
}
|
|
97
91
|
|
|
92
|
+
async _checkCollation() {
|
|
93
|
+
this._initalCollateCheck = true
|
|
94
|
+
|
|
95
|
+
const icuPrep = await this.prepare(`SELECT collname FROM pg_collation WHERE collname = 'en-x-icu';`)
|
|
96
|
+
const icuResp = await icuPrep.all([])
|
|
97
|
+
|
|
98
|
+
if (icuResp.length > 0) {
|
|
99
|
+
this.class.CQN2SQL.prototype.orderBy = this.class.CQN2SQL.prototype.orderByICU
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Selects the first two characters of the collation name as key
|
|
105
|
+
* Select the smallest collation name as value (could also be max)
|
|
106
|
+
* Filter the collations by the provider c (libc)
|
|
107
|
+
* Filters the collation names by /.._../ Where '>' points at the '_' that is an actual '_'
|
|
108
|
+
* The group by is done by the key column to make sure that only one collation per key is returned
|
|
109
|
+
*/
|
|
110
|
+
const cSQL = `
|
|
111
|
+
SELECT
|
|
112
|
+
SUBSTRING(collname, 1, 2) AS K,
|
|
113
|
+
MIN(collname) AS V
|
|
114
|
+
FROM
|
|
115
|
+
pg_collation
|
|
116
|
+
WHERE
|
|
117
|
+
collprovider = 'c' AND
|
|
118
|
+
collname LIKE '__>___' ESCAPE '>'
|
|
119
|
+
GROUP BY k
|
|
120
|
+
`
|
|
121
|
+
|
|
122
|
+
const cPrep = await this.prepare(cSQL)
|
|
123
|
+
const cResp = await cPrep.all([])
|
|
124
|
+
if (cResp.length > 0) {
|
|
125
|
+
const collationMap = (this.class.CQN2SQL.prototype.collationMap = cResp.reduce((ret, row) => {
|
|
126
|
+
ret[row.k] = row.v
|
|
127
|
+
return ret
|
|
128
|
+
}, {}))
|
|
129
|
+
collationMap.default = collationMap.en || collationMap[Object.keys(collationMap)[0]]
|
|
130
|
+
this.class.CQN2SQL.prototype.orderBy = this.class.CQN2SQL.prototype.orderByLIBC
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// REVISIT: print a warning when no collation is found
|
|
135
|
+
}
|
|
136
|
+
|
|
98
137
|
prepare(sql) {
|
|
99
138
|
const query = {
|
|
139
|
+
_streams: 0,
|
|
100
140
|
text: sql,
|
|
101
141
|
// Track queries name for postgres referencing prepare statements
|
|
102
142
|
// sha1 as it needs to be less then 63 characters
|
|
@@ -105,7 +145,9 @@ class PostgresService extends SQLService {
|
|
|
105
145
|
return {
|
|
106
146
|
run: async values => {
|
|
107
147
|
// REVISIT: SQLService provides empty values as {} for plain SQL statements - PostgreSQL driver expects array or nothing - see issue #78
|
|
108
|
-
|
|
148
|
+
let newQuery = this._prepareStreams(query, values)
|
|
149
|
+
if (typeof newQuery.then === 'function') newQuery = await newQuery
|
|
150
|
+
const result = await this.dbc.query(newQuery)
|
|
109
151
|
return { changes: result.rowCount }
|
|
110
152
|
},
|
|
111
153
|
get: async values => {
|
|
@@ -122,6 +164,14 @@ class PostgresService extends SQLService {
|
|
|
122
164
|
throw Object.assign(e, { sql: sql + '\n' + new Array(e.position).fill(' ').join('') + '^' })
|
|
123
165
|
}
|
|
124
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
|
+
},
|
|
125
175
|
}
|
|
126
176
|
}
|
|
127
177
|
|
|
@@ -133,6 +183,60 @@ class PostgresService extends SQLService {
|
|
|
133
183
|
return values
|
|
134
184
|
}
|
|
135
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
|
+
|
|
136
240
|
async exec(sql) {
|
|
137
241
|
return this.dbc.query(sql)
|
|
138
242
|
}
|
|
@@ -178,26 +282,29 @@ class PostgresService extends SQLService {
|
|
|
178
282
|
}
|
|
179
283
|
|
|
180
284
|
static CQN2SQL = class CQN2Postgres extends SQLService.CQN2SQL {
|
|
181
|
-
|
|
285
|
+
_orderBy(orderBy, localized, locale) {
|
|
182
286
|
return orderBy.map(
|
|
183
287
|
localized
|
|
184
288
|
? c =>
|
|
185
289
|
this.expr(c) +
|
|
186
|
-
(c.element?.[this.class._localized] ? ` COLLATE "${
|
|
290
|
+
(c.element?.[this.class._localized] ? ` COLLATE "${locale}"` : '') +
|
|
187
291
|
(c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
|
|
188
292
|
: c => this.expr(c) + (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC'),
|
|
189
293
|
)
|
|
190
294
|
}
|
|
191
295
|
|
|
296
|
+
orderBy(orderBy) {
|
|
297
|
+
return this._orderBy(orderBy)
|
|
298
|
+
}
|
|
299
|
+
|
|
192
300
|
orderByICU(orderBy, localized) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
)
|
|
301
|
+
const locale = `${this.context.locale.replace('_', '-')}-x-icu`
|
|
302
|
+
return this._orderBy(orderBy, localized, locale)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
orderByLIBC(orderBy, localized) {
|
|
306
|
+
const locale = this.collationMap[this.context.locale] || this.collationMap.default
|
|
307
|
+
return this._orderBy(orderBy, localized && locale, locale)
|
|
201
308
|
}
|
|
202
309
|
|
|
203
310
|
from(from) {
|
|
@@ -209,18 +316,9 @@ class PostgresService extends SQLService {
|
|
|
209
316
|
return super.from(from)
|
|
210
317
|
}
|
|
211
318
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (!SELECT.columns) return '*'
|
|
216
|
-
const unique = {}
|
|
217
|
-
return SELECT.columns
|
|
218
|
-
.map(x => `${this.column_expr(x)} as ${this.quote(this.column_name(x))}`)
|
|
219
|
-
.filter(x => {
|
|
220
|
-
if (unique[x]) return false
|
|
221
|
-
unique[x] = true
|
|
222
|
-
return true
|
|
223
|
-
})
|
|
319
|
+
column_alias4(x, q) {
|
|
320
|
+
if (!x.as && 'val' in x) return String(x.val)
|
|
321
|
+
return super.column_alias4(x, q)
|
|
224
322
|
}
|
|
225
323
|
|
|
226
324
|
SELECT_expand({ SELECT }, sql) {
|
|
@@ -228,19 +326,25 @@ class PostgresService extends SQLService {
|
|
|
228
326
|
const queryAlias = this.quote(SELECT.from?.as || (SELECT.expand === 'root' && 'root'))
|
|
229
327
|
const cols = SELECT.columns.map(x => {
|
|
230
328
|
const name = this.column_name(x)
|
|
231
|
-
|
|
329
|
+
const outputConverter = this.output_converter4(x.element, `${queryAlias}.${this.quote(name)}`)
|
|
330
|
+
let col = `${outputConverter} as ${this.doubleQuote(name)}`
|
|
232
331
|
|
|
233
332
|
if (x.SELECT?.count) {
|
|
234
333
|
// Return both the sub select and the count for @odata.count
|
|
235
334
|
const qc = cds.ql.clone(x, { columns: [{ func: 'count' }], one: 1, limit: 0, orderBy: 0 })
|
|
236
|
-
col +=
|
|
335
|
+
col += `,${this.expr(qc)} as ${this.doubleQuote(`${name}@odata.count`)}`
|
|
237
336
|
}
|
|
238
337
|
return col
|
|
239
338
|
})
|
|
240
|
-
|
|
339
|
+
// REVISIT: Remove SELECT ${cols} by adjusting SELECT_columns
|
|
340
|
+
let obj = `row_to_json(${queryAlias}.*)`
|
|
241
341
|
return `SELECT ${
|
|
242
342
|
SELECT.one || SELECT.expand === 'root' ? obj : `coalesce(json_agg(${obj}),'[]'::json)`
|
|
243
|
-
} 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, '""')}"`
|
|
244
348
|
}
|
|
245
349
|
|
|
246
350
|
INSERT(q, isUpsert = false) {
|
|
@@ -251,20 +355,24 @@ class PostgresService extends SQLService {
|
|
|
251
355
|
// Adjusts json path expressions to be postgres specific
|
|
252
356
|
.replace(/->>'\$(?:(?:\."(.*?)")|(?:\[(\d*)\]))'/g, (a, b, c) => (b ? `->>'${b}'` : `->>${c}`))
|
|
253
357
|
// Adjusts json function to be postgres specific
|
|
254
|
-
.replace('json_each(?)', 'json_array_elements($1)')
|
|
358
|
+
.replace('json_each(?)', 'json_array_elements($1::JSON)')
|
|
255
359
|
.replace(/json_type\((\w+),'\$\."(\w+)"'\)/g, (_a, b, c) => `json_typeof(${b}->'${c}')`))
|
|
256
360
|
}
|
|
257
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
|
+
|
|
258
368
|
val(val) {
|
|
259
369
|
const ret = super.val(val)
|
|
260
370
|
return ret === '?' ? `$${this.values.length}` : ret
|
|
261
371
|
}
|
|
262
372
|
|
|
263
|
-
operator(x) {
|
|
373
|
+
operator(x, i, xpr) {
|
|
264
374
|
if (x === 'regexp') return '~'
|
|
265
|
-
|
|
266
|
-
if (x === '!=') return 'is distinct from'
|
|
267
|
-
else return x
|
|
375
|
+
else return super.operator(x, i, xpr)
|
|
268
376
|
}
|
|
269
377
|
|
|
270
378
|
defaultValue(defaultValue = this.context.timestamp.toISOString()) {
|
|
@@ -306,14 +414,14 @@ class PostgresService extends SQLService {
|
|
|
306
414
|
// REVISIT: Remove that with upcomming fixes in cds.linked
|
|
307
415
|
Double: (e, t) => `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
|
|
308
416
|
DecimalFloat: (e, t) => `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
|
|
309
|
-
Binary: e => `
|
|
310
|
-
LargeBinary: e => `
|
|
417
|
+
Binary: e => `DECODE(${e},'base64')`,
|
|
418
|
+
LargeBinary: e => `DECODE(${e},'base64')`,
|
|
311
419
|
}
|
|
312
420
|
|
|
313
421
|
static OutputConverters = {
|
|
314
422
|
...super.OutputConverters,
|
|
315
|
-
Binary: e => e,
|
|
316
|
-
LargeBinary: e => e,
|
|
423
|
+
Binary: e => `ENCODE(${e},'base64')`,
|
|
424
|
+
LargeBinary: e => `ENCODE(${e},'base64')`,
|
|
317
425
|
Date: e => `to_char(${e}, 'YYYY-MM-DD')`,
|
|
318
426
|
Time: e => `to_char(${e}, 'HH24:MI:SS')`,
|
|
319
427
|
DateTime: e => `to_char(${e}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
|
|
@@ -363,7 +471,7 @@ class PostgresService extends SQLService {
|
|
|
363
471
|
}
|
|
364
472
|
}
|
|
365
473
|
|
|
366
|
-
async tenant({ database, tenant }) {
|
|
474
|
+
async tenant({ database, tenant }, clean = false) {
|
|
367
475
|
const creds = {
|
|
368
476
|
database: database,
|
|
369
477
|
usergroup: `${database}_USERS`,
|
|
@@ -373,18 +481,20 @@ class PostgresService extends SQLService {
|
|
|
373
481
|
creds.password = creds.user
|
|
374
482
|
|
|
375
483
|
try {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
await tx
|
|
387
|
-
|
|
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
|
+
}
|
|
388
498
|
|
|
389
499
|
// Update credentials to new Schema owner
|
|
390
500
|
await this.disconnect()
|
|
@@ -393,7 +503,7 @@ class PostgresService extends SQLService {
|
|
|
393
503
|
// Create new schema using schema owner
|
|
394
504
|
await this.tx(async tx => {
|
|
395
505
|
await tx.run(`DROP SCHEMA IF EXISTS "${creds.schema}" CASCADE`)
|
|
396
|
-
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(() => {})
|
|
397
507
|
})
|
|
398
508
|
} finally {
|
|
399
509
|
await this.disconnect()
|
|
@@ -401,4 +511,204 @@ class PostgresService extends SQLService {
|
|
|
401
511
|
}
|
|
402
512
|
}
|
|
403
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
|
+
|
|
404
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,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/postgres",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.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",
|
|
@@ -20,11 +27,11 @@
|
|
|
20
27
|
"npm": ">=8"
|
|
21
28
|
},
|
|
22
29
|
"scripts": {
|
|
23
|
-
"
|
|
24
|
-
"
|
|
30
|
+
"test": "npm start && jest --silent",
|
|
31
|
+
"start": "docker-compose -f pg-stack.yml up -d"
|
|
25
32
|
},
|
|
26
33
|
"dependencies": {
|
|
27
|
-
"@cap-js/db-service": "^1.0
|
|
34
|
+
"@cap-js/db-service": "^1.2.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"
|