Skip to content

Commit

Permalink
Merge branch 'main' into new-build-api
Browse files Browse the repository at this point in the history
  • Loading branch information
johannes-vogel authored Nov 3, 2023
2 parents 505c0b9 + 95b6ee9 commit be3436f
Show file tree
Hide file tree
Showing 18 changed files with 570 additions and 103 deletions.
38 changes: 38 additions & 0 deletions .github/workflows/conventional-commits.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: 'Adheres to conventional commit standard'

on:
pull_request_target:
types:
- opened
- edited
- synchronize

permissions:
pull-requests: read

jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# Configure which types are allowed (newline-delimited).
# Default: https://github.com/commitizen/conventional-commit-types
types: |
feat
fix
docs
style
refactor
perf
test
build
ci
chore
revert
wip
deps
11 changes: 11 additions & 0 deletions .reuse/dep5
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,14 @@ Disclaimer: The code in this project may include calls to APIs ("API Calls") of
Files: *
Copyright: 2022 SAP SE or an SAP affiliate company and cds-dbs contributors
License: Apache-2.0

Files:
postgres/test/beershop/*
postgres/test/odata-string-functions.test.js
postgres/test/ql.test.js
postgres/test/service.test.js
Copyright:
2022 SAP SE or an SAP affiliate company and cds-dbs contributors
Copyright (c) 2020 SAP Mentors & Friends
License: MIT
Comment: The content of these files, entirely or in part, comes from https://github.com/sapmentors/cds-pg, which is MIT licensed.
20 changes: 15 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,18 @@ The `type` can be any of `feat`, `fix` or `chore`.

The prefix is used to calculate the semver release level, and the section of the release notes to place the commit message in.

| **type** | when to use | release level | release note section |
| --------- | ----------------------------------- | ------------- | -------------------- |
| feat | a feature has been added | `minor` | **Features** |
| fix | a bug has been patched | `patch` | **Bug fixes** |
| chore | any changes that aren't user-facing | none | none |
| **type** | When to Use | Release Level | Release Note Section |
| ---------- | ----------------------------------- | ------------- | -------------------- |
| feat | A feature has been added | `minor` | **Added** |
| fix | A bug has been patched | `patch` | **Fixed** |
| deps | Changes to the dependencies | `patch` | **Changed** |
| perf | Performance improvements | none | **Performance Improvements** |
| chore | Any changes that aren't user-facing | none | none |
| docs | Documentation updates | none | none |
| style | Code style and formatting changes | none | none |
| refactor | Code refactoring | none | none | |
| test | Adding tests or test-related changes| none | none |
| build | Build system or tooling changes | none | none |
| ci | Continuous Integration/Deployment | none | none |
| revert | Reverting a previous commit | none | none |
| wip | Work in progress (temporary) | none | none |
9 changes: 9 additions & 0 deletions LICENSES/MIT.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
MIT License

Copyright (c) <year> <copyright holders>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
41 changes: 40 additions & 1 deletion db-service/lib/cqn4sql.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,46 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
}

if (queryNeedsJoins) {
transformedQuery[kind].from = translateAssocsToJoins(transformedQuery[kind].from)
if (inferred.UPDATE || inferred.DELETE) {
const prop = inferred.UPDATE ? 'UPDATE' : 'DELETE'
const subquery = {
SELECT: {
from: { ...transformedFrom },
columns: [], // primary keys of the query target will be added later
where: [...transformedProp.where],
},
}
// The alias of the original query is now the alias for the subquery
// so that potential references in the where clause to the alias match.
// Hence, replace the alias of the original query with the next
// available alias, so that each alias is unique.
const uniqueSubqueryAlias = getNextAvailableTableAlias(transformedFrom.as)
transformedFrom.as = uniqueSubqueryAlias

// calculate the primary keys of the target entity, there is always exactly
// one query source for UPDATE / DELETE
const queryTarget = Object.values(originalQuery.sources)[0]
const keys = Object.values(queryTarget.elements).filter(e => e.key === true)
const primaryKey = { list: [] }
keys.forEach(k => {
// cqn4sql will add the table alias to the column later, no need to add it here
subquery.SELECT.columns.push({ ref: [k.name] })

// add the alias of the main query to the list of primary key references
primaryKey.list.push({ ref: [transformedFrom.as, k.name] })
})

const transformedSubquery = cqn4sql(subquery)

// replace where condition of original query with the transformed subquery
// correlate UPDATE / DELETE query with subquery by primary key matches
transformedQuery[prop].where = [primaryKey, 'in', transformedSubquery]

if (prop === 'UPDATE') transformedQuery.UPDATE.entity = transformedFrom
else transformedQuery.DELETE.from = transformedFrom
} else {
transformedQuery[kind].from = translateAssocsToJoins(transformedQuery[kind].from)
}
}
}
}
Expand Down
18 changes: 10 additions & 8 deletions db-service/lib/infer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -680,10 +680,6 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
? { ref: [...baseColumn.ref, ...column.ref], $refLinks: [...baseColumn.$refLinks, ...column.$refLinks] }
: column
if (isColumnJoinRelevant(colWithBase)) {
if (originalQuery.UPDATE)
throw new Error(
'Path expressions for UPDATE statements are not supported. Use “where exists” with infix filters instead.',
)
Object.defineProperty(column, 'isJoinRelevant', { value: true })
joinTree.mergeColumn(colWithBase, originalQuery.outerQueries)
}
Expand Down Expand Up @@ -873,20 +869,26 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
if (leafOfCalculatedElementRef.value) mergePathsIntoJoinTree(leafOfCalculatedElementRef.value, basePath)

mergePathIfNecessary(basePath, arg)
} else if (arg.xpr) {
arg.xpr.forEach(step => {
} else if (arg.xpr || arg.args) {
const prop = arg.xpr ? 'xpr' : 'args'
arg[prop].forEach(step => {
const subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
if (step.ref) {
const subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
step.$refLinks.forEach((link, i) => {
const { definition } = link
if (definition.value) {
mergePathsIntoJoinTree(definition.value)
mergePathsIntoJoinTree(definition.value, subPath)
} else {
subPath.$refLinks.push(link)
subPath.ref.push(step.ref[i])
}
})
mergePathIfNecessary(subPath, step)
} else if (step.args || step.xpr) {
const nestedProp = step.xpr ? 'xpr' : 'args'
step[nestedProp].forEach(a => {
mergePathsIntoJoinTree(a, subPath)
})
}
})
}
Expand Down
5 changes: 5 additions & 0 deletions db-service/test/bookshop/db/booksWithExpr.cds
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,15 @@ entity Books {
authorAdrText = author.addressText;

authorAge: Integer = years_between( author.sortCode, author.sortCode );
authorAgeNativePG: Integer = DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth);

// calculated element is `xpr` which has subsequent `xpr`
authorAgeInDogYears: Integer = ( DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) ) * 7;
}

entity Authors {
key ID : Integer;

firstName : String;
lastName : String;

Expand Down
2 changes: 2 additions & 0 deletions db-service/test/cds-infer/calculated-elements.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ describe('Infer types of calculated elements in select list', () => {
authorAdrText: Books.elements.authorAdrText,
authorAge: Books.elements.authorAge,
youngAuthorName: Books.elements.youngAuthorName,
authorAgeNativePG: Books.elements.authorAgeNativePG,
authorAgeInDogYears: Books.elements.authorAgeInDogYears,
})
})
})
121 changes: 71 additions & 50 deletions db-service/test/cqn4sql/DELETE.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,80 +69,101 @@ describe('DELETE', () => {
}`)
expect(query.DELETE).to.deep.equal(expected.DELETE)
})
it('DELETE with where exists expansion and path expression', () => {
cds.model = cds.compile.for.nodejs(cds.model)
const { DELETE } = cds.ql
let d = DELETE.from('bookshop.Books:author').where(`books.title = 'Harry Potter'`)
const query = cqn4sql(d)

// this is the final exists subquery
const subquery = CQL`
SELECT author.ID from bookshop.Authors as author
left join bookshop.Books as books on books.author_ID = author.ID
where exists (
SELECT 1 from bookshop.Books as Books2 where Books2.author_ID = author.ID
) and books.title = 'Harry Potter'
`
const expected = JSON.parse(`{
"DELETE": {
"from": {
"ref": [
"bookshop.Authors"
],
"as": "author2"
}
}
}`)
expected.DELETE.where = [
{
list: [
{
ref: ['author2', 'ID'],
},
],
},
'in',
subquery,
]
expect(query.DELETE).to.deep.equal(expected.DELETE)
})

it('DELETE with assoc filter and where exists expansion', () => {
const { DELETE } = cds.ql
let d = DELETE.from('bookshop.Reproduce[author = null and ID = 99]:accessGroup')
const query = cqn4sql(d)

const expected = {
"DELETE": {
"from": {
"ref": [
"bookshop.AccessGroups"
],
"as": "accessGroup"
DELETE: {
from: {
ref: ['bookshop.AccessGroups'],
as: 'accessGroup',
},
"where": [
"exists",
where: [
'exists',
{
"SELECT": {
"from": {
"ref": [
"bookshop.Reproduce"
],
"as": "Reproduce"
SELECT: {
from: {
ref: ['bookshop.Reproduce'],
as: 'Reproduce',
},
"columns": [
columns: [
{
"val": 1
}
val: 1,
},
],
"where": [
where: [
{
"ref": [
"Reproduce",
"accessGroup_ID"
]
ref: ['Reproduce', 'accessGroup_ID'],
},
"=",
'=',
{
"ref": [
"accessGroup",
"ID"
]
ref: ['accessGroup', 'ID'],
},
"and",
'and',
{
"xpr": [
xpr: [
{
"ref": [
"Reproduce",
"author_ID"
]
ref: ['Reproduce', 'author_ID'],
},
"=",
'=',
{
"val": null
}
]
val: null,
},
],
},
"and",
'and',
{
"ref": [
"Reproduce",
"ID"
]
ref: ['Reproduce', 'ID'],
},
"=",
'=',
{
"val": 99
}
]
}
}
]
}
val: 99,
},
],
},
},
],
},
}
expect(query.DELETE).to.deep.equal(expected.DELETE)
})
Expand Down
Loading

0 comments on commit be3436f

Please sign in to comment.