@cap-js/postgres 1.10.0 → 1.10.2

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,22 @@
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.10.2](https://github.com/cap-js/cds-dbs/compare/postgres-v1.10.1...postgres-v1.10.2) (2024-10-28)
8
+
9
+
10
+ ### Fixed
11
+
12
+ * properly support `default`, `cds.on.insert` and `cds.on.update` for `UPSERT` queries ([#425](https://github.com/cap-js/cds-dbs/issues/425)) ([338e9f5](https://github.com/cap-js/cds-dbs/commit/338e9f5de9109d36013208547fc648c17ce8c7b0))
13
+
14
+ ## [1.10.1](https://github.com/cap-js/cds-dbs/compare/postgres-v1.10.0...postgres-v1.10.1) (2024-10-15)
15
+
16
+
17
+ ### Fixed
18
+
19
+ * add cds schema for postgres build plugin ([#843](https://github.com/cap-js/cds-dbs/issues/843)) ([6306d5c](https://github.com/cap-js/cds-dbs/commit/6306d5ce50c071b38a3d9f61b0820ea713a782d8))
20
+ * Improved behavioral consistency between the database services ([#837](https://github.com/cap-js/cds-dbs/issues/837)) ([b6f7187](https://github.com/cap-js/cds-dbs/commit/b6f718701e48dfb1c4c3d98ee016ec45930f8e7b))
21
+ * null as default value ([#845](https://github.com/cap-js/cds-dbs/issues/845)) ([0041ec0](https://github.com/cap-js/cds-dbs/commit/0041ec0a26c29b30f91470d93611b29acd837216))
22
+
7
23
  ## [1.10.0](https://github.com/cap-js/cds-dbs/compare/postgres-v1.9.1...postgres-v1.10.0) (2024-07-25)
8
24
 
9
25
 
@@ -253,7 +253,7 @@ GROUP BY k
253
253
  return this.dbc.query(sql)
254
254
  }
255
255
 
256
- onPlainSQL(req, next) {
256
+ async onPlainSQL(req, next) {
257
257
  const query = req.query
258
258
  if (this.options.independentDeploy) {
259
259
  // REVISIT: Should not be needed when deployment supports all types or sends CQNs
@@ -289,8 +289,19 @@ GROUP BY k
289
289
  // eslint-disable-next-line no-unused-vars
290
290
  req.query = query.replace(/('|")(\1|[^\1]*?\1)|(\?)/g, (a, _b, _c, d, _e, _f, _g) => (d ? '$' + i++ : a))
291
291
  }
292
-
293
- return super.onPlainSQL(req, next)
292
+ try {
293
+ return await super.onPlainSQL(req, next)
294
+ }
295
+ catch (err) {
296
+ if (err.code === '3F000') {
297
+ if (this.options?.credentials?.schema) {
298
+ cds.error`Failed to configure schema ("${this.options?.credentials?.schema}") before plainSQL call: ${req.query}`
299
+ } else {
300
+ cds.error`No schema was configure / detected before plainSQL call: ${req.query}`
301
+ }
302
+ }
303
+ throw err
304
+ }
294
305
  }
295
306
 
296
307
  async onSELECT({ query, data }) {
@@ -413,11 +424,32 @@ GROUP BY k
413
424
 
414
425
  // REVISIT: this should probably be made a bit easier to adopt
415
426
  return (this.sql = this.sql
416
- // Adjusts json path expressions to be postgres specific
417
- .replace(/->>'\$(?:(?:\."(.*?)")|(?:\[(\d*)\]))'/g, (a, b, c) => (b ? `->>'${b}'` : `->>${c}`))
418
427
  // Adjusts json function to be postgres specific
419
428
  .replace('json_each(?)', 'json_array_elements($1::json)')
420
- .replace(/json_type\((\w+),'\$\."(\w+)"'\)/g, (_a, b, c) => `json_typeof(${b}->'${c}')`))
429
+ )
430
+ }
431
+
432
+ UPSERT(q, isUpsert = false) {
433
+ super.UPSERT(q, isUpsert)
434
+
435
+ // REVISIT: this should probably be made a bit easier to adopt
436
+ return (this.sql = this.sql
437
+ // Adjusts json function to be postgres specific
438
+ .replace('json_each(?)', 'json_array_elements($1::json)')
439
+ )
440
+ }
441
+
442
+ managed_extract(name, element, converter) {
443
+ const { UPSERT, INSERT } = this.cqn
444
+ const extract = !(INSERT?.entries || UPSERT?.entries) && (INSERT?.rows || UPSERT?.rows)
445
+ ? `value->>${this.columns.indexOf(name)}`
446
+ : `value->>'${name.replace(/'/g, "''")}'`
447
+ const sql = converter?.(extract) || extract
448
+ return { extract, sql }
449
+ }
450
+
451
+ managed_default(name, managed, src) {
452
+ return `(CASE WHEN json_typeof(value->${this.managed_extract(name).extract.slice(8)}) IS NULL THEN ${managed} ELSE ${src} END)`
421
453
  }
422
454
 
423
455
  param({ ref }) {
@@ -459,7 +491,7 @@ GROUP BY k
459
491
  }
460
492
 
461
493
  defaultValue(defaultValue = this.context.timestamp.toISOString()) {
462
- return this.string(`${defaultValue}`)
494
+ return typeof defaultValue === 'string' ? this.string(`${defaultValue}`) : defaultValue
463
495
  }
464
496
 
465
497
  static Functions = { ...super.Functions, ...require('./cql-functions') }
@@ -492,27 +524,31 @@ GROUP BY k
492
524
  // Used for INSERT statements
493
525
  static InputConverters = {
494
526
  ...super.InputConverters,
495
- // UUID: (e) => `CAST(${e} as UUID)`, // UUID is strict in formatting sflight does not comply
496
- boolean: e => `CASE ${e} WHEN 'true' THEN true WHEN 'false' THEN false END`,
497
- Float: (e, t) => `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
498
- Decimal: (e, t) => `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
499
- Integer: e => `CAST(${e} as integer)`,
500
- Int64: e => `CAST(${e} as bigint)`,
501
- Date: e => `CAST(${e} as DATE)`,
502
- Time: e => `CAST(${e} as TIME)`,
503
- DateTime: e => `CAST(${e} as TIMESTAMP)`,
504
- Timestamp: e => `CAST(${e} as TIMESTAMP)`,
527
+ // UUID: (e) => e[0] === '$' ? e : `CAST(${e} as UUID)`, // UUID is strict in formatting sflight does not comply
528
+ boolean: e => e[0] === '$' ? e : `CASE ${e} WHEN 'true' THEN true WHEN 'false' THEN false END`,
529
+ // REVISIT: Postgres and HANA round Decimal numbers differently therefore precision and scale are removed
530
+ // Float: (e, t) => e[0] === '$' ? e : `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
531
+ // Decimal: (e, t) => e[0] === '$' ? e : `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
532
+ Float: e => e[0] === '$' ? e : `CAST(${e} as decimal)`,
533
+ Decimal: e => e[0] === '$' ? e : `CAST(${e} as decimal)`,
534
+ Integer: e => e[0] === '$' ? e : `CAST(${e} as integer)`,
535
+ Int64: e => e[0] === '$' ? e : `CAST(${e} as bigint)`,
536
+ Date: e => e[0] === '$' ? e : `CAST(${e} as DATE)`,
537
+ Time: e => e[0] === '$' ? e : `CAST(${e} as TIME)`,
538
+ DateTime: e => e[0] === '$' ? e : `CAST(${e} as TIMESTAMP)`,
539
+ Timestamp: e => e[0] === '$' ? e : `CAST(${e} as TIMESTAMP)`,
505
540
  // REVISIT: Remove that with upcomming fixes in cds.linked
506
- Double: (e, t) => `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
507
- DecimalFloat: (e, t) => `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
508
- Binary: e => `DECODE(${e},'base64')`,
509
- LargeBinary: e => `DECODE(${e},'base64')`,
541
+ Double: (e, t) => e[0] === '$' ? e : `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
542
+ DecimalFloat: (e, t) => e[0] === '$' ? e : `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
543
+ Binary: e => e[0] === '$' ? e : `DECODE(${e},'base64')`,
544
+ LargeBinary: e => e[0] === '$' ? e : `DECODE(${e},'base64')`,
510
545
 
511
546
  // 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})`,
547
+ 'cds.hana.CLOB': e => e[0] === '$' ? e : `DECODE(${e},'base64')`,
548
+ 'cds.hana.BINARY': e => e[0] === '$' ? e : `DECODE(${e},'base64')`,
549
+ // REVISIT: have someone take a look at how this syntax exactly works in postgres with postgis
550
+ 'cds.hana.ST_POINT': e => `(${e})::point`,
551
+ 'cds.hana.ST_GEOMETRY': e => `(${e})::polygon`,
516
552
  }
517
553
 
518
554
  static OutputConverters = {
@@ -532,10 +568,14 @@ GROUP BY k
532
568
  Int64: cds.env.features.ieee754compatible ? expr => `cast(${expr} as varchar)` : undefined,
533
569
  // REVISIT: always cast to string in next major
534
570
  // Reading decimal as string to not loose precision
535
- Decimal: cds.env.features.ieee754compatible ? expr => `cast(${expr} as varchar)` : undefined,
571
+ Decimal: cds.env.features.ieee754compatible ? (expr, elem) => elem?.scale
572
+ ? `to_char(${expr},'FM${'0'.padStart(elem.precision, '9')}${'D'.padEnd(elem.scale + 1, '0')}')`
573
+ : `cast(${expr} as varchar)`
574
+ : undefined,
536
575
 
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`,
576
+ // Convert ST types back to WKT format
577
+ 'cds.hana.ST_POINT': expr => `ST_AsText(${expr})`,
578
+ 'cds.hana.ST_POINT': expr => `ST_AsText(${expr})`,
539
579
  }
540
580
  }
541
581
 
@@ -567,14 +607,21 @@ GROUP BY k
567
607
  `)
568
608
  await this.exec(`CREATE DATABASE "${creds.database}" OWNER="${creds.user}" TEMPLATE=template0`)
569
609
  } catch {
610
+ // Failed to connect to database
611
+ if (!this.dbc) {
612
+ return this.database({ database })
613
+ }
570
614
  // Failed to reset database
571
615
  } finally {
572
- await this.dbc.end()
573
- delete this.dbc
574
-
575
- // Update credentials to new Database owner
576
- await this.disconnect()
577
- this.options.credentials = Object.assign({}, system, creds)
616
+ // Only clean when successfully connected
617
+ if (this.dbc) {
618
+ await this.dbc.end()
619
+ delete this.dbc
620
+
621
+ // Update credentials to new Database owner
622
+ await this.disconnect()
623
+ this.options.credentials = Object.assign({}, system, creds)
624
+ }
578
625
  }
579
626
  }
580
627
 
@@ -589,18 +636,20 @@ GROUP BY k
589
636
 
590
637
  try {
591
638
  if (!clean) {
592
- await this.tx(async tx => {
593
- // await tx.run(`DROP USER IF EXISTS "${creds.user}"`)
594
- await tx
595
- .run(`CREATE USER "${creds.user}" IN GROUP "${creds.usergroup}" PASSWORD '${creds.password}'`)
596
- .catch(e => {
597
- if (e.code === '42710') return
598
- throw e
599
- })
600
- })
601
- await this.tx(async tx => {
602
- await tx.run(`GRANT CREATE, CONNECT ON DATABASE "${creds.database}" TO "${creds.user}";`)
603
- })
639
+ await cds
640
+ .run(`CREATE USER "${creds.user}" IN GROUP "${creds.usergroup}" PASSWORD '${creds.password}'`)
641
+ .catch(e => {
642
+ if (e.code === '42710') return
643
+ throw e
644
+ })
645
+ // Retry granting priviledges as this is being done by multiple instances
646
+ // Postgres just rejects when other connections are granting the same user
647
+ const grant = (i = 0) => cds.run(`GRANT CREATE, CONNECT ON DATABASE "${creds.database}" TO "${creds.user}";`)
648
+ .catch((err) => {
649
+ if (i > 100) throw err
650
+ return grant(i + 1)
651
+ })
652
+ await grant()
604
653
  }
605
654
 
606
655
  // Update credentials to new Schema owner
@@ -610,7 +659,7 @@ GROUP BY k
610
659
  // Create new schema using schema owner
611
660
  await this.tx(async tx => {
612
661
  await tx.run(`DROP SCHEMA IF EXISTS "${creds.schema}" CASCADE`)
613
- if (!clean) await tx.run(`CREATE SCHEMA "${creds.schema}" AUTHORIZATION "${creds.user}"`).catch(() => { })
662
+ if (!clean) await tx.run(`CREATE SCHEMA "${creds.schema}" AUTHORIZATION "${creds.user}"`)
614
663
  })
615
664
  } finally {
616
665
  await this.disconnect()
@@ -24,6 +24,26 @@ const StandardFunctions = {
24
24
  minute: x => `date_part('minute', ${castVal(x)})`,
25
25
  second: x => `floor(date_part('second', ${castVal(x)}))`,
26
26
  fractionalseconds: x => `CAST(date_part('second', ${castVal(x)}) - floor(date_part('second', ${castVal(x)})) AS DECIMAL)`,
27
+ totalseconds: x => `(
28
+ (
29
+ (
30
+ CAST(substring(${x},2,strpos(${x},'DT') - 2) AS INTEGER)
31
+ ) + (
32
+ EXTRACT (EPOCH FROM
33
+ CAST(
34
+ replace(
35
+ replace(
36
+ replace(
37
+ substring(${x},strpos(${x},'DT') + 2),
38
+ 'H',':'
39
+ ),'M',':'
40
+ ),'S','Z'
41
+ )
42
+ as TIME)
43
+ ) - 0.5
44
+ )
45
+ ) * 86400
46
+ )`,
27
47
  now: function() {
28
48
  return this.session_context({val: '$now'})
29
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/postgres",
3
- "version": "1.10.0",
3
+ "version": "1.10.2",
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": {
@@ -23,8 +23,8 @@
23
23
  "CHANGELOG.md"
24
24
  ],
25
25
  "scripts": {
26
- "test": "npm start && jest --silent",
27
- "start": "docker-compose -f pg-stack.yml up -d"
26
+ "test": "npm start && cds-test $(../test/find)",
27
+ "start": "docker compose -f pg-stack.yml up -d"
28
28
  },
29
29
  "dependencies": {
30
30
  "@cap-js/db-service": "^1.9.0",
@@ -67,6 +67,12 @@
67
67
  }
68
68
  },
69
69
  "db": "sql"
70
+ },
71
+ "schema": {
72
+ "buildTaskType": {
73
+ "name": "postgres",
74
+ "description": "Postgres database build plugin"
75
+ }
70
76
  }
71
77
  },
72
78
  "license": "SEE LICENSE"