@cap-js/postgres 1.6.0 → 1.8.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 +22 -0
- package/lib/PostgresService.js +64 -28
- package/lib/{func.js → cql-functions.js} +8 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@
|
|
|
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.8.0](https://github.com/cap-js/cds-dbs/compare/postgres-v1.7.0...postgres-v1.8.0) (2024-05-08)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* 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))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
* 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))
|
|
18
|
+
* 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))
|
|
19
|
+
* 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))
|
|
20
|
+
* 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))
|
|
21
|
+
|
|
22
|
+
## [1.7.0](https://github.com/cap-js/cds-dbs/compare/postgres-v1.6.0...postgres-v1.7.0) (2024-04-12)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
* Odata built-in query functions ([#558](https://github.com/cap-js/cds-dbs/issues/558)) ([6e63367](https://github.com/cap-js/cds-dbs/commit/6e6336757129c4a9dac56f93fd768bb41d071c46))
|
|
28
|
+
|
|
7
29
|
## [1.6.0](https://github.com/cap-js/cds-dbs/compare/postgres-v1.5.1...postgres-v1.6.0) (2024-03-22)
|
|
8
30
|
|
|
9
31
|
|
package/lib/PostgresService.js
CHANGED
|
@@ -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 })
|
|
@@ -397,8 +403,8 @@ GROUP BY k
|
|
|
397
403
|
// Adjusts json path expressions to be postgres specific
|
|
398
404
|
.replace(/->>'\$(?:(?:\."(.*?)")|(?:\[(\d*)\]))'/g, (a, b, c) => (b ? `->>'${b}'` : `->>${c}`))
|
|
399
405
|
// Adjusts json function to be postgres specific
|
|
400
|
-
.replace('json_each(?)', '
|
|
401
|
-
.replace(/json_type\((\w+),'\$\."(\w+)"'\)/g, (_a, b, c) => `
|
|
406
|
+
.replace('json_each(?)', 'json_array_elements($1::json)')
|
|
407
|
+
.replace(/json_type\((\w+),'\$\."(\w+)"'\)/g, (_a, b, c) => `json_typeof(${b}->'${c}')`))
|
|
402
408
|
}
|
|
403
409
|
|
|
404
410
|
param({ ref }) {
|
|
@@ -431,11 +437,19 @@ GROUP BY k
|
|
|
431
437
|
return 'FOR SHARE'
|
|
432
438
|
}
|
|
433
439
|
|
|
440
|
+
// Postgres requires own quote function, becuase the effective name is lower case
|
|
441
|
+
quote(s) {
|
|
442
|
+
if (typeof s !== 'string') return '"' + s + '"'
|
|
443
|
+
if (s.includes('"')) return '"' + s.replace(/"/g, '""').toLowerCase() + '"'
|
|
444
|
+
if (s in this.class.ReservedWords || !/^[A-Za-z_][A-Za-z_$0-9]*$/.test(s)) return '"' + s.toLowerCase() + '"'
|
|
445
|
+
return s
|
|
446
|
+
}
|
|
447
|
+
|
|
434
448
|
defaultValue(defaultValue = this.context.timestamp.toISOString()) {
|
|
435
449
|
return this.string(`${defaultValue}`)
|
|
436
450
|
}
|
|
437
451
|
|
|
438
|
-
static Functions = { ...super.Functions, ...require('./
|
|
452
|
+
static Functions = { ...super.Functions, ...require('./cql-functions') }
|
|
439
453
|
|
|
440
454
|
static ReservedWords = { ...super.ReservedWords, ...require('./ReservedWords.json') }
|
|
441
455
|
|
|
@@ -443,6 +457,7 @@ GROUP BY k
|
|
|
443
457
|
...super.TypeMap,
|
|
444
458
|
// REVISIT: check whether we should use native UUID support
|
|
445
459
|
UUID: () => `VARCHAR(36)`,
|
|
460
|
+
UInt8: () => `INT`,
|
|
446
461
|
String: e => `VARCHAR(${e.length || 5000})`,
|
|
447
462
|
Binary: () => `BYTEA`,
|
|
448
463
|
Double: () => 'FLOAT8',
|
|
@@ -452,6 +467,13 @@ GROUP BY k
|
|
|
452
467
|
Time: () => 'TIME',
|
|
453
468
|
DateTime: () => 'TIMESTAMP',
|
|
454
469
|
Timestamp: () => 'TIMESTAMP',
|
|
470
|
+
|
|
471
|
+
// HANA Types
|
|
472
|
+
'cds.hana.CLOB': () => 'BYTEA',
|
|
473
|
+
'cds.hana.BINARY': () => 'BYTEA',
|
|
474
|
+
'cds.hana.TINYINT': () => 'SMALLINT',
|
|
475
|
+
'cds.hana.ST_POINT': () => 'POINT',
|
|
476
|
+
'cds.hana.ST_GEOMETRY': () => 'POLYGON',
|
|
455
477
|
}
|
|
456
478
|
|
|
457
479
|
// Used for INSERT statements
|
|
@@ -472,6 +494,12 @@ GROUP BY k
|
|
|
472
494
|
DecimalFloat: (e, t) => `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
|
|
473
495
|
Binary: e => `DECODE(${e},'base64')`,
|
|
474
496
|
LargeBinary: e => `DECODE(${e},'base64')`,
|
|
497
|
+
|
|
498
|
+
// HANA Types
|
|
499
|
+
'cds.hana.CLOB': e => `DECODE(${e},'base64')`,
|
|
500
|
+
'cds.hana.BINARY': e => `DECODE(${e},'base64')`,
|
|
501
|
+
'cds.hana.ST_POINT': e => `POINT(((${e})::json->>'x')::float, ((${e})::json->>'y')::float)`,
|
|
502
|
+
'cds.hana.ST_GEOMETRY': e => `POLYGON(${e})`,
|
|
475
503
|
}
|
|
476
504
|
|
|
477
505
|
static OutputConverters = {
|
|
@@ -487,6 +515,14 @@ GROUP BY k
|
|
|
487
515
|
Association: e => `jsonb(${e})`,
|
|
488
516
|
struct: e => `jsonb(${e})`,
|
|
489
517
|
array: e => `jsonb(${e})`,
|
|
518
|
+
// Reading int64 as string to not loose precision
|
|
519
|
+
Int64: expr => `cast(${expr} as varchar)`,
|
|
520
|
+
// REVISIT: always cast to string in next major
|
|
521
|
+
// Reading decimal as string to not loose precision
|
|
522
|
+
Decimal: cds.env.features.string_decimals ? expr => `cast(${expr} as varchar)` : undefined,
|
|
523
|
+
|
|
524
|
+
// Convert point back to json format
|
|
525
|
+
'cds.hana.ST_POINT': expr => `CASE WHEN (${expr}) IS NOT NULL THEN json_object('x':(${expr})[0],'y':(${expr})[1])::varchar END`,
|
|
490
526
|
}
|
|
491
527
|
}
|
|
492
528
|
|
|
@@ -561,7 +597,7 @@ GROUP BY k
|
|
|
561
597
|
// Create new schema using schema owner
|
|
562
598
|
await this.tx(async tx => {
|
|
563
599
|
await tx.run(`DROP SCHEMA IF EXISTS "${creds.schema}" CASCADE`)
|
|
564
|
-
if (!clean) await tx.run(`CREATE SCHEMA "${creds.schema}" AUTHORIZATION "${creds.user}"`).catch(() => {})
|
|
600
|
+
if (!clean) await tx.run(`CREATE SCHEMA "${creds.schema}" AUTHORIZATION "${creds.user}"`).catch(() => { })
|
|
565
601
|
})
|
|
566
602
|
} finally {
|
|
567
603
|
await this.disconnect()
|
|
@@ -589,7 +625,7 @@ class QueryStream extends Query {
|
|
|
589
625
|
})
|
|
590
626
|
this.connection.flush()
|
|
591
627
|
}
|
|
592
|
-
: () => {},
|
|
628
|
+
: () => { },
|
|
593
629
|
})
|
|
594
630
|
this.push = this.stream.push.bind(this.stream)
|
|
595
631
|
|
|
@@ -704,7 +740,7 @@ class ParameterStream extends Writable {
|
|
|
704
740
|
}
|
|
705
741
|
|
|
706
742
|
// Used by the client to handle timeouts
|
|
707
|
-
callback() {}
|
|
743
|
+
callback() { }
|
|
708
744
|
|
|
709
745
|
_write(chunk, enc, cb) {
|
|
710
746
|
return this.flush(chunk, cb)
|
|
@@ -6,7 +6,8 @@ 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
12
|
indexof: (x, y) => `strpos(${x},${y}) - 1`, // sqlite instr is 1 indexed
|
|
12
13
|
startswith: (x, y) => `strpos(${x},${y}) = 1`, // sqlite instr is 1 indexed
|
|
@@ -18,9 +19,14 @@ const StandardFunctions = {
|
|
|
18
19
|
year: x => `date_part('year', ${castVal(x)})`,
|
|
19
20
|
month: x => `date_part('month', ${castVal(x)})`,
|
|
20
21
|
day: x => `date_part('day', ${castVal(x)})`,
|
|
22
|
+
time: x => `to_char(${castVal(x)}, 'HH24:MI:SS')`,
|
|
21
23
|
hour: x => `date_part('hour', ${castVal(x)})`,
|
|
22
24
|
minute: x => `date_part('minute', ${castVal(x)})`,
|
|
23
|
-
second: x => `date_part('second', ${castVal(x)})`,
|
|
25
|
+
second: x => `floor(date_part('second', ${castVal(x)}))`,
|
|
26
|
+
fractionalseconds: x => `CAST(date_part('second', ${castVal(x)}) - floor(date_part('second', ${castVal(x)})) AS DECIMAL)`,
|
|
27
|
+
now: function() {
|
|
28
|
+
return this.session_context({val: '$now'})
|
|
29
|
+
}
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
const isTime = /^\d{1,2}:\d{1,2}:\d{1,2}$/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/postgres",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
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": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"start": "docker-compose -f pg-stack.yml up -d"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@cap-js/db-service": "^1.
|
|
34
|
+
"@cap-js/db-service": "^1.9.0",
|
|
35
35
|
"pg": "^8"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|