@sap/cds-compiler 3.9.2 → 4.0.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 +98 -0
- package/README.md +0 -1
- package/bin/cdsc.js +11 -23
- package/bin/cdsse.js +3 -3
- package/doc/API.md +5 -0
- package/doc/CHANGELOG_ARCHIVE.md +1 -1
- package/doc/CHANGELOG_BETA.md +17 -1
- package/doc/CHANGELOG_DEPRECATED.md +28 -0
- package/lib/api/.eslintrc.json +1 -1
- package/lib/api/main.js +26 -8
- package/lib/api/options.js +2 -0
- package/lib/base/error.js +2 -0
- package/lib/base/message-registry.js +144 -65
- package/lib/base/messages.js +213 -107
- package/lib/base/model.js +11 -11
- package/lib/checks/.eslintrc.json +1 -1
- package/lib/checks/annotationsOData.js +2 -2
- package/lib/checks/elements.js +1 -1
- package/lib/checks/enricher.js +26 -3
- package/lib/checks/onConditions.js +67 -12
- package/lib/checks/queryNoDbArtifacts.js +106 -105
- package/lib/checks/sql-snippets.js +2 -0
- package/lib/checks/types.js +12 -6
- package/lib/checks/validator.js +2 -2
- package/lib/compiler/assert-consistency.js +10 -8
- package/lib/compiler/builtins.js +8 -2
- package/lib/compiler/checks.js +52 -35
- package/lib/compiler/define.js +31 -26
- package/lib/compiler/extend.js +120 -65
- package/lib/compiler/finalize-parse-cdl.js +12 -43
- package/lib/compiler/generate.js +16 -5
- package/lib/compiler/index.js +8 -5
- package/lib/compiler/kick-start.js +4 -3
- package/lib/compiler/populate.js +96 -95
- package/lib/compiler/propagator.js +7 -8
- package/lib/compiler/resolve.js +377 -103
- package/lib/compiler/shared.js +794 -517
- package/lib/compiler/tweak-assocs.js +8 -6
- package/lib/compiler/utils.js +44 -0
- package/lib/edm/annotations/genericTranslation.js +24 -6
- package/lib/edm/csn2edm.js +47 -45
- package/lib/edm/edm.js +34 -31
- package/lib/edm/edmAnnoPreprocessor.js +0 -23
- package/lib/edm/edmInboundChecks.js +7 -2
- package/lib/edm/edmPreprocessor.js +18 -17
- package/lib/edm/edmUtils.js +8 -4
- package/lib/gen/Dictionary.json +18 -0
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +4 -2
- package/lib/gen/languageParser.js +5006 -4582
- package/lib/json/from-csn.js +159 -114
- package/lib/json/to-csn.js +60 -89
- package/lib/language/antlrParser.js +17 -13
- package/lib/language/docCommentParser.js +11 -1
- package/lib/language/genericAntlrParser.js +13 -10
- package/lib/language/language.g4 +168 -97
- package/lib/main.d.ts +128 -36
- package/lib/main.js +1 -1
- package/lib/model/csnRefs.js +24 -5
- package/lib/model/csnUtils.js +9 -8
- package/lib/model/revealInternalProperties.js +7 -12
- package/lib/modelCompare/compare.js +1 -1
- package/lib/modelCompare/utils/filter.js +40 -2
- package/lib/optionProcessor.js +0 -3
- package/lib/render/toCdl.js +247 -214
- package/lib/render/toHdbcds.js +197 -181
- package/lib/render/toSql.js +325 -289
- package/lib/render/utils/common.js +42 -4
- package/lib/render/utils/delta.js +1 -1
- package/lib/render/utils/sql.js +3 -3
- package/lib/transform/braceExpression.js +2 -2
- package/lib/transform/db/.eslintrc.json +1 -1
- package/lib/transform/db/applyTransformations.js +3 -3
- package/lib/transform/db/associations.js +24 -12
- package/lib/transform/db/expansion.js +17 -18
- package/lib/transform/db/flattening.js +17 -21
- package/lib/transform/db/rewriteCalculatedElements.js +171 -64
- package/lib/transform/db/views.js +3 -4
- package/lib/transform/draft/db.js +21 -12
- package/lib/transform/draft/odata.js +4 -0
- package/lib/transform/forOdataNew.js +11 -10
- package/lib/transform/forRelationalDB.js +12 -7
- package/lib/transform/localized.js +5 -3
- package/lib/transform/odata/toFinalBaseType.js +5 -5
- package/lib/transform/odata/typesExposure.js +3 -3
- package/lib/transform/parseExpr.js +3 -0
- package/lib/transform/transformUtilsNew.js +43 -23
- package/lib/transform/translateAssocsToJoins.js +7 -6
- package/lib/transform/universalCsn/.eslintrc.json +1 -1
- package/lib/transform/universalCsn/coreComputed.js +7 -5
- package/lib/transform/universalCsn/universalCsnEnricher.js +12 -12
- package/lib/utils/file.js +3 -3
- package/lib/utils/moduleResolve.js +1 -1
- package/package.json +2 -2
- package/share/messages/{duplicate-autoexposed.md → def-duplicate-autoexposed.md} +5 -1
- package/share/messages/message-explanations.json +1 -1
|
@@ -4,17 +4,19 @@ const { setProp } = require('../../base/model');
|
|
|
4
4
|
const { CompilerAssertion } = require('../../base/error');
|
|
5
5
|
const {
|
|
6
6
|
forEachDefinition, applyTransformationsOnNonDictionary, applyTransformationsOnDictionary, implicitAs, cloneCsnNonDict, getUtils,
|
|
7
|
+
forEachMemberRecursively,
|
|
7
8
|
} = require('../../model/csnUtils');
|
|
8
9
|
const { getBranches } = require('./flattening');
|
|
9
10
|
const { getColumnMap } = require('./views');
|
|
11
|
+
const { checkForeignKeyAccess } = require('../../checks/onConditions');
|
|
10
12
|
|
|
11
13
|
const cloneCsnOptions = { hiddenPropertiesToClone: [ '_art', '_links', '$env', '$scope' ] };
|
|
14
|
+
|
|
15
|
+
|
|
12
16
|
/**
|
|
13
17
|
* Rewrite usage of calculated Elements into the expression itself.
|
|
14
18
|
* Delete calculated elements in entities after processing so they don't materialize on the db.
|
|
15
19
|
*
|
|
16
|
-
* TODO: Calculated elements on-write (`stored: true`)
|
|
17
|
-
*
|
|
18
20
|
* @param {CSN.Model} csn
|
|
19
21
|
* @param {CSN.Options} options
|
|
20
22
|
* @param {string} pathDelimiter
|
|
@@ -32,7 +34,7 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
|
|
|
32
34
|
if (artifact.query || artifact.projection) {
|
|
33
35
|
views.push({ artifact, artifactName });
|
|
34
36
|
}
|
|
35
|
-
else {
|
|
37
|
+
else if (artifact.elements) { // can happen with CSN input
|
|
36
38
|
rewriteInEntity(artifact);
|
|
37
39
|
entities.push({ artifact, artifactName });
|
|
38
40
|
}
|
|
@@ -44,8 +46,17 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
|
|
|
44
46
|
entities.forEach(({ artifactName }) => {
|
|
45
47
|
applyTransformationsOnNonDictionary(csn.definitions, artifactName, {
|
|
46
48
|
ref: (_parent, _prop, ref, _path, root, index) => {
|
|
47
|
-
if (_parent._art && _parent._art.value) {
|
|
48
|
-
|
|
49
|
+
if (_parent._art?.value && !_parent._art.value.stored) {
|
|
50
|
+
if (_parent._art.value.ref) {
|
|
51
|
+
// Ensure that we don't break any navigation by only replacing the real element at the end
|
|
52
|
+
const leafLength = getLeafLength(_parent._links);
|
|
53
|
+
root[index].ref = [ ...root[index].ref.slice(0, -1 * leafLength), ..._parent._art.value.ref ];
|
|
54
|
+
setProp(root[index], '_links', [ ...root[index]._links.slice(0, leafLength), ..._parent._art.value._links ]);
|
|
55
|
+
setProp(root[index], '_art', _parent._art);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
root[index] = _parent._art.value;
|
|
59
|
+
}
|
|
49
60
|
// Note: Depends on A2J rejecting deeply nested filters
|
|
50
61
|
applyTransformationsOnNonDictionary(root, index, {
|
|
51
62
|
ref: (__parent, _, _ref) => {
|
|
@@ -55,7 +66,11 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
|
|
|
55
66
|
});
|
|
56
67
|
}
|
|
57
68
|
},
|
|
58
|
-
}, {
|
|
69
|
+
}, {
|
|
70
|
+
drillRef: true,
|
|
71
|
+
// skip "type" to avoid going into type.ref
|
|
72
|
+
skipStandard: { type: 1 },
|
|
73
|
+
}, [ 'definitions' ]);
|
|
59
74
|
});
|
|
60
75
|
|
|
61
76
|
|
|
@@ -75,11 +90,91 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
|
|
|
75
90
|
}, {}, [ 'definitions' ]);
|
|
76
91
|
});
|
|
77
92
|
|
|
78
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Check that all paths in calculated elements-on write either access normal fields
|
|
95
|
+
* (structures are already rejected by the compiler) or access the foreign keys of
|
|
96
|
+
* associations. Non-fk fields must be rejected.
|
|
97
|
+
* Filters and parameters are not allowed.
|
|
98
|
+
*
|
|
99
|
+
* Note: This coding is similar to checks/onConditions.js, but does not check $self or other
|
|
100
|
+
* ON-condition related stuff.
|
|
101
|
+
*
|
|
102
|
+
* @param {object} parent
|
|
103
|
+
* @param {(string|object)[]} value
|
|
104
|
+
* @param {CSN.Path} csnPath
|
|
105
|
+
*/
|
|
106
|
+
function checkPathsInStoredCalcElement( parent, value, csnPath ) {
|
|
107
|
+
const { _links } = parent;
|
|
108
|
+
|
|
109
|
+
// If there is only one path step, it's been checked before.
|
|
110
|
+
for (let i = 0; i < value.length - 1; ++i) {
|
|
111
|
+
let hasPathError = false;
|
|
112
|
+
const step = value[i];
|
|
113
|
+
const stepArt = _links[i].art;
|
|
114
|
+
|
|
115
|
+
if (stepArt.target) {
|
|
116
|
+
const id = step.id || step;
|
|
117
|
+
if (stepArt.on) {
|
|
118
|
+
// It's an unmanaged association - traversal is always forbidden
|
|
119
|
+
error('ref-unexpected-navigation', csnPath, { '#': 'calc-unmanaged', id, elemref: parent });
|
|
120
|
+
hasPathError = true;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// It's a managed association - access of the foreign keys is allowed
|
|
124
|
+
checkForeignKeyAccess(parent, i, csnPath, (errorIndex) => {
|
|
125
|
+
error('ref-unexpected-navigation', csnPath, {
|
|
126
|
+
'#': 'calc-non-fk', id, elemref: parent, name: value[errorIndex].id || value[errorIndex],
|
|
127
|
+
});
|
|
128
|
+
hasPathError = true;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (typeof step === 'object') {
|
|
133
|
+
if (step.where) {
|
|
134
|
+
error('ref-unexpected-filter', csnPath, { '#': 'calc', elemref: parent });
|
|
135
|
+
hasPathError = true;
|
|
136
|
+
}
|
|
137
|
+
if (step.args) {
|
|
138
|
+
error('ref-unexpected-args', csnPath, { '#': 'calc', elemref: parent });
|
|
139
|
+
hasPathError = true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (hasPathError)
|
|
143
|
+
break; // avoid too many consequent errors
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Last pass, turn .value in tables into a simple 'val' so we don't need to rewrite/flatten properly - will kill them later
|
|
79
148
|
entities.forEach(({ artifact, artifactName }) => {
|
|
80
149
|
dummifyInEntity(artifact, [ 'definitions', artifactName ]);
|
|
150
|
+
|
|
151
|
+
forEachMemberRecursively({ elements: artifact.elements }, (element, elementName, _prop, path) => {
|
|
152
|
+
if (element.value?.stored) {
|
|
153
|
+
applyTransformationsOnNonDictionary(element, 'value', {
|
|
154
|
+
ref(parent, prop, value, csnPath) {
|
|
155
|
+
checkPathsInStoredCalcElement(parent, value, csnPath);
|
|
156
|
+
},
|
|
157
|
+
}, {}, path);
|
|
158
|
+
}
|
|
159
|
+
}, [ 'definitions', artifactName ]);
|
|
81
160
|
});
|
|
82
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Get the length of the effective leaf - since structures are not flat yet it might be more than 1.
|
|
164
|
+
* Walk from the back until we find the first association/composition.
|
|
165
|
+
*
|
|
166
|
+
* @param {object[]} links
|
|
167
|
+
* @returns {number}
|
|
168
|
+
*/
|
|
169
|
+
function getLeafLength( links ) {
|
|
170
|
+
for (let i = links.length - 1; i >= 0; i--) {
|
|
171
|
+
const { art } = links[i];
|
|
172
|
+
if (art.target)
|
|
173
|
+
return links.length - i - 1;
|
|
174
|
+
}
|
|
175
|
+
return links.length;
|
|
176
|
+
}
|
|
177
|
+
|
|
83
178
|
/**
|
|
84
179
|
* Rewrite calculated-elements-columns in views/projections and replace them
|
|
85
180
|
* with their "root"-expression.
|
|
@@ -173,11 +268,33 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
|
|
|
173
268
|
* @param {CSN.Artifact} artifact The artifact currently being processed
|
|
174
269
|
*/
|
|
175
270
|
function rewriteInEntity( artifact ) {
|
|
271
|
+
let reorderElements = false;
|
|
176
272
|
applyTransformationsOnDictionary(artifact.elements, {
|
|
177
273
|
value: (parent, prop, value) => {
|
|
178
|
-
|
|
274
|
+
if (value.stored)
|
|
275
|
+
reorderElements = true;
|
|
276
|
+
replaceValuesWithBaseValue(parent);
|
|
179
277
|
},
|
|
180
278
|
});
|
|
279
|
+
// on-write must appear at the end of the elements. Order of the on-write between themselves
|
|
280
|
+
// should be as written.
|
|
281
|
+
if (reorderElements) {
|
|
282
|
+
const newElements = Object.create(null);
|
|
283
|
+
const onWrite = [];
|
|
284
|
+
for (const name in artifact.elements) {
|
|
285
|
+
const element = artifact.elements[name];
|
|
286
|
+
if (element.value?.stored)
|
|
287
|
+
onWrite.push(name);
|
|
288
|
+
else
|
|
289
|
+
newElements[name] = element;
|
|
290
|
+
}
|
|
291
|
+
// Add the on-write to the end
|
|
292
|
+
onWrite.forEach((name) => {
|
|
293
|
+
newElements[name] = artifact.elements[name];
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
artifact.elements = newElements;
|
|
297
|
+
}
|
|
181
298
|
}
|
|
182
299
|
|
|
183
300
|
/**
|
|
@@ -185,14 +302,13 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
|
|
|
185
302
|
* - a .val thing
|
|
186
303
|
* - a .ref to a non-value thing
|
|
187
304
|
*
|
|
188
|
-
* @param {object
|
|
189
|
-
* @param {object} value
|
|
305
|
+
* @param {object} parent
|
|
190
306
|
*/
|
|
191
|
-
function replaceValuesWithBaseValue( parent
|
|
192
|
-
if (value.val
|
|
193
|
-
return;
|
|
307
|
+
function replaceValuesWithBaseValue( parent ) {
|
|
308
|
+
if (parent.value.val !== undefined)
|
|
309
|
+
return; // literal; no need to traverse
|
|
194
310
|
|
|
195
|
-
const stack = [ { parent, value } ];
|
|
311
|
+
const stack = [ { parent, value: parent.value } ];
|
|
196
312
|
while (stack.length > 0) {
|
|
197
313
|
const current = stack.pop();
|
|
198
314
|
|
|
@@ -209,25 +325,20 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
|
|
|
209
325
|
},
|
|
210
326
|
});
|
|
211
327
|
}
|
|
212
|
-
else if (current.value.ref && current.value._art?.value) {
|
|
213
|
-
// TODO: Check for calculated elements on-write
|
|
328
|
+
else if (current.value.ref && current.value._art?.value && !current.value._art?.value.stored) {
|
|
214
329
|
const linksBase = current.value._links;
|
|
215
330
|
const refBase = current.value.ref;
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
refBase,
|
|
223
|
-
linksBase,
|
|
224
|
-
}));
|
|
331
|
+
const newValue = replaceInRef(current.value, current.value._art.value, current.isInXpr, refBase, linksBase);
|
|
332
|
+
const prop = Array.isArray(current.parent) ? current.parent.indexOf(current.value) : 'value';
|
|
333
|
+
if (prop === -1)
|
|
334
|
+
throw new CompilerAssertion('Calculated Elements: Value not in parent; should never happen!');
|
|
335
|
+
current.parent[prop] = newValue;
|
|
336
|
+
stack.push(Object.assign(current, { value: newValue, refBase, linksBase }));
|
|
225
337
|
}
|
|
226
|
-
|
|
227
|
-
else if (current.value.val) { // this is the base case - or a ref to a non-calculated element
|
|
338
|
+
else if (current.value.val) {
|
|
228
339
|
if (current.isInXpr) { // inside of expressions we directly need the val
|
|
229
340
|
current.parent.val = current.value.val;
|
|
230
|
-
delete current.parent.value;
|
|
341
|
+
delete current.parent.value; // TODO: current.parent could be an array!
|
|
231
342
|
}
|
|
232
343
|
else { // outside of expressions, i.e. as normal elements, we need it in a .value wrapper
|
|
233
344
|
current.parent.value = current.value;
|
|
@@ -243,18 +354,18 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
|
|
|
243
354
|
*
|
|
244
355
|
* We either "trick" it into the correct place in an .xpr or we simply overwrite the existing .ref
|
|
245
356
|
*
|
|
246
|
-
* @param {object}
|
|
357
|
+
* @param {object} oldValue
|
|
247
358
|
* @param {object} newValue
|
|
248
359
|
* @param {boolean} isInXpr
|
|
249
360
|
* @param {Array} refBase
|
|
250
361
|
* @param {Array} linksBase
|
|
251
|
-
* @
|
|
362
|
+
* @returns {object|Array}
|
|
252
363
|
*/
|
|
253
|
-
function replaceInRef(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
value
|
|
257
|
-
|
|
364
|
+
function replaceInRef( oldValue, newValue, isInXpr, refBase, linksBase ) {
|
|
365
|
+
const clone = { value: cloneCsnNonDict({ value: newValue }, cloneCsnOptions).value };
|
|
366
|
+
if (oldValue.stored)
|
|
367
|
+
clone.value.stored = oldValue.stored;
|
|
368
|
+
|
|
258
369
|
const refPrefix = refBase.slice(0, -1);
|
|
259
370
|
const linksPrefix = linksBase.slice(0, -1);
|
|
260
371
|
if (newValue.xpr) {
|
|
@@ -268,24 +379,15 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
|
|
|
268
379
|
}
|
|
269
380
|
},
|
|
270
381
|
}, {
|
|
271
|
-
// Do not rewrite refs inside
|
|
382
|
+
// Do not rewrite refs inside an association-where; avoids endless loop
|
|
272
383
|
skipStandard: { where: true },
|
|
273
384
|
});
|
|
274
|
-
if (indexInParent > -1) // a .xpr in a .xpr
|
|
275
|
-
parent[indexInParent] = clone.value;
|
|
276
|
-
else
|
|
277
|
-
parent.value = clone.value;
|
|
278
385
|
}
|
|
279
|
-
else {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
else
|
|
283
|
-
parent.value = clone.value;
|
|
284
|
-
if (clone.value.ref && clone.value.ref[0] !== '$self' && clone.value.ref[0] !== '$projection' ) {
|
|
285
|
-
clone.value.ref = [ ...refPrefix, ...clone.value.ref ];
|
|
286
|
-
clone.value._links = [ ...linksPrefix, ...clone.value._links ]; // TODO: Make non-enum, increment idx
|
|
287
|
-
}
|
|
386
|
+
else if (clone.value.ref && clone.value.ref[0] !== '$self' && clone.value.ref[0] !== '$projection' ) {
|
|
387
|
+
clone.value.ref = [ ...refPrefix, ...clone.value.ref ];
|
|
388
|
+
clone.value._links = [ ...linksPrefix, ...clone.value._links ]; // TODO: Make non-enum, increment idx
|
|
288
389
|
}
|
|
390
|
+
return clone.value;
|
|
289
391
|
}
|
|
290
392
|
|
|
291
393
|
/**
|
|
@@ -295,6 +397,7 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
|
|
|
295
397
|
*
|
|
296
398
|
* @param {CSN.Elements} elements Artifact elements
|
|
297
399
|
* @param {object} carrier The thing that will "carry" the columns - .SELECT or .projection
|
|
400
|
+
* @returns {Function[]} Cleanup callbacks that remove `_`-links.
|
|
298
401
|
*/
|
|
299
402
|
function calculateColumns( elements, carrier ) {
|
|
300
403
|
carrier.columns = [ '*' ];
|
|
@@ -358,6 +461,7 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
|
|
|
358
461
|
* @param {CSN.Elements} elements
|
|
359
462
|
* @param {CSN.QuerySelect} SELECT
|
|
360
463
|
* @param {boolean} containsExpandInline
|
|
464
|
+
* @returns {Function[]} Cleanup callbacks that remove `_`-links.
|
|
361
465
|
*/
|
|
362
466
|
function makeAllCalculatedElementsExplicitColumns( elements, SELECT, containsExpandInline ) {
|
|
363
467
|
const cleanupCallbacks = [];
|
|
@@ -366,7 +470,7 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
|
|
|
366
470
|
const hasStar = SELECT.columns.includes('*');
|
|
367
471
|
const unfoldingMap = {};
|
|
368
472
|
let starContainsCalculated = false;
|
|
369
|
-
let
|
|
473
|
+
let containsCalcOnRead = false;
|
|
370
474
|
for (const name in elements) {
|
|
371
475
|
const originalRef = columnMap[name] && columnMap[name].ref || [ name ];
|
|
372
476
|
|
|
@@ -377,17 +481,19 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
|
|
|
377
481
|
else
|
|
378
482
|
element = columnMap[name]?._art || columnMap[name]?._element || root[name] || elements[name];
|
|
379
483
|
const branches = getBranches(element, name, effectiveType, pathDelimiter); // TODO: is our elements[name] really the root[name]?
|
|
380
|
-
if (
|
|
381
|
-
|
|
484
|
+
if (hasCalcOnReadLeaf(branches)) {
|
|
485
|
+
containsCalcOnRead = true;
|
|
382
486
|
const columns = [];
|
|
383
487
|
for (const branchName in branches) {
|
|
488
|
+
const branch = branches[branchName];
|
|
489
|
+
const leafElement = branch.steps[branch.steps.length - 1];
|
|
384
490
|
if (columnMap[branchName]) { // Existing column - don't overwrite, we need $env!
|
|
385
491
|
columns.push(columnMap[branchName]);
|
|
386
492
|
}
|
|
387
493
|
else {
|
|
388
494
|
// TODO: Hm, will we have a $env in the leaf of the thing then?
|
|
389
495
|
const column = { ref: [ ...originalRef, ...branches[branchName].ref.slice(1) ], as: branchName };
|
|
390
|
-
setProp(column, '_element',
|
|
496
|
+
setProp(column, '_element', leafElement);
|
|
391
497
|
cleanupCallbacks.push(() => delete column._element);
|
|
392
498
|
columns.push(column);
|
|
393
499
|
}
|
|
@@ -409,10 +515,10 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
|
|
|
409
515
|
}
|
|
410
516
|
}
|
|
411
517
|
|
|
412
|
-
if (containsExpandInline &&
|
|
518
|
+
if (containsExpandInline && containsCalcOnRead) {
|
|
413
519
|
error('query-unsupported-calc', SELECT.$path, { '#': 'std' });
|
|
414
520
|
}
|
|
415
|
-
else if (
|
|
521
|
+
else if (containsCalcOnRead) {
|
|
416
522
|
const newColumns = [];
|
|
417
523
|
if (hasStar && !starContainsCalculated)
|
|
418
524
|
newColumns.push('*');
|
|
@@ -422,22 +528,23 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
|
|
|
422
528
|
newColumns.push(...columns);
|
|
423
529
|
}
|
|
424
530
|
|
|
425
|
-
|
|
426
531
|
SELECT.columns = newColumns;
|
|
427
532
|
}
|
|
428
533
|
return cleanupCallbacks;
|
|
429
534
|
}
|
|
430
535
|
|
|
431
536
|
/**
|
|
537
|
+
* Returns true if any leaf node is a calculated element on-read.
|
|
538
|
+
* On-write behaves like regular elements, hence they do not count here.
|
|
432
539
|
*
|
|
433
540
|
* @param {object} branches
|
|
434
541
|
* @returns {boolean}
|
|
435
542
|
*/
|
|
436
|
-
function
|
|
543
|
+
function hasCalcOnReadLeaf( branches ) {
|
|
437
544
|
for (const branchName in branches) {
|
|
438
545
|
const branch = branches[branchName].steps;
|
|
439
546
|
const leaf = branch[branch.length - 1];
|
|
440
|
-
if (
|
|
547
|
+
if (hasOnReadValue(leaf))
|
|
441
548
|
return true;
|
|
442
549
|
}
|
|
443
550
|
|
|
@@ -451,13 +558,13 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
|
|
|
451
558
|
* @param {object} baseLeaf Leaf to start at
|
|
452
559
|
* @returns {boolean}
|
|
453
560
|
*/
|
|
454
|
-
function
|
|
561
|
+
function hasOnReadValue( baseLeaf ) {
|
|
455
562
|
const visited = new WeakSet();
|
|
456
563
|
const stack = [ baseLeaf ];
|
|
457
564
|
while (stack.length > 0) {
|
|
458
565
|
const leaf = stack.pop();
|
|
459
566
|
if (!visited.has(leaf)) { // Don't re-process things
|
|
460
|
-
if (leaf.value)
|
|
567
|
+
if (leaf.value && !leaf.value.stored)
|
|
461
568
|
return true;
|
|
462
569
|
else if (leaf._art)
|
|
463
570
|
stack.push(leaf._art);
|
|
@@ -575,9 +682,9 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
|
|
|
575
682
|
* @param {CSN.Model} csn
|
|
576
683
|
*/
|
|
577
684
|
function processCalculatedElementsInEntities( csn ) {
|
|
578
|
-
forEachDefinition(csn, (artifact,
|
|
685
|
+
forEachDefinition(csn, (artifact, definitionName) => {
|
|
579
686
|
if (artifact.kind === 'entity' && !(artifact.query || artifact.projection))
|
|
580
|
-
|
|
687
|
+
removeDummyValueInEntity(artifact, [ 'definitions', definitionName ]);
|
|
581
688
|
});
|
|
582
689
|
}
|
|
583
690
|
|
|
@@ -590,13 +697,13 @@ function processCalculatedElementsInEntities( csn ) {
|
|
|
590
697
|
* @todo calculated elements that "live" on the database?
|
|
591
698
|
* @todo error when artifact is empty afterwards? Probably better as a CSN check!
|
|
592
699
|
*/
|
|
593
|
-
function
|
|
700
|
+
function removeDummyValueInEntity( artifact, path ) {
|
|
594
701
|
applyTransformationsOnDictionary(artifact.elements, {
|
|
595
702
|
value: (parent, prop, value, p, root) => {
|
|
596
703
|
if (!value.stored)
|
|
597
704
|
delete root[p[p.length - 1]];
|
|
598
705
|
},
|
|
599
|
-
}, {}, path);
|
|
706
|
+
}, {}, path.concat( 'elements' ));
|
|
600
707
|
}
|
|
601
708
|
|
|
602
709
|
/**
|
|
@@ -4,7 +4,6 @@ const {
|
|
|
4
4
|
getUtils, cloneCsnNonDict, applyTransformationsOnNonDictionary,
|
|
5
5
|
} = require('../../model/csnUtils');
|
|
6
6
|
const { implicitAs } = require('../../model/csnRefs');
|
|
7
|
-
const { isBetaEnabled } = require('../../base/model');
|
|
8
7
|
const { ModelError } = require('../../base/error');
|
|
9
8
|
|
|
10
9
|
/**
|
|
@@ -208,11 +207,11 @@ function getViewTransformer( csn, options, messageFunctions, transformCommon ) {
|
|
|
208
207
|
*/
|
|
209
208
|
function handleAssociationElement( query, elements, columnMap, publishedMixins, elem, elemName, elementsPath, queryPath ) {
|
|
210
209
|
if (isUnion(queryPath) && options.transformation === 'hdbcds') {
|
|
211
|
-
if (
|
|
210
|
+
if (doA2J) {
|
|
212
211
|
if (elem.keys)
|
|
213
|
-
info(
|
|
212
|
+
info('query-ignoring-assoc-in-union', queryPath, { name: elemName, '#': 'managed' });
|
|
214
213
|
else
|
|
215
|
-
info(
|
|
214
|
+
info('query-ignoring-assoc-in-union', queryPath, { name: elemName, '#': 'std' });
|
|
216
215
|
|
|
217
216
|
elem._ignore = true;
|
|
218
217
|
}
|
|
@@ -126,7 +126,7 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
|
|
|
126
126
|
keys.push(elt);
|
|
127
127
|
}, [ 'definitions', artifactName ], true, { elementsOnly: true });
|
|
128
128
|
|
|
129
|
-
// In contrast to EDM, the DB entity may have more than one technical keys but should have
|
|
129
|
+
// In contrast to EDM, the DB entity may have more than one technical keys but should have ideally exactly one key of type cds.UUID
|
|
130
130
|
if (keys.length !== 1)
|
|
131
131
|
warning(null, [ 'definitions', artifactName ], 'Entity annotated with “@odata.draft.enabled” should have exactly one key element');
|
|
132
132
|
|
|
@@ -183,20 +183,27 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
|
|
|
183
183
|
if (artifact.$location)
|
|
184
184
|
setProp(draftsArtifact, '$location', artifact.$location);
|
|
185
185
|
|
|
186
|
+
const calcOnWriteElements = [];
|
|
187
|
+
|
|
186
188
|
// Copy all elements
|
|
187
189
|
for (const elemName in artifact.elements) {
|
|
188
190
|
const origElem = artifact.elements[elemName];
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
191
|
+
if (origElem.value?.stored) {
|
|
192
|
+
calcOnWriteElements.push(elemName);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
let elem;
|
|
196
|
+
if ((isDeprecatedEnabled(options, '_renderVirtualElements') && origElem.virtual) || !origElem.virtual)
|
|
197
|
+
elem = copyAndAddElement(origElem, draftsArtifact, draftsArtifactName, elemName)[elemName];
|
|
198
|
+
if (elem) {
|
|
199
|
+
// Remove "virtual" - cap/issues 4956
|
|
200
|
+
if (elem.virtual)
|
|
201
|
+
delete elem.virtual;
|
|
202
|
+
|
|
203
|
+
// explicitly set nullable if not key and not unmanaged association
|
|
204
|
+
if (!elem.key && !elem.on)
|
|
205
|
+
elem.notNull = false;
|
|
206
|
+
}
|
|
200
207
|
}
|
|
201
208
|
}
|
|
202
209
|
|
|
@@ -276,6 +283,8 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
|
|
|
276
283
|
// we don't delete them (but mark them as implicit so that toCdl does not render them)
|
|
277
284
|
// draftAdministrativeData.DraftAdministrativeData.implicitForeignKeys = true;
|
|
278
285
|
}
|
|
286
|
+
|
|
287
|
+
calcOnWriteElements.forEach(elemName => copyAndAddElement(artifact.elements[elemName], draftsArtifact, draftsArtifactName, elemName)[elemName]);
|
|
279
288
|
}
|
|
280
289
|
|
|
281
290
|
/**
|
|
@@ -16,6 +16,10 @@ const { makeMessageFunction } = require('../../base/messages');
|
|
|
16
16
|
* - Element must not be an 'array of' for OData V2 TODO: move to the validator
|
|
17
17
|
* - Perform checks for exposed non-abstract entities and views - check media type and key-ness
|
|
18
18
|
*
|
|
19
|
+
* ATTENTION: generateDrafts propagates annotations from the draft nodes to the
|
|
20
|
+
* returns element of the draft actions. Shortcut/Convenience annotations
|
|
21
|
+
* are NOT replaced/expanded (eg. @label => @Common.Label).
|
|
22
|
+
*
|
|
19
23
|
* @param {CSN.Model} csn
|
|
20
24
|
* @param {CSN.Options} options
|
|
21
25
|
* @param {Array} [services] Will be calculated JIT if not provided
|
|
@@ -51,7 +51,7 @@ const { addLocalizationViews } = require('./localized');
|
|
|
51
51
|
// -- exposed associations do not point to non-exposed targets
|
|
52
52
|
// -- structured types must not contain associations for OData V2
|
|
53
53
|
// - Element must not be an 'array of' for OData V2 TODO: move to the validator
|
|
54
|
-
// (Linter
|
|
54
|
+
// (Linter Candidate, move as hard error into EdmPreproc on V2 generation)
|
|
55
55
|
// - Perform checks for exposed non-abstract entities and views - check media type and
|
|
56
56
|
// key-ness (requires that containers have been identified) (Linter candidate, scenario check)
|
|
57
57
|
// Annotations related:
|
|
@@ -201,7 +201,7 @@ function transform4odataWithCsn(inputModel, options) {
|
|
|
201
201
|
// Flatten on-conditions in unmanaged associations
|
|
202
202
|
/* FIXME (HJB): Is this comment still correct? processOnCond only strips $self
|
|
203
203
|
We should not remove $self prefixes in structured OData to not
|
|
204
|
-
|
|
204
|
+
interfere with path resolution
|
|
205
205
|
*/
|
|
206
206
|
// This must be done before all the draft logic as all
|
|
207
207
|
// composition targets are annotated with @odata.draft.enabled in this step
|
|
@@ -230,12 +230,13 @@ function transform4odataWithCsn(inputModel, options) {
|
|
|
230
230
|
def['@cds.persistence.name'] = getArtifactDatabaseNameOf(defName, options.sqlMapping, csn, 'hana'); // hana to allow naming mode "hdbcds"
|
|
231
231
|
|
|
232
232
|
forEachMemberRecursively(def, (member, memberName, propertyName) => {
|
|
233
|
-
if (memberName === '' && propertyName === 'params')
|
|
234
|
-
return; // ignore "returns" type
|
|
235
233
|
// Annotate elements, foreign keys, parameters, etc. with their DB names if requested
|
|
236
234
|
// Only these are actually required and don't annotate virtual elements in entities or types
|
|
237
235
|
// as they have no DB representation (although in views)
|
|
238
|
-
if (options.sqlMapping && typeof member === 'object' &&
|
|
236
|
+
if (options.sqlMapping && typeof member === 'object' &&
|
|
237
|
+
!(member.kind === 'action' || member.kind === 'function') &&
|
|
238
|
+
!(propertyName === 'enum' || propertyName === 'returns') &&
|
|
239
|
+
(!member.virtual || def.query)) {
|
|
239
240
|
// If we have a 'preserved dotted name' (i.e. we are a result of flattening), use that for the @cds.persistence.name annotation
|
|
240
241
|
member['@cds.persistence.name'] = getElementDatabaseNameOf(member._flatElementNameWithDots || memberName, options.sqlMapping, 'hana'); // hana to allow "hdbcds"
|
|
241
242
|
}
|
|
@@ -387,12 +388,12 @@ function transform4odataWithCsn(inputModel, options) {
|
|
|
387
388
|
}
|
|
388
389
|
|
|
389
390
|
// Apply default type facets to each type definition and every member
|
|
390
|
-
// But do not apply default string length
|
|
391
|
+
// But do not apply default string length (as in DB)
|
|
391
392
|
function setDefaultTypeFacets(def) {
|
|
392
|
-
addDefaultTypeFacets(def.items || def,
|
|
393
|
-
forEachMemberRecursively(def, m=>addDefaultTypeFacets(m.items || m,
|
|
393
|
+
addDefaultTypeFacets(def.items || def, null)
|
|
394
|
+
forEachMemberRecursively(def, m=>addDefaultTypeFacets(m.items || m, null));
|
|
394
395
|
if(def.returns)
|
|
395
|
-
addDefaultTypeFacets(def.returns.items || def.returns,
|
|
396
|
+
addDefaultTypeFacets(def.returns.items || def.returns, null);
|
|
396
397
|
}
|
|
397
398
|
|
|
398
399
|
// Handles on-conditions in unmanaged associations
|
|
@@ -409,7 +410,7 @@ function transform4odataWithCsn(inputModel, options) {
|
|
|
409
410
|
// TODO: Shouldn't this only run on the on-condition and not the whole assoc-node?
|
|
410
411
|
applyTransformationsOnNonDictionary({ assoc }, 'assoc', {
|
|
411
412
|
ref: (node, prop, ref) => {
|
|
412
|
-
// remove leading $self when at the
|
|
413
|
+
// remove leading $self when at the beginning of a ref
|
|
413
414
|
if (ref.length > 1 && ref[0] === '$self')
|
|
414
415
|
node.ref.splice(0, 1);
|
|
415
416
|
}
|
|
@@ -31,6 +31,7 @@ const cdsPersistence = require('./db/cdsPersistence');
|
|
|
31
31
|
const temporal = require('./db/temporal');
|
|
32
32
|
const associations = require('./db/associations')
|
|
33
33
|
const { ModelError } = require('../base/error');
|
|
34
|
+
const { getDefaultTypeLengths } = require('../render/utils/common');
|
|
34
35
|
|
|
35
36
|
// By default: Do not process non-entities/views
|
|
36
37
|
function forEachDefinition(csn, cb) {
|
|
@@ -61,7 +62,7 @@ function forEachDefinition(csn, cb) {
|
|
|
61
62
|
* - (045) The query is stripped from entities that are annotated with '@cds.persistence.table',
|
|
62
63
|
* essentially converting views to entities.
|
|
63
64
|
* - (060) Users of primitive type 'UUID' (which is renamed to 'String' in 000) get length 36'.
|
|
64
|
-
* - (070) Default length
|
|
65
|
+
* - (070) Default length N is supplied for strings if not specified.
|
|
65
66
|
* - (080) Annotation definitions are ignored (note that annotation assignments are filtered out by toCdl).
|
|
66
67
|
* - (090) Compositions become associations.
|
|
67
68
|
* - (100) 'masked' is ignored (a), and attribute 'localized' is removed (b)
|
|
@@ -113,6 +114,8 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
|
|
|
113
114
|
checkCSNVersion(csn, options);
|
|
114
115
|
|
|
115
116
|
const pathDelimiter = (options.sqlMapping === 'hdbcds') ? '.' : '_';
|
|
117
|
+
// There is also an explicit default length via options.defaultStringLength
|
|
118
|
+
const implicitDefaultLengths = getDefaultTypeLengths(options.sqlDialect);
|
|
116
119
|
|
|
117
120
|
let csnUtils;
|
|
118
121
|
let message, error, warning, info; // message functions
|
|
@@ -160,6 +163,8 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
|
|
|
160
163
|
});
|
|
161
164
|
timetrace.stop('Validate');
|
|
162
165
|
|
|
166
|
+
rewriteCalculatedElementsInViews(csn, options, pathDelimiter, error);
|
|
167
|
+
|
|
163
168
|
// Needs to happen before tuple expansion, so the newly generated WHERE-conditions have it applied
|
|
164
169
|
handleExists(csn, options, error, inspectRef, initDefinition, dropDefinitionCache);
|
|
165
170
|
|
|
@@ -184,8 +189,6 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
|
|
|
184
189
|
assertUnique.prepare(csn, options, error, info)
|
|
185
190
|
]);
|
|
186
191
|
|
|
187
|
-
rewriteCalculatedElementsInViews(csn, options, pathDelimiter, error);
|
|
188
|
-
|
|
189
192
|
if(doA2J) {
|
|
190
193
|
// Expand a structured thing in: keys, columns, order by, group by
|
|
191
194
|
expansion.expandStructureReferences(csn, options, pathDelimiter, {error, info, throwWithAnyError}, csnUtils);
|
|
@@ -252,9 +255,11 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
|
|
|
252
255
|
transformCsn(csn, {
|
|
253
256
|
type: (val, node, key) => {
|
|
254
257
|
renamePrimitiveTypesAndUuid(val, node, key);
|
|
255
|
-
addDefaultTypeFacets(node);
|
|
258
|
+
addDefaultTypeFacets(node, implicitDefaultLengths);
|
|
256
259
|
},
|
|
257
|
-
//
|
|
260
|
+
// no support for array-of - turn into CLOB/Text
|
|
261
|
+
// must be done after A2J or compiler checks could change
|
|
262
|
+
// (e.g. annotation def checks for arrayed types)
|
|
258
263
|
items: (val, node) => {
|
|
259
264
|
node.type = 'cds.LargeString';
|
|
260
265
|
delete node.items;
|
|
@@ -572,7 +577,7 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
|
|
|
572
577
|
addStringAnnotationTo('@cds.persistence.name', getArtifactDatabaseNameOf(artifactName, options.sqlMapping, csn, options.sqlDialect), artifact);
|
|
573
578
|
|
|
574
579
|
forEachMemberRecursively(artifact, (member, memberName, property, path) => {
|
|
575
|
-
if (
|
|
580
|
+
if (property === 'returns')
|
|
576
581
|
return; // ignore "returns" type
|
|
577
582
|
transformCommon(member, memberName, path);
|
|
578
583
|
// (240 b) Annotate elements, foreign keys, parameters etc with their DB names
|
|
@@ -1146,7 +1151,7 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
|
|
|
1146
1151
|
// if it's not the first entry, add a ',' ...
|
|
1147
1152
|
if (i)
|
|
1148
1153
|
flattenedIndex.push(',');
|
|
1149
|
-
// ... then add the
|
|
1154
|
+
// ... then add the flattened element name as a single ref
|
|
1150
1155
|
flattenedIndex.push({ ref: [ elem ] });
|
|
1151
1156
|
// ... then check if we have to propagate a 'asc'/'desc', omitting the last, which will be copied automatically
|
|
1152
1157
|
if ((idx + 1) < index.length && (index[idx + 1] === 'asc' || index[idx + 1] === 'desc') && i < elems.length - 1)
|