@sap/cds-compiler 2.12.0 → 2.13.6
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 +110 -15
- package/bin/cdsc.js +13 -13
- package/bin/cdsse.js +2 -2
- package/doc/CHANGELOG_BETA.md +13 -6
- package/doc/CHANGELOG_DEPRECATED.md +22 -6
- package/doc/NameResolution.md +21 -16
- package/lib/api/main.js +28 -63
- package/lib/api/options.js +3 -3
- package/lib/api/validate.js +0 -5
- package/lib/backends.js +15 -23
- package/lib/base/dictionaries.js +0 -8
- package/lib/base/error.js +26 -0
- package/lib/base/keywords.js +7 -17
- package/lib/base/location.js +9 -4
- package/lib/base/message-registry.js +25 -4
- package/lib/base/messages.js +16 -26
- package/lib/base/model.js +2 -63
- package/lib/base/optionProcessorHelper.js +158 -123
- package/lib/checks/annotationsOData.js +1 -1
- package/lib/checks/cdsPersistence.js +2 -1
- package/lib/checks/enricher.js +17 -1
- package/lib/checks/invalidTarget.js +3 -1
- package/lib/checks/managedWithoutKeys.js +3 -1
- package/lib/checks/selectItems.js +4 -4
- package/lib/checks/sql-snippets.js +27 -26
- package/lib/checks/types.js +1 -1
- package/lib/checks/validator.js +4 -7
- package/lib/compiler/assert-consistency.js +5 -3
- package/lib/compiler/builtins.js +8 -6
- package/lib/compiler/checks.js +14 -3
- package/lib/compiler/cycle-detector.js +1 -1
- package/lib/compiler/define.js +1103 -0
- package/lib/compiler/extend.js +983 -0
- package/lib/compiler/finalize-parse-cdl.js +231 -0
- package/lib/compiler/index.js +32 -13
- package/lib/compiler/kick-start.js +190 -0
- package/lib/compiler/moduleLayers.js +4 -4
- package/lib/compiler/populate.js +1226 -0
- package/lib/compiler/propagator.js +111 -46
- package/lib/compiler/resolve.js +1433 -0
- package/lib/compiler/shared.js +64 -37
- package/lib/compiler/tweak-assocs.js +529 -0
- package/lib/compiler/utils.js +197 -33
- package/lib/edm/.eslintrc.json +5 -0
- package/lib/edm/annotations/genericTranslation.js +5 -9
- package/lib/edm/annotations/preprocessAnnotations.js +2 -2
- package/lib/edm/csn2edm.js +9 -8
- package/lib/edm/edm.js +11 -12
- package/lib/edm/edmPreprocessor.js +137 -73
- package/lib/edm/edmUtils.js +116 -22
- package/lib/gen/Dictionary.json +10 -3
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +9 -1
- package/lib/gen/language.tokens +86 -83
- package/lib/gen/languageLexer.interp +10 -1
- package/lib/gen/languageLexer.js +860 -833
- package/lib/gen/languageLexer.tokens +78 -75
- package/lib/gen/languageParser.js +5282 -4265
- package/lib/json/from-csn.js +12 -1
- package/lib/json/to-csn.js +126 -66
- package/lib/language/docCommentParser.js +2 -2
- package/lib/language/genericAntlrParser.js +76 -3
- package/lib/language/language.g4 +297 -130
- package/lib/language/multiLineStringParser.js +5 -5
- package/lib/main.d.ts +468 -59
- package/lib/main.js +35 -9
- package/lib/model/api.js +3 -1
- package/lib/model/csnRefs.js +225 -156
- package/lib/model/csnUtils.js +192 -223
- package/lib/model/enrichCsn.js +70 -29
- package/lib/model/revealInternalProperties.js +27 -6
- package/lib/model/sortViews.js +2 -1
- package/lib/modelCompare/compare.js +17 -12
- package/lib/optionProcessor.js +5 -4
- package/lib/render/manageConstraints.js +35 -32
- package/lib/render/toCdl.js +73 -288
- package/lib/render/toHdbcds.js +25 -23
- package/lib/render/toSql.js +98 -41
- package/lib/render/utils/common.js +5 -10
- package/lib/render/utils/sql.js +4 -3
- package/lib/render/utils/stringEscapes.js +111 -0
- package/lib/sql-identifier.js +1 -1
- package/lib/transform/.eslintrc.json +5 -0
- package/lib/transform/db/.eslintrc.json +2 -0
- package/lib/transform/db/applyTransformations.js +35 -12
- package/lib/transform/db/assertUnique.js +1 -1
- package/lib/transform/db/associations.js +103 -305
- package/lib/transform/db/cdsPersistence.js +2 -2
- package/lib/transform/db/constraints.js +55 -52
- package/lib/transform/db/expansion.js +46 -24
- package/lib/transform/db/flattening.js +553 -102
- package/lib/transform/db/groupByOrderBy.js +3 -1
- package/lib/transform/db/transformExists.js +59 -6
- package/lib/transform/db/views.js +5 -4
- package/lib/transform/draft/.eslintrc.json +38 -0
- package/lib/transform/{db/draft.js → draft/db.js} +6 -5
- package/lib/transform/draft/odata.js +227 -0
- package/lib/transform/forHanaNew.js +67 -183
- package/lib/transform/forOdataNew.js +17 -171
- package/lib/transform/localized.js +34 -19
- package/lib/transform/odata/generateForeignKeyElements.js +1 -1
- package/lib/transform/odata/referenceFlattener.js +95 -89
- package/lib/transform/odata/structureFlattener.js +1 -1
- package/lib/transform/odata/toFinalBaseType.js +86 -12
- package/lib/transform/odata/typesExposure.js +5 -5
- package/lib/transform/odata/utils.js +2 -2
- package/lib/transform/transformUtilsNew.js +36 -22
- package/lib/transform/translateAssocsToJoins.js +2 -19
- package/lib/transform/universalCsn/.eslintrc.json +36 -0
- package/lib/transform/universalCsn/coreComputed.js +170 -0
- package/lib/transform/universalCsn/universalCsnEnricher.js +715 -0
- package/lib/transform/universalCsn/utils.js +63 -0
- package/lib/utils/objectUtils.js +30 -0
- package/package.json +1 -1
- package/share/messages/README.md +26 -0
- package/lib/compiler/definer.js +0 -2361
- package/lib/compiler/resolver.js +0 -3079
- package/lib/transform/universalCsnEnricher.js +0 -237
|
@@ -1,333 +1,55 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
applyTransformationsOnNonDictionary,
|
|
5
|
+
applyTransformations,
|
|
6
6
|
getUtils,
|
|
7
7
|
} = require('../../model/csnUtils');
|
|
8
8
|
|
|
9
|
-
const transformUtils = require('../transformUtilsNew');
|
|
10
|
-
const { setProp } = require('../../base/model');
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Return a callback function for forEachDefinition that flattens the foreign keys of managed associations.
|
|
15
|
-
* Foreign keys that are managed associations are replaced by their respective foreign keys.
|
|
16
|
-
*
|
|
17
|
-
* @param {CSN.Model} csn
|
|
18
|
-
* @param {CSN.Options} options
|
|
19
|
-
* @param {string} pathDelimiter
|
|
20
|
-
* @returns {(artifact: CSN.Artifact, artifactName: string) => void} Callback for forEachDefinition
|
|
21
|
-
*/
|
|
22
|
-
function getForeignKeyFlattener(csn, options, pathDelimiter) {
|
|
23
|
-
const doA2J = !(options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds');
|
|
24
|
-
const {
|
|
25
|
-
isManagedAssociationElement,
|
|
26
|
-
isStructured,
|
|
27
|
-
inspectRef,
|
|
28
|
-
} = getUtils(csn);
|
|
29
|
-
|
|
30
|
-
const {
|
|
31
|
-
flattenStructuredElement,
|
|
32
|
-
flattenStructStepsInRef,
|
|
33
|
-
} = transformUtils.getTransformers(csn, options, pathDelimiter);
|
|
34
|
-
|
|
35
|
-
return handleManagedAssociationFKs;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Flatten and create the foreign key elements of managed associaitons
|
|
39
|
-
*
|
|
40
|
-
* @param {CSN.Artifact} art
|
|
41
|
-
* @param {string} artName
|
|
42
|
-
*/
|
|
43
|
-
function handleManagedAssociationFKs(art, artName) {
|
|
44
|
-
if ((art.kind === 'entity' || art.kind === 'view') && doA2J) {
|
|
45
|
-
forAllElements(art, artName, (parent, elements, pathToElements) => {
|
|
46
|
-
forEachGeneric(parent, 'elements', (element, elemName) => {
|
|
47
|
-
if (element.keys && isManagedAssociationElement(element)) {
|
|
48
|
-
// replace foreign keys that are managed associations by their respective foreign keys
|
|
49
|
-
flattenFKs(element, elemName, [ ...pathToElements, elemName ]);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Flattens all foreign keys
|
|
58
|
-
*
|
|
59
|
-
* Structures will be resolved to individual elements with scalar types
|
|
60
|
-
*
|
|
61
|
-
* Associations will be replaced by their respective foreign keys
|
|
62
|
-
*
|
|
63
|
-
* If a structure contains an assoc, this will also be resolved and vice versa
|
|
64
|
-
*
|
|
65
|
-
* @param {CSN.Element} assoc
|
|
66
|
-
* @param {string} assocName
|
|
67
|
-
* @param {CSN.Path} path
|
|
68
|
-
*/
|
|
69
|
-
function flattenFKs(assoc, assocName, path) {
|
|
70
|
-
let finished = false;
|
|
71
|
-
while (!finished) {
|
|
72
|
-
const newKeys = [];
|
|
73
|
-
finished = processKeys(assoc, assocName, path, newKeys);
|
|
74
|
-
assoc.keys = newKeys;
|
|
75
|
-
}
|
|
76
|
-
assoc.keys = assoc.keys.filter(o => !o.$toDelete);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Walk over the keys and replace structures by their leafs, managed associations by their foreign keys and keep scalar values as-is.
|
|
81
|
-
*
|
|
82
|
-
* @param {CSN.Element} assoc
|
|
83
|
-
* @param {string} assocName
|
|
84
|
-
* @param {CSN.Path} path
|
|
85
|
-
* @param {object[]} collector New keys array to collect the flattened stuff in
|
|
86
|
-
* @returns {boolean} True if all keys are scalar - false if there are things that still need to be processed.
|
|
87
|
-
*/
|
|
88
|
-
function processKeys(assoc, assocName, path, collector) {
|
|
89
|
-
let finished = true;
|
|
90
|
-
for (let i = 0; i < assoc.keys.length; i++) {
|
|
91
|
-
const pathToKey = path.concat([ 'keys', i ]);
|
|
92
|
-
const { art } = inspectRef(pathToKey);
|
|
93
|
-
const { ref } = assoc.keys[i];
|
|
94
|
-
if (isStructured(art)) {
|
|
95
|
-
finished = false;
|
|
96
|
-
// Mark this element to filter it later - not needed after expansion
|
|
97
|
-
setProp(assoc.keys[i], '$toDelete', true);
|
|
98
|
-
const flat = flattenStructuredElement(art, ref[ref.length - 1], [], pathToKey);
|
|
99
|
-
Object.keys(flat).forEach((flatElemName) => {
|
|
100
|
-
const key = assoc.keys[i];
|
|
101
|
-
const clone = cloneCsn(assoc.keys[i], options);
|
|
102
|
-
if (clone.as) {
|
|
103
|
-
const lastRef = clone.ref[clone.ref.length - 1];
|
|
104
|
-
// Cut off the last ref part from the beginning of the flat name
|
|
105
|
-
const flatBaseName = flatElemName.slice(lastRef.length);
|
|
106
|
-
// Join it to the existing table alias
|
|
107
|
-
clone.as += flatBaseName;
|
|
108
|
-
// do not loose the $ref for nested keys
|
|
109
|
-
if (key.$ref) {
|
|
110
|
-
let aliasedLeaf = key.$ref[key.$ref.length - 1];
|
|
111
|
-
aliasedLeaf += flatBaseName;
|
|
112
|
-
setProp(clone, '$ref', key.$ref.slice(0, key.$ref.length - 1).concat(aliasedLeaf));
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
if (clone.ref) {
|
|
116
|
-
clone.ref[clone.ref.length - 1] = flatElemName;
|
|
117
|
-
// Now we need to properly flatten the whole ref
|
|
118
|
-
clone.ref = flattenStructStepsInRef(clone.ref, pathToKey);
|
|
119
|
-
}
|
|
120
|
-
if (!clone.as)
|
|
121
|
-
clone.as = flatElemName;
|
|
122
|
-
// Directly work on csn.definitions - this way the changes take effect in csnRefs/inspectRef immediately
|
|
123
|
-
// Add the newly generated foreign keys to the end - they will be picked up later on
|
|
124
|
-
// Recursive solutions run into call stack issues
|
|
125
|
-
collector.push(clone);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
else if (art.target) {
|
|
129
|
-
finished = false;
|
|
130
|
-
// Mark this element to filter it later - not needed after expansion
|
|
131
|
-
setProp(assoc.keys[i], '$toDelete', true);
|
|
132
|
-
// Directly work on csn.definitions - this way the changes take effect in csnRefs/inspectRef immediately
|
|
133
|
-
// Add the newly generated foreign keys to the end - they will be picked up later on
|
|
134
|
-
// Recursive solutions run into call stack issues
|
|
135
|
-
art.keys.forEach(key => collector.push(cloneAndExtendRef(key, assoc.keys[i])));
|
|
136
|
-
}
|
|
137
|
-
else if (assoc.keys[i].ref && !assoc.keys[i].as) {
|
|
138
|
-
assoc.keys[i].as = assoc.keys[i].ref[assoc.keys[i].ref.length - 1];
|
|
139
|
-
collector.push(assoc.keys[i]);
|
|
140
|
-
}
|
|
141
|
-
else {
|
|
142
|
-
collector.push(assoc.keys[i]);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return finished;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Clone base and extend the .ref and .as of the clone with the .ref and .as of ref.
|
|
150
|
-
*
|
|
151
|
-
* @param {object} key A foreign key entry (of a managed assoc as a fk of another assoc)
|
|
152
|
-
* @param {object} base The fk-ref that has key as a fk
|
|
153
|
-
* @returns {object} The clone of base
|
|
154
|
-
*/
|
|
155
|
-
function cloneAndExtendRef(key, base) {
|
|
156
|
-
const { ref } = base;
|
|
157
|
-
const clone = cloneCsn(base, options);
|
|
158
|
-
if (key.ref) {
|
|
159
|
-
// We build a ref that contains the aliased fk - that element will be created later on, so this ref is not resolvable yet
|
|
160
|
-
// Therefore we keep it as $ref - ref is the non-aliased, resolvable "clone"
|
|
161
|
-
// Later on, after we know that these foreign key elements are created, we replace ref with this $ref
|
|
162
|
-
let $ref;
|
|
163
|
-
if (base.$ref) {
|
|
164
|
-
// if a base $ref is provided, use it to correctly resolve association chains
|
|
165
|
-
const refChain = [ base.$ref[base.$ref.length - 1] ].concat(key.as || key.ref);
|
|
166
|
-
$ref = base.$ref.slice(0, base.$ref.length - 1).concat(refChain);
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
$ref = base.ref.concat( key.as || key.ref); // Keep along the aliases
|
|
170
|
-
}
|
|
171
|
-
setProp(clone, '$ref', $ref);
|
|
172
|
-
clone.ref = clone.ref.concat(key.ref);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (!clone.as && clone.ref && clone.ref.length > 0)
|
|
176
|
-
clone.as = ref[ref.length - 1] + pathDelimiter + (key.as || key.ref.join(pathDelimiter));
|
|
177
|
-
else
|
|
178
|
-
clone.as += pathDelimiter + (key.as || key.ref.join(pathDelimiter));
|
|
179
|
-
|
|
180
|
-
return clone;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
9
|
|
|
185
10
|
/**
|
|
186
|
-
*
|
|
11
|
+
* In all .elements of entities and views (and their bound actions/functions), create the on-condition for
|
|
12
|
+
* a managed associations. This needs to happen after the .keys are expanded and the corresponding elements are created.
|
|
187
13
|
*
|
|
188
14
|
* @param {CSN.Model} csn
|
|
189
|
-
* @param {CSN.Options} options
|
|
190
15
|
* @param {string} pathDelimiter
|
|
191
|
-
* @
|
|
192
|
-
* @param {Function} messageFunctions.error
|
|
193
|
-
* @returns {(artifact: CSN.Artifact, artifactName: string) => void} Callback for forEachDefinition
|
|
16
|
+
* @returns {CSN.Model} Return the input csn, with the transformations applied
|
|
194
17
|
*/
|
|
195
|
-
function
|
|
196
|
-
const { error } = messageFunctions;
|
|
197
|
-
const doA2J = !(options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds');
|
|
18
|
+
function attachOnConditions(csn, pathDelimiter) {
|
|
198
19
|
const {
|
|
199
|
-
|
|
20
|
+
isManagedAssociation,
|
|
200
21
|
} = getUtils(csn);
|
|
201
22
|
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
* Create the foreign key elements for managed associations.
|
|
211
|
-
* Create them in-place, right after the corresponding association.
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* @param {CSN.Artifact} art
|
|
215
|
-
* @param {string} artName
|
|
216
|
-
*/
|
|
217
|
-
function createForeignKeyElements(art, artName) {
|
|
218
|
-
if ((art.kind === 'entity' || art.kind === 'view') && doA2J) {
|
|
219
|
-
forAllElements(art, artName, (parent, elements, pathToElements) => {
|
|
220
|
-
const elementsArray = [];
|
|
221
|
-
forEachGeneric(parent, 'elements', (element, elemName) => {
|
|
222
|
-
elementsArray.push([ elemName, element ]);
|
|
223
|
-
if (element.keys && isManagedAssociationElement(element)) {
|
|
224
|
-
for (let i = 0; i < element.keys.length; i++) {
|
|
225
|
-
const foreignKey = element.keys[i];
|
|
226
|
-
const path = [ ...pathToElements, elemName, 'keys', i ];
|
|
227
|
-
foreignKey.ref = flattenStructStepsInRef(foreignKey.ref, path);
|
|
228
|
-
const [ fkName, fkElem ] = getForeignKeyArtifact(element, elemName, foreignKey, path);
|
|
229
|
-
if (parent.elements[fkName]) {
|
|
230
|
-
error(null, [ ...pathToElements, elemName ], { name: fkName, art: elemName },
|
|
231
|
-
'Generated foreign key element $(NAME) for association $(ART) conflicts with existing element');
|
|
232
|
-
}
|
|
233
|
-
else {
|
|
234
|
-
elementsArray.push([ fkName, fkElem ]);
|
|
235
|
-
}
|
|
236
|
-
applyCachedAlias(foreignKey);
|
|
237
|
-
// join ref array as the struct / assoc steps are not necessary anymore
|
|
238
|
-
foreignKey.ref = [ foreignKey.ref.join(pathDelimiter) ];
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
// Don't fake consistency of the model by adding empty elements {}
|
|
244
|
-
if (elementsArray.length === 0)
|
|
245
|
-
return;
|
|
246
|
-
|
|
247
|
-
parent.elements = elementsArray.reduce((previous, [ name, element ]) => {
|
|
248
|
-
previous[name] = element;
|
|
249
|
-
return previous;
|
|
250
|
-
}, Object.create(null));
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* We save the aliased representation of the fk in $ref - I suppose to stay resolvable?
|
|
256
|
-
*
|
|
257
|
-
* We now use this $ref for ref and delete it.
|
|
258
|
-
*
|
|
259
|
-
* @param {object} foreignKey
|
|
260
|
-
* @todo With nested projections, we solve similar problems, maybe adapt?
|
|
261
|
-
*/
|
|
262
|
-
function applyCachedAlias(foreignKey) {
|
|
263
|
-
// If we have a $ref use that - it resolves aliased FKs correctly
|
|
264
|
-
if (foreignKey.$ref) {
|
|
265
|
-
foreignKey.ref = foreignKey.$ref;
|
|
266
|
-
delete foreignKey.$ref;
|
|
23
|
+
const alreadyHandled = new WeakMap();
|
|
24
|
+
applyTransformations(csn, {
|
|
25
|
+
elements: (parent, prop, elements) => {
|
|
26
|
+
for (const elemName in elements) {
|
|
27
|
+
const elem = elements[elemName];
|
|
28
|
+
// (140) Generate the ON-condition for managed associations
|
|
29
|
+
if (isManagedAssociation(elem))
|
|
30
|
+
transformManagedAssociation(elem, elemName);
|
|
267
31
|
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
32
|
+
}, /* only for views and entities */
|
|
33
|
+
}, [], { skipIgnore: false, allowArtifact: artifact => (artifact.kind === 'entity') });
|
|
271
34
|
|
|
272
|
-
|
|
273
|
-
* Return a callback function for forEachDefinition that
|
|
274
|
-
*
|
|
275
|
-
* @param {CSN.Model} csn
|
|
276
|
-
* @param {CSN.Options} options
|
|
277
|
-
* @param {string} pathDelimiter
|
|
278
|
-
* @returns {(artifact: CSN.Artifact, artifactName: string) => void} Callback for forEachDefinition
|
|
279
|
-
*/
|
|
280
|
-
function getManagedAssociationTransformer(csn, options, pathDelimiter) {
|
|
281
|
-
const doA2J = !(options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds');
|
|
282
|
-
const {
|
|
283
|
-
isManagedAssociationElement,
|
|
284
|
-
} = getUtils(csn);
|
|
285
|
-
|
|
286
|
-
return handleAssociations;
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
*
|
|
290
|
-
* Generate foreign keys for managed associations
|
|
291
|
-
* Forbid aliases for foreign keys
|
|
292
|
-
*
|
|
293
|
-
* @param {CSN.Artifact} artifact
|
|
294
|
-
* @param {string} artifactName
|
|
295
|
-
*/
|
|
296
|
-
function handleAssociations(artifact, artifactName) {
|
|
297
|
-
// Do things specific for entities and views (pass 1)
|
|
298
|
-
if (artifact.kind === 'entity' || artifact.kind === 'view') {
|
|
299
|
-
const alreadyHandled = new WeakMap();
|
|
300
|
-
forAllElements(artifact, artifactName, (parent, elements) => {
|
|
301
|
-
for (const elemName in elements) {
|
|
302
|
-
const elem = elements[elemName];
|
|
303
|
-
// (140) Generate foreign key elements and ON-condition for managed associations
|
|
304
|
-
// (unless explicitly asked to keep assocs unchanged)
|
|
305
|
-
if (doA2J && isManagedAssociationElement(elem))
|
|
306
|
-
transformManagedAssociation(parent, artifactName, elem, elemName, alreadyHandled);
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
}
|
|
35
|
+
return csn;
|
|
311
36
|
|
|
312
37
|
/**
|
|
313
38
|
* Create the foreign key elements for a managed association and build the on-condition
|
|
314
39
|
*
|
|
315
|
-
* @param {CSN.Artifact} artifact
|
|
316
|
-
* @param {string} artifactName
|
|
317
40
|
* @param {Object} elem The association to process
|
|
318
41
|
* @param {string} elemName
|
|
319
|
-
* @param {WeakMap} alreadyHandled To cache which elements were already processed
|
|
320
42
|
* @returns {void}
|
|
321
43
|
*/
|
|
322
|
-
function transformManagedAssociation(
|
|
44
|
+
function transformManagedAssociation(elem, elemName) {
|
|
323
45
|
// No need to run over this - we already did, possibly because it was referenced in the ON-Condition
|
|
324
46
|
// of another association - see a few lines lower
|
|
325
47
|
if (alreadyHandled.has(elem))
|
|
326
48
|
return;
|
|
327
|
-
//
|
|
49
|
+
// Assemble an ON-condition with the foreign keys created in earlier steps
|
|
328
50
|
const onCondParts = [];
|
|
329
51
|
let joinWithAnd = false;
|
|
330
|
-
if (elem.keys.length === 0) {
|
|
52
|
+
if (elem.keys.length === 0) { // TODO: really kill instead of _ignore?
|
|
331
53
|
elem._ignore = true;
|
|
332
54
|
}
|
|
333
55
|
else {
|
|
@@ -366,24 +88,100 @@ function getManagedAssociationTransformer(csn, options, pathDelimiter) {
|
|
|
366
88
|
if (elem.key)
|
|
367
89
|
delete elem.key;
|
|
368
90
|
|
|
369
|
-
|
|
370
91
|
// If the managed association has a 'not null' property => remove it
|
|
371
92
|
if (elem.notNull)
|
|
372
93
|
delete elem.notNull;
|
|
373
94
|
|
|
374
|
-
|
|
375
95
|
// The association is now unmanaged, i.e. actually it should no longer have foreign keys
|
|
376
96
|
// at all. But the processing of backlink associations below expects to have them, so
|
|
377
|
-
// we don't delete them
|
|
97
|
+
// we don't delete them
|
|
98
|
+
// TODO: maybe make non-enumerable, so we become recompilable in the future?
|
|
378
99
|
|
|
379
100
|
// Remember that we already processed this
|
|
380
101
|
alreadyHandled.set(elem, true);
|
|
381
102
|
}
|
|
382
103
|
}
|
|
383
104
|
|
|
105
|
+
/**
|
|
106
|
+
* @param {CSN.Model} csn
|
|
107
|
+
* @param {string} pathDelimiter
|
|
108
|
+
* @returns {(artifact: CSN.Artifact, artifactName: string) => void} Callback for forEachDefinition
|
|
109
|
+
*/
|
|
110
|
+
function getManagedAssocStepsInOnConditionFinalizer(csn, pathDelimiter) {
|
|
111
|
+
const {
|
|
112
|
+
inspectRef,
|
|
113
|
+
} = getUtils(csn);
|
|
114
|
+
|
|
115
|
+
return handleManagedAssocStepsInOnCondition;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Loop over all elements and for all unmanaged associations translate
|
|
119
|
+
* <assoc base>.<managed assoc>.<fk> to <assoc base>.<managed assoc>_<fk>
|
|
120
|
+
*
|
|
121
|
+
* Or in other words: Allow using the foreign keys of managed associations in on-conditions
|
|
122
|
+
*
|
|
123
|
+
* @param {CSN.Artifact} artifact Artifact to check
|
|
124
|
+
* @param {string} artifactName Name of the artifact
|
|
125
|
+
*/
|
|
126
|
+
function handleManagedAssocStepsInOnCondition(artifact, artifactName) {
|
|
127
|
+
for (const elemName in artifact.elements) {
|
|
128
|
+
const elem = artifact.elements[elemName];
|
|
129
|
+
// The association is an unmanaged on
|
|
130
|
+
if (!elem.keys && elem.target && elem.on) {
|
|
131
|
+
applyTransformationsOnNonDictionary(elem, 'on', {
|
|
132
|
+
ref: (refOwner, prop, ref, path) => {
|
|
133
|
+
// [<assoc base>.]<managed assoc>.<field>
|
|
134
|
+
if (ref.length > 1) {
|
|
135
|
+
const { links } = inspectRef(path);
|
|
136
|
+
if (links) {
|
|
137
|
+
// eslint-disable-next-line for-direction
|
|
138
|
+
for (let i = links.length - 1; i >= 0; i--) {
|
|
139
|
+
const link = links[i];
|
|
140
|
+
// We found the latest managed assoc path step
|
|
141
|
+
if (link.art && link.art.target && link.art.keys &&
|
|
142
|
+
// Doesn't work when ref-target (filter condition) or similar is used
|
|
143
|
+
!ref.slice(i).some(refElement => typeof refElement !== 'string')) {
|
|
144
|
+
// We join the managed assoc with everything following it
|
|
145
|
+
const sourceElementName = ref.slice(i).join(pathDelimiter);
|
|
146
|
+
const source = findSource(links, i - 1) || artifact;
|
|
147
|
+
// allow specifying managed assoc on the source side
|
|
148
|
+
const fks = link.art.keys.filter(fk => ref[i] + pathDelimiter + fk.ref[0] === sourceElementName);
|
|
149
|
+
if (fks && fks.length >= 1) {
|
|
150
|
+
const fk = fks[0];
|
|
151
|
+
const managedAssocStepName = refOwner.ref[i];
|
|
152
|
+
const fkName = `${managedAssocStepName}${pathDelimiter}${fk.as}`;
|
|
153
|
+
if (source && source.elements[fkName])
|
|
154
|
+
refOwner.ref = [ ...ref.slice(0, i), fkName ];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
}, {}, [ 'definitions', artifactName, 'elements', elemName ]);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Find out where the managed association is
|
|
167
|
+
*
|
|
168
|
+
* @param {Array} links
|
|
169
|
+
* @param {number} startIndex
|
|
170
|
+
* @returns {Object| undefined} CSN definition of the source of the managed association
|
|
171
|
+
*/
|
|
172
|
+
function findSource(links, startIndex) {
|
|
173
|
+
for (let i = startIndex; i >= 0; i--) {
|
|
174
|
+
const link = links[i];
|
|
175
|
+
// We found the latest assoc step - now check where that points to
|
|
176
|
+
if (link.art && link.art.target)
|
|
177
|
+
return csn.definitions[link.art.target];
|
|
178
|
+
}
|
|
384
179
|
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
385
184
|
module.exports = {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
getManagedAssociationTransformer,
|
|
185
|
+
attachOnConditions,
|
|
186
|
+
getManagedAssocStepsInOnConditionFinalizer,
|
|
389
187
|
};
|
|
@@ -20,7 +20,7 @@ function getAnnoProcessor() {
|
|
|
20
20
|
* @param {CSN.Artifact} artifact
|
|
21
21
|
*/
|
|
22
22
|
function handleCdsPersistence(artifact) {
|
|
23
|
-
const ignoreArtifact = (artifact.kind === 'entity'
|
|
23
|
+
const ignoreArtifact = (artifact.kind === 'entity') &&
|
|
24
24
|
(artifact.abstract ||
|
|
25
25
|
hasAnnotationValue(artifact, '@cds.persistence.skip') ||
|
|
26
26
|
hasAnnotationValue(artifact, exists));
|
|
@@ -91,7 +91,7 @@ function getAssocToSkippedIgnorer(csn, options, messageFunctions) {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
* Check
|
|
94
|
+
* Check whether the given artifact is an unreachable association target because it will not "realy" hit the database:
|
|
95
95
|
* - @cds.persistence.skip/exists
|
|
96
96
|
* - abstract
|
|
97
97
|
*
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { forEachDefinition } = require('../../base/model');
|
|
4
|
-
const {
|
|
4
|
+
const { applyTransformations, hasAnnotationValue, getResultingName } = require('../../model/csnUtils');
|
|
5
5
|
const { csnRefs } = require('../../model/csnRefs');
|
|
6
|
+
const { forEach, forEachKey } = require('../../utils/objectUtils');
|
|
6
7
|
|
|
7
8
|
const COMPOSITION = 'cds.Composition';
|
|
8
9
|
const ASSOCIATION = 'cds.Association';
|
|
@@ -28,34 +29,36 @@ function createReferentialConstraints(csn, options) {
|
|
|
28
29
|
// compositions must be processed first, as the <up_> links for them must result in `ON DELETE CASCADE`
|
|
29
30
|
const compositions = [];
|
|
30
31
|
const associations = [];
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
32
|
+
applyTransformations(csn, {
|
|
33
|
+
elements: (parent, prop, elements, path) => {
|
|
34
|
+
// Step I: iterate compositions, enrich dependent keys for <up_> association in target entity of composition
|
|
35
|
+
for (const elementName in elements) {
|
|
36
|
+
const element = elements[elementName];
|
|
37
|
+
const ePath = path.concat([ 'elements', elementName ]); // Save a copy in this scope for the late callback
|
|
38
|
+
if (element.type === COMPOSITION && element.$selfOnCondition) {
|
|
39
|
+
compositions.push({
|
|
40
|
+
fn: () => {
|
|
41
|
+
foreignKeyConstraintForUpLinkOfComposition(element, parent, ePath);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Step II: iterate associations, enrich dependent keys (in entity containing the association)
|
|
48
|
+
for (const elementName in elements) {
|
|
49
|
+
const element = elements[elementName];
|
|
50
|
+
const ePath = path.concat([ 'elements', elementName ]); // Save a copy in this scope for the late callback
|
|
51
|
+
if (element.keys && isToOne(element) && element.type === ASSOCIATION || element.type === COMPOSITION && treatCompositionLikeAssociation(element)) {
|
|
52
|
+
associations.push({
|
|
53
|
+
fn: () => {
|
|
54
|
+
foreignKeyConstraintForAssociation(element, ePath );
|
|
55
|
+
},
|
|
56
|
+
});
|
|
55
57
|
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
});
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
}, [], { skipIgnore: false, skipArtifact: a => a.query || a.kind !== 'entity' });
|
|
61
|
+
|
|
59
62
|
// create constraints on foreign keys
|
|
60
63
|
// always process unmanaged first, up_ links must be flagged
|
|
61
64
|
// before they are processed
|
|
@@ -215,8 +218,7 @@ function createReferentialConstraints(csn, options) {
|
|
|
215
218
|
* Skip referential constraint if the parent table (association target, or artifact where composition is defined)
|
|
216
219
|
* of the relation is:
|
|
217
220
|
* - a query
|
|
218
|
-
* -
|
|
219
|
-
* - TODO: Revisit -- annotated with '@cds.persistence.exists:true'
|
|
221
|
+
* - annotated with '@cds.persistence.skip:true'
|
|
220
222
|
*
|
|
221
223
|
* The following decision table reflects the current implementation:
|
|
222
224
|
*
|
|
@@ -491,18 +493,20 @@ function createReferentialConstraints(csn, options) {
|
|
|
491
493
|
const dependentKey = [ elementName ];
|
|
492
494
|
const onDeleteRules = new Set();
|
|
493
495
|
onDeleteRules.add($foreignKeyConstraint.onDelete);
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
496
|
+
forEach(artifact.elements, (foreignKeyName, foreignKey) => {
|
|
497
|
+
// find all other `$foreignKeyConstraint`s with same `$sourceAssociation` and same `parentTable`
|
|
498
|
+
const matchingForeignKeyFound = foreignKey.$foreignKeyConstraint &&
|
|
499
|
+
foreignKey.$foreignKeyConstraint.sourceAssociation === $foreignKeyConstraint.sourceAssociation &&
|
|
500
|
+
foreignKey.$foreignKeyConstraint.parentTable === $foreignKeyConstraint.parentTable;
|
|
501
|
+
if (!matchingForeignKeyFound)
|
|
502
|
+
return;
|
|
503
|
+
|
|
504
|
+
const $foreignKeyConstraintCopy = Object.assign({}, foreignKey.$foreignKeyConstraint);
|
|
505
|
+
delete foreignKey.$foreignKeyConstraint;
|
|
506
|
+
parentKey.push($foreignKeyConstraintCopy.parentKey);
|
|
507
|
+
dependentKey.push(foreignKeyName);
|
|
508
|
+
onDeleteRules.add($foreignKeyConstraintCopy.onDelete);
|
|
509
|
+
});
|
|
506
510
|
// onDelete Rule is the "weakest" rule applicable. Precedence: RESTRICT > SET NULL > CASCADE
|
|
507
511
|
const onDelete = onDeleteRules.has('RESTRICT') ? 'RESTRICT' : 'CASCADE';
|
|
508
512
|
let onDeleteRemark = null;
|
|
@@ -510,14 +514,14 @@ function createReferentialConstraints(csn, options) {
|
|
|
510
514
|
if (options.testMode && onDelete === 'CASCADE')
|
|
511
515
|
onDeleteRemark = `Up_ link for Composition "${$foreignKeyConstraint.upLinkFor}" implies existential dependency`;
|
|
512
516
|
referentialConstraints[`${getResultingName(csn, 'quoted', artifactName)}_${$foreignKeyConstraint.sourceAssociation}`] = {
|
|
513
|
-
identifier
|
|
517
|
+
// constraint identifier start with `c__` to avoid name clashes
|
|
518
|
+
identifier: `c__${getResultingName(csn, options.forHana.names, artifactName)}_${$foreignKeyConstraint.sourceAssociation}`,
|
|
514
519
|
foreignKey: dependentKey,
|
|
515
520
|
parentKey,
|
|
516
521
|
dependentTable: artifactName,
|
|
517
522
|
parentTable,
|
|
518
523
|
onDelete,
|
|
519
524
|
onDeleteRemark, // explain why this particular rule is chosen
|
|
520
|
-
// TODO: do we want to switch off validation / enforcement via annotation on association?
|
|
521
525
|
validated: $foreignKeyConstraint.validated,
|
|
522
526
|
enforced: $foreignKeyConstraint.enforced,
|
|
523
527
|
};
|
|
@@ -545,15 +549,14 @@ function assertConstraintIdentifierUniqueness(artifact, artifactName, path, erro
|
|
|
545
549
|
if (!(artifact.$tableConstraints && artifact.$tableConstraints.referential && artifact.$tableConstraints.unique))
|
|
546
550
|
return;
|
|
547
551
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
.
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
});
|
|
552
|
+
forEachKey(artifact.$tableConstraints.unique, (uniqueConstraintKey) => {
|
|
553
|
+
const uniqueConstraintIdentifier = `${artifactName}_${uniqueConstraintKey}`; // final unique constraint identifier will be generated in renderer likewise
|
|
554
|
+
if (artifact.$tableConstraints.referential[uniqueConstraintIdentifier]) {
|
|
555
|
+
error(null, path,
|
|
556
|
+
{ name: uniqueConstraintIdentifier, art: artifactName },
|
|
557
|
+
'Duplicate constraint name $(NAME) in artifact $(ART)');
|
|
558
|
+
}
|
|
559
|
+
});
|
|
557
560
|
}
|
|
558
561
|
|
|
559
562
|
module.exports = { createReferentialConstraints, assertConstraintIdentifierUniqueness };
|