@cap-js/db-service 1.13.0 → 1.14.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 +13 -0
- package/lib/SQLService.js +4 -0
- package/lib/cql-functions.js +3 -1
- package/lib/cqn2sql.js +14 -7
- package/lib/cqn4sql.js +9 -16
- package/lib/infer/index.js +4 -3
- package/lib/utils.js +19 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
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.14.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.13.0...db-service-v1.14.0) (2024-10-15)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* assoc-like calc elements after exists predicate ([#831](https://github.com/cap-js/cds-dbs/issues/831)) ([05f7d75](https://github.com/cap-js/cds-dbs/commit/05f7d75837495d58cc4f72ad628077bdebb0acf6))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
* 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))
|
|
18
|
+
* Treat assoc-like calculated elements as unmanaged assocs ([#830](https://github.com/cap-js/cds-dbs/issues/830)) ([cbe0df7](https://github.com/cap-js/cds-dbs/commit/cbe0df7a66fec0d421947767adc8621ed8bf236c))
|
|
19
|
+
|
|
7
20
|
## [1.13.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.12.1...db-service-v1.13.0) (2024-10-01)
|
|
8
21
|
|
|
9
22
|
|
package/lib/SQLService.js
CHANGED
package/lib/cql-functions.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const cds = require("@sap/cds")
|
|
2
|
+
|
|
1
3
|
const StandardFunctions = {
|
|
2
4
|
// OData: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_CanonicalFunctions
|
|
3
5
|
|
|
@@ -59,7 +61,7 @@ const StandardFunctions = {
|
|
|
59
61
|
* @param {string} x
|
|
60
62
|
* @returns {string}
|
|
61
63
|
*/
|
|
62
|
-
countdistinct: x => `count(distinct ${x ||
|
|
64
|
+
countdistinct: x => `count(distinct ${x || cds.error`countdistinct requires a ref to be counted`})`,
|
|
63
65
|
/**
|
|
64
66
|
* Generates SQL statement that produces the index of the first occurrence of the second string in the first string
|
|
65
67
|
* @param {string} x
|
package/lib/cqn2sql.js
CHANGED
|
@@ -18,8 +18,8 @@ const DEBUG = (() => {
|
|
|
18
18
|
return cds.debug('sql|sqlite')
|
|
19
19
|
//if (DEBUG) {
|
|
20
20
|
// return DEBUG
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// (sql, ...more) => DEBUG (sql.replace(/(?:SELECT[\n\r\s]+(json_group_array\()?[\n\r\s]*json_insert\((\n|\r|.)*?\)[\n\r\s]*\)?[\n\r\s]+as[\n\r\s]+_json_[\n\r\s]+FROM[\n\r\s]*\(|\)[\n\r\s]*(\)[\n\r\s]+AS )|\)$)/gim,(a,b,c,d) => d || ''), ...more)
|
|
22
|
+
// FIXME: looses closing ) on INSERT queries
|
|
23
23
|
//}
|
|
24
24
|
})()
|
|
25
25
|
|
|
@@ -88,6 +88,7 @@ class CQN2SQLRenderer {
|
|
|
88
88
|
this.values = [] // prepare values, filled in by subroutines
|
|
89
89
|
this[kind]((this.cqn = q)) // actual sql rendering happens here
|
|
90
90
|
if (vars?.length && !this.values?.length) this.values = vars
|
|
91
|
+
if (vars && Object.keys(vars).length && !this.values?.length) this.values = vars
|
|
91
92
|
const sanitize_values = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
|
|
92
93
|
DEBUG?.(
|
|
93
94
|
this.sql,
|
|
@@ -116,8 +117,13 @@ class CQN2SQLRenderer {
|
|
|
116
117
|
* @param {import('./infer/cqn').CREATE} q
|
|
117
118
|
*/
|
|
118
119
|
CREATE(q) {
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
let { target } = q
|
|
121
|
+
let query = target?.query || q.CREATE.as
|
|
122
|
+
if (!target || target._unresolved) {
|
|
123
|
+
const entity = q.CREATE.entity
|
|
124
|
+
target = typeof entity === 'string' ? { name: entity } : q.CREATE.entity
|
|
125
|
+
}
|
|
126
|
+
|
|
121
127
|
const name = this.name(target.name)
|
|
122
128
|
// Don't allow place holders inside views
|
|
123
129
|
delete this.values
|
|
@@ -203,8 +209,9 @@ class CQN2SQLRenderer {
|
|
|
203
209
|
*/
|
|
204
210
|
DROP(q) {
|
|
205
211
|
const { target } = q
|
|
206
|
-
const isView = target
|
|
207
|
-
|
|
212
|
+
const isView = target?.query || target?.projection || q.DROP.view
|
|
213
|
+
const name = target?.name || q.DROP.table?.ref?.[0] || q.DROP.view?.ref?.[0]
|
|
214
|
+
return (this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} IF EXISTS ${this.quote(this.name(name))}`)
|
|
208
215
|
}
|
|
209
216
|
|
|
210
217
|
// SELECT Statements ------------------------------------------------
|
|
@@ -223,7 +230,7 @@ class CQN2SQLRenderer {
|
|
|
223
230
|
|
|
224
231
|
// REVISIT: When selecting from an entity that is not in the model the from.where are not normalized (as cqn4sql is skipped)
|
|
225
232
|
if (!where && from?.ref?.length === 1 && from.ref[0]?.where) where = from.ref[0]?.where
|
|
226
|
-
|
|
233
|
+
const columns = this.SELECT_columns(q)
|
|
227
234
|
let sql = `SELECT`
|
|
228
235
|
if (distinct) sql += ` DISTINCT`
|
|
229
236
|
if (!_empty(columns)) sql += ` ${columns}`
|
package/lib/cqn4sql.js
CHANGED
|
@@ -4,7 +4,7 @@ const cds = require('@sap/cds')
|
|
|
4
4
|
|
|
5
5
|
const infer = require('./infer')
|
|
6
6
|
const { computeColumnsToBeSearched } = require('./search')
|
|
7
|
-
const { prettyPrintRef } = require('./utils')
|
|
7
|
+
const { prettyPrintRef, isCalculatedOnRead, isCalculatedElement } = require('./utils')
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* For operators of <eqOps>, this is replaced by comparing all leaf elements with null, combined with and.
|
|
@@ -317,10 +317,6 @@ function cqn4sql(originalQuery, model) {
|
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
function isCalculatedOnRead(def) {
|
|
321
|
-
return def?.value && !def.value.stored
|
|
322
|
-
}
|
|
323
|
-
|
|
324
320
|
/**
|
|
325
321
|
* Walks over a list of columns (ref's, xpr, subqueries, val), applies flattening on structured types and expands wildcards.
|
|
326
322
|
*
|
|
@@ -809,7 +805,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
809
805
|
const subqueryBase = {}
|
|
810
806
|
for (const [key, value] of Object.entries(column)) {
|
|
811
807
|
if (!(key in { ref: true, expand: true })) {
|
|
812
|
-
|
|
808
|
+
subqueryBase[key] = value
|
|
813
809
|
}
|
|
814
810
|
}
|
|
815
811
|
const subquery = {
|
|
@@ -1365,20 +1361,17 @@ function cqn4sql(originalQuery, model) {
|
|
|
1365
1361
|
|
|
1366
1362
|
const as = getNextAvailableTableAlias(getLastStringSegment(next.alias))
|
|
1367
1363
|
next.alias = as
|
|
1368
|
-
if (next.definition.value) {
|
|
1369
|
-
throw new Error(
|
|
1370
|
-
`Calculated elements cannot be used in “exists” predicates in: “exists ${tokenStream[i + 1].ref
|
|
1371
|
-
.map(idOnly)
|
|
1372
|
-
.join('.')}”`,
|
|
1373
|
-
)
|
|
1374
|
-
}
|
|
1375
1364
|
if (!next.definition.target) {
|
|
1365
|
+
let type = next.definition.type
|
|
1366
|
+
if (isCalculatedElement(next.definition)) {
|
|
1367
|
+
// try to infer the type at the leaf for better error message
|
|
1368
|
+
const { $refLinks } = next.definition.value
|
|
1369
|
+
type = $refLinks?.at(-1).definition.type || 'expression'
|
|
1370
|
+
}
|
|
1376
1371
|
throw new Error(
|
|
1377
1372
|
`Expecting path “${tokenStream[i + 1].ref
|
|
1378
1373
|
.map(idOnly)
|
|
1379
|
-
.join('.')}” following “EXISTS” predicate to end with association/composition, found “${
|
|
1380
|
-
next.definition.type
|
|
1381
|
-
}”`,
|
|
1374
|
+
.join('.')}” following “EXISTS” predicate to end with association/composition, found “${type}”`,
|
|
1382
1375
|
)
|
|
1383
1376
|
}
|
|
1384
1377
|
const { definition: fkSource } = next
|
package/lib/infer/index.js
CHANGED
|
@@ -4,6 +4,7 @@ const cds = require('@sap/cds')
|
|
|
4
4
|
|
|
5
5
|
const JoinTree = require('./join-tree')
|
|
6
6
|
const { pseudos } = require('./pseudos')
|
|
7
|
+
const { isCalculatedOnRead } = require('../utils')
|
|
7
8
|
const cdsTypes = cds.linked({
|
|
8
9
|
definitions: {
|
|
9
10
|
Timestamp: { type: 'cds.Timestamp' },
|
|
@@ -746,7 +747,7 @@ function infer(originalQuery, model) {
|
|
|
746
747
|
joinTree.mergeColumn(colWithBase, originalQuery.outerQueries)
|
|
747
748
|
}
|
|
748
749
|
}
|
|
749
|
-
if (leafArt
|
|
750
|
+
if (isCalculatedOnRead(leafArt)) {
|
|
750
751
|
linkCalculatedElement(column, $baseLink, baseColumn)
|
|
751
752
|
}
|
|
752
753
|
|
|
@@ -1054,7 +1055,7 @@ function infer(originalQuery, model) {
|
|
|
1054
1055
|
if (element.type !== 'cds.LargeBinary') {
|
|
1055
1056
|
queryElements[k] = element
|
|
1056
1057
|
}
|
|
1057
|
-
if (element
|
|
1058
|
+
if (isCalculatedOnRead(element)) {
|
|
1058
1059
|
linkCalculatedElement(element)
|
|
1059
1060
|
}
|
|
1060
1061
|
}
|
|
@@ -1071,7 +1072,7 @@ function infer(originalQuery, model) {
|
|
|
1071
1072
|
if (exclude(name) || name in queryElements) return true
|
|
1072
1073
|
const element = tableAliases[0].tableAlias.elements[name]
|
|
1073
1074
|
if (element.type !== 'cds.LargeBinary') queryElements[name] = element
|
|
1074
|
-
if (element
|
|
1075
|
+
if (isCalculatedOnRead(element)) {
|
|
1075
1076
|
linkCalculatedElement(element)
|
|
1076
1077
|
}
|
|
1077
1078
|
})
|
package/lib/utils.js
CHANGED
|
@@ -21,7 +21,26 @@ function prettyPrintRef(ref, model = null) {
|
|
|
21
21
|
}, '')
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Determines if a definition is calculated on read.
|
|
26
|
+
* - Stored calculated elements are not unfolded
|
|
27
|
+
* - Association like calculated elements have been re-written by the compiler
|
|
28
|
+
* they essentially behave like unmanaged associations as their calculations
|
|
29
|
+
* have been incorporated into an on-condition which is handled elsewhere
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} def - The definition to check.
|
|
32
|
+
* @returns {boolean} - Returns true if the definition is calculated on read, otherwise false.
|
|
33
|
+
*/
|
|
34
|
+
function isCalculatedOnRead(def) {
|
|
35
|
+
return isCalculatedElement(def) && !def.value.stored && !def.on
|
|
36
|
+
}
|
|
37
|
+
function isCalculatedElement(def) {
|
|
38
|
+
return def?.value
|
|
39
|
+
}
|
|
40
|
+
|
|
24
41
|
// export the function to be used in other modules
|
|
25
42
|
module.exports = {
|
|
26
43
|
prettyPrintRef,
|
|
44
|
+
isCalculatedOnRead,
|
|
45
|
+
isCalculatedElement
|
|
27
46
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/db-service",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "CDS base database service",
|
|
5
5
|
"homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service",
|
|
6
6
|
"repository": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"CHANGELOG.md"
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
25
|
-
"test": "
|
|
25
|
+
"test": "cds-test"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"generic-pool": "^3.9.0"
|