Skip to content

Commit 70cf34d

Browse files
Merge branch 'main' into sqlite-compliance-tests
2 parents ddcc19c + 3465cba commit 70cf34d

69 files changed

Lines changed: 1001 additions & 1121 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release-please.yml

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,46 +26,28 @@ jobs:
2626

2727
## debug info
2828
- run: echo '${{ toJSON(steps.release.outputs) }} '
29-
# Run tests
30-
- run: npm install -g @sap/cds-dk
31-
# Run tests
32-
- run: npm ci
33-
if: ${{ steps.release.outputs.releases_created }}
34-
# hana express setup
35-
- id: hxe
36-
if: ${{ steps.release.outputs.releases_created }}
37-
uses: ./.github/actions/hxe
38-
with:
39-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40-
# testing hana/sqlite/postgres/db-service
41-
- run: npm test -ws
42-
if: ${{ steps.release.outputs.releases_created }}
43-
env:
44-
FORCE_COLOR: true
45-
TAG: ${{ steps.hxe.outputs.TAG }}
46-
IMAGE_ID: ${{ steps.hxe.outputs.IMAGE_ID }}
4729

4830
# Publish packages
4931
- name: Publish db-service
5032
if: ${{ steps.release.outputs.db-service--release_created }}
51-
run: npm publish --workspace db-service --access public
33+
run: npm publish --workspace db-service --access public --tag next
5234
env:
5335
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
5436

5537
- name: Publish sqlite
5638
if: ${{ steps.release.outputs.sqlite--release_created }}
57-
run: npm publish --workspace sqlite --access public
39+
run: npm publish --workspace sqlite --access public --tag next
5840
env:
5941
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
6042

6143
- name: Publish postgres
6244
if: ${{ steps.release.outputs.postgres--release_created }}
63-
run: npm publish --workspace postgres --access public
45+
run: npm publish --workspace postgres --access public --tag next
6446
env:
6547
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
6648

6749
- name: Publish SAP HANA
6850
if: ${{ steps.release.outputs.hana--release_created }}
69-
run: npm publish --workspace hana --access public
51+
run: npm publish --workspace hana --access public --tag next
7052
env:
7153
NODE_AUTH_TOKEN: ${{secrets.npm_token}}

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
uses: martinbeentjes/npm-get-version-action@3cf273023a0dda27efcd3164bdfb51908dd46a5b
2828
- name: Create a GitHub release
2929
# v1.15.0
30-
uses: ncipollo/release-action@36e78ab6296394ce36f72f6488e68c2353b50514
30+
uses: ncipollo/release-action@fa4749311e8d6997e78065947cc502ba74a057d6
3131
with:
3232
tag: 'v${{ steps.package-version.outputs.current-version}}'
3333
name: 'Release v${{ steps.package-version.outputs.current-version}}'

.github/workflows/test.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,8 @@ jobs:
3030
with:
3131
node-version: ${{ matrix.node }}
3232
cache: 'npm'
33-
33+
- run: npm add cds-9.0.0.tgz cds-dk-9.0.1.tgz cds-compiler-6.0.2.tgz --save-dev
3434
- run: npm ci
35-
- run: npm install -g @sap/cds-dk
3635
- run: npm run lint
3736
- id: hxe
3837
uses: ./.github/actions/hxe

.release-please-manifest.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"db-service": "1.20.0",
3-
"sqlite": "1.11.0",
4-
"postgres": "1.14.0",
5-
"hana": "1.9.0"
2+
"db-service": "2.0.0",
3+
"sqlite": "2.0.0",
4+
"postgres": "2.0.0",
5+
"hana": "2.0.0"
66
}

cds-9.0.0.tgz

391 KB
Binary file not shown.

cds-compiler-6.0.2.tgz

1.13 MB
Binary file not shown.

cds-dk-9.0.1.tgz

361 KB
Binary file not shown.

db-service/CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,26 @@
44
- The format is based on [Keep a Changelog](http://keepachangelog.com/).
55
- This project adheres to [Semantic Versioning](http://semver.org/).
66

7+
## [2.0.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.20.0...db-service-v2.0.0) (2025-05-07)
8+
9+
10+
### ⚠ BREAKING CHANGES
11+
12+
* update peer dependency to @sap/cds@9 ([#1178](https://github.com/cap-js/cds-dbs/issues/1178))
13+
14+
15+
### Fixed
16+
17+
* Adopt to recurse `DistanceTo` cqn format ([#1093](https://github.com/cap-js/cds-dbs/issues/1093)) ([246e0b3](https://github.com/cap-js/cds-dbs/commit/246e0b38840f7e132ea49cae335b6be7a55354b3))
18+
* current_utctimestamp as default ([#1161](https://github.com/cap-js/cds-dbs/issues/1161)) ([7c6b2f5](https://github.com/cap-js/cds-dbs/commit/7c6b2f5a6837afbeb1e24daef9a49e25cf7e92f0))
19+
* exists within expression is properly detected ([#1156](https://github.com/cap-js/cds-dbs/issues/1156)) ([5a7b50c](https://github.com/cap-js/cds-dbs/commit/5a7b50cb02776cf6052c79bd276421dd87161882))
20+
* resilience for query re-use scenarios ([#1175](https://github.com/cap-js/cds-dbs/issues/1175)) ([2352767](https://github.com/cap-js/cds-dbs/commit/2352767465ea88db77dc89bcaa76e268583146e1))
21+
22+
23+
### Changed
24+
25+
* update peer dependency to @sap/cds@9 ([#1178](https://github.com/cap-js/cds-dbs/issues/1178)) ([0507edd](https://github.com/cap-js/cds-dbs/commit/0507edd4e1dcb98983b1fb65ade1344d978b7524))
26+
727
## [1.20.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.19.1...db-service-v1.20.0) (2025-04-17)
828

929

db-service/lib/SQLService.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -404,8 +404,6 @@ class SQLService extends DatabaseService {
404404
let kind = q.kind || Object.keys(q)[0]
405405
if (kind in { INSERT: 1, DELETE: 1, UPSERT: 1, UPDATE: 1 }) {
406406
q = resolveView(q, this.model, this) // REVISIT: before resolveView was called on flat cqn obtained from cqn4sql -> is it correct to call on original q instead?
407-
let target = q[kind]._transitions?.[0].target
408-
if (target) q._target = target // REVISIT: Why isn't that done in resolveView?
409407
}
410408
let cqn2sql = new this.class.CQN2SQL(this)
411409
return cqn2sql.render(q, values)

db-service/lib/cql-functions.js

Lines changed: 231 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict'
22

3+
const cds = require('@sap/cds')
4+
35
// OData: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_CanonicalFunctions
46
const StandardFunctions = {
57
/**
@@ -18,10 +20,15 @@ const StandardFunctions = {
1820
} catch {
1921
val = sub[2] || sub[3] || ''
2022
}
21-
arg.val = arg.__proto__.val = val
23+
arg.val = val
2224
const refs = ref.list
23-
const { toString } = ref
24-
return '(' + refs.map(ref2 => this.contains(this.tolower(toString(ref2)), this.tolower(arg))).join(' or ') + ')'
25+
return `(${refs.map(ref => this.expr({
26+
func: 'contains',
27+
args: [
28+
{ func: 'tolower', args: [ref] },
29+
{ func: 'tolower', args: [arg] },
30+
]
31+
})).join(' or ')})`
2532
},
2633

2734
// ==============================
@@ -141,7 +148,7 @@ const StandardFunctions = {
141148
* @returns {string} - SQL statement
142149
*/
143150
now: function () {
144-
return this.session_context({ val: '$now' })
151+
return this.expr({ func: 'session_context', args: [{ val: '$now' }] })
145152
},
146153

147154
/**
@@ -184,6 +191,226 @@ const HANAFunctions = {
184191
* @returns {string} - SQL statement
185192
*/
186193
current_timestamp: p => (p ? `current_timestamp(${p})` : 'current_timestamp'),
194+
195+
/**
196+
* Generates SQL statement for the hierarchy function
197+
* @param {string} [p] -
198+
* @returns {string} - SQL statement
199+
*/
200+
HIERARCHY: function (args) {
201+
let uniqueCounter = this._with?.length ?? 0
202+
let src = args.xpr[1]
203+
204+
// Ensure that the orderBy column are exposed by the source for hierarchy sorting
205+
const orderBy = args.xpr.find((_, i, arr) => /ORDER/i.test(arr[i - 2]) && /BY/i.test(arr[i - 1]))
206+
207+
const passThroughColumns = src.SELECT.columns.map(c => ({ ref: ['Source', this.column_name(c)] }))
208+
src.as = 'H' + (uniqueCounter++)
209+
src = this.expr(this.with(src))
210+
211+
let recursive = cds.ql(`
212+
SELECT
213+
1 as HIERARCHY_LEVEL,
214+
NODE_ID as HIERARCHY_ROOT_ID
215+
FROM ${src} AS Source
216+
WHERE parent_ID IS NULL
217+
UNION ALL
218+
SELECT
219+
Parent.HIERARCHY_LEVEL + 1,
220+
Parent.HIERARCHY_ROOT_ID
221+
FROM ${src} AS Source
222+
JOIN H${uniqueCounter} AS Parent ON Source.PARENT_ID=Parent.NODE_ID
223+
ORDER BY HIERARCHY_LEVEL DESC${orderBy ? `,${orderBy}` : ''}`)
224+
recursive.as = 'H' + (uniqueCounter++)
225+
recursive.SET.args[0].SELECT.columns = [...recursive.SET.args[0].SELECT.columns, ...passThroughColumns]
226+
recursive.SET.args[1].SELECT.columns = [...recursive.SET.args[1].SELECT.columns, ...passThroughColumns]
227+
recursive = this.expr(this.with(recursive))
228+
229+
let ranked = cds.ql(`
230+
SELECT
231+
HIERARCHY_LEVEL,
232+
row_number() over () as HIERARCHY_RANK,
233+
HIERARCHY_ROOT_ID
234+
FROM ${recursive} AS Source`)
235+
ranked.as = 'H' + (uniqueCounter++)
236+
ranked.SELECT.columns = [...ranked.SELECT.columns, ...passThroughColumns]
237+
ranked = this.expr(this.with(ranked))
238+
239+
let Hierarchy = cds.ql(`
240+
SELECT
241+
HIERARCHY_LEVEL,
242+
HIERARCHY_RANK,
243+
(SELECT HIERARCHY_RANK FROM ${ranked} AS Ranked WHERE Ranked.NODE_ID = Source.PARENT_ID) AS HIERARCHY_PARENT_RANK,
244+
(SELECT HIERARCHY_RANK FROM ${ranked} AS Ranked WHERE Ranked.NODE_ID = Source.HIERARCHY_ROOT_ID) AS HIERARCHY_ROOT_RANK,
245+
coalesce(
246+
(SELECT MIN(HIERARCHY_RANK) FROM ${ranked} AS Ranked WHERE Ranked.HIERARCHY_RANK > Source.HIERARCHY_RANK AND Ranked.HIERARCHY_LEVEL <= Source.HIERARCHY_LEVEL),
247+
(SELECT MAX(HIERARCHY_RANK) + 1 FROM ${ranked})
248+
) - Source.HIERARCHY_RANK AS HIERARCHY_TREE_SIZE
249+
FROM ${ranked} AS Source`)
250+
Hierarchy.as = 'H' + (uniqueCounter++)
251+
Hierarchy.SELECT.columns = [...Hierarchy.SELECT.columns, ...passThroughColumns]
252+
Hierarchy = this.expr(this.with(Hierarchy))
253+
254+
return Hierarchy
255+
},
256+
257+
/**
258+
* Generates SQL statement for the hierarchy_descendants function
259+
* @param {string} [p] -
260+
* @returns {string} - SQL statement
261+
*/
262+
HIERARCHY_DESCENDANTS: function (args) {
263+
// Find Hierarchy function call source query
264+
const passThroughColumns = args.xpr[1].args[0].xpr[1].SELECT.columns.map(c => ({ ref: [this.column_name(c)] }))
265+
// REVISIT: currently only supports func: HIERARCHY as source
266+
const src = this.expr(args.xpr[1])
267+
268+
let uniqueCounter = this._with?.length ?? 0
269+
270+
let alias = args.xpr.find((_, i, arr) => /AS/i.test(arr[i - 1]))
271+
const where = args.xpr.find((a, i, arr) => a.xpr && /WHERE/i.test(arr[i - 1]) && /START/i.test(arr[i - 2]))
272+
const distance = args.xpr.find((a, i, arr) => typeof a.val === 'number' && (/DISTANCE/i.test(arr[i - 1]) || /DISTANCE/i.test(arr[i - 2])))
273+
const distanceFrom = args.xpr.find((a, i, arr) => /FROM/.test(a) && /DISTANCE/i.test(arr[i - 1]))
274+
275+
if (alias.startsWith('"') && alias.endsWith('"')) alias = alias.slice(1, -1).replace(/""/g, '"')
276+
277+
let HierarchyDescendants = cds.ql(`
278+
SELECT
279+
HIERARCHY_LEVEL,
280+
HIERARCHY_PARENT_RANK,
281+
HIERARCHY_RANK,
282+
HIERARCHY_ROOT_RANK,
283+
HIERARCHY_TREE_SIZE,
284+
0 as HIERARCHY_DISTANCE
285+
FROM ${src} AS ![${alias}]
286+
UNION ALL
287+
SELECT
288+
Source.HIERARCHY_LEVEL,
289+
Source.HIERARCHY_PARENT_RANK,
290+
Source.HIERARCHY_RANK,
291+
Source.HIERARCHY_ROOT_RANK,
292+
Source.HIERARCHY_TREE_SIZE,
293+
Child.HIERARCHY_DISTANCE + 1
294+
FROM ${src} AS Source
295+
JOIN H${uniqueCounter} AS Child ON Source.PARENT_ID=Child.NODE_ID`)
296+
HierarchyDescendants.as = 'H' + uniqueCounter
297+
HierarchyDescendants.SET.args[0].SELECT.where = where.xpr
298+
HierarchyDescendants.SET.args[0].SELECT.columns = [...HierarchyDescendants.SET.args[0].SELECT.columns, ...passThroughColumns.map(r => ({ ref: [alias, r.ref[0]] }))]
299+
HierarchyDescendants.SET.args[1].SELECT.columns = [...HierarchyDescendants.SET.args[1].SELECT.columns, ...passThroughColumns.map(r => ({ ref: ['Source', r.ref[0]] }))]
300+
301+
HierarchyDescendants = this.with(HierarchyDescendants)
302+
HierarchyDescendants.as = 'HierarchyDescendants'
303+
304+
return this.expr({
305+
SELECT: {
306+
columns: [
307+
{ ref: ['HIERARCHY_LEVEL'] },
308+
{ ref: ['HIERARCHY_PARENT_RANK'] },
309+
{ ref: ['HIERARCHY_RANK'] },
310+
{ ref: ['HIERARCHY_ROOT_RANK'] },
311+
{ ref: ['HIERARCHY_TREE_SIZE'] },
312+
{
313+
SELECT: {
314+
columns: [{ func: 'MAX', args: [{ ref: ['HIERARCHY_DISTANCE'] }] }],
315+
from: HierarchyDescendants,
316+
where: [{ ref: [HierarchyDescendants.as, 'HIERARCHY_RANK'] }, '=', { ref: [src, 'HIERARCHY_RANK'] }]
317+
},
318+
as: 'HIERARCHY_DISTANCE',
319+
},
320+
...passThroughColumns,
321+
],
322+
from: { ref: [src] },
323+
where: [
324+
{ ref: ['HIERARCHY_RANK'] },
325+
'IN',
326+
{
327+
SELECT: {
328+
columns: [{ ref: ['HIERARCHY_RANK'] }],
329+
from: HierarchyDescendants,
330+
where: [{ ref: ['HIERARCHY_DISTANCE'] }, distanceFrom ? '>=' : '=', distance]
331+
}
332+
}
333+
]
334+
}
335+
})
336+
},
337+
338+
/**
339+
* Generates SQL statement for the hierarchy_ancestors function
340+
* @param {string} [p] -
341+
* @returns {string} - SQL statement
342+
*/
343+
HIERARCHY_ANCESTORS: function (args) {
344+
// Find Hierarchy function call source query
345+
const passThroughColumns = args.xpr[1].args[0].xpr[1].SELECT.columns.map(c => ({ ref: [this.column_name(c)] }))
346+
// REVISIT: currently only supports func: HIERARCHY as source
347+
const src = this.expr(args.xpr[1])
348+
349+
let uniqueCounter = this._with?.length ?? 0
350+
351+
let alias = args.xpr.find((_, i, arr) => /AS/i.test(arr[i - 1]))
352+
const where = args.xpr.find((a, i, arr) => a.xpr && /WHERE/i.test(arr[i - 1]) && /START/i.test(arr[i - 2]))
353+
354+
if (alias.startsWith('"') && alias.endsWith('"')) alias = alias.slice(1, -1).replace(/""/g, '"')
355+
356+
let HierarchyAncestors = cds.ql(`
357+
SELECT
358+
HIERARCHY_LEVEL,
359+
HIERARCHY_PARENT_RANK,
360+
HIERARCHY_RANK,
361+
HIERARCHY_ROOT_RANK,
362+
HIERARCHY_TREE_SIZE,
363+
0 as HIERARCHY_DISTANCE
364+
FROM ${src} AS ![${alias}]
365+
UNION ALL
366+
SELECT
367+
Source.HIERARCHY_LEVEL,
368+
Source.HIERARCHY_PARENT_RANK,
369+
Source.HIERARCHY_RANK,
370+
Source.HIERARCHY_ROOT_RANK,
371+
Source.HIERARCHY_TREE_SIZE,
372+
Child.HIERARCHY_DISTANCE - 1
373+
FROM ${src} AS Source
374+
JOIN H${uniqueCounter} AS Child ON Source.NODE_ID=Child.PARENT_ID`)
375+
HierarchyAncestors.as = 'H' + uniqueCounter
376+
HierarchyAncestors.SET.args[0].SELECT.where = where.xpr
377+
HierarchyAncestors.SET.args[0].SELECT.columns = [...HierarchyAncestors.SET.args[0].SELECT.columns, ...passThroughColumns.map(r => ({ ref: [alias, r.ref[0]] }))]
378+
HierarchyAncestors.SET.args[1].SELECT.columns = [...HierarchyAncestors.SET.args[1].SELECT.columns, ...passThroughColumns.map(r => ({ ref: ['Source', r.ref[0]] }))]
379+
380+
HierarchyAncestors = this.with(HierarchyAncestors)
381+
HierarchyAncestors.as = 'HierarchyAncestors'
382+
return this.expr({
383+
SELECT: {
384+
columns: [
385+
{ ref: ['HIERARCHY_LEVEL'] },
386+
{ ref: ['HIERARCHY_PARENT_RANK'] },
387+
{ ref: ['HIERARCHY_RANK'] },
388+
{ ref: ['HIERARCHY_ROOT_RANK'] },
389+
{ ref: ['HIERARCHY_TREE_SIZE'] },
390+
{
391+
SELECT: {
392+
columns: [{ func: 'MIN', args: [{ ref: ['HIERARCHY_DISTANCE'] }] }],
393+
from: HierarchyAncestors,
394+
where: [{ ref: [HierarchyAncestors.as, 'HIERARCHY_RANK'] }, '=', { ref: [src, 'HIERARCHY_RANK'] }]
395+
},
396+
as: 'HIERARCHY_DISTANCE',
397+
},
398+
...passThroughColumns,
399+
],
400+
from: { ref: [src] },
401+
where: [
402+
{ ref: ['HIERARCHY_RANK'] },
403+
'IN',
404+
{
405+
SELECT: {
406+
columns: [{ ref: ['HIERARCHY_RANK'] }],
407+
from: HierarchyAncestors,
408+
}
409+
}
410+
]
411+
}
412+
})
413+
},
187414
}
188415

189416
for (let each in HANAFunctions) HANAFunctions[each.toUpperCase()] = HANAFunctions[each]

0 commit comments

Comments
 (0)