@sap/cds-compiler 2.10.4 → 2.12.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 +136 -0
- package/bin/.eslintrc.json +1 -2
- package/bin/cds_update_identifiers.js +10 -8
- package/bin/cdsc.js +58 -35
- package/bin/cdsse.js +1 -0
- package/bin/cdsv2m.js +3 -2
- package/doc/CHANGELOG_ARCHIVE.md +1 -1
- package/doc/CHANGELOG_BETA.md +16 -0
- package/lib/api/.eslintrc.json +2 -0
- package/lib/api/main.js +10 -36
- package/lib/api/options.js +17 -8
- package/lib/api/validate.js +30 -3
- package/lib/backends.js +12 -13
- package/lib/base/dictionaries.js +2 -1
- package/lib/base/keywords.js +3 -2
- package/lib/base/message-registry.js +64 -11
- package/lib/base/messages.js +38 -18
- package/lib/base/model.js +6 -4
- package/lib/base/optionProcessorHelper.js +148 -86
- package/lib/checks/.eslintrc.json +2 -0
- package/lib/checks/actionsFunctions.js +2 -1
- package/lib/checks/emptyOrOnlyVirtual.js +2 -2
- package/lib/checks/foreignKeys.js +4 -4
- package/lib/checks/managedInType.js +4 -4
- package/lib/checks/queryNoDbArtifacts.js +1 -3
- package/lib/checks/selectItems.js +4 -0
- package/lib/checks/sql-snippets.js +93 -0
- package/lib/checks/unknownMagic.js +6 -3
- package/lib/checks/validator.js +8 -0
- package/lib/compiler/assert-consistency.js +14 -5
- package/lib/compiler/base.js +64 -0
- package/lib/compiler/builtins.js +62 -16
- package/lib/compiler/checks.js +34 -10
- package/lib/compiler/definer.js +91 -112
- package/lib/compiler/index.js +30 -30
- package/lib/compiler/propagator.js +8 -4
- package/lib/compiler/resolver.js +279 -63
- package/lib/compiler/shared.js +65 -230
- package/lib/compiler/utils.js +191 -0
- package/lib/edm/annotations/genericTranslation.js +35 -18
- package/lib/edm/annotations/preprocessAnnotations.js +1 -1
- package/lib/edm/csn2edm.js +4 -3
- package/lib/edm/edm.js +8 -8
- package/lib/edm/edmPreprocessor.js +61 -59
- package/lib/edm/edmUtils.js +14 -15
- package/lib/gen/Dictionary.json +82 -40
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +19 -1
- package/lib/gen/language.tokens +80 -73
- package/lib/gen/languageLexer.interp +27 -1
- package/lib/gen/languageLexer.js +925 -826
- package/lib/gen/languageLexer.tokens +72 -65
- package/lib/gen/languageParser.js +4817 -4102
- package/lib/json/from-csn.js +57 -26
- package/lib/json/to-csn.js +244 -51
- package/lib/language/antlrParser.js +12 -1
- package/lib/language/docCommentParser.js +1 -1
- package/lib/language/errorStrategy.js +26 -8
- package/lib/language/genericAntlrParser.js +106 -30
- package/lib/language/language.g4 +200 -70
- package/lib/language/multiLineStringParser.js +536 -0
- package/lib/main.d.ts +220 -21
- package/lib/main.js +6 -3
- package/lib/model/api.js +2 -2
- package/lib/model/csnRefs.js +218 -86
- package/lib/model/csnUtils.js +99 -178
- package/lib/model/enrichCsn.js +84 -43
- package/lib/model/revealInternalProperties.js +25 -8
- package/lib/model/sortViews.js +8 -1
- package/lib/modelCompare/compare.js +2 -1
- package/lib/optionProcessor.js +33 -18
- package/lib/render/.eslintrc.json +1 -2
- package/lib/render/DuplicateChecker.js +2 -2
- package/lib/render/manageConstraints.js +1 -1
- package/lib/render/toCdl.js +202 -82
- package/lib/render/toHdbcds.js +194 -135
- package/lib/render/toRename.js +7 -10
- package/lib/render/toSql.js +91 -51
- package/lib/render/utils/common.js +24 -5
- package/lib/render/utils/sql.js +6 -4
- package/lib/transform/braceExpression.js +4 -2
- package/lib/transform/db/applyTransformations.js +189 -0
- package/lib/transform/db/associations.js +389 -0
- package/lib/transform/db/cdsPersistence.js +150 -0
- package/lib/transform/db/constraints.js +275 -119
- package/lib/transform/db/draft.js +6 -4
- package/lib/transform/db/expansion.js +10 -9
- package/lib/transform/db/flattening.js +23 -8
- package/lib/transform/db/temporal.js +236 -0
- package/lib/transform/db/transformExists.js +106 -25
- package/lib/transform/db/views.js +485 -0
- package/lib/transform/forHanaNew.js +90 -1036
- package/lib/transform/forOdataNew.js +11 -3
- package/lib/transform/localized.js +5 -14
- package/lib/transform/odata/generateForeignKeyElements.js +2 -2
- package/lib/transform/transformUtilsNew.js +34 -20
- package/lib/transform/translateAssocsToJoins.js +15 -23
- package/lib/transform/universalCsnEnricher.js +217 -47
- package/lib/utils/file.js +13 -6
- package/lib/utils/term.js +65 -42
- package/lib/utils/timetrace.js +55 -27
- package/package.json +1 -1
- package/lib/transform/db/helpers.js +0 -58
|
@@ -239,15 +239,14 @@ function expandStructureReferences(csn, options, pathDelimiter, { error, info, t
|
|
|
239
239
|
* @param {CSN.Artifact} root All elements visible fromt he query source ($combined)
|
|
240
240
|
* @param {CSN.Column[]} columns
|
|
241
241
|
* @param {string[]} excluding
|
|
242
|
-
* @returns {{columns: Array,
|
|
242
|
+
* @returns {{columns: Array, toMany: Array}} Object with rewritten columns (.expand/.inline) and with any .expand + to-many
|
|
243
243
|
*/
|
|
244
244
|
function rewrite(root, columns, excluding) {
|
|
245
245
|
const allToMany = [];
|
|
246
246
|
const newThing = [];
|
|
247
247
|
// Replace stars - needs to happen here since the .expand/.inline first path step affects the root *
|
|
248
248
|
columns = replaceStar(root, columns, excluding);
|
|
249
|
-
for (
|
|
250
|
-
const col = columns[i];
|
|
249
|
+
for (const col of columns) {
|
|
251
250
|
if (col.expand) {
|
|
252
251
|
// TODO: Can col.ref be empty without an as? Assumption is it cannot - if it has, it's an error, we throw, compiler checks.
|
|
253
252
|
const { expanded, toManys } = expandInline(root, col, col.ref || [], [ dbName(col) ]);
|
|
@@ -451,7 +450,7 @@ function expandStructureReferences(csn, options, pathDelimiter, { error, info, t
|
|
|
451
450
|
else
|
|
452
451
|
newThing.push(col);
|
|
453
452
|
}
|
|
454
|
-
else if (col.ref && col.$scope === '$magic' && col.ref[0] === '$user' && !col.as) {
|
|
453
|
+
else if (col.ref && col.$scope === '$magic' && ( col.ref[0] === '$user' || col.ref[0] === '$session' ) && !col.as) {
|
|
455
454
|
col.as = implicitAs(col.ref);
|
|
456
455
|
newThing.push(col);
|
|
457
456
|
}
|
|
@@ -477,6 +476,7 @@ function expandStructureReferences(csn, options, pathDelimiter, { error, info, t
|
|
|
477
476
|
*/
|
|
478
477
|
function expandRef(art, ref, alias, isKey, withAlias) {
|
|
479
478
|
const expanded = [];
|
|
479
|
+
/** @type {Array<[CSN.Element, any[], any[]]>} */
|
|
480
480
|
const stack = [ [ art, ref, [ alias || ref[ref.length - 1] ] ] ];
|
|
481
481
|
while (stack.length > 0) {
|
|
482
482
|
const [ current, currentRef, currentAlias ] = stack.pop();
|
|
@@ -552,9 +552,11 @@ function expandStructureReferences(csn, options, pathDelimiter, { error, info, t
|
|
|
552
552
|
for (const part of Object.keys(base)) {
|
|
553
553
|
if (excluding.indexOf(part) === -1) {
|
|
554
554
|
// The thing is shadowed - ignore names present because of .inline, as those "disappear"
|
|
555
|
-
if (names[part] !== undefined && !subs[names[part]].inline) {
|
|
556
|
-
|
|
557
|
-
|
|
555
|
+
if (names[part] !== undefined && !subs[names[part]].inline) { // Only works for a single * - but a second is forbidden anyway
|
|
556
|
+
if (names[part] > stars[0]) { // explicit definitions BEFORE the star should stay "infront" of the star
|
|
557
|
+
replaced[part] = true;
|
|
558
|
+
star.push(subs[names[part]]);
|
|
559
|
+
}
|
|
558
560
|
}
|
|
559
561
|
else { // the thing is not shadowed - use the name from the base
|
|
560
562
|
star.push({ ref: [ part ] });
|
|
@@ -562,8 +564,7 @@ function expandStructureReferences(csn, options, pathDelimiter, { error, info, t
|
|
|
562
564
|
}
|
|
563
565
|
}
|
|
564
566
|
// Finally: Replace the stars and leave out the shadowed things
|
|
565
|
-
for (
|
|
566
|
-
const sub = subs[i];
|
|
567
|
+
for (const sub of subs) {
|
|
567
568
|
if (sub !== '*' && !replaced[dbName(sub)])
|
|
568
569
|
final.push(sub);
|
|
569
570
|
else if (sub === '*')
|
|
@@ -14,7 +14,7 @@ const { setProp } = require('../../base/model');
|
|
|
14
14
|
* @param {CSN.Model} csn
|
|
15
15
|
*/
|
|
16
16
|
function removeLeadingSelf(csn) {
|
|
17
|
-
const magicVars = [ '$now' ];
|
|
17
|
+
const magicVars = [ '$now', '$self', '$projection', '$user', '$session', '$at' ];
|
|
18
18
|
forEachDefinition(csn, (artifact, artifactName) => {
|
|
19
19
|
if (artifact.kind === 'entity' || artifact.kind === 'view') {
|
|
20
20
|
forAllElements(artifact, artifactName, (parent, elements) => {
|
|
@@ -79,6 +79,22 @@ function resolveTypeReferences(csn, options, resolved, pathDelimiter) {
|
|
|
79
79
|
if (!isBuiltinType(type)) {
|
|
80
80
|
const directLocalized = parent.localized || false;
|
|
81
81
|
toFinalBaseType(parent, resolved);
|
|
82
|
+
// structured types might not have the child-types replaced.
|
|
83
|
+
// Drill down to ensure this.
|
|
84
|
+
if (parent.elements) {
|
|
85
|
+
const stack = [ parent.elements ];
|
|
86
|
+
while (stack.length > 0) {
|
|
87
|
+
const elements = stack.pop();
|
|
88
|
+
for (const e of Object.values(elements)) {
|
|
89
|
+
if (e.type && !isBuiltinType(e.type))
|
|
90
|
+
toFinalBaseType(e, resolved);
|
|
91
|
+
|
|
92
|
+
if (e.elements)
|
|
93
|
+
stack.push(e.elements);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
82
98
|
if (!directLocalized)
|
|
83
99
|
removeLocalized(parent);
|
|
84
100
|
}
|
|
@@ -233,10 +249,9 @@ function flattenElements(csn, options, pathDelimiter, error) {
|
|
|
233
249
|
|
|
234
250
|
if (flatElement.type && isAssocOrComposition(flatElement.type) && flatElement.on) {
|
|
235
251
|
// Make refs resolvable by fixing the first ref step
|
|
236
|
-
for (
|
|
237
|
-
const onPart = flatElement.on[i];
|
|
252
|
+
for (const onPart of flatElement.on) {
|
|
238
253
|
if (onPart.ref) {
|
|
239
|
-
const firstRef =
|
|
254
|
+
const firstRef = onPart.ref[0];
|
|
240
255
|
|
|
241
256
|
/*
|
|
242
257
|
when element is defined in the current name resolution scope, like
|
|
@@ -253,7 +268,7 @@ function flattenElements(csn, options, pathDelimiter, error) {
|
|
|
253
268
|
const possibleFlatName = prefix + pathDelimiter + firstRef;
|
|
254
269
|
|
|
255
270
|
if (flatElems[possibleFlatName])
|
|
256
|
-
|
|
271
|
+
onPart.ref[0] = possibleFlatName;
|
|
257
272
|
}
|
|
258
273
|
}
|
|
259
274
|
}
|
|
@@ -271,7 +286,7 @@ function flattenElements(csn, options, pathDelimiter, error) {
|
|
|
271
286
|
previous[name] = element;
|
|
272
287
|
return previous;
|
|
273
288
|
}, Object.create(null));
|
|
274
|
-
});
|
|
289
|
+
}, true);
|
|
275
290
|
}
|
|
276
291
|
|
|
277
292
|
/**
|
|
@@ -289,11 +304,11 @@ function flattenElements(csn, options, pathDelimiter, error) {
|
|
|
289
304
|
/**
|
|
290
305
|
* Walk the element chain
|
|
291
306
|
*
|
|
292
|
-
* @param {
|
|
307
|
+
* @param {object} e
|
|
293
308
|
* @param {string} name
|
|
294
309
|
*/
|
|
295
310
|
function walkElements(e, name) {
|
|
296
|
-
if (isBuiltinType(e)) {
|
|
311
|
+
if (isBuiltinType(e.type)) {
|
|
297
312
|
branches[subbranchNames.concat(name).join(pathDelimiter)] = subbranchElements.concat(e);
|
|
298
313
|
}
|
|
299
314
|
else {
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
getUtils, getNormalizedQuery, hasAnnotationValue, forEachMember,
|
|
5
|
+
} = require('../../model/csnUtils');
|
|
6
|
+
const { implicitAs } = require('../../model/csnRefs');
|
|
7
|
+
const { setProp } = require('../../base/model');
|
|
8
|
+
const { getTransformers } = require('../transformUtilsNew');
|
|
9
|
+
|
|
10
|
+
const validToString = '@cds.valid.to';
|
|
11
|
+
const validFromString = '@cds.valid.from';
|
|
12
|
+
/**
|
|
13
|
+
* Get the forEachDefinition callback function that adds a where condition to views that
|
|
14
|
+
* - are annotated with @cds.valid.from and @cds.valid.to,
|
|
15
|
+
* - have only one @cds.valid.from and @cds.valid.to,
|
|
16
|
+
* - and both annotations come from the same entity
|
|
17
|
+
*
|
|
18
|
+
* If the view has one of the annotations but the other conditions are not met, an error will be raised.
|
|
19
|
+
*
|
|
20
|
+
* @param {CSN.Model} csn
|
|
21
|
+
* @param {object} messageFunctions
|
|
22
|
+
* @param {Function} messageFunctions.info
|
|
23
|
+
* @returns {(artifact: CSN.Artifact, artifactName: string) => void} Callback for forEachDefinition applying the where-condition to views.
|
|
24
|
+
*/
|
|
25
|
+
function getViewDecorator(csn, messageFunctions) {
|
|
26
|
+
const { info } = messageFunctions;
|
|
27
|
+
const { get$combined } = getUtils(csn);
|
|
28
|
+
return addTemporalWhereConditionToView;
|
|
29
|
+
/**
|
|
30
|
+
* Add a where condition to views that
|
|
31
|
+
* - are annotated with @cds.valid.from and @cds.valid.to,
|
|
32
|
+
* - have only one @cds.valid.from and @cds.valid.to,
|
|
33
|
+
* - and both annotations come from the same entity
|
|
34
|
+
*
|
|
35
|
+
* If the view has one of the annotations but the other conditions are not met, an error will be raised.
|
|
36
|
+
*
|
|
37
|
+
* @param {CSN.Artifact} artifact
|
|
38
|
+
* @param {string} artifactName
|
|
39
|
+
*/
|
|
40
|
+
function addTemporalWhereConditionToView(artifact, artifactName) {
|
|
41
|
+
const normalizedQuery = getNormalizedQuery(artifact);
|
|
42
|
+
if (normalizedQuery && normalizedQuery.query && normalizedQuery.query.SELECT) {
|
|
43
|
+
// BLOCKER: We need information to handle $combined
|
|
44
|
+
// What we are trying to achieve by this:
|
|
45
|
+
// Forbid joining/selecting from two or more temporal entities
|
|
46
|
+
// Idea: Follow the query-tree and check each from
|
|
47
|
+
// Collect all source-entities and compute our own $combined
|
|
48
|
+
const $combined = get$combined(normalizedQuery.query);
|
|
49
|
+
const [ from, to ] = getFromToElements($combined);
|
|
50
|
+
// exactly one validFrom & validTo
|
|
51
|
+
if (from.length === 1 && to.length === 1) {
|
|
52
|
+
// and both are from the same origin
|
|
53
|
+
if (from[0].source === to[0].source && from[0].parent === to[0].parent) {
|
|
54
|
+
if (!hasFalsyTemporalAnnotations(normalizedQuery.query.SELECT, artifact.elements, from[0], to[0])) {
|
|
55
|
+
const fromPath = {
|
|
56
|
+
ref: [
|
|
57
|
+
from[0].parent,
|
|
58
|
+
from[0].name,
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const toPath = {
|
|
63
|
+
ref: [
|
|
64
|
+
to[0].parent,
|
|
65
|
+
to[0].name,
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
const atFrom = { ref: [ '$at', 'from' ] };
|
|
71
|
+
const atTo = { ref: [ '$at', 'to' ] };
|
|
72
|
+
|
|
73
|
+
const cond = [ '(', fromPath, '<', atTo, 'and', toPath, '>', atFrom, ')' ];
|
|
74
|
+
|
|
75
|
+
if (normalizedQuery.query.SELECT.where) { // if there is an existing where-clause, extend it by adding 'and (temporal clause)'
|
|
76
|
+
normalizedQuery.query.SELECT.where = [ '(', ...normalizedQuery.query.SELECT.where, ')', 'and', ...cond ];
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
normalizedQuery.query.SELECT.where = cond;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
info(null, [ 'definitions', artifactName ], `No temporal WHERE clause added as "${from[0].error_parent}"."${from[0].name}" and "${to[0].error_parent}"."${to[0].name}" are not of same origin`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else if (from.length > 0 || to.length > 0) {
|
|
88
|
+
const missingAnnotation = from.length > to.length ? validToString : validFromString;
|
|
89
|
+
info(null, [ 'definitions', artifactName ],
|
|
90
|
+
{ anno: missingAnnotation },
|
|
91
|
+
'No temporal WHERE clause added because $(ANNO) is missing');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get all elements tagged with @cds.valid.from/to from the union of all entities of the from-clause.
|
|
98
|
+
*
|
|
99
|
+
* @param {any} combined union of all entities of the from-clause
|
|
100
|
+
* @returns {Array[]} Array where first field is array of elements with @cds.valid.from, second field is array of elements with @cds.valid.to.
|
|
101
|
+
*/
|
|
102
|
+
function getFromToElements(combined) {
|
|
103
|
+
const from = [];
|
|
104
|
+
const to = [];
|
|
105
|
+
for (const name in combined) {
|
|
106
|
+
let elt = combined[name];
|
|
107
|
+
if (!Array.isArray(elt))
|
|
108
|
+
elt = [ elt ];
|
|
109
|
+
elt.forEach((e) => {
|
|
110
|
+
if (hasAnnotationValue(e.element, validFromString))
|
|
111
|
+
from.push(e);
|
|
112
|
+
|
|
113
|
+
if (hasAnnotationValue(e.element, validToString))
|
|
114
|
+
to.push(e);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return [ from, to ];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check if the given SELECT has a falsy @cds.valid.from and a falsy @cds.valid.to
|
|
123
|
+
*
|
|
124
|
+
* @param {CSN.QuerySelect} SELECT
|
|
125
|
+
* @param {CSN.Elements} elements
|
|
126
|
+
* @param {object} from
|
|
127
|
+
* @param {object} to
|
|
128
|
+
* @returns {boolean} True if both are present and false.
|
|
129
|
+
*/
|
|
130
|
+
function hasFalsyTemporalAnnotations(SELECT, elements, from, to) {
|
|
131
|
+
let fromElement = elements[from.name];
|
|
132
|
+
let toElement = elements[to.name];
|
|
133
|
+
|
|
134
|
+
if (SELECT.columns) {
|
|
135
|
+
for (const col of SELECT.columns) {
|
|
136
|
+
if (col.ref) {
|
|
137
|
+
const implicitAlias = implicitAs(col.ref);
|
|
138
|
+
if (implicitAlias === from.name)
|
|
139
|
+
fromElement = elements[col.as || implicitAlias];
|
|
140
|
+
else if (implicitAlias === to.name)
|
|
141
|
+
toElement = elements[col.as || implicitAlias];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return fromElement && toElement &&
|
|
146
|
+
hasAnnotationValue(fromElement, validFromString, false) &&
|
|
147
|
+
hasAnnotationValue(toElement, validToString, false);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get the forEachDefinition callback function that collects all usages of @cds.valid.from/to/key and checks that
|
|
153
|
+
* - the assignment is on a valid element
|
|
154
|
+
* - the annotation is only assigned once
|
|
155
|
+
* - key is only used in conjunction with from and to
|
|
156
|
+
*
|
|
157
|
+
* Furthermore, @cds.valid.from and @cds.valid.key is processed - @cds.valid.from is marked as key or marked as unique if @cds.valid.key is used.
|
|
158
|
+
* If @cds.valid.key is used, the real key-elements have their key-property removed (set non-enumerable as $key) and instead the @cds.valid.key-marked elements have it added.
|
|
159
|
+
*
|
|
160
|
+
* @param {CSN.Model} csn
|
|
161
|
+
* @param {CSN.Options} options
|
|
162
|
+
* @param {string} pathDelimiter
|
|
163
|
+
* @param {object} messageFunctions
|
|
164
|
+
* @param {Function} messageFunctions.error
|
|
165
|
+
* @returns {(artifact: CSN.Artifact, artifactName: string) => void} Callback for forEachDefinition processing the annotations.
|
|
166
|
+
*/
|
|
167
|
+
function getAnnotationHandler(csn, options, pathDelimiter, messageFunctions) {
|
|
168
|
+
const { error } = messageFunctions;
|
|
169
|
+
const {
|
|
170
|
+
extractValidFromToKeyElement, checkAssignment, checkMultipleAssignments, recurseElements,
|
|
171
|
+
} = getTransformers(csn, options, pathDelimiter);
|
|
172
|
+
|
|
173
|
+
return handleTemporalAnnotations;
|
|
174
|
+
/**
|
|
175
|
+
* @param {CSN.Artifact} artifact
|
|
176
|
+
* @param {string} artifactName
|
|
177
|
+
*/
|
|
178
|
+
function handleTemporalAnnotations(artifact, artifactName) {
|
|
179
|
+
const validFrom = [];
|
|
180
|
+
const validTo = [];
|
|
181
|
+
const validKey = [];
|
|
182
|
+
|
|
183
|
+
recurseElements(artifact, [ 'definitions', artifactName ], (member, path) => {
|
|
184
|
+
const [ f, t, k ] = extractValidFromToKeyElement(member, path);
|
|
185
|
+
validFrom.push(...f);
|
|
186
|
+
validTo.push(...t);
|
|
187
|
+
validKey.push(...k);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (artifact.kind === 'entity' && !artifact.query) {
|
|
191
|
+
validFrom.forEach(obj => checkAssignment(validFromString, obj.element, obj.path, artifact));
|
|
192
|
+
validTo.forEach(obj => checkAssignment(validToString, obj.element, obj.path, artifact));
|
|
193
|
+
validKey.forEach(obj => checkAssignment('@cds.valid.key', obj.element, obj.path, artifact));
|
|
194
|
+
checkMultipleAssignments(validFrom, validFromString, artifact, artifactName);
|
|
195
|
+
checkMultipleAssignments(validTo, validToString, artifact, artifactName, true);
|
|
196
|
+
checkMultipleAssignments(validKey, '@cds.valid.key', artifact, artifactName);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// if there is an cds.valid.key, make this the only primary key
|
|
200
|
+
// otherwise add all cds.valid.from to primary key tuple
|
|
201
|
+
if (validKey.length) {
|
|
202
|
+
if (!validFrom.length || !validTo.length) {
|
|
203
|
+
error(null, [ 'definitions', artifactName ],
|
|
204
|
+
'Expecting “@cds.valid.from” and “@cds.valid.to” if “@cds.valid.key” is used');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
forEachMember(artifact, (member) => {
|
|
208
|
+
if (member.key) {
|
|
209
|
+
member.unique = true;
|
|
210
|
+
delete member.key;
|
|
211
|
+
// Remember that this element was a key in the original artifact.
|
|
212
|
+
// This is needed for localized convenience view generation.
|
|
213
|
+
setProp(member, '$key', true);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
validKey.forEach((member) => {
|
|
217
|
+
member.element.key = true;
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
validFrom.forEach((member) => {
|
|
221
|
+
member.element.unique = true;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
validFrom.forEach((member) => {
|
|
226
|
+
member.element.key = true;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
module.exports = {
|
|
234
|
+
getViewDecorator,
|
|
235
|
+
getAnnotationHandler,
|
|
236
|
+
};
|
|
@@ -49,10 +49,14 @@ const { csnRefs } = require('../../model/csnRefs');
|
|
|
49
49
|
*/
|
|
50
50
|
function handleExists(csn, options, error) {
|
|
51
51
|
const { inspectRef } = csnRefs(csn);
|
|
52
|
+
const generatedExists = new WeakMap();
|
|
52
53
|
forEachDefinition(csn, (artifact, artifactName) => {
|
|
54
|
+
if (artifact.projection) // do the same hack we do for the other stuff...
|
|
55
|
+
artifact.query = { SELECT: artifact.projection };
|
|
56
|
+
|
|
53
57
|
if (artifact.query) {
|
|
54
58
|
forAllQueries(artifact.query, (query, path) => {
|
|
55
|
-
if (!query
|
|
59
|
+
if (!generatedExists.has(query)) {
|
|
56
60
|
const toProcess = []; // Collect all expressions we need to process here
|
|
57
61
|
if (query.SELECT && query.SELECT.where && query.SELECT.where.length > 1)
|
|
58
62
|
toProcess.push([ path.slice(0, -1), path.concat('where') ]);
|
|
@@ -66,6 +70,7 @@ function handleExists(csn, options, error) {
|
|
|
66
70
|
toProcess.push([ path.slice(0, -1), path.concat([ 'from', 'on' ]) ]);
|
|
67
71
|
|
|
68
72
|
for (const [ , exprPath ] of toProcess) {
|
|
73
|
+
forbidAssocInExists(exprPath);
|
|
69
74
|
const expr = nestExists(exprPath);
|
|
70
75
|
walkCsnPath(csn, exprPath.slice(0, -1))[exprPath[exprPath.length - 1]] = expr;
|
|
71
76
|
}
|
|
@@ -81,6 +86,12 @@ function handleExists(csn, options, error) {
|
|
|
81
86
|
}
|
|
82
87
|
}, [ 'definitions', artifactName, 'query' ]);
|
|
83
88
|
}
|
|
89
|
+
|
|
90
|
+
if (artifact.projection) { // undo our hack
|
|
91
|
+
artifact.projection = artifact.query.SELECT;
|
|
92
|
+
|
|
93
|
+
delete artifact.query;
|
|
94
|
+
}
|
|
84
95
|
});
|
|
85
96
|
|
|
86
97
|
/**
|
|
@@ -233,6 +244,63 @@ function handleExists(csn, options, error) {
|
|
|
233
244
|
return startAssoc;
|
|
234
245
|
}
|
|
235
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Check that associations in filters (in an exists expression) are only fk-accesses. Everything else is forbidden.
|
|
249
|
+
*
|
|
250
|
+
* @param {CSN.Path} exprPath
|
|
251
|
+
* @returns {void}
|
|
252
|
+
*/
|
|
253
|
+
function forbidAssocInExists(exprPath) {
|
|
254
|
+
const expr = walkCsnPath(csn, exprPath);
|
|
255
|
+
for (let i = 0; i < expr.length; i++) {
|
|
256
|
+
if (i < expr.length - 1 && expr[i] === 'exists' && expr[i + 1].ref) {
|
|
257
|
+
i++;
|
|
258
|
+
const current = expr[i];
|
|
259
|
+
|
|
260
|
+
const { links } = inspectRef(exprPath.concat(i));
|
|
261
|
+
|
|
262
|
+
const assocs = links.filter(link => link.art && link.art.target).map(link => current.ref[link.idx]);
|
|
263
|
+
|
|
264
|
+
checkForInvalidAssoc(assocs);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* @param {object[]} assocs Array of refs of assocs - possibly with a .where to check
|
|
271
|
+
*/
|
|
272
|
+
function checkForInvalidAssoc(assocs) {
|
|
273
|
+
for (const assoc of assocs) {
|
|
274
|
+
if (assoc.where) {
|
|
275
|
+
for (let i = 0; i < assoc.where.length; i++) {
|
|
276
|
+
const part = assoc.where[i];
|
|
277
|
+
|
|
278
|
+
if (part._links && !(assoc.where[i - 1] && assoc.where[i - 1] === 'exists')) {
|
|
279
|
+
for (const link of part._links) {
|
|
280
|
+
if (link.art && link.art.target) {
|
|
281
|
+
if (link.art.keys) { // managed - allow FK access
|
|
282
|
+
if (part._links[link.idx + 1] !== undefined) { // there is a next path step - check if it is a fk
|
|
283
|
+
if (!(part._links[link.idx + 1] && link.art.keys.some(fk => fk._art === part._links[link.idx + 1].art)))
|
|
284
|
+
error(null, part.$path, { id: assoc.id, name: assoc.where[part.$path[part.$path.length - 1]].ref[link.idx] }, 'Unexpected non foreign key access after managed association $(NAME) in filter expression of $(ID)');
|
|
285
|
+
}
|
|
286
|
+
else { // no traversal, ends on managed
|
|
287
|
+
error(null, part.$path, { id: assoc.id, name: assoc.where[part.$path[part.$path.length - 1]].ref[link.idx] }, 'Unexpected managed association $(NAME) in filter expression of $(ID)');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else { // unmanaged - always wrong
|
|
291
|
+
error(null, part.$path, { id: assoc.id, name: assoc.where[part.$path[part.$path.length - 1]].ref[link.idx] }, 'Unexpected unmanaged association $(NAME) in filter expression of $(ID)');
|
|
292
|
+
}
|
|
293
|
+
// Recursively drill down if the assoc-step has a filter
|
|
294
|
+
if (part.ref[link.idx].where)
|
|
295
|
+
checkForInvalidAssoc([ part.ref[link.idx] ]);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
236
304
|
/**
|
|
237
305
|
* Walk to the expr using the given path and scan it for the "exists" + "ref" pattern.
|
|
238
306
|
* If such a pattern is found, nest association steps therein into filters.
|
|
@@ -297,16 +365,23 @@ function handleExists(csn, options, error) {
|
|
|
297
365
|
const subselect = getSubselect(root.target, ref, sources);
|
|
298
366
|
|
|
299
367
|
const target = subselect.SELECT.from.as; // use subquery alias as target - prevent shadowing
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
368
|
+
const extension = root.keys ? translateManagedAssocToWhere(root, target, isPrefixedWithTableAlias, base, current) : translateUnmanagedAssocToWhere(root, target, isPrefixedWithTableAlias, base, current);
|
|
369
|
+
if (extension.length > 3)
|
|
370
|
+
subselect.SELECT.where.push('('); // add braces around the on-condition part to ensure precedence is kept
|
|
371
|
+
|
|
372
|
+
subselect.SELECT.where.push(...extension);
|
|
373
|
+
|
|
374
|
+
if (extension.length > 3)
|
|
375
|
+
subselect.SELECT.where.push(')');
|
|
306
376
|
|
|
307
377
|
newExpr.push('exists');
|
|
308
|
-
if (ref && ref.where)
|
|
309
|
-
|
|
378
|
+
if (ref && ref.where) {
|
|
379
|
+
const remappedWhere = remapExistingWhere(target, ref.where);
|
|
380
|
+
if (remappedWhere.length > 3)
|
|
381
|
+
subselect.SELECT.where.push(...[ 'and', '(', ...remappedWhere, ')' ]);
|
|
382
|
+
else
|
|
383
|
+
subselect.SELECT.where.push(...[ 'and', ...remappedWhere ]);
|
|
384
|
+
}
|
|
310
385
|
|
|
311
386
|
newExpr.push(subselect);
|
|
312
387
|
toContinue.push([ exprPath.concat(newExpr.length - 1), exprPath.concat([ newExpr.length - 1, 'SELECT', 'where' ]) ]);
|
|
@@ -344,21 +419,24 @@ function handleExists(csn, options, error) {
|
|
|
344
419
|
*
|
|
345
420
|
* @param {CSN.Element} root
|
|
346
421
|
* @param {string} target
|
|
347
|
-
* @param {CSN.Query} subselect This subselect will in the end replace <assoc> in EXISTS <assoc>
|
|
348
422
|
* @param {boolean} isPrefixedWithTableAlias
|
|
349
423
|
* @param {string} base
|
|
350
424
|
* @param {Token} current
|
|
425
|
+
* @returns {object[]} The stuff to add to the where
|
|
351
426
|
*/
|
|
352
|
-
function translateManagedAssocToWhere(root, target,
|
|
427
|
+
function translateManagedAssocToWhere(root, target, isPrefixedWithTableAlias, base, current) {
|
|
428
|
+
const whereExtension = [];
|
|
353
429
|
for (let j = 0; j < root.keys.length; j++) {
|
|
354
430
|
const lop = { ref: [ target, ...root.keys[j].ref ] }; // target side
|
|
355
431
|
const rop = { ref: (isPrefixedWithTableAlias ? [] : [ base ]).concat([ ...toRawRef(current.ref), ...root.keys[j].ref ]) }; // source side
|
|
356
432
|
|
|
357
433
|
if (j > 0)
|
|
358
|
-
|
|
434
|
+
whereExtension.push('and');
|
|
359
435
|
|
|
360
|
-
|
|
436
|
+
whereExtension.push(...[ lop, '=', rop ]);
|
|
361
437
|
}
|
|
438
|
+
|
|
439
|
+
return whereExtension;
|
|
362
440
|
}
|
|
363
441
|
|
|
364
442
|
/**
|
|
@@ -384,12 +462,13 @@ function handleExists(csn, options, error) {
|
|
|
384
462
|
*
|
|
385
463
|
* @param {CSN.Element} root
|
|
386
464
|
* @param {string} target
|
|
387
|
-
* @param {CSN.Query} subselect This subselect will in the end replace <assoc> in EXISTS <assoc>
|
|
388
465
|
* @param {boolean} isPrefixedWithTableAlias
|
|
389
466
|
* @param {string} base
|
|
390
467
|
* @param {Token} current
|
|
468
|
+
* @returns {object[]} The stuff to add to the where
|
|
391
469
|
*/
|
|
392
|
-
function translateUnmanagedAssocToWhere(root, target,
|
|
470
|
+
function translateUnmanagedAssocToWhere(root, target, isPrefixedWithTableAlias, base, current) {
|
|
471
|
+
const whereExtension = [];
|
|
393
472
|
for (let j = 0; j < root.on.length; j++) {
|
|
394
473
|
const part = root.on[j];
|
|
395
474
|
|
|
@@ -397,7 +476,7 @@ function handleExists(csn, options, error) {
|
|
|
397
476
|
// but also keep along stuff like null and undefined, so compiler
|
|
398
477
|
// can have a chance to complain/ we can fail later nicely maybe
|
|
399
478
|
if (!(part && part.ref)) {
|
|
400
|
-
|
|
479
|
+
whereExtension.push(part);
|
|
401
480
|
continue;
|
|
402
481
|
}
|
|
403
482
|
|
|
@@ -407,30 +486,32 @@ function handleExists(csn, options, error) {
|
|
|
407
486
|
// Dollar Self Backlink
|
|
408
487
|
if (isValidDollarSelf(root.on[j], root.$path.concat([ 'on', j ]), root.on[j + 1], root.on[j + 2], root.$path.concat([ 'on', j + 2 ]))) {
|
|
409
488
|
if (root.on[j].ref[0] === '$self' && root.on[j].ref.length === 1)
|
|
410
|
-
|
|
489
|
+
whereExtension.push(...translateDollarSelfToWhere(base, target, root.on[j + 2], root.$path.concat([ 'on', j + 2 ])));
|
|
411
490
|
else
|
|
412
|
-
|
|
491
|
+
whereExtension.push(...translateDollarSelfToWhere(base, target, root.on[j], root.$path.concat([ 'on', j ])));
|
|
413
492
|
|
|
414
493
|
j += 2;
|
|
415
494
|
}
|
|
416
495
|
else if (links && links[0].art === root) { // target side
|
|
417
|
-
|
|
496
|
+
whereExtension.push({ ref: [ target, ...part.ref.slice(1) ] });
|
|
418
497
|
}
|
|
419
498
|
else if (part.$scope === '$self') { // source side - "absolute" scope
|
|
420
499
|
// cut off the $self, as we prefix the entity name now
|
|
421
|
-
|
|
500
|
+
whereExtension.push({ ref: [ base, ...part.ref.slice(1) ] });
|
|
422
501
|
}
|
|
423
502
|
else if (art) { // source side - with local scope
|
|
424
503
|
if (isPrefixedWithTableAlias)
|
|
425
|
-
|
|
504
|
+
whereExtension.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
|
|
426
505
|
else
|
|
427
|
-
|
|
506
|
+
whereExtension.push({ ref: [ base, ...current.ref.slice(0, -1), ...part.ref ] });
|
|
428
507
|
}
|
|
429
508
|
else { // operator - or any other leftover
|
|
430
|
-
|
|
509
|
+
whereExtension.push(part);
|
|
431
510
|
}
|
|
432
511
|
}
|
|
433
512
|
|
|
513
|
+
return whereExtension;
|
|
514
|
+
|
|
434
515
|
/**
|
|
435
516
|
* Check that an expression triple is a valid $self
|
|
436
517
|
*
|
|
@@ -581,7 +662,7 @@ function handleExists(csn, options, error) {
|
|
|
581
662
|
};
|
|
582
663
|
// Because the generated things don't have _links, _art etc. set
|
|
583
664
|
// We could also make getParent more robust to calculate the links JIT if they are missing
|
|
584
|
-
|
|
665
|
+
generatedExists.set(subselect, true);
|
|
585
666
|
|
|
586
667
|
const nonEnumElements = Object.create(null);
|
|
587
668
|
nonEnumElements.dummy = {
|
|
@@ -638,7 +719,7 @@ function handleExists(csn, options, error) {
|
|
|
638
719
|
*
|
|
639
720
|
* @param {string} base The source entity/query source name
|
|
640
721
|
* @param {string} target The target entity/query source name
|
|
641
|
-
* @param {
|
|
722
|
+
* @param {object} assoc The association element - the "not-$self" side of the comparison
|
|
642
723
|
* @param {CSN.Path} path
|
|
643
724
|
* @returns {TokenStream} The WHERE representing the $self comparison
|
|
644
725
|
*/
|