@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 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
 
@@ -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 })
@@ -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(?)', 'jsonb_array_elements($1::jsonb)')
401
- .replace(/json_type\((\w+),'\$\."(\w+)"'\)/g, (_a, b, c) => `jsonb_typeof(${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('./func') }
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
- 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
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.6.0",
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.7.0",
34
+ "@cap-js/db-service": "^1.9.0",
35
35
  "pg": "^8"
36
36
  },
37
37
  "peerDependencies": {