@sap/cds-compiler 2.5.0 → 2.10.4
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 +191 -9
- package/bin/cdsc.js +2 -2
- package/doc/CHANGELOG_BETA.md +33 -3
- package/lib/api/main.js +29 -101
- package/lib/api/options.js +15 -11
- package/lib/api/validate.js +12 -8
- package/lib/backends.js +0 -81
- package/lib/base/keywords.js +32 -2
- package/lib/base/message-registry.js +63 -9
- package/lib/base/messages.js +63 -21
- package/lib/base/model.js +2 -3
- package/lib/checks/defaultValues.js +27 -2
- package/lib/checks/elements.js +1 -6
- package/lib/checks/foreignKeys.js +0 -6
- package/lib/checks/managedWithoutKeys.js +17 -0
- package/lib/checks/nonexpandableStructured.js +38 -0
- package/lib/checks/onConditions.js +9 -45
- package/lib/checks/queryNoDbArtifacts.js +25 -7
- package/lib/checks/selectItems.js +25 -2
- package/lib/checks/types.js +26 -2
- package/lib/checks/unknownMagic.js +38 -0
- package/lib/checks/utils.js +61 -0
- package/lib/checks/validator.js +60 -7
- package/lib/compiler/assert-consistency.js +16 -7
- package/lib/compiler/builtins.js +2 -0
- package/lib/compiler/checks.js +6 -4
- package/lib/compiler/definer.js +99 -42
- package/lib/compiler/index.js +73 -27
- package/lib/compiler/resolver.js +288 -157
- package/lib/compiler/shared.js +31 -11
- package/lib/edm/annotations/genericTranslation.js +182 -186
- package/lib/edm/csn2edm.js +103 -108
- package/lib/edm/edm.js +18 -21
- package/lib/edm/edmPreprocessor.js +361 -114
- package/lib/edm/edmUtils.js +103 -33
- package/lib/gen/Dictionary.json +22 -0
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +12 -1
- package/lib/gen/language.tokens +57 -53
- package/lib/gen/languageLexer.interp +10 -1
- package/lib/gen/languageLexer.js +770 -744
- package/lib/gen/languageLexer.tokens +49 -46
- package/lib/gen/languageParser.js +4713 -4279
- package/lib/json/from-csn.js +103 -45
- package/lib/json/to-csn.js +296 -117
- package/lib/language/antlrParser.js +4 -3
- package/lib/language/errorStrategy.js +1 -0
- package/lib/language/genericAntlrParser.js +21 -12
- package/lib/language/language.g4 +99 -31
- package/lib/main.d.ts +81 -3
- package/lib/main.js +30 -7
- package/lib/model/api.js +78 -0
- package/lib/model/csnRefs.js +329 -142
- package/lib/model/csnUtils.js +235 -58
- package/lib/model/enrichCsn.js +18 -1
- package/lib/model/revealInternalProperties.js +2 -1
- package/lib/modelCompare/compare.js +37 -20
- package/lib/optionProcessor.js +9 -3
- package/lib/render/.eslintrc.json +4 -1
- package/lib/render/DuplicateChecker.js +8 -5
- package/lib/render/toCdl.js +112 -33
- package/lib/render/toHdbcds.js +134 -64
- package/lib/render/toSql.js +91 -38
- package/lib/render/utils/common.js +8 -13
- package/lib/render/utils/sql.js +3 -3
- package/lib/sql-identifier.js +6 -1
- package/lib/transform/db/assertUnique.js +5 -6
- package/lib/transform/db/constraints.js +29 -13
- package/lib/transform/db/draft.js +8 -6
- package/lib/transform/db/expansion.js +582 -0
- package/lib/transform/db/flattening.js +325 -0
- package/lib/transform/db/groupByOrderBy.js +2 -2
- package/lib/transform/db/transformExists.js +284 -63
- package/lib/transform/forHanaNew.js +98 -381
- package/lib/transform/forOdataNew.js +21 -22
- package/lib/transform/localized.js +37 -10
- package/lib/transform/odata/attachPath.js +19 -4
- package/lib/transform/odata/generateForeignKeyElements.js +11 -10
- package/lib/transform/odata/referenceFlattener.js +60 -39
- package/lib/transform/odata/sortByAssociationDependency.js +2 -2
- package/lib/transform/odata/structuralPath.js +72 -0
- package/lib/transform/odata/structureFlattener.js +19 -18
- package/lib/transform/odata/typesExposure.js +22 -12
- package/lib/transform/transformUtilsNew.js +134 -78
- package/lib/transform/translateAssocsToJoins.js +17 -14
- package/lib/transform/universalCsnEnricher.js +67 -0
- package/lib/utils/file.js +0 -11
- package/lib/utils/moduleResolve.js +6 -8
- package/package.json +1 -1
- package/lib/json/walker.js +0 -26
- package/lib/transform/sqlite +0 -0
- package/lib/utils/string.js +0 -17
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { forAllQueries, forEachDefinition } = require('../../model/csnUtils');
|
|
3
|
+
const { forAllQueries, forEachDefinition, walkCsnPath } = require('../../model/csnUtils');
|
|
4
4
|
const { setProp } = require('../../base/model');
|
|
5
5
|
const { getRealName } = require('../../render/utils/common');
|
|
6
|
+
const { csnRefs } = require('../../model/csnRefs');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Turn a `exists assoc[filter = 100]` into a `exists (select 1 as dummy from assoc.target where <assoc on condition> and assoc.target.filter = 100)`.
|
|
@@ -43,23 +44,40 @@ const { getRealName } = require('../../render/utils/common');
|
|
|
43
44
|
* The final subselect looks like (select 1 as dummy from E where F.backToE.id = E.id and filter = 100).
|
|
44
45
|
*
|
|
45
46
|
* @param {CSN.Model} csn
|
|
47
|
+
* @param {CSN.Options} options
|
|
46
48
|
* @param {Function} error
|
|
47
49
|
*/
|
|
48
|
-
function handleExists(csn, error) {
|
|
50
|
+
function handleExists(csn, options, error) {
|
|
51
|
+
const { inspectRef } = csnRefs(csn);
|
|
49
52
|
forEachDefinition(csn, (artifact, artifactName) => {
|
|
50
53
|
if (artifact.query) {
|
|
51
|
-
forAllQueries(artifact.query, (query) => {
|
|
54
|
+
forAllQueries(artifact.query, (query, path) => {
|
|
52
55
|
if (!query.$generatedExists) {
|
|
56
|
+
const toProcess = []; // Collect all expressions we need to process here
|
|
53
57
|
if (query.SELECT && query.SELECT.where && query.SELECT.where.length > 1)
|
|
54
|
-
|
|
58
|
+
toProcess.push([ path.slice(0, -1), path.concat('where') ]);
|
|
55
59
|
|
|
56
60
|
|
|
57
61
|
if (query.SELECT && query.SELECT.columns)
|
|
58
|
-
|
|
62
|
+
toProcess.push([ path.slice(0, -1), path.concat('columns') ]);
|
|
59
63
|
|
|
60
64
|
|
|
61
65
|
if (query.SELECT && query.SELECT.from.on )
|
|
62
|
-
|
|
66
|
+
toProcess.push([ path.slice(0, -1), path.concat([ 'from', 'on' ]) ]);
|
|
67
|
+
|
|
68
|
+
for (const [ , exprPath ] of toProcess) {
|
|
69
|
+
const expr = nestExists(exprPath);
|
|
70
|
+
walkCsnPath(csn, exprPath.slice(0, -1))[exprPath[exprPath.length - 1]] = expr;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
while (toProcess.length > 0) {
|
|
74
|
+
const [ queryPath, exprPath ] = toProcess.pop();
|
|
75
|
+
// leftovers can happen with nested exists - we then need to drill down into the created SELECT
|
|
76
|
+
// to check for further exists
|
|
77
|
+
const { result, leftovers } = processExists(queryPath, exprPath);
|
|
78
|
+
walkCsnPath(csn, exprPath.slice(0, -1))[exprPath[exprPath.length - 1]] = result;
|
|
79
|
+
toProcess.push(...leftovers.reverse()); // any leftovers - schedule for further processing
|
|
80
|
+
}
|
|
63
81
|
}
|
|
64
82
|
}, [ 'definitions', artifactName, 'query' ]);
|
|
65
83
|
}
|
|
@@ -108,14 +126,158 @@ function handleExists(csn, error) {
|
|
|
108
126
|
}
|
|
109
127
|
|
|
110
128
|
/**
|
|
111
|
-
*
|
|
129
|
+
* Get the index of the first association that is found - starting the
|
|
130
|
+
* search at the given startIndex.
|
|
112
131
|
*
|
|
113
|
-
* @param {
|
|
114
|
-
* @param {
|
|
115
|
-
* @returns {
|
|
132
|
+
* @param {number} startIndex Where to start searching
|
|
133
|
+
* @param {object[]} links links for a ref, produced by inspectRef
|
|
134
|
+
* @returns {number|null} Null if no association was found
|
|
135
|
+
*/
|
|
136
|
+
function getFirstAssocIndex(startIndex, links) {
|
|
137
|
+
for (let i = startIndex; i < links.length; i++) {
|
|
138
|
+
if (links[i] && links[i].art && links[i].art.target)
|
|
139
|
+
return i;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* For a given ref-array, this function is called for the first assoc-ref in the array.
|
|
147
|
+
*
|
|
148
|
+
* It then runs over the rest of the array and puts all other steps in the first assocs filter.
|
|
149
|
+
* If the rest contains another assoc, we put all following things into that assocs filter and
|
|
150
|
+
* add the sub-assoc to the previous assoc filter.
|
|
151
|
+
*
|
|
152
|
+
* Or in other words:
|
|
153
|
+
* - exists toF[1=1].toG[1=1].toH[1=1] is found
|
|
154
|
+
* - we get called with toF[1=1].toG[1=1].toH[1=1]
|
|
155
|
+
* - we return toF[1=1 and exists toG[1=1 and exists toH[1=1]]]
|
|
156
|
+
*
|
|
157
|
+
* @param {number} startIndex The index of the thing AFTER _main in the ref-array
|
|
158
|
+
* @param {string|object} startAssoc The path step that is the first assoc
|
|
159
|
+
* @param {Array} startRest Any path steps after startAssoc
|
|
160
|
+
* @param {CSN.Path} path to the overall ref where _main is contained
|
|
161
|
+
* @returns {Array} Return the now-nested ref-array
|
|
162
|
+
*/
|
|
163
|
+
function nestFilters(startIndex, startAssoc, startRest, path) {
|
|
164
|
+
let revert;
|
|
165
|
+
if (!startAssoc.where) { // initialize first filter if not present
|
|
166
|
+
if (typeof startAssoc === 'string') {
|
|
167
|
+
startAssoc = {
|
|
168
|
+
id: startAssoc,
|
|
169
|
+
where: [],
|
|
170
|
+
};
|
|
171
|
+
revert = () => {
|
|
172
|
+
startAssoc = startAssoc.id;
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
startAssoc.where = [];
|
|
177
|
+
revert = () => {
|
|
178
|
+
delete startAssoc.where;
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const stack = [ [ null, startAssoc, startRest, startIndex ] ];
|
|
183
|
+
const { links } = inspectRef(path);
|
|
184
|
+
while (stack.length > 0) {
|
|
185
|
+
// previous: to nest "up" if the previous assoc did not originaly have a filter
|
|
186
|
+
// assoc: the assoc path step
|
|
187
|
+
// rest: path steps after assoc
|
|
188
|
+
// index: index of after-assoc in the overall ref-array - so we know where to start looking for the next assoc
|
|
189
|
+
const workPackage = stack.pop();
|
|
190
|
+
const [ previous, , rest, index ] = workPackage;
|
|
191
|
+
let [ , assoc, , ] = workPackage;
|
|
192
|
+
|
|
193
|
+
const firstAssocIndex = getFirstAssocIndex(index, links);
|
|
194
|
+
|
|
195
|
+
const head = rest.slice(0, firstAssocIndex - index);
|
|
196
|
+
const nextAssoc = rest[firstAssocIndex - index];
|
|
197
|
+
const tail = rest.slice(firstAssocIndex - index + 1);
|
|
198
|
+
|
|
199
|
+
const hasAssoc = nextAssoc !== undefined;
|
|
200
|
+
|
|
201
|
+
if (!assoc.where && hasAssoc) { // no existing filter - and there is stuff we need to nest afterwards
|
|
202
|
+
if (typeof assoc === 'string') {
|
|
203
|
+
assoc = {
|
|
204
|
+
id: assoc,
|
|
205
|
+
where: [],
|
|
206
|
+
};
|
|
207
|
+
// We need to "hook" this into the previous filter.
|
|
208
|
+
// Since we create a new object, we don't have a handy reference we can just manipulate
|
|
209
|
+
if (previous)
|
|
210
|
+
previous.where[previous.where.length - 1] = { ref: [ assoc ] };
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
assoc.where = [];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else if (assoc.where && assoc.where.length > 0 && (hasAssoc || rest.length > 0)) {
|
|
217
|
+
assoc.where.push('and');
|
|
218
|
+
} // merge with existing filter
|
|
219
|
+
|
|
220
|
+
if (hasAssoc)
|
|
221
|
+
assoc.where.push('exists', { ref: [ ...head, nextAssoc ] });
|
|
222
|
+
else if (rest.length > 0)
|
|
223
|
+
assoc.where.push({ ref: rest });
|
|
224
|
+
|
|
225
|
+
if (hasAssoc)
|
|
226
|
+
stack.push([ assoc, nextAssoc, tail, firstAssocIndex ]);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Seems like we did not have anything to nest into the filter - then kill it
|
|
230
|
+
if (startAssoc.where.length === 0 && revert !== undefined)
|
|
231
|
+
revert();
|
|
232
|
+
|
|
233
|
+
return startAssoc;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Walk to the expr using the given path and scan it for the "exists" + "ref" pattern.
|
|
238
|
+
* If such a pattern is found, nest association steps therein into filters.
|
|
239
|
+
*
|
|
240
|
+
* @param {CSN.Path} exprPath
|
|
241
|
+
* @returns {Array}
|
|
242
|
+
*/
|
|
243
|
+
function nestExists(exprPath) {
|
|
244
|
+
const expr = walkCsnPath(csn, exprPath);
|
|
245
|
+
for (let i = 0; i < expr.length; i++) {
|
|
246
|
+
if (i < expr.length - 1 && expr[i] === 'exists' && expr[i + 1].ref) {
|
|
247
|
+
i++;
|
|
248
|
+
const current = expr[i];
|
|
249
|
+
const {
|
|
250
|
+
ref, head, tail,
|
|
251
|
+
} = getFirstAssoc(current, exprPath.concat(i));
|
|
252
|
+
|
|
253
|
+
const lastAssoc = getLastAssoc(current, exprPath.concat(i));
|
|
254
|
+
// toE.toF.id -> we must not end on a non-assoc - this will also be caught downstream by
|
|
255
|
+
// '“EXISTS” can only be used with associations/compositions, found $(TYPE)'
|
|
256
|
+
// But the error might not be clear, since it could be because of our rewritten stuff. The later check
|
|
257
|
+
// checks for exists id -> our rewrite turns toE.toF.id into toE[exists toF[exists id]], leading to the same error
|
|
258
|
+
if (lastAssoc.tail.length > 0)
|
|
259
|
+
error(null, current.$path, { id: lastAssoc.tail[0].id ? lastAssoc.tail[0].id : lastAssoc.tail[0], name: lastAssoc.ref.id ? lastAssoc.ref.id : lastAssoc.ref }, 'Unexpected path step $(ID) after association $(NAME) in "EXISTS"');
|
|
260
|
+
|
|
261
|
+
const newThing = [ ...head, nestFilters(head.length + 1, ref, tail, exprPath.concat([ i ])) ];
|
|
262
|
+
expr[i].ref = newThing;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return expr;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Process the given expr of the given query and translate a `EXISTS assoc` into a `EXISTS (subquery)`. Also, return paths to things we need to process in a second step.
|
|
271
|
+
*
|
|
272
|
+
* @param {CSN.Path} queryPath Path to the query-object
|
|
273
|
+
* @param {CSN.Path} exprPath Path to the expression-array to process
|
|
274
|
+
* @returns {{result: TokenStream, leftovers: Array[]}} result: A new token stream expression - the same as expr, but with the expanded EXISTS, leftovers: path-tuples to further subqueries to process.
|
|
116
275
|
*/
|
|
117
|
-
function processExists(
|
|
276
|
+
function processExists(queryPath, exprPath) {
|
|
277
|
+
const toContinue = [];
|
|
118
278
|
const newExpr = [];
|
|
279
|
+
const query = walkCsnPath(csn, queryPath);
|
|
280
|
+
const expr = walkCsnPath(csn, exprPath);
|
|
119
281
|
const queryBase = query.SELECT.from.ref ? (query.SELECT.from.as || query.SELECT.from.ref[0]) : null;
|
|
120
282
|
const sources = getQuerySources(query.SELECT);
|
|
121
283
|
|
|
@@ -123,18 +285,15 @@ function handleExists(csn, error) {
|
|
|
123
285
|
if (i < expr.length - 1 && expr[i] === 'exists' && expr[i + 1].ref) {
|
|
124
286
|
i++;
|
|
125
287
|
const current = expr[i];
|
|
126
|
-
const isPrefixedWithTableAlias = firstLinkIsEntityOrQuerySource(
|
|
127
|
-
const base = getBase(queryBase, isPrefixedWithTableAlias, current);
|
|
128
|
-
const { root, ref
|
|
288
|
+
const isPrefixedWithTableAlias = firstLinkIsEntityOrQuerySource(exprPath.concat(i));
|
|
289
|
+
const base = getBase(queryBase, isPrefixedWithTableAlias, current, exprPath.concat(i));
|
|
290
|
+
const { root, ref } = getFirstAssoc(current, exprPath.concat(i));
|
|
129
291
|
|
|
130
|
-
if (
|
|
131
|
-
error(null,
|
|
132
|
-
|
|
292
|
+
if (!root.target) {
|
|
293
|
+
error(null, exprPath.concat(i), { type: root.type }, '“EXISTS” can only be used with associations/compositions, found $(TYPE)');
|
|
294
|
+
return { result: [], leftovers: [] };
|
|
133
295
|
}
|
|
134
296
|
|
|
135
|
-
if (!root.target)
|
|
136
|
-
return error(null, current.$path, { type: root.type }, '"EXISTS" can only be used with associations/compositions, found $(TYPE)');
|
|
137
|
-
|
|
138
297
|
const subselect = getSubselect(root.target, ref, sources);
|
|
139
298
|
|
|
140
299
|
const target = subselect.SELECT.from.as; // use subquery alias as target - prevent shadowing
|
|
@@ -150,18 +309,24 @@ function handleExists(csn, error) {
|
|
|
150
309
|
subselect.SELECT.where.push(...[ 'and', ...remapExistingWhere(target, ref.where) ]);
|
|
151
310
|
|
|
152
311
|
newExpr.push(subselect);
|
|
312
|
+
toContinue.push([ exprPath.concat(newExpr.length - 1), exprPath.concat([ newExpr.length - 1, 'SELECT', 'where' ]) ]);
|
|
153
313
|
}
|
|
154
314
|
else { // Drill down into other places that might contain a `EXISTS <assoc>`
|
|
155
|
-
if (expr[i].xpr)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
315
|
+
if (expr[i].xpr) {
|
|
316
|
+
const { result, leftovers } = processExists(queryPath, exprPath.concat([ i, 'xpr' ]));
|
|
317
|
+
expr[i].xpr = result;
|
|
318
|
+
toContinue.push(...leftovers);
|
|
319
|
+
}
|
|
320
|
+
if (expr[i].args && Array.isArray(expr[i].args)) {
|
|
321
|
+
const { result, leftovers } = processExists(queryPath, exprPath.concat([ i, 'args' ]));
|
|
322
|
+
expr[i].args = result;
|
|
323
|
+
toContinue.push(...leftovers);
|
|
324
|
+
}
|
|
160
325
|
newExpr.push(expr[i]);
|
|
161
326
|
}
|
|
162
327
|
}
|
|
163
328
|
|
|
164
|
-
return newExpr;
|
|
329
|
+
return { result: newExpr, leftovers: toContinue };
|
|
165
330
|
}
|
|
166
331
|
|
|
167
332
|
/**
|
|
@@ -227,23 +392,35 @@ function handleExists(csn, error) {
|
|
|
227
392
|
function translateUnmanagedAssocToWhere(root, target, subselect, isPrefixedWithTableAlias, base, current) {
|
|
228
393
|
for (let j = 0; j < root.on.length; j++) {
|
|
229
394
|
const part = root.on[j];
|
|
395
|
+
|
|
396
|
+
// we can only resolve stuff on refs - skip literals like =
|
|
397
|
+
// but also keep along stuff like null and undefined, so compiler
|
|
398
|
+
// can have a chance to complain/ we can fail later nicely maybe
|
|
399
|
+
if (!(part && part.ref)) {
|
|
400
|
+
subselect.SELECT.where.push(part);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// root.$path should be safe - we can only reference things in exists that exist when we enrich
|
|
405
|
+
// so all of them should have a $path.
|
|
406
|
+
const { art, links } = inspectRef(root.$path.concat([ 'on', j ]));
|
|
230
407
|
// Dollar Self Backlink
|
|
231
|
-
if (isValidDollarSelf(root.on[j], root.on[j + 1], root.on[j + 2])) {
|
|
408
|
+
if (isValidDollarSelf(root.on[j], root.$path.concat([ 'on', j ]), root.on[j + 1], root.on[j + 2], root.$path.concat([ 'on', j + 2 ]))) {
|
|
232
409
|
if (root.on[j].ref[0] === '$self' && root.on[j].ref.length === 1)
|
|
233
|
-
subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j + 2]));
|
|
410
|
+
subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j + 2], root.$path.concat([ 'on', j + 2 ])));
|
|
234
411
|
else
|
|
235
|
-
subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j]));
|
|
412
|
+
subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j], root.$path.concat([ 'on', j ])));
|
|
236
413
|
|
|
237
414
|
j += 2;
|
|
238
415
|
}
|
|
239
|
-
else if (
|
|
416
|
+
else if (links && links[0].art === root) { // target side
|
|
240
417
|
subselect.SELECT.where.push({ ref: [ target, ...part.ref.slice(1) ] });
|
|
241
418
|
}
|
|
242
419
|
else if (part.$scope === '$self') { // source side - "absolute" scope
|
|
243
420
|
// cut off the $self, as we prefix the entity name now
|
|
244
421
|
subselect.SELECT.where.push({ ref: [ base, ...part.ref.slice(1) ] });
|
|
245
422
|
}
|
|
246
|
-
else if (
|
|
423
|
+
else if (art) { // source side - with local scope
|
|
247
424
|
if (isPrefixedWithTableAlias)
|
|
248
425
|
subselect.SELECT.where.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
|
|
249
426
|
else
|
|
@@ -258,43 +435,82 @@ function handleExists(csn, error) {
|
|
|
258
435
|
* Check that an expression triple is a valid $self
|
|
259
436
|
*
|
|
260
437
|
* @param {Token} leftSide
|
|
438
|
+
* @param {CSN.Path} pathLeft
|
|
261
439
|
* @param {Token} middle
|
|
262
440
|
* @param {Token} rightSide
|
|
441
|
+
* @param {CSN.Path} pathRight
|
|
263
442
|
* @returns {boolean}
|
|
264
443
|
*/
|
|
265
|
-
function isValidDollarSelf(leftSide, middle, rightSide) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
444
|
+
function isValidDollarSelf(leftSide, pathLeft, middle, rightSide, pathRight) {
|
|
445
|
+
if (leftSide && leftSide.ref && rightSide && rightSide.ref && middle === '=') {
|
|
446
|
+
const right = inspectRef(pathRight);
|
|
447
|
+
const left = inspectRef(pathLeft);
|
|
448
|
+
|
|
449
|
+
if (!right || !left)
|
|
450
|
+
return false;
|
|
451
|
+
|
|
452
|
+
const rightSideArt = right.art;
|
|
453
|
+
const leftSideArt = left.art;
|
|
454
|
+
|
|
455
|
+
return leftSide.ref[0] === '$self' && leftSide.ref.length === 1 && rightSideArt && rightSideArt.target ||
|
|
456
|
+
rightSide.ref[0] === '$self' && rightSide.ref.length === 1 && leftSideArt && leftSideArt.target;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return false;
|
|
273
460
|
}
|
|
274
461
|
}
|
|
275
462
|
|
|
276
463
|
/**
|
|
277
|
-
* From the given expression (having
|
|
464
|
+
* From the given expression (having inspectRef -> links), find the first association.
|
|
278
465
|
*
|
|
279
466
|
* @param {object} xprPart
|
|
280
|
-
* @
|
|
467
|
+
* @param {CSN.Path} path
|
|
468
|
+
* @returns {{head: Array, root: CSN.Element, ref: string|object, tail: Array}} The first assoc (root), the corresponding ref (ref), anything before the ref (head) and the rest of the ref (tail).
|
|
281
469
|
*/
|
|
282
|
-
function getFirstAssoc(xprPart) {
|
|
470
|
+
function getFirstAssoc(xprPart, path) {
|
|
471
|
+
const { links, art } = inspectRef(path);
|
|
283
472
|
for (let i = 0; i < xprPart.ref.length - 1; i++) {
|
|
284
|
-
if (
|
|
285
|
-
return {
|
|
473
|
+
if (links[i].art && links[i].art.target) {
|
|
474
|
+
return {
|
|
475
|
+
head: (i === 0 ? [] : xprPart.ref.slice(0, i)), root: links[i].art, ref: xprPart.ref[i], tail: xprPart.ref.slice(i + 1),
|
|
476
|
+
};
|
|
477
|
+
}
|
|
286
478
|
}
|
|
287
|
-
return {
|
|
479
|
+
return {
|
|
480
|
+
head: (xprPart.ref.length === 1 ? [] : xprPart.ref.slice(0, xprPart.ref.length - 1)), root: art, ref: xprPart.ref[xprPart.ref.length - 1], tail: [],
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Get the last association from the expression part - similar to getFirstAssoc
|
|
486
|
+
*
|
|
487
|
+
* @param {object} xprPart
|
|
488
|
+
* @param {CSN.Path} path
|
|
489
|
+
* @returns {{head: Array, root: CSN.Element, ref: string|object, tail: Array}} The last assoc (root), the corresponding ref (ref), anything before the ref (head) and the rest of the ref (tail).
|
|
490
|
+
*/
|
|
491
|
+
function getLastAssoc(xprPart, path) {
|
|
492
|
+
const { links, art } = inspectRef(path);
|
|
493
|
+
for (let i = xprPart.ref.length - 1; i > -1; i--) {
|
|
494
|
+
if (links[i].art && links[i].art.target) {
|
|
495
|
+
return {
|
|
496
|
+
head: (i === 0 ? [] : xprPart.ref.slice(0, i)), root: links[i].art, ref: xprPart.ref[i], tail: xprPart.ref.slice(i + 1),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
head: (xprPart.ref.length === 1 ? [] : xprPart.ref.slice(0, xprPart.ref.length - 1)), root: art, ref: xprPart.ref[xprPart.ref.length - 1], tail: [],
|
|
502
|
+
};
|
|
288
503
|
}
|
|
289
504
|
|
|
290
505
|
/**
|
|
291
|
-
* Check (using
|
|
506
|
+
* Check (using inspectRef -> links), wether the first path step is an entity or query source
|
|
292
507
|
*
|
|
293
|
-
* @param {
|
|
508
|
+
* @param {CSN.Path} path
|
|
294
509
|
* @returns {boolean}
|
|
295
510
|
*/
|
|
296
|
-
function firstLinkIsEntityOrQuerySource(
|
|
297
|
-
|
|
511
|
+
function firstLinkIsEntityOrQuerySource(path) {
|
|
512
|
+
const { links } = inspectRef(path);
|
|
513
|
+
return links && (links[0].art.kind === 'entity' || links[0].art.query || links[0].art.from);
|
|
298
514
|
}
|
|
299
515
|
|
|
300
516
|
/**
|
|
@@ -314,13 +530,14 @@ function handleExists(csn, error) {
|
|
|
314
530
|
* we can be sure that resolving the ref requires $env information.
|
|
315
531
|
*
|
|
316
532
|
* @param {object} xpr
|
|
533
|
+
* @param {CSN.Path} path
|
|
317
534
|
* @returns {string|undefined} undefined in case of errors
|
|
318
535
|
* @throws {Error} Throws if xpr.ref but no xpr.$env
|
|
319
536
|
* @todo $env is going to be removed from CSN, but csnRefs will provide it
|
|
320
537
|
*/
|
|
321
538
|
// eslint-disable-next-line consistent-return
|
|
322
|
-
function getParent(xpr) {
|
|
323
|
-
if (firstLinkIsEntityOrQuerySource(
|
|
539
|
+
function getParent(xpr, path) {
|
|
540
|
+
if (firstLinkIsEntityOrQuerySource(path)) {
|
|
324
541
|
return xpr.ref[0];
|
|
325
542
|
}
|
|
326
543
|
else if (xpr.$env) {
|
|
@@ -382,14 +599,15 @@ function handleExists(csn, error) {
|
|
|
382
599
|
* @param {string|null} queryBase
|
|
383
600
|
* @param {boolean} isPrefixedWithTableAlias
|
|
384
601
|
* @param {CSN.Column} current
|
|
602
|
+
* @param {CSN.Path} path
|
|
385
603
|
* @returns {string}
|
|
386
604
|
*/
|
|
387
|
-
function getBase(queryBase, isPrefixedWithTableAlias, current) {
|
|
605
|
+
function getBase(queryBase, isPrefixedWithTableAlias, current, path) {
|
|
388
606
|
if (queryBase)
|
|
389
607
|
return getRealName(csn, queryBase);
|
|
390
608
|
else if (isPrefixedWithTableAlias)
|
|
391
609
|
return current.ref[0];
|
|
392
|
-
return getParent(current);
|
|
610
|
+
return getParent(current, path);
|
|
393
611
|
}
|
|
394
612
|
|
|
395
613
|
|
|
@@ -421,31 +639,34 @@ function handleExists(csn, error) {
|
|
|
421
639
|
* @param {string} base The source entity/query source name
|
|
422
640
|
* @param {string} target The target entity/query source name
|
|
423
641
|
* @param {CSN.Element} assoc The association element - the "not-$self" side of the comparison
|
|
642
|
+
* @param {CSN.Path} path
|
|
424
643
|
* @returns {TokenStream} The WHERE representing the $self comparison
|
|
425
644
|
*/
|
|
426
|
-
function translateDollarSelfToWhere(base, target, assoc) {
|
|
645
|
+
function translateDollarSelfToWhere(base, target, assoc, path) {
|
|
427
646
|
const where = [];
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const
|
|
647
|
+
const { art } = inspectRef(path);
|
|
648
|
+
if (art.keys) {
|
|
649
|
+
for (let i = 0; i < art.keys.length; i++) {
|
|
650
|
+
const lop = { ref: [ target, ...assoc.ref.slice(1), ...art.keys[i].ref ] }; // target side
|
|
651
|
+
const rop = { ref: [ base, ...art.keys[i].ref ] }; // source side
|
|
432
652
|
if (i > 0)
|
|
433
653
|
where.push('and');
|
|
434
654
|
|
|
435
655
|
where.push(...[ lop, '=', rop ]);
|
|
436
656
|
}
|
|
437
657
|
}
|
|
438
|
-
else if (
|
|
439
|
-
for (let i = 0; i <
|
|
440
|
-
const part =
|
|
441
|
-
|
|
658
|
+
else if (art.on) {
|
|
659
|
+
for (let i = 0; i < art.on.length; i++) {
|
|
660
|
+
const part = art.on[i];
|
|
661
|
+
const partInspect = inspectRef(art.$path.concat([ 'on', i ]));
|
|
662
|
+
if (partInspect.links && partInspect.links[0].art === art) { // target side
|
|
442
663
|
where.push({ ref: [ base, ...part.ref.slice(1) ] });
|
|
443
664
|
}
|
|
444
665
|
else if (part.$scope === '$self') { // source side - "absolute" scope
|
|
445
666
|
// Same message as in forHanaNew/transformDollarSelfComparisonWithUnmanagedAssoc
|
|
446
667
|
error(null, part.$path, 'An association that uses "$self" in its ON-condition can\'t be compared to "$self"');
|
|
447
668
|
}
|
|
448
|
-
else if (
|
|
669
|
+
else if (partInspect.art) { // source side - with local scope
|
|
449
670
|
where.push({ ref: [ target, ...assoc.ref.slice(1, -1), ...part.ref ] });
|
|
450
671
|
}
|
|
451
672
|
else { // operator - or any other leftover
|