@cap-js/postgres 1.7.0 → 1.9.1

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,35 @@
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.9.1](https://github.com/cap-js/cds-dbs/compare/postgres-v1.9.0...postgres-v1.9.1) (2024-07-09)
8
+
9
+
10
+ ### Fixed
11
+
12
+ * expand reach of `cds.features.ieee754compatible` to `int64` ([#722](https://github.com/cap-js/cds-dbs/issues/722)) ([7eef5e9](https://github.com/cap-js/cds-dbs/commit/7eef5e9c5ec286285b2552abd1e673175c59fdc1))
13
+
14
+ ## [1.9.0](https://github.com/cap-js/cds-dbs/compare/postgres-v1.8.0...postgres-v1.9.0) (2024-05-29)
15
+
16
+
17
+ ### Added
18
+
19
+ * Add simple queries feature flag ([#660](https://github.com/cap-js/cds-dbs/issues/660)) ([3335202](https://github.com/cap-js/cds-dbs/commit/33352024201a96cc6bdfa30a0fe3fff4227dee10))
20
+
21
+ ## [1.8.0](https://github.com/cap-js/cds-dbs/compare/postgres-v1.7.0...postgres-v1.8.0) (2024-05-08)
22
+
23
+
24
+ ### Added
25
+
26
+ * 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))
27
+
28
+
29
+ ### Fixed
30
+
31
+ * 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))
32
+ * 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))
33
+ * 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))
34
+ * 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))
35
+
7
36
  ## [1.7.0](https://github.com/cap-js/cds-dbs/compare/postgres-v1.6.0...postgres-v1.7.0) (2024-04-12)
8
37
 
9
38
 
package/README.md CHANGED
@@ -47,7 +47,7 @@ However, some preliminary checks and cleanups help:
47
47
 
48
48
  ### schema migration
49
49
 
50
- `@cap-js/postgres` brings the same schema evolution capabilities to PostgreSQL known from HANA and SQLite.
50
+ `@cap-js/postgres` brings the same schema evolution capabilities to PostgreSQL known from SAP HANA and SQLite.
51
51
  Enabling schema migration in an existing `cds-pg`-based project consists of generating and deploying a "csn-snapshot" of your database structure.
52
52
 
53
53
  #### local development
@@ -1,6 +1,6 @@
1
1
  const { SQLService } = require('@cap-js/db-service')
2
2
  const { Client, Query } = require('pg')
3
- const cds = require('@sap/cds/lib')
3
+ const cds = require('@sap/cds')
4
4
  const crypto = require('crypto')
5
5
  const { Writable, Readable } = require('stream')
6
6
  const sessionVariableMap = require('./session.json')
@@ -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
- // REVISIT: SQLService provides empty values as {} for plain SQL statements - PostgreSQL driver expects array or nothing - see issue #78
150
- let newQuery = this._prepareStreams(query, values)
151
- if (typeof newQuery.then === 'function') newQuery = await newQuery
152
- const result = await this.dbc.query(newQuery)
153
- return { changes: result.rowCount }
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
- // REVISIT: SQLService provides empty values as {} for plain SQL statements - PostgreSQL driver expects array or nothing - see issue #78
157
- const result = await this.dbc.query({ ...query, values: this._getValues(values) })
158
- return result.rows[0]
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 Object.assign(e, { sql: sql + '\n' + new Array(e.position).fill(' ').join('') + '^' })
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 Object.assign(e, { sql: sql + '\n' + new Array(e.position).fill(' ').join('') + '^' })
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[index]
301
+ const binary = columns.splice(index, 1)
292
302
  // SELECT without binary column
293
- columns.splice(index, 1)
294
- const { sql, values } = this.cqn2sql(query, data)
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 = [binary]
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[binary.as || binary.ref[binary.ref.length - 1]] = stream
311
+ res[this.class.CQN2SQL.prototype.column_name(binary[0])] = stream
306
312
  return res
307
313
  }
308
314
  return super.onSELECT({ query, data })
@@ -364,7 +370,8 @@ GROUP BY k
364
370
  return super.column_alias4(x, q)
365
371
  }
366
372
 
367
- SELECT_expand({ SELECT }, sql) {
373
+ SELECT_expand(q, sql) {
374
+ const { SELECT } = q
368
375
  if (!SELECT.columns) return sql
369
376
  const queryAlias = this.quote(SELECT.from?.as || (SELECT.expand === 'root' && 'root'))
370
377
  const cols = SELECT.columns.map(x => {
@@ -379,10 +386,22 @@ GROUP BY k
379
386
  }
380
387
  return col
381
388
  })
389
+ const isRoot = SELECT.expand === 'root'
390
+ const isSimple = cds.env.features.sql_simple_queries &&
391
+ isRoot && // Simple queries are only allowed to have a root
392
+ !Object.keys(q.elements).some(e =>
393
+ q.elements[e].isAssociation || // Indicates columns contains an expand
394
+ q.elements[e].$assocExpand || // REVISIT: sometimes associations are structs
395
+ q.elements[e].items // Array types require to be inlined with a json result
396
+ )
397
+
398
+ const subQuery = `SELECT ${cols} FROM (${sql}) as ${queryAlias}`
399
+ if (isSimple) return subQuery
400
+
382
401
  // REVISIT: Remove SELECT ${cols} by adjusting SELECT_columns
383
402
  let obj = `to_jsonb(${queryAlias}.*)`
384
- return `SELECT ${SELECT.one || SELECT.expand === 'root' ? obj : `coalesce(jsonb_agg (${obj}),'[]'::jsonb)`
385
- } as _json_ FROM (SELECT ${cols} FROM (${sql}) as ${queryAlias}) as ${queryAlias}`
403
+ return `SELECT ${SELECT.one || isRoot ? obj : `coalesce(jsonb_agg (${obj}),'[]'::jsonb)`
404
+ } as _json_ FROM (${subQuery}) as ${queryAlias}`
386
405
  }
387
406
 
388
407
  doubleQuote(name) {
@@ -397,8 +416,8 @@ GROUP BY k
397
416
  // Adjusts json path expressions to be postgres specific
398
417
  .replace(/->>'\$(?:(?:\."(.*?)")|(?:\[(\d*)\]))'/g, (a, b, c) => (b ? `->>'${b}'` : `->>${c}`))
399
418
  // Adjusts json function to be postgres specific
400
- .replace('json_each(?)', 'jsonb_array_elements($1::jsonb)')
401
- .replace(/json_type\((\w+),'\$\."(\w+)"'\)/g, (_a, b, c) => `jsonb_typeof(${b}->'${c}')`))
419
+ .replace('json_each(?)', 'json_array_elements($1::json)')
420
+ .replace(/json_type\((\w+),'\$\."(\w+)"'\)/g, (_a, b, c) => `json_typeof(${b}->'${c}')`))
402
421
  }
403
422
 
404
423
  param({ ref }) {
@@ -431,11 +450,19 @@ GROUP BY k
431
450
  return 'FOR SHARE'
432
451
  }
433
452
 
453
+ // Postgres requires own quote function, becuase the effective name is lower case
454
+ quote(s) {
455
+ if (typeof s !== 'string') return '"' + s + '"'
456
+ if (s.includes('"')) return '"' + s.replace(/"/g, '""').toLowerCase() + '"'
457
+ if (s in this.class.ReservedWords || !/^[A-Za-z_][A-Za-z_$0-9]*$/.test(s)) return '"' + s.toLowerCase() + '"'
458
+ return s
459
+ }
460
+
434
461
  defaultValue(defaultValue = this.context.timestamp.toISOString()) {
435
462
  return this.string(`${defaultValue}`)
436
463
  }
437
464
 
438
- static Functions = { ...super.Functions, ...require('./func') }
465
+ static Functions = { ...super.Functions, ...require('./cql-functions') }
439
466
 
440
467
  static ReservedWords = { ...super.ReservedWords, ...require('./ReservedWords.json') }
441
468
 
@@ -443,6 +470,7 @@ GROUP BY k
443
470
  ...super.TypeMap,
444
471
  // REVISIT: check whether we should use native UUID support
445
472
  UUID: () => `VARCHAR(36)`,
473
+ UInt8: () => `INT`,
446
474
  String: e => `VARCHAR(${e.length || 5000})`,
447
475
  Binary: () => `BYTEA`,
448
476
  Double: () => 'FLOAT8',
@@ -452,6 +480,13 @@ GROUP BY k
452
480
  Time: () => 'TIME',
453
481
  DateTime: () => 'TIMESTAMP',
454
482
  Timestamp: () => 'TIMESTAMP',
483
+
484
+ // HANA Types
485
+ 'cds.hana.CLOB': () => 'BYTEA',
486
+ 'cds.hana.BINARY': () => 'BYTEA',
487
+ 'cds.hana.TINYINT': () => 'SMALLINT',
488
+ 'cds.hana.ST_POINT': () => 'POINT',
489
+ 'cds.hana.ST_GEOMETRY': () => 'POLYGON',
455
490
  }
456
491
 
457
492
  // Used for INSERT statements
@@ -472,6 +507,12 @@ GROUP BY k
472
507
  DecimalFloat: (e, t) => `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
473
508
  Binary: e => `DECODE(${e},'base64')`,
474
509
  LargeBinary: e => `DECODE(${e},'base64')`,
510
+
511
+ // HANA Types
512
+ 'cds.hana.CLOB': e => `DECODE(${e},'base64')`,
513
+ 'cds.hana.BINARY': e => `DECODE(${e},'base64')`,
514
+ 'cds.hana.ST_POINT': e => `POINT(((${e})::json->>'x')::float, ((${e})::json->>'y')::float)`,
515
+ 'cds.hana.ST_GEOMETRY': e => `POLYGON(${e})`,
475
516
  }
476
517
 
477
518
  static OutputConverters = {
@@ -487,6 +528,14 @@ GROUP BY k
487
528
  Association: e => `jsonb(${e})`,
488
529
  struct: e => `jsonb(${e})`,
489
530
  array: e => `jsonb(${e})`,
531
+ // Reading int64 as string to not loose precision
532
+ Int64: cds.env.features.ieee754compatible ? expr => `cast(${expr} as varchar)` : undefined,
533
+ // REVISIT: always cast to string in next major
534
+ // Reading decimal as string to not loose precision
535
+ Decimal: cds.env.features.ieee754compatible ? expr => `cast(${expr} as varchar)` : undefined,
536
+
537
+ // Convert point back to json format
538
+ 'cds.hana.ST_POINT': expr => `CASE WHEN (${expr}) IS NOT NULL THEN json_object('x':(${expr})[0],'y':(${expr})[1])::varchar END`,
490
539
  }
491
540
  }
492
541
 
@@ -517,7 +566,7 @@ GROUP BY k
517
566
  GRANT "${creds.usergroup}" TO "${creds.user}" WITH ADMIN OPTION;
518
567
  `)
519
568
  await this.exec(`CREATE DATABASE "${creds.database}" OWNER="${creds.user}" TEMPLATE=template0`)
520
- } catch (e) {
569
+ } catch {
521
570
  // Failed to reset database
522
571
  } finally {
523
572
  await this.dbc.end()
@@ -561,7 +610,7 @@ GROUP BY k
561
610
  // Create new schema using schema owner
562
611
  await this.tx(async tx => {
563
612
  await tx.run(`DROP SCHEMA IF EXISTS "${creds.schema}" CASCADE`)
564
- if (!clean) await tx.run(`CREATE SCHEMA "${creds.schema}" AUTHORIZATION "${creds.user}"`).catch(() => {})
613
+ if (!clean) await tx.run(`CREATE SCHEMA "${creds.schema}" AUTHORIZATION "${creds.user}"`).catch(() => { })
565
614
  })
566
615
  } finally {
567
616
  await this.disconnect()
@@ -589,7 +638,7 @@ class QueryStream extends Query {
589
638
  })
590
639
  this.connection.flush()
591
640
  }
592
- : () => {},
641
+ : () => { },
593
642
  })
594
643
  this.push = this.stream.push.bind(this.stream)
595
644
 
@@ -704,7 +753,7 @@ class ParameterStream extends Writable {
704
753
  }
705
754
 
706
755
  // Used by the client to handle timeouts
707
- callback() {}
756
+ callback() { }
708
757
 
709
758
  _write(chunk, enc, cb) {
710
759
  return this.flush(chunk, cb)
@@ -6,10 +6,11 @@ const StandardFunctions = {
6
6
  if (x.val === '$now') sql += '::timestamp'
7
7
  return sql
8
8
  },
9
- countdistinct: x => `count(distinct ${x || '*'})`,
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
- indexof: (x, y) => `strpos(${x},${y}) - 1`, // sqlite instr is 1 indexed
12
- startswith: (x, y) => `strpos(${x},${y}) = 1`, // sqlite instr is 1 indexed
12
+ indexof: (x, y) => `strpos(${x},${y}) - 1`, // strpos is 1 indexed
13
+ startswith: (x, y) => `strpos(${x},${y}) = 1`, // strpos is 1 indexed
13
14
  endswith: (x, y) => `substr(${x},length(${x}) + 1 - length(${y})) = ${y}`,
14
15
  matchesPattern: (x, y) => `regexp_like(${x}, ${y})`,
15
16
  matchespattern: (x, y) => `regexp_like(${x}, ${y})`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/postgres",
3
- "version": "1.7.0",
3
+ "version": "1.9.1",
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": {
@@ -22,16 +22,12 @@
22
22
  "lib",
23
23
  "CHANGELOG.md"
24
24
  ],
25
- "engines": {
26
- "node": ">=16",
27
- "npm": ">=8"
28
- },
29
25
  "scripts": {
30
26
  "test": "npm start && jest --silent",
31
27
  "start": "docker-compose -f pg-stack.yml up -d"
32
28
  },
33
29
  "dependencies": {
34
- "@cap-js/db-service": "^1.7.0",
30
+ "@cap-js/db-service": "^1.9.0",
35
31
  "pg": "^8"
36
32
  },
37
33
  "peerDependencies": {