@sap/cds 5.9.0 → 5.9.3

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/app/fiori/routes.js +15 -8
  3. package/lib/compile/cdsc.js +1 -19
  4. package/lib/compile/etc/_localized.js +5 -4
  5. package/lib/compile/for/drafts.js +1 -1
  6. package/lib/compile/for/java.js +1 -1
  7. package/lib/compile/for/nodejs.js +1 -1
  8. package/lib/compile/for/odata.js +1 -1
  9. package/lib/connect/bindings.js +1 -1
  10. package/lib/connect/index.js +2 -3
  11. package/lib/env/requires.js +1 -1
  12. package/lib/index.js +2 -1
  13. package/lib/serve/Service-methods.js +28 -1
  14. package/lib/serve/adapters.js +6 -6
  15. package/lib/serve/factory.js +14 -9
  16. package/lib/serve/index.js +4 -3
  17. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -1
  18. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/BatchRequestListBuilder.js +3 -1
  19. package/libx/_runtime/cds-services/adapter/odata-v4/utils/dispatcherUtils.js +13 -7
  20. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +1 -3
  21. package/libx/_runtime/cds-services/services/Service.js +1 -1
  22. package/libx/_runtime/common/composition/data.js +22 -13
  23. package/libx/_runtime/common/composition/delete.js +14 -12
  24. package/libx/_runtime/common/generic/auth/restrict.js +3 -1
  25. package/libx/_runtime/{cds-services/services/utils → common/generic/auth}/restrictions.js +8 -1
  26. package/libx/_runtime/common/generic/input.js +1 -0
  27. package/libx/_runtime/common/generic/put.js +1 -0
  28. package/libx/_runtime/common/utils/cqn.js +5 -10
  29. package/libx/_runtime/common/utils/cqn2cqn4sql.js +39 -65
  30. package/libx/_runtime/common/utils/foreignKeyPropagations.js +28 -8
  31. package/libx/_runtime/common/utils/path.js +3 -3
  32. package/libx/_runtime/common/utils/resolveView.js +3 -0
  33. package/libx/_runtime/common/utils/structured.js +6 -1
  34. package/libx/_runtime/db/Service.js +10 -0
  35. package/libx/_runtime/db/data-conversion/post-processing.js +5 -0
  36. package/libx/_runtime/db/expand/expand-v2.js +13 -5
  37. package/libx/_runtime/db/expand/expandCQNToJoin.js +56 -26
  38. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +8 -8
  39. package/libx/_runtime/db/utils/generateAliases.js +9 -0
  40. package/libx/_runtime/extensibility/uiflex/handler/transformWRITE.js +1 -0
  41. package/libx/_runtime/fiori/generic/read.js +83 -31
  42. package/libx/_runtime/fiori/generic/readOverDraft.js +31 -19
  43. package/libx/_runtime/fiori/utils/handler.js +3 -0
  44. package/libx/_runtime/fiori/utils/where.js +38 -25
  45. package/libx/_runtime/hana/execute.js +18 -1
  46. package/libx/_runtime/hana/search2cqn4sql.js +4 -1
  47. package/libx/_runtime/remote/utils/client.js +4 -6
  48. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +19 -12
  49. package/libx/odata/cqn2odata.js +24 -27
  50. package/package.json +2 -2
@@ -19,9 +19,18 @@ const { deleteCondition, readAndDeleteKeywords, removeIsActiveEntityRecursively
19
19
  const { getColumns } = require('../../cds-services/services/utils/columns')
20
20
  const { adaptStreamCQN } = require('../../cds-services/adapter/odata-v4/utils/stream')
21
21
 
22
+ const _findSubselect = where => {
23
+ return where.find((e, i) => {
24
+ if (e.xpr) {
25
+ return _findSubselect(e.xpr)
26
+ }
27
+ return e.SELECT && where[i - 1] === 'exists'
28
+ })
29
+ }
30
+
22
31
  const _findRootSubSelectFor = query => {
23
32
  if (query.SELECT.where) {
24
- const subSelect = query.SELECT.where.find((e, i) => e.SELECT && query.SELECT.where[i - 1] === 'exists')
33
+ const subSelect = _findSubselect(query.SELECT.where)
25
34
  return subSelect ? _findRootSubSelectFor(subSelect) : query
26
35
  }
27
36
  return query
@@ -422,8 +431,8 @@ const _allActive = (req, columns, model) => {
422
431
  cqn.leftJoin(ensureDraftsSuffix(table.ref[0]) + ' as drafts').on(`${table.as}.${ids[0]} = drafts.${ids[0]}`)
423
432
 
424
433
  for (let i = 1; i < ids.length; i++) {
425
- // REVISIT: this is extremely expensive as it repeatedly invokes the compiler's cds.parse.expr -> better extend plain CQN yourself here
426
- cqn.and(`${table.as}.${ids[i]} = drafts.${ids[i]}`)
434
+ // this 'and' belongs to the join condition and is not a where and
435
+ cqn.and({ ref: [table.as, ids[i]] }, '=', { ref: ['drafts', ids[i]] })
427
436
  }
428
437
  }
429
438
 
@@ -557,27 +566,38 @@ const _alignAliasForUnion = (table, as, select) => {
557
566
  return select
558
567
  }
559
568
 
560
- const _findJoinInQuery = (query, parentAlias) => {
561
- const targetAlias = query.SELECT.from.as
562
- const isTargetRef = el => targetAlias && el.ref && el.ref.length > 1 && el.ref[0] === targetAlias
563
- if (query.SELECT && query.SELECT.where)
564
- return query.SELECT.where.reduce((links, el, idx, where) => {
565
- if (el.ref && el.ref[0] === parentAlias && el.ref[el.ref.length - 1] !== 'IsActiveEntity') {
566
- if (where[idx - 1] && where[idx - 1] === '=' && isTargetRef(where[idx - 2])) {
567
- if (links.length) links.push('and')
568
- links.push(el, '=', where[idx - 2])
569
- }
570
- if (where[idx + 1] && where[idx + 1] === '=' && isTargetRef(where[idx + 2])) {
571
- if (links.length) links.push('and')
572
- links.push(el, '=', where[idx + 2])
573
- }
569
+ const isTargetRef = (el, targetAlias) => targetAlias && el.ref && el.ref.length > 1 && el.ref[0] === targetAlias
570
+
571
+ const _joinFromWhere = (where, parentAlias, targetAlias) => {
572
+ return where.reduce((links, el, idx, where) => {
573
+ if (el.xpr) {
574
+ const result = _joinFromWhere(el.xpr, parentAlias, targetAlias)
575
+ if (result.length) {
576
+ if (links.length) links.push('and')
577
+ links.push(...result)
574
578
  }
575
579
  return links
576
- }, [])
577
- return []
580
+ }
581
+
582
+ if (el.ref && el.ref[0] === parentAlias && el.ref[el.ref.length - 1] !== 'IsActiveEntity') {
583
+ if (where[idx - 1] && where[idx - 1] === '=' && isTargetRef(where[idx - 2], targetAlias)) {
584
+ if (links.length) links.push('and')
585
+ links.push(el, '=', where[idx - 2])
586
+ }
587
+ if (where[idx + 1] && where[idx + 1] === '=' && isTargetRef(where[idx + 2], targetAlias)) {
588
+ if (links.length) links.push('and')
589
+ links.push(el, '=', where[idx + 2])
590
+ }
591
+ }
592
+ return links
593
+ }, [])
578
594
  }
579
595
 
580
- const _isFiltered = where => where.some(element => !(element in ['(', ')']))
596
+ const _findJoinInQuery = (query, parentAlias) => {
597
+ const targetAlias = query.SELECT.from.as
598
+ if (query.SELECT && query.SELECT.where) return _joinFromWhere(query.SELECT.where, parentAlias, targetAlias)
599
+ return []
600
+ }
581
601
 
582
602
  const _isDraftField = element => element.ref && element.ref.length > 1 && element.ref[0] === 'DraftAdministrativeData'
583
603
 
@@ -597,6 +617,10 @@ const _isLogicalFunction = (where, index) => {
597
617
  const _getWhereForActive = where => {
598
618
  const activeWhere = []
599
619
  for (let i = 0; i < where.length; i++) {
620
+ if (where[i].xpr) {
621
+ activeWhere.push({ xpr: _getWhereForActive(where[i].xpr) })
622
+ continue
623
+ }
600
624
  if (_isDraftField(where[i])) {
601
625
  activeWhere.push({ val: null })
602
626
  } else if (_functionContainsDraftField(where[i])) {
@@ -721,10 +745,20 @@ const _getSiblingScenario = (req, columns, model, siblingIndex, nav) => {
721
745
  const draftAdminAlias = _isDraftAdminScenario(req) && req.query.SELECT.from.as
722
746
  const params = [...req.params].reverse()
723
747
  const _getSiblingQueryFromWhere = (query, queryIndex, parentQuery) => {
724
- if (query.SELECT && query.SELECT.where) {
725
- const indexExists = query.SELECT.where.indexOf('exists')
726
- if (indexExists > -1 && queryIndex > 0) {
727
- return _getSiblingQueryFromWhere(query.SELECT.where[indexExists + 1], queryIndex - 1, query)
748
+ if (query.SELECT && query.SELECT.where && queryIndex > 0) {
749
+ for (let i = 0; i < query.SELECT.where.length; i++) {
750
+ if (query.SELECT.where[i].xpr && queryIndex > 0) {
751
+ const sibilingQueryFromWhere = _getSiblingQueryFromWhere(
752
+ { SELECT: { where: query.SELECT.where[i].xpr } },
753
+ queryIndex - 1,
754
+ query
755
+ )
756
+ if (sibilingQueryFromWhere) return sibilingQueryFromWhere
757
+ }
758
+
759
+ if (query.SELECT.where[i] === 'exists' && queryIndex > 0) {
760
+ return _getSiblingQueryFromWhere(query.SELECT.where[i + 1], queryIndex - 1, query)
761
+ }
728
762
  }
729
763
  }
730
764
  const target = { name: query.SELECT.from.ref[0].id || query.SELECT.from.ref[0], as: query.SELECT.from.as }
@@ -741,9 +775,16 @@ const _getSiblingScenario = (req, columns, model, siblingIndex, nav) => {
741
775
  return _getSiblingQueryFromWhere(req.query, siblingIndex)
742
776
  }
743
777
 
744
- const _mergeSiblingIntoCQN = (cqn, { cqn: siblingCQN }, siblingIndex) => {
745
- const _replaceWhereExists = (query, _siblingIndex) => {
746
- if (query.SELECT && query.SELECT.where) {
778
+ const _replaceWhereExists = (query, _siblingIndex, siblingCQN) => {
779
+ if (query.SELECT && query.SELECT.where) {
780
+ for (let i = 0; i < query.SELECT.where.length; i++) {
781
+ const whereElement = query.SELECT.where[i]
782
+ if (whereElement.xpr) {
783
+ const res = _replaceWhereExists({ SELECT: { where: whereElement.xpr } }, _siblingIndex, siblingCQN)
784
+ if (res) return res
785
+ continue
786
+ }
787
+
747
788
  const indexExists = query.SELECT.where.indexOf('exists')
748
789
  if (indexExists > -1) {
749
790
  if (_siblingIndex > 0) return _replaceWhereExists(query.SELECT.where[indexExists + 1], _siblingIndex - 1)
@@ -751,7 +792,10 @@ const _mergeSiblingIntoCQN = (cqn, { cqn: siblingCQN }, siblingIndex) => {
751
792
  }
752
793
  }
753
794
  }
754
- return _replaceWhereExists(cqn, siblingIndex)
795
+ }
796
+
797
+ const _mergeSiblingIntoCQN = (cqn, { cqn: siblingCQN }, siblingIndex) => {
798
+ return _replaceWhereExists(cqn, siblingIndex, siblingCQN)
755
799
  }
756
800
 
757
801
  const _getDraftDoc = (req, draftName, draftWhere) => {
@@ -796,6 +840,11 @@ const _getOrderByEnrichedColumns = (orderBy, columns, entity) => {
796
840
 
797
841
  const _replaceDraftAlias = where => {
798
842
  where.forEach(element => {
843
+ if (element.xpr) {
844
+ _replaceDraftAlias(element.xpr)
845
+ return
846
+ }
847
+
799
848
  if (_isDraftField(element)) {
800
849
  element.ref[0] = 'filterAdmin'
801
850
  }
@@ -981,7 +1030,10 @@ const _validatedDraftOfWhichIAmOwner = (req, draftWhere, draftParameters, column
981
1030
  _isValidDraftOfWhichIAmOwner(draftParameters.isActiveEntity) && _draftOfWhichIAmOwner(req, draftWhere, columns)
982
1031
 
983
1032
  const _draftInSubSelect = (where, req) => {
984
- return where.some(({ SELECT }) => {
1033
+ return where.some(({ SELECT, xpr }) => {
1034
+ if (xpr) {
1035
+ return _draftInSubSelect(xpr, req)
1036
+ }
985
1037
  if (SELECT && SELECT.where) {
986
1038
  const isActiveEntity = readAndDeleteKeywords(['IsActiveEntity'], SELECT.where, false)
987
1039
  if (isActiveEntity) {
@@ -1022,7 +1074,7 @@ const _generateCQN = (originalFrom, req, columns, model) => {
1022
1074
  return _draftAdminTable(req)
1023
1075
  }
1024
1076
 
1025
- if (!req.query.SELECT.where || !_isFiltered(req.query.SELECT.where)) {
1077
+ if (!req.query.SELECT.where) {
1026
1078
  return _allActive(req, columns, model)
1027
1079
  }
1028
1080
 
@@ -1032,7 +1084,7 @@ const _generateCQN = (originalFrom, req, columns, model) => {
1032
1084
  if (
1033
1085
  draftParameters.isActiveEntity &&
1034
1086
  _isTrue(draftParameters.isActiveEntity.value.val) &&
1035
- !draftParameters.siblingIsActive &&
1087
+ !(draftParameters.siblingIsActive && draftParameters.siblingIsActive.value.val === null) &&
1036
1088
  !draftParameters.hasDraftEntity
1037
1089
  ) {
1038
1090
  return _allActive(req, columns, model)
@@ -5,18 +5,14 @@ const { readAndDeleteKeywords } = require('../utils/where')
5
5
  const { cqn2cqn4sql } = require('../../common/utils/cqn2cqn4sql')
6
6
  const { isActiveEntityRequested } = require('../../../_runtime/fiori/utils/where')
7
7
 
8
- const _modifyCQN = (cqnDraft, where, context) => {
9
- const whereDraft = [...where]
10
- const result = readAndDeleteKeywords(['IsActiveEntity'], whereDraft)
11
- cqnDraft.where(whereDraft)
12
-
13
- if (result && result.value.val === false) {
14
- const fromRef = cqnDraft.SELECT.from.ref
15
- cqnDraft.SELECT.from.ref[fromRef.length - 1] = ensureDraftsSuffix(fromRef[fromRef.length - 1])
16
- }
8
+ const _modifyWhere = (where, context) => {
9
+ for (let i = 0; i < where.length; i++) {
10
+ const element = where[i]
17
11
 
18
- for (let i = 0; i < cqnDraft.SELECT.where.length; i++) {
19
- const element = cqnDraft.SELECT.where[i]
12
+ if (element.xpr) {
13
+ _modifyWhere(element.xpr, context)
14
+ continue
15
+ }
20
16
 
21
17
  if (element.SELECT) {
22
18
  const subCqnDraft = SELECT.from(
@@ -27,12 +23,25 @@ const _modifyCQN = (cqnDraft, where, context) => {
27
23
  [1]
28
24
  )
29
25
 
30
- cqnDraft.SELECT.where[i] = subCqnDraft
26
+ where[i] = subCqnDraft
31
27
  _modifyCQN(subCqnDraft, element.SELECT.where, context)
32
28
  }
33
29
  }
34
30
  }
35
31
 
32
+ const _modifyCQN = (cqnDraft, where, context) => {
33
+ const whereDraft = [...where]
34
+ const result = readAndDeleteKeywords(['IsActiveEntity'], whereDraft)
35
+ cqnDraft.where(whereDraft)
36
+
37
+ if (result && result.value.val === false) {
38
+ const fromRef = cqnDraft.SELECT.from.ref
39
+ cqnDraft.SELECT.from.ref[fromRef.length - 1] = ensureDraftsSuffix(fromRef[fromRef.length - 1])
40
+ }
41
+
42
+ _modifyWhere(cqnDraft.SELECT.where, context)
43
+ }
44
+
36
45
  const _hasNavToNonDraftEnclosedAssoc = (pathSegments, definitions, excludeAssoc) => {
37
46
  if (pathSegments.length < 2) return false
38
47
  const entity = definitions[pathSegments[0]]
@@ -67,19 +76,22 @@ const _shouldReadOverDraft = (req, definitions) => {
67
76
  const rootEntityName = typeof firstFromRef === 'string' ? firstFromRef : firstFromRef.id
68
77
  const rootEntity = definitions[rootEntityName]
69
78
 
70
- // read over the draft only if the root entity is draft-enabled and
71
- // the navigation starts with a draft entity (IsActiveEntity=false)
72
- if (!!rootEntity._isDraftEnabled && isActiveEntityRequested(firstFromRef.where)) return false
79
+ // read over the draft only if the root entity is draft-enabled
80
+ if (!rootEntity._isDraftEnabled) return false
73
81
 
74
- // read over the draft if the navigation target is an association and
75
- // only if it isn't annotated with @odata.draft.enclosed
76
- const pathSegments = fromRef.map(path => (typeof path === 'string' ? path : path.id))
82
+ // read over the draft only if the navigation starts from a draft entity, e.g.,
83
+ // /Books(ID=1, IsActiveEntity=false)
84
+ if (isActiveEntityRequested(firstFromRef.where)) return false
77
85
 
86
+ const pathSegments = fromRef.map(path => (typeof path === 'string' ? path : path.id))
78
87
  const excludeAssoc = assoc => {
79
88
  if (assoc.name === 'DraftAdministrativeData' || assoc.name === 'SiblingEntity') return true
80
89
  return false
81
90
  }
82
91
 
92
+ // Read over the draft only if:
93
+ // - the navigation target is an association and
94
+ // - isn't annotated with the @odata.draft.enclosed annotation
83
95
  return _hasNavToNonDraftEnclosedAssoc(pathSegments, definitions, excludeAssoc)
84
96
  }
85
97
 
@@ -109,7 +121,7 @@ const _readOverDraftHandler = async function (req, next) {
109
121
 
110
122
  const hasDraftEntity = hasDraft(definitions, sqlQuery)
111
123
 
112
- if (hasDraftEntity && sqlQuery.SELECT.where && sqlQuery.SELECT.where.length > 0) {
124
+ if (hasDraftEntity && sqlQuery.SELECT.where && sqlQuery.SELECT.where.length) {
113
125
  let cqnDraft = SELECT.from({
114
126
  ref: [...sqlQuery.SELECT.from.ref],
115
127
  as: sqlQuery.SELECT.from.as
@@ -93,6 +93,9 @@ const getUpdateDraftAdminCQN = ({ user }, draftUUID) => {
93
93
  const _addAlias = (where, tableName) => {
94
94
  // copy where
95
95
  return where.map(element => {
96
+ if (element.xpr) {
97
+ return { xpr: _addAlias(element.xpr, tableName) }
98
+ }
96
99
  if (element.ref && element.ref.length === 1) {
97
100
  // and copy ref
98
101
  return { ref: [tableName, element.ref[0]] }
@@ -57,7 +57,9 @@ const _removeIsActiveEntityCondition = where => {
57
57
  } else if (where[i] === 'and' && where[i + 1] === '(' && _isActiveEntity(where[i + 2])) {
58
58
  i = i + 6
59
59
  } else if (where[i].xpr) {
60
- newWhere.push({ xpr: _removeIsActiveEntityCondition(where[i].xpr) })
60
+ if (where[i].xpr.length) {
61
+ newWhere.push({ xpr: _removeIsActiveEntityCondition(where[i].xpr) })
62
+ }
61
63
  i++
62
64
  } else {
63
65
  newWhere.push(where[i])
@@ -65,7 +67,7 @@ const _removeIsActiveEntityCondition = where => {
65
67
  }
66
68
  }
67
69
 
68
- if (newWhere[0] === 'and') {
70
+ if (newWhere[0] === 'and' || newWhere[0] === 'or') {
69
71
  newWhere.splice(0, 1)
70
72
  } else if (newWhere[0] === '(' && newWhere[1] === 'and') {
71
73
  newWhere.splice(0, 2)
@@ -90,32 +92,35 @@ const deleteCondition = (index, whereCondition, isXpr = false) => {
90
92
  }
91
93
 
92
94
  const readAndDeleteKeywords = (keywords, whereCondition, toDelete = true) => {
93
- let index = whereCondition.findIndex(({ xpr }) => xpr)
94
- if (index !== -1) {
95
- const result = readAndDeleteKeywords(keywords, whereCondition[index].xpr, toDelete)
96
- if (result) {
97
- if (whereCondition[index].xpr.length === 0) {
98
- deleteCondition(index, whereCondition, true)
95
+ let index = -1
96
+ for (let i = 0; i < whereCondition.length; i++) {
97
+ const entry = whereCondition[i]
98
+ if (entry.xpr) {
99
+ const result = readAndDeleteKeywords(keywords, entry.xpr, toDelete)
100
+ if (result) {
101
+ if (entry.xpr.length === 0) {
102
+ deleteCondition(i, whereCondition, true)
103
+ }
104
+ return result
105
+ }
106
+ } else if (entry.ref) {
107
+ const refLastIndex = entry.ref.length - 1
108
+
109
+ if (keywords.length === 1) {
110
+ if (entry.ref[refLastIndex] === keywords[0]) {
111
+ index = i
112
+ break
113
+ }
99
114
  }
100
- return result
101
- }
102
- }
103
-
104
- index = whereCondition.findIndex(({ ref }) => {
105
- if (!ref) {
106
- return false
107
- }
108
-
109
- const refLastIndex = ref.length - 1
110
-
111
- if (keywords.length === 1) {
112
- return ref[refLastIndex] === keywords[0]
113
- }
114
115
 
115
- if (keywords.length === 2 && ref.length >= 2) {
116
- return ref[refLastIndex - 1] === keywords[0] && ref[refLastIndex] === keywords[1]
116
+ if (keywords.length === 2 && entry.ref.length >= 2) {
117
+ if (entry.ref[refLastIndex - 1] === keywords[0] && entry.ref[refLastIndex] === keywords[1]) {
118
+ index = i
119
+ break
120
+ }
121
+ }
117
122
  }
118
- })
123
+ }
119
124
 
120
125
  if (index === -1) {
121
126
  return
@@ -135,6 +140,10 @@ const readAndDeleteKeywords = (keywords, whereCondition, toDelete = true) => {
135
140
 
136
141
  const removeIsActiveEntityRecursively = where => {
137
142
  for (const entry of where) {
143
+ if (entry.xpr) {
144
+ entry.xpr = removeIsActiveEntityRecursively(entry.xpr)
145
+ continue
146
+ }
138
147
  if (entry.SELECT && entry.SELECT.where && entry.SELECT.from.ref && !entry.SELECT.from.ref[0].endsWith('_drafts')) {
139
148
  entry.SELECT.where = removeIsActiveEntityRecursively(entry.SELECT.where)
140
149
 
@@ -153,6 +162,10 @@ const isActiveEntityRequested = where => {
153
162
  let i = 0
154
163
 
155
164
  while (where[i]) {
165
+ if (where[i].xpr) {
166
+ const isRequested = isActiveEntityRequested(where.xpr)
167
+ if (isRequested) return true
168
+ }
156
169
  if (
157
170
  where[i].ref &&
158
171
  where[i].ref[where[i].ref.length - 1] === 'IsActiveEntity' &&
@@ -280,11 +280,28 @@ function executeInsertCQN(model, dbc, query, user, locale, txTimestamp) {
280
280
  }
281
281
 
282
282
  return _executeSimpleSQL(dbc, sql, values).then(affectedRows => {
283
+ const entriesOrRows = query.INSERT.entries || query.INSERT.rows
284
+ const affectedRowsCount = Array.isArray(affectedRows)
285
+ ? affectedRows.reduce((sum, rows) => sum + rows, 0)
286
+ : affectedRows
287
+ if (entriesOrRows && entriesOrRows.length !== affectedRowsCount) {
288
+ LOG._warn &&
289
+ LOG.warn(
290
+ `INSERT input deviates from affected rows (input: ${entriesOrRows.length}, affectedRows: ${affectedRowsCount})`,
291
+ {
292
+ sql,
293
+ args: values && values.length,
294
+ values,
295
+ query
296
+ }
297
+ )
298
+ throw new Error('Possible data loss by INSERT into HANA db. Please, update a corresponding HANA driver.')
299
+ }
283
300
  // InsertResult needs an object per row with its values
284
301
  // query.INSERT.values -> one row
285
302
  if (query.INSERT.values) return [{ affectedRows: 1, values: [values] }]
286
303
  // query.INSERT.entries or .rows -> multiple rows
287
- if (query.INSERT.entries || query.INSERT.rows) return values.map(v => ({ affectedRows: 1, values: v }))
304
+ if (entriesOrRows) return values.map(v => ({ affectedRows: 1, values: v }))
288
305
  // INSERT into SELECT
289
306
  return [{ affectedRows }]
290
307
  })
@@ -37,8 +37,11 @@ const search2cqn4sql = (query, entity, options) => {
37
37
  if (resolveLocalizedDataAtRuntime) {
38
38
  const onCondition = entity._relations[localizedAssociation.name].join(localizedAssociation.target, entity.name)
39
39
 
40
+ // REVISIT this is dirty but works for now
40
41
  // replace $user_locale placeholder with the user locale or the HANA session context
41
- onCondition[onCondition.length - 2] = { val: locale || "SESSION_CONTEXT('LOCALE')" }
42
+ if (onCondition[0].xpr) {
43
+ onCondition[0].xpr[onCondition[0].xpr.length - 1] = { val: locale || "SESSION_CONTEXT('LOCALE')" }
44
+ } else onCondition[onCondition.length - 2] = { val: locale || "SESSION_CONTEXT('LOCALE')" }
42
45
 
43
46
  // inner join the target table with the _texts table (the _texts table contains
44
47
  // the translated texts)
@@ -339,12 +339,10 @@ const _cqnToReqOptions = (query, kind, model, target) => {
339
339
  const queryObject = cds.odata.urlify(query, { kind, model })
340
340
  const reqOptions = {
341
341
  method: queryObject.method,
342
- url: encodeURI(
343
- queryObject.path
344
- // ugly workaround for Okra not allowing spaces in ( x eq 1 )
345
- .replace(/\( /g, '(')
346
- .replace(/ \)/g, ')')
347
- )
342
+ url: queryObject.path
343
+ // ugly workaround for Okra not allowing spaces in ( x eq 1 )
344
+ .replace(/\( /g, '(')
345
+ .replace(/ \)/g, ')')
348
346
  }
349
347
  if (queryObject.method !== 'GET' && queryObject.method !== 'HEAD') {
350
348
  reqOptions.data = kind === 'odata-v2' ? convertV2PayloadData(queryObject.body, target) : queryObject.body
@@ -17,29 +17,36 @@ const _getConvertibleEntries = req => {
17
17
  return [...orders, ...groups, ...filters, ...havings]
18
18
  }
19
19
 
20
- // REVISIT once sql can handle structured keys properly, this handler should not be required anymore
21
- const _handler = function (req) {
22
- // do simple checks upfront and exit early
23
- if (!req.query || typeof req.query === 'string') return
24
- if (!req.query.SELECT.orderBy && !req.query.SELECT.groupBy && !req.query.SELECT.where && !req.query.SELECT.having) {
25
- return
26
- }
27
-
28
- if (!req.target || !req.target.elements) return
20
+ const _convert = (refEntries, req) => {
21
+ for (const refEntry of refEntries) {
22
+ if (refEntry.xpr) {
23
+ _convert(refEntry.xpr, req)
24
+ continue
25
+ }
29
26
 
30
- for (const refEntry of _getConvertibleEntries(req)) {
31
27
  if (!refEntry.ref || refEntry.ref.length < 2) {
32
28
  // only check refs in format {ref: ['assoc', 'id']}
33
29
  continue
34
30
  }
35
-
36
31
  const element = req.target.elements[refEntry.ref[0]]
37
32
  if (!element || !element.is2one) return
38
-
39
33
  _convertRefForAssocToOneManaged(element, refEntry)
40
34
  }
41
35
  }
42
36
 
37
+ // REVISIT once sql can handle structured keys properly, this handler should not be required anymore
38
+ const _handler = function (req) {
39
+ // do simple checks upfront and exit early
40
+ if (!(req.query && req.query.SELECT) || typeof req.query === 'string') return
41
+ if (!req.query.SELECT.orderBy && !req.query.SELECT.groupBy && !req.query.SELECT.where && !req.query.SELECT.having) {
42
+ return
43
+ }
44
+
45
+ if (!req.target || !req.target.elements) return
46
+
47
+ _convert(_getConvertibleEntries(req), req)
48
+ }
49
+
43
50
  _handler._initial = true
44
51
 
45
52
  module.exports = _handler
@@ -83,14 +83,10 @@ function _args(args) {
83
83
 
84
84
  if (hasValidProps(cur, 'func', 'args')) {
85
85
  res.push(`${cur.func}(${_args(cur.args)})`)
86
- }
87
-
88
- if (hasValidProps(cur, 'ref')) {
89
- res.push(cur.ref.join('/'))
90
- }
91
-
92
- if (hasValidProps(cur, 'val')) {
93
- res.push(formatVal(cur.val))
86
+ } else if (hasValidProps(cur, 'ref')) {
87
+ res.push(_format(cur))
88
+ } else if (hasValidProps(cur, 'val')) {
89
+ res.push(_format(cur))
94
90
  }
95
91
  }
96
92
 
@@ -98,13 +94,13 @@ function _args(args) {
98
94
  }
99
95
 
100
96
  const _in = (column, /* in */ collection, target, kind, isLambda) => {
101
- const ref = isLambda ? [LAMBDA_VARIABLE, ...column.ref].join('/') : column.ref.join('/')
97
+ const ref = _format(column, null, target, kind, isLambda)
102
98
  // { val: [ 1, 2, 3 ] } or { list: [ { val: 1}, { val: 2}, { val: 3} ] }
103
99
  const values = collection.val || collection.list
104
100
  if (values && values.length) {
105
101
  // REVISIT: what about OData `in` operator?
106
- const expressions = values.map(value => `${ref} eq ${_format(value, ref, target, kind, isLambda)}`)
107
- return expressions.join(' or ')
102
+ const expressions = values.map(value => `${ref}%20eq%20${_format(value, ref, target, kind, isLambda)}`)
103
+ return expressions.join('%20or%20')
108
104
  }
109
105
  }
110
106
 
@@ -119,9 +115,10 @@ const _odataV2Func = (func, args) => {
119
115
  }
120
116
 
121
117
  const _format = (cur, element, target, kind, isLambda) => {
122
- if (typeof cur !== 'object') return formatVal(cur, element, target, kind)
123
- if (hasValidProps(cur, 'ref')) return isLambda ? [LAMBDA_VARIABLE, ...cur.ref].join('/') : cur.ref.join('/')
124
- if (hasValidProps(cur, 'val')) return formatVal(cur.val, element, target, kind)
118
+ if (typeof cur !== 'object') return encodeURIComponent(formatVal(cur, element, target, kind))
119
+ if (hasValidProps(cur, 'ref'))
120
+ return encodeURIComponent(isLambda ? [LAMBDA_VARIABLE, ...cur.ref].join('/') : cur.ref.join('/'))
121
+ if (hasValidProps(cur, 'val')) return encodeURIComponent(formatVal(cur.val, element, target, kind))
125
122
  if (hasValidProps(cur, 'xpr')) return `(${_xpr(cur.xpr, target, kind, isLambda)})`
126
123
  // REVISIT: How to detect the types for all functions?
127
124
  if (hasValidProps(cur, 'func', 'args')) {
@@ -156,7 +153,7 @@ function _xpr(expr, target, kind, isLambda) {
156
153
  } else if (isOrIsNotValue) {
157
154
  // REVISIT: "is" only used for null values?
158
155
  const operator = isOrIsNotValue[1] /* 'is not' */ ? 'ne' : 'eq'
159
- res.push(...[operator, formatVal(isOrIsNotValue[2])])
156
+ res.push(...[operator, _format({ val: isOrIsNotValue[2] })])
160
157
  } else if (cur === 'between') {
161
158
  // ref gt low.val and ref lt high.val
162
159
  const between = [expr[i - 1], 'gt', expr[i + 1], 'and', expr[i - 1], 'lt', expr[i + 3]]
@@ -188,7 +185,7 @@ function _xpr(expr, target, kind, isLambda) {
188
185
  }
189
186
  }
190
187
 
191
- return res.join(' ')
188
+ return res.join('%20')
192
189
  }
193
190
 
194
191
  const _keysOfWhere = (where, kind, target) => {
@@ -202,11 +199,11 @@ const _keysOfWhere = (where, kind, target) => {
202
199
  const res = []
203
200
  for (const cur of where) {
204
201
  if (hasValidProps(cur, 'ref')) {
205
- res.push(cur.ref.join('/'))
202
+ res.push(_format(cur))
206
203
  } else if (hasValidProps(cur, 'val')) {
207
204
  // find previous ref
208
205
  const element = res[res.length - 2]
209
- res.push(formatVal(cur.val, element, target, kind))
206
+ res.push(_format(cur, element, target, kind))
210
207
  } else if (cur === 'and') {
211
208
  res.push(',')
212
209
  } else {
@@ -267,15 +264,15 @@ const _parseColumnsV2 = (columns, prefix = []) => {
267
264
 
268
265
  if (hasValidProps(column, 'expand')) {
269
266
  const parsed = _parseColumnsV2(column.expand, [refName])
270
- expand.push(refName, ...parsed.expand)
267
+ expand.push(encodeURIComponent(refName), ...parsed.expand)
271
268
  select.push(...parsed.select)
272
269
  } else {
273
- select.push(refName)
270
+ select.push(encodeURIComponent(refName))
274
271
  }
275
272
  }
276
273
 
277
274
  if (column === '*') {
278
- select.push(`${prefix.join('/')}/*`)
275
+ select.push(encodeURIComponent(`${prefix.join('/')}/*`))
279
276
  }
280
277
  }
281
278
 
@@ -288,7 +285,7 @@ const _parseColumns = columns => {
288
285
 
289
286
  for (const column of columns) {
290
287
  if (hasValidProps(column, 'ref')) {
291
- let refName = column.ref.join('/')
288
+ let refName = _format(column)
292
289
  if (hasValidProps(column, 'expand')) {
293
290
  // REVISIT: incomplete, see test Foo?$expand=invoices($count=true;$expand=item($search="some"))
294
291
  if (!columns.some(c => !c.expand)) select.push(refName)
@@ -350,16 +347,16 @@ function $orderBy(orderBy) {
350
347
 
351
348
  for (const cur of orderBy) {
352
349
  if (hasValidProps(cur, 'ref', 'sort')) {
353
- res.push(cur.ref.join('/') + ' ' + cur.sort)
350
+ res.push(_format(cur) + '%20' + cur.sort)
354
351
  continue
355
352
  }
356
353
 
357
354
  if (hasValidProps(cur, 'ref')) {
358
- res.push(cur.ref.join('/'))
355
+ res.push(_format(cur))
359
356
  }
360
357
 
361
358
  if (hasValidProps(cur, 'func', 'sort')) {
362
- res.push(`${cur.func}(${_args(cur.args)})` + ' ' + cur.sort)
359
+ res.push(`${cur.func}(${_args(cur.args)})` + '%20' + cur.sort)
363
360
  continue
364
361
  }
365
362
 
@@ -382,7 +379,7 @@ function parseSearch(search) {
382
379
 
383
380
  if (hasValidProps(cur, 'val')) {
384
381
  // search term must not be formatted
385
- res.push(`"${cur.val}"`)
382
+ res.push(`"${encodeURIComponent(cur.val)}"`)
386
383
  }
387
384
 
388
385
  if (typeof cur === 'string') {
@@ -398,7 +395,7 @@ function parseSearch(search) {
398
395
  }
399
396
 
400
397
  function $search(search, kind) {
401
- const expr = parseSearch(search).join(' ').replace('( ', '(').replace(' )', ')')
398
+ const expr = parseSearch(search).join('%20').replace('(%20', '(').replace('%20)', ')')
402
399
 
403
400
  if (expr) {
404
401
  // odata-v2 may support custom query option "search"