@cap-js/db-service 1.7.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 +18 -0
- package/lib/SQLService.js +20 -6
- package/lib/cql-functions.js +14 -7
- package/lib/cqn4sql.js +33 -4
- package/lib/deep-queries.js +15 -2
- package/lib/infer/index.js +20 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@
|
|
|
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/db-service-v1.7.0...db-service-v1.8.0) (2024-04-12)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* 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))
|
|
13
|
+
* support HANA stored procedures ([#542](https://github.com/cap-js/cds-dbs/issues/542)) ([52a00a0](https://github.com/cap-js/cds-dbs/commit/52a00a0d642ba3c58dcad97b3ea1456f1bf3b04a))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
* **`expand`:** Only accept on structures, assocs or table aliases ([#551](https://github.com/cap-js/cds-dbs/issues/551)) ([3248512](https://github.com/cap-js/cds-dbs/commit/32485129147cd1b376f1d2faf2ea7c7232ba3794))
|
|
19
|
+
* **`order by`:** for localized sorting, prepend table alias ([#546](https://github.com/cap-js/cds-dbs/issues/546)) ([a273a92](https://github.com/cap-js/cds-dbs/commit/a273a9278b2551ed3381795effe28cf8de41b1bd))
|
|
20
|
+
* etag with stream_compat ([#562](https://github.com/cap-js/cds-dbs/issues/562)) ([b0a3a41](https://github.com/cap-js/cds-dbs/commit/b0a3a418fbcff7eb7e7b8fa4ff031e1c0c0faac4))
|
|
21
|
+
* exclude `cds.LargeBinary` from wildcard expansion ([#577](https://github.com/cap-js/cds-dbs/issues/577)) ([6661d63](https://github.com/cap-js/cds-dbs/commit/6661d635b2895a13d47e42495acf6fbd7247c535))
|
|
22
|
+
* Reduce insert queries for deep update ([#568](https://github.com/cap-js/cds-dbs/issues/568)) ([55e5114](https://github.com/cap-js/cds-dbs/commit/55e511471743c0445d41e8297f5530abe167a270))
|
|
23
|
+
* Reduced count query complexity when possible ([#553](https://github.com/cap-js/cds-dbs/issues/553)) ([3331f02](https://github.com/cap-js/cds-dbs/commit/3331f0224f02bd2e6cc9c6d2cd5f1c37a36ec8dd))
|
|
24
|
+
|
|
7
25
|
## [1.7.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.6.4...db-service-v1.7.0) (2024-03-22)
|
|
8
26
|
|
|
9
27
|
|
package/lib/SQLService.js
CHANGED
|
@@ -134,7 +134,15 @@ class SQLService extends DatabaseService {
|
|
|
134
134
|
if (cds.env.features.stream_compat) {
|
|
135
135
|
if (query._streaming) {
|
|
136
136
|
this._changeToStreams(cqn.SELECT.columns, rows, true, true)
|
|
137
|
-
|
|
137
|
+
if (!rows.length) return
|
|
138
|
+
|
|
139
|
+
const result = rows[0]
|
|
140
|
+
// stream is always on position 0. Further properties like etag are inserted later.
|
|
141
|
+
let [key, val] = Object.entries(result)[0]
|
|
142
|
+
result.value = val
|
|
143
|
+
delete result[key]
|
|
144
|
+
|
|
145
|
+
return result
|
|
138
146
|
}
|
|
139
147
|
} else {
|
|
140
148
|
this._changeToStreams(cqn.SELECT.columns, rows, query.SELECT.one, false)
|
|
@@ -265,7 +273,7 @@ class SQLService extends DatabaseService {
|
|
|
265
273
|
* @param {string} sql
|
|
266
274
|
*/
|
|
267
275
|
hasResults(sql) {
|
|
268
|
-
return
|
|
276
|
+
return /^\s*(SELECT|WITH|CALL|PRAGMA table_info)/i.test(sql)
|
|
269
277
|
}
|
|
270
278
|
|
|
271
279
|
/**
|
|
@@ -281,17 +289,23 @@ class SQLService extends DatabaseService {
|
|
|
281
289
|
const [max, offset = 0] = one ? [1] : _ ? [_.rows?.val, _.offset?.val] : []
|
|
282
290
|
if (max === undefined || (n < max && (n || !offset))) return n + offset
|
|
283
291
|
}
|
|
284
|
-
|
|
285
|
-
|
|
292
|
+
|
|
293
|
+
// Keep original query columns when potentially used insde conditions
|
|
294
|
+
const { having, groupBy } = query.SELECT
|
|
295
|
+
const columns = (having?.length || groupBy?.length)
|
|
296
|
+
? query.SELECT.columns.filter(c => !c.expand)
|
|
297
|
+
: [{ val: 1 }]
|
|
298
|
+
const cq = SELECT.one([{ func: 'count' }]).from(
|
|
286
299
|
cds.ql.clone(query, {
|
|
300
|
+
columns,
|
|
287
301
|
localized: false,
|
|
288
302
|
expand: false,
|
|
289
303
|
limit: undefined,
|
|
290
304
|
orderBy: undefined,
|
|
291
305
|
}),
|
|
292
306
|
)
|
|
293
|
-
const { count
|
|
294
|
-
return count
|
|
307
|
+
const { count } = await this.onSELECT({ query: cq })
|
|
308
|
+
return count
|
|
295
309
|
}
|
|
296
310
|
|
|
297
311
|
/**
|
package/lib/cql-functions.js
CHANGED
|
@@ -152,6 +152,13 @@ const StandardFunctions = {
|
|
|
152
152
|
current_time: p => (p ? `current_time(${p})` : 'current_time'),
|
|
153
153
|
current_timestamp: p => (p ? `current_timestamp(${p})` : 'current_timestamp'),
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Generates SQL statement that produces current point in time (date and time with time zone)
|
|
157
|
+
* @returns {string}
|
|
158
|
+
*/
|
|
159
|
+
now: function() {
|
|
160
|
+
return this.session_context({val: '$now'})
|
|
161
|
+
},
|
|
155
162
|
/**
|
|
156
163
|
* Generates SQL statement that produces the year of a given timestamp
|
|
157
164
|
* @param {string} x
|
|
@@ -189,29 +196,27 @@ const StandardFunctions = {
|
|
|
189
196
|
* /
|
|
190
197
|
second: x => `cast( strftime('%S',${x}) as Integer )`,
|
|
191
198
|
|
|
199
|
+
// REVISIT: make precision configurable
|
|
192
200
|
/**
|
|
193
201
|
* Generates SQL statement that produces the fractional seconds of a given timestamp
|
|
194
202
|
* @param {string} x
|
|
195
203
|
* @returns {string}
|
|
196
204
|
*/
|
|
197
|
-
fractionalseconds: x => `cast( strftime('%
|
|
205
|
+
fractionalseconds: x => `cast( substr( strftime('%f', ${x}), length(strftime('%f', ${x})) - 3) as REAL)`,
|
|
198
206
|
|
|
199
207
|
/**
|
|
200
208
|
* maximum date time value
|
|
201
209
|
* @returns {string}
|
|
202
210
|
*/
|
|
203
|
-
maxdatetime: () => '9999-12-
|
|
211
|
+
maxdatetime: () => "'9999-12-31T23:59:59.999Z'",
|
|
204
212
|
/**
|
|
205
213
|
* minimum date time value
|
|
206
214
|
* @returns {string}
|
|
207
215
|
*/
|
|
208
|
-
mindatetime: () => '0001-01-
|
|
216
|
+
mindatetime: () => "'0001-01-01T00:00:00.000Z'",
|
|
209
217
|
|
|
210
218
|
// odata spec defines the date time offset type as a normal ISO time stamp
|
|
211
219
|
// Where the timezone can either be 'Z' (for UTC) or [+|-]xx:xx for the time offset
|
|
212
|
-
// sqlite understands this so by splitting the timezone from the actual date
|
|
213
|
-
// prefixing it with 1970 it allows sqlite to give back the number of seconds
|
|
214
|
-
// which can be divided by 60 back to minutes
|
|
215
220
|
/**
|
|
216
221
|
* Generates SQL statement that produces the offset in minutes of a given date time offset string
|
|
217
222
|
* @param {string} x
|
|
@@ -219,7 +224,9 @@ const StandardFunctions = {
|
|
|
219
224
|
*/
|
|
220
225
|
totaloffsetminutes: x => `case
|
|
221
226
|
when substr(${x}, length(${x})) = 'z' then 0
|
|
222
|
-
else
|
|
227
|
+
else sign( cast( substr(${x}, length(${x}) - 5) as Integer )) *
|
|
228
|
+
( cast( strftime('%H', substr(${x}, length(${x}) - 4 )) as Integer ) * 60 +
|
|
229
|
+
cast( strftime('%M', substr(${x},length(${x}) - 4 )) as Integer ))
|
|
223
230
|
end`,
|
|
224
231
|
|
|
225
232
|
// odata spec defines the value format for totalseconds as a duration like: P12DT23H59M59.999999999999S
|
package/lib/cqn4sql.js
CHANGED
|
@@ -854,7 +854,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
854
854
|
function getTransformedOrderByGroupBy(columns, inOrderBy = false) {
|
|
855
855
|
const res = []
|
|
856
856
|
for (let i = 0; i < columns.length; i++) {
|
|
857
|
-
|
|
857
|
+
let col = columns[i]
|
|
858
858
|
if (isCalculatedOnRead(col.$refLinks?.[col.$refLinks.length - 1].definition)) {
|
|
859
859
|
const calcElement = resolveCalculatedElement(col, true)
|
|
860
860
|
res.push(calcElement)
|
|
@@ -877,8 +877,37 @@ function cqn4sql(originalQuery, model) {
|
|
|
877
877
|
res.push(...getTransformedOrderByGroupBy([dollarSelfReplacement], inOrderBy))
|
|
878
878
|
continue
|
|
879
879
|
}
|
|
880
|
-
const { target } = col.$refLinks[0]
|
|
881
|
-
|
|
880
|
+
const { target, definition } = col.$refLinks[0]
|
|
881
|
+
let tableAlias = null
|
|
882
|
+
if (target.SELECT?.columns && inOrderBy) {
|
|
883
|
+
// usually TA is omitted if order by ref is a column
|
|
884
|
+
// if a localized sorting is requested, we add `COLLATE`s
|
|
885
|
+
// later on, which transforms the simple name to an expression
|
|
886
|
+
// --> in an expression, only source elements can be addressed, hence we must add TA
|
|
887
|
+
if (target.SELECT.localized && definition.type === 'cds.String') {
|
|
888
|
+
const referredCol = target.SELECT.columns.find(c => {
|
|
889
|
+
return c.as === col.ref[0] || c.ref?.at(-1) === col.ref[0]
|
|
890
|
+
})
|
|
891
|
+
if (referredCol) {
|
|
892
|
+
// keep sort and nulls properties
|
|
893
|
+
referredCol.sort = col.sort
|
|
894
|
+
referredCol.nulls = col.nulls
|
|
895
|
+
col = referredCol
|
|
896
|
+
if (definition.kind === 'element') {
|
|
897
|
+
tableAlias = getQuerySourceName(col)
|
|
898
|
+
} else {
|
|
899
|
+
// we must replace the reference with the underlying expression
|
|
900
|
+
const { val, func, args, xpr } = col
|
|
901
|
+
if (val) res.push({ val })
|
|
902
|
+
if (func) res.push({ func, args })
|
|
903
|
+
if (xpr) res.push({ xpr })
|
|
904
|
+
continue
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
} else {
|
|
909
|
+
tableAlias = getQuerySourceName(col) // do not prepend TA if orderBy column addresses element of query
|
|
910
|
+
}
|
|
882
911
|
const leaf = col.$refLinks[col.$refLinks.length - 1].definition
|
|
883
912
|
if (leaf.virtual === true) continue // already in getFlatColumnForElement
|
|
884
913
|
let baseName
|
|
@@ -972,7 +1001,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
972
1001
|
const { index, tableAlias } = inferred.$combinedElements[k][0]
|
|
973
1002
|
const element = tableAlias.elements[k]
|
|
974
1003
|
// ignore FK for odata csn / ignore blobs from wildcard expansion
|
|
975
|
-
if (isManagedAssocInFlatMode(element) ||
|
|
1004
|
+
if (isManagedAssocInFlatMode(element) || element.type === 'cds.LargeBinary') return
|
|
976
1005
|
// for wildcard on subquery in from, just reference the elements
|
|
977
1006
|
if (tableAlias.SELECT && !element.elements && !element.target) {
|
|
978
1007
|
wildcardColumns.push(index ? { ref: [index, k] } : { ref: [k] })
|
package/lib/deep-queries.js
CHANGED
|
@@ -242,10 +242,23 @@ const _getDeepQueries = (diff, target, root = false) => {
|
|
|
242
242
|
queries.push(...subQueries)
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
|
|
245
|
+
const insertQueries = new Map()
|
|
246
|
+
|
|
247
|
+
return queries.map(q => {
|
|
248
|
+
// Merge all INSERT statements for each target
|
|
249
|
+
if (q.INSERT) {
|
|
250
|
+
const target = q.target
|
|
251
|
+
if (insertQueries.has(target)) {
|
|
252
|
+
insertQueries.get(target).INSERT.entries.push(...q.INSERT.entries)
|
|
253
|
+
return
|
|
254
|
+
} else {
|
|
255
|
+
insertQueries.set(target, q)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
246
258
|
Object.defineProperty(q, handledDeep, { value: true })
|
|
259
|
+
return q
|
|
247
260
|
})
|
|
248
|
-
|
|
261
|
+
.filter(a => a)
|
|
249
262
|
}
|
|
250
263
|
|
|
251
264
|
module.exports = {
|
package/lib/infer/index.js
CHANGED
|
@@ -727,6 +727,9 @@ function infer(originalQuery, model) {
|
|
|
727
727
|
function resolveInline(col, namePrefix = col.as || col.flatName) {
|
|
728
728
|
const { inline, $refLinks } = col
|
|
729
729
|
const $leafLink = $refLinks[$refLinks.length - 1]
|
|
730
|
+
if(!$leafLink.definition.target && !$leafLink.definition.elements) {
|
|
731
|
+
throw new Error(`Unexpected “inline” on “${col.ref.map(idOnly)}”; can only be used after a reference to a structure, association or table alias`)
|
|
732
|
+
}
|
|
730
733
|
let elements = {}
|
|
731
734
|
inline.forEach(inlineCol => {
|
|
732
735
|
inferQueryElement(inlineCol, false, $leafLink, { inExpr: true, inNestedProjection: true, baseColumn: col })
|
|
@@ -780,6 +783,9 @@ function infer(originalQuery, model) {
|
|
|
780
783
|
function resolveExpand(col) {
|
|
781
784
|
const { expand, $refLinks } = col
|
|
782
785
|
const $leafLink = $refLinks?.[$refLinks.length - 1] || inferred.SELECT.from.$refLinks.at(-1) // fallback to anonymous expand
|
|
786
|
+
if(!$leafLink.definition.target && !$leafLink.definition.elements) {
|
|
787
|
+
throw new Error(`Unexpected “expand” on “${col.ref.map(idOnly)}”; can only be used after a reference to a structure, association or table alias`)
|
|
788
|
+
}
|
|
783
789
|
const target = getDefinition($leafLink.definition.target)
|
|
784
790
|
if (target) {
|
|
785
791
|
const expandSubquery = {
|
|
@@ -795,19 +801,20 @@ function infer(originalQuery, model) {
|
|
|
795
801
|
? new cds.struct({ elements: inferredExpandSubquery.elements })
|
|
796
802
|
: new cds.array({ items: new cds.struct({ elements: inferredExpandSubquery.elements }) })
|
|
797
803
|
return Object.defineProperty(res, '$assocExpand', { value: true })
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
804
|
+
} else if ($leafLink.definition.elements) {
|
|
805
|
+
let elements = {}
|
|
806
|
+
expand.forEach(e => {
|
|
807
|
+
if (e === '*') {
|
|
808
|
+
elements = { ...elements, ...$leafLink.definition.elements }
|
|
809
|
+
} else {
|
|
810
|
+
inferQueryElement(e, false, $leafLink, { inExpr: true, inNestedProjection: true })
|
|
811
|
+
if (e.expand) elements[e.as || e.flatName] = resolveExpand(e)
|
|
812
|
+
if (e.inline) elements = { ...elements, ...resolveInline(e) }
|
|
813
|
+
else elements[e.as || e.flatName] = e.$refLinks ? e.$refLinks[e.$refLinks.length - 1].definition : e
|
|
814
|
+
}
|
|
815
|
+
})
|
|
816
|
+
return new cds.struct({ elements })
|
|
817
|
+
}
|
|
811
818
|
}
|
|
812
819
|
|
|
813
820
|
function stepNotFoundInPredecessor(step, def) {
|
package/package.json
CHANGED