@sap/cds-compiler 2.7.0 → 2.11.2
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 +167 -0
- package/bin/cdsc.js +42 -25
- package/bin/cdsse.js +1 -0
- package/doc/CHANGELOG_BETA.md +10 -0
- package/lib/api/.eslintrc.json +2 -0
- package/lib/api/main.js +17 -33
- package/lib/api/options.js +25 -13
- package/lib/api/validate.js +33 -9
- package/lib/backends.js +9 -8
- package/lib/base/dictionaries.js +2 -1
- package/lib/base/keywords.js +32 -2
- package/lib/base/message-registry.js +26 -2
- package/lib/base/messages.js +25 -9
- package/lib/base/model.js +5 -3
- package/lib/base/optionProcessorHelper.js +56 -22
- package/lib/checks/onConditions.js +5 -0
- package/lib/checks/selectItems.js +4 -0
- package/lib/checks/types.js +26 -2
- package/lib/checks/unknownMagic.js +41 -0
- package/lib/checks/validator.js +7 -2
- package/lib/compiler/assert-consistency.js +18 -5
- package/lib/compiler/base.js +65 -0
- package/lib/compiler/builtins.js +30 -1
- package/lib/compiler/checks.js +5 -2
- package/lib/compiler/definer.js +145 -120
- package/lib/compiler/index.js +16 -4
- package/lib/compiler/propagator.js +5 -2
- package/lib/compiler/resolver.js +207 -47
- package/lib/compiler/shared.js +47 -200
- package/lib/compiler/utils.js +173 -0
- package/lib/edm/annotations/genericTranslation.js +183 -187
- package/lib/edm/csn2edm.js +94 -98
- package/lib/edm/edm.js +16 -20
- package/lib/edm/edmPreprocessor.js +302 -115
- package/lib/edm/edmUtils.js +31 -12
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +28 -1
- package/lib/gen/language.tokens +79 -69
- package/lib/gen/languageLexer.interp +28 -1
- package/lib/gen/languageLexer.js +879 -805
- package/lib/gen/languageLexer.tokens +71 -62
- package/lib/gen/languageParser.js +5308 -4308
- package/lib/json/from-csn.js +59 -30
- package/lib/json/to-csn.js +354 -105
- package/lib/language/antlrParser.js +11 -0
- package/lib/language/errorStrategy.js +1 -0
- package/lib/language/genericAntlrParser.js +81 -14
- package/lib/language/language.g4 +163 -31
- package/lib/main.d.ts +136 -17
- package/lib/main.js +7 -1
- package/lib/model/api.js +78 -0
- package/lib/model/csnRefs.js +115 -32
- package/lib/model/csnUtils.js +71 -33
- package/lib/model/enrichCsn.js +36 -9
- package/lib/model/revealInternalProperties.js +20 -4
- package/lib/modelCompare/compare.js +2 -1
- package/lib/optionProcessor.js +33 -16
- package/lib/render/.eslintrc.json +3 -1
- package/lib/render/DuplicateChecker.js +1 -1
- package/lib/render/toCdl.js +60 -17
- package/lib/render/toHdbcds.js +122 -74
- package/lib/render/toSql.js +57 -32
- package/lib/render/utils/common.js +6 -10
- package/lib/sql-identifier.js +6 -1
- package/lib/transform/db/constraints.js +273 -119
- package/lib/transform/db/draft.js +9 -6
- package/lib/transform/db/expansion.js +19 -7
- package/lib/transform/db/flattening.js +31 -7
- package/lib/transform/db/transformExists.js +344 -66
- package/lib/transform/db/views.js +438 -0
- package/lib/transform/forHanaNew.js +65 -436
- package/lib/transform/forOdataNew.js +21 -10
- package/lib/transform/localized.js +2 -0
- package/lib/transform/odata/attachPath.js +19 -4
- package/lib/transform/odata/generateForeignKeyElements.js +11 -10
- package/lib/transform/odata/referenceFlattener.js +44 -38
- package/lib/transform/odata/sortByAssociationDependency.js +2 -2
- package/lib/transform/odata/structuralPath.js +72 -0
- package/lib/transform/odata/structureFlattener.js +13 -10
- package/lib/transform/odata/typesExposure.js +22 -12
- package/lib/transform/transformUtilsNew.js +55 -9
- package/lib/transform/translateAssocsToJoins.js +11 -17
- package/lib/transform/universalCsnEnricher.js +67 -0
- package/lib/utils/file.js +5 -3
- package/lib/utils/term.js +65 -42
- package/lib/utils/timetrace.js +48 -26
- package/package.json +1 -1
|
@@ -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,42 @@ 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);
|
|
52
|
+
const generatedExists = new WeakMap();
|
|
49
53
|
forEachDefinition(csn, (artifact, artifactName) => {
|
|
50
54
|
if (artifact.query) {
|
|
51
|
-
forAllQueries(artifact.query, (query) => {
|
|
52
|
-
if (!query
|
|
55
|
+
forAllQueries(artifact.query, (query, path) => {
|
|
56
|
+
if (!generatedExists.has(query)) {
|
|
57
|
+
const toProcess = []; // Collect all expressions we need to process here
|
|
53
58
|
if (query.SELECT && query.SELECT.where && query.SELECT.where.length > 1)
|
|
54
|
-
|
|
59
|
+
toProcess.push([ path.slice(0, -1), path.concat('where') ]);
|
|
55
60
|
|
|
56
61
|
|
|
57
62
|
if (query.SELECT && query.SELECT.columns)
|
|
58
|
-
|
|
63
|
+
toProcess.push([ path.slice(0, -1), path.concat('columns') ]);
|
|
59
64
|
|
|
60
65
|
|
|
61
66
|
if (query.SELECT && query.SELECT.from.on )
|
|
62
|
-
|
|
67
|
+
toProcess.push([ path.slice(0, -1), path.concat([ 'from', 'on' ]) ]);
|
|
68
|
+
|
|
69
|
+
for (const [ , exprPath ] of toProcess) {
|
|
70
|
+
forbidAssocInExists(exprPath);
|
|
71
|
+
const expr = nestExists(exprPath);
|
|
72
|
+
walkCsnPath(csn, exprPath.slice(0, -1))[exprPath[exprPath.length - 1]] = expr;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
while (toProcess.length > 0) {
|
|
76
|
+
const [ queryPath, exprPath ] = toProcess.pop();
|
|
77
|
+
// leftovers can happen with nested exists - we then need to drill down into the created SELECT
|
|
78
|
+
// to check for further exists
|
|
79
|
+
const { result, leftovers } = processExists(queryPath, exprPath);
|
|
80
|
+
walkCsnPath(csn, exprPath.slice(0, -1))[exprPath[exprPath.length - 1]] = result;
|
|
81
|
+
toProcess.push(...leftovers.reverse()); // any leftovers - schedule for further processing
|
|
82
|
+
}
|
|
63
83
|
}
|
|
64
84
|
}, [ 'definitions', artifactName, 'query' ]);
|
|
65
85
|
}
|
|
@@ -108,14 +128,215 @@ function handleExists(csn, error) {
|
|
|
108
128
|
}
|
|
109
129
|
|
|
110
130
|
/**
|
|
111
|
-
*
|
|
131
|
+
* Get the index of the first association that is found - starting the
|
|
132
|
+
* search at the given startIndex.
|
|
112
133
|
*
|
|
113
|
-
* @param {
|
|
114
|
-
* @param {
|
|
115
|
-
* @returns {
|
|
134
|
+
* @param {number} startIndex Where to start searching
|
|
135
|
+
* @param {object[]} links links for a ref, produced by inspectRef
|
|
136
|
+
* @returns {number|null} Null if no association was found
|
|
137
|
+
*/
|
|
138
|
+
function getFirstAssocIndex(startIndex, links) {
|
|
139
|
+
for (let i = startIndex; i < links.length; i++) {
|
|
140
|
+
if (links[i] && links[i].art && links[i].art.target)
|
|
141
|
+
return i;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* For a given ref-array, this function is called for the first assoc-ref in the array.
|
|
149
|
+
*
|
|
150
|
+
* It then runs over the rest of the array and puts all other steps in the first assocs filter.
|
|
151
|
+
* If the rest contains another assoc, we put all following things into that assocs filter and
|
|
152
|
+
* add the sub-assoc to the previous assoc filter.
|
|
153
|
+
*
|
|
154
|
+
* Or in other words:
|
|
155
|
+
* - exists toF[1=1].toG[1=1].toH[1=1] is found
|
|
156
|
+
* - we get called with toF[1=1].toG[1=1].toH[1=1]
|
|
157
|
+
* - we return toF[1=1 and exists toG[1=1 and exists toH[1=1]]]
|
|
158
|
+
*
|
|
159
|
+
* @param {number} startIndex The index of the thing AFTER _main in the ref-array
|
|
160
|
+
* @param {string|object} startAssoc The path step that is the first assoc
|
|
161
|
+
* @param {Array} startRest Any path steps after startAssoc
|
|
162
|
+
* @param {CSN.Path} path to the overall ref where _main is contained
|
|
163
|
+
* @returns {Array} Return the now-nested ref-array
|
|
164
|
+
*/
|
|
165
|
+
function nestFilters(startIndex, startAssoc, startRest, path) {
|
|
166
|
+
let revert;
|
|
167
|
+
if (!startAssoc.where) { // initialize first filter if not present
|
|
168
|
+
if (typeof startAssoc === 'string') {
|
|
169
|
+
startAssoc = {
|
|
170
|
+
id: startAssoc,
|
|
171
|
+
where: [],
|
|
172
|
+
};
|
|
173
|
+
revert = () => {
|
|
174
|
+
startAssoc = startAssoc.id;
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
startAssoc.where = [];
|
|
179
|
+
revert = () => {
|
|
180
|
+
delete startAssoc.where;
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const stack = [ [ null, startAssoc, startRest, startIndex ] ];
|
|
185
|
+
const { links } = inspectRef(path);
|
|
186
|
+
while (stack.length > 0) {
|
|
187
|
+
// previous: to nest "up" if the previous assoc did not originaly have a filter
|
|
188
|
+
// assoc: the assoc path step
|
|
189
|
+
// rest: path steps after assoc
|
|
190
|
+
// index: index of after-assoc in the overall ref-array - so we know where to start looking for the next assoc
|
|
191
|
+
const workPackage = stack.pop();
|
|
192
|
+
const [ previous, , rest, index ] = workPackage;
|
|
193
|
+
let [ , assoc, , ] = workPackage;
|
|
194
|
+
|
|
195
|
+
const firstAssocIndex = getFirstAssocIndex(index, links);
|
|
196
|
+
|
|
197
|
+
const head = rest.slice(0, firstAssocIndex - index);
|
|
198
|
+
const nextAssoc = rest[firstAssocIndex - index];
|
|
199
|
+
const tail = rest.slice(firstAssocIndex - index + 1);
|
|
200
|
+
|
|
201
|
+
const hasAssoc = nextAssoc !== undefined;
|
|
202
|
+
|
|
203
|
+
if (!assoc.where && hasAssoc) { // no existing filter - and there is stuff we need to nest afterwards
|
|
204
|
+
if (typeof assoc === 'string') {
|
|
205
|
+
assoc = {
|
|
206
|
+
id: assoc,
|
|
207
|
+
where: [],
|
|
208
|
+
};
|
|
209
|
+
// We need to "hook" this into the previous filter.
|
|
210
|
+
// Since we create a new object, we don't have a handy reference we can just manipulate
|
|
211
|
+
if (previous)
|
|
212
|
+
previous.where[previous.where.length - 1] = { ref: [ assoc ] };
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
assoc.where = [];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if (assoc.where && assoc.where.length > 0 && (hasAssoc || rest.length > 0)) {
|
|
219
|
+
assoc.where.push('and');
|
|
220
|
+
} // merge with existing filter
|
|
221
|
+
|
|
222
|
+
if (hasAssoc)
|
|
223
|
+
assoc.where.push('exists', { ref: [ ...head, nextAssoc ] });
|
|
224
|
+
else if (rest.length > 0)
|
|
225
|
+
assoc.where.push({ ref: rest });
|
|
226
|
+
|
|
227
|
+
if (hasAssoc)
|
|
228
|
+
stack.push([ assoc, nextAssoc, tail, firstAssocIndex ]);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Seems like we did not have anything to nest into the filter - then kill it
|
|
232
|
+
if (startAssoc.where.length === 0 && revert !== undefined)
|
|
233
|
+
revert();
|
|
234
|
+
|
|
235
|
+
return startAssoc;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Check that associations in filters (in an exists expression) are only fk-accesses. Everything else is forbidden.
|
|
240
|
+
*
|
|
241
|
+
* @param {CSN.Path} exprPath
|
|
242
|
+
* @returns {void}
|
|
243
|
+
*/
|
|
244
|
+
function forbidAssocInExists(exprPath) {
|
|
245
|
+
const expr = walkCsnPath(csn, exprPath);
|
|
246
|
+
for (let i = 0; i < expr.length; i++) {
|
|
247
|
+
if (i < expr.length - 1 && expr[i] === 'exists' && expr[i + 1].ref) {
|
|
248
|
+
i++;
|
|
249
|
+
const current = expr[i];
|
|
250
|
+
|
|
251
|
+
const { links } = inspectRef(exprPath.concat(i));
|
|
252
|
+
|
|
253
|
+
const assocs = links.filter(link => link.art && link.art.target).map(link => current.ref[link.idx]);
|
|
254
|
+
|
|
255
|
+
checkForInvalidAssoc(assocs);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* @param {object[]} assocs Array of refs of assocs - possibly with a .where to check
|
|
262
|
+
*/
|
|
263
|
+
function checkForInvalidAssoc(assocs) {
|
|
264
|
+
for (const assoc of assocs) {
|
|
265
|
+
if (assoc.where) {
|
|
266
|
+
for (let i = 0; i < assoc.where.length; i++) {
|
|
267
|
+
const part = assoc.where[i];
|
|
268
|
+
|
|
269
|
+
if (part._links && !(assoc.where[i - 1] && assoc.where[i - 1] === 'exists')) {
|
|
270
|
+
for (const link of part._links) {
|
|
271
|
+
if (link.art && link.art.target) {
|
|
272
|
+
if (link.art.keys) { // managed - allow FK access
|
|
273
|
+
if (part._links[link.idx + 1] !== undefined) { // there is a next path step - check if it is a fk
|
|
274
|
+
if (!(part._links[link.idx + 1] && link.art.keys.some(fk => fk._art === part._links[link.idx + 1].art)))
|
|
275
|
+
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)');
|
|
276
|
+
}
|
|
277
|
+
else { // no traversal, ends on managed
|
|
278
|
+
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)');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else { // unmanaged - always wrong
|
|
282
|
+
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)');
|
|
283
|
+
}
|
|
284
|
+
// Recursively drill down if the assoc-step has a filter
|
|
285
|
+
if (part.ref[link.idx].where)
|
|
286
|
+
checkForInvalidAssoc([ part.ref[link.idx] ]);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Walk to the expr using the given path and scan it for the "exists" + "ref" pattern.
|
|
297
|
+
* If such a pattern is found, nest association steps therein into filters.
|
|
298
|
+
*
|
|
299
|
+
* @param {CSN.Path} exprPath
|
|
300
|
+
* @returns {Array}
|
|
116
301
|
*/
|
|
117
|
-
function
|
|
302
|
+
function nestExists(exprPath) {
|
|
303
|
+
const expr = walkCsnPath(csn, exprPath);
|
|
304
|
+
for (let i = 0; i < expr.length; i++) {
|
|
305
|
+
if (i < expr.length - 1 && expr[i] === 'exists' && expr[i + 1].ref) {
|
|
306
|
+
i++;
|
|
307
|
+
const current = expr[i];
|
|
308
|
+
const {
|
|
309
|
+
ref, head, tail,
|
|
310
|
+
} = getFirstAssoc(current, exprPath.concat(i));
|
|
311
|
+
|
|
312
|
+
const lastAssoc = getLastAssoc(current, exprPath.concat(i));
|
|
313
|
+
// toE.toF.id -> we must not end on a non-assoc - this will also be caught downstream by
|
|
314
|
+
// '“EXISTS” can only be used with associations/compositions, found $(TYPE)'
|
|
315
|
+
// But the error might not be clear, since it could be because of our rewritten stuff. The later check
|
|
316
|
+
// checks for exists id -> our rewrite turns toE.toF.id into toE[exists toF[exists id]], leading to the same error
|
|
317
|
+
if (lastAssoc.tail.length > 0)
|
|
318
|
+
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"');
|
|
319
|
+
|
|
320
|
+
const newThing = [ ...head, nestFilters(head.length + 1, ref, tail, exprPath.concat([ i ])) ];
|
|
321
|
+
expr[i].ref = newThing;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return expr;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* 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.
|
|
330
|
+
*
|
|
331
|
+
* @param {CSN.Path} queryPath Path to the query-object
|
|
332
|
+
* @param {CSN.Path} exprPath Path to the expression-array to process
|
|
333
|
+
* @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.
|
|
334
|
+
*/
|
|
335
|
+
function processExists(queryPath, exprPath) {
|
|
336
|
+
const toContinue = [];
|
|
118
337
|
const newExpr = [];
|
|
338
|
+
const query = walkCsnPath(csn, queryPath);
|
|
339
|
+
const expr = walkCsnPath(csn, exprPath);
|
|
119
340
|
const queryBase = query.SELECT.from.ref ? (query.SELECT.from.as || query.SELECT.from.ref[0]) : null;
|
|
120
341
|
const sources = getQuerySources(query.SELECT);
|
|
121
342
|
|
|
@@ -123,18 +344,13 @@ function handleExists(csn, error) {
|
|
|
123
344
|
if (i < expr.length - 1 && expr[i] === 'exists' && expr[i + 1].ref) {
|
|
124
345
|
i++;
|
|
125
346
|
const current = expr[i];
|
|
126
|
-
const isPrefixedWithTableAlias = firstLinkIsEntityOrQuerySource(
|
|
127
|
-
const base = getBase(queryBase, isPrefixedWithTableAlias, current);
|
|
128
|
-
const { root, ref
|
|
129
|
-
|
|
130
|
-
if (tail.length > 0) {
|
|
131
|
-
error(null, current.$path, { id: tail[0].id ? tail[0].id : tail[0], name: ref.id ? ref.id : ref }, 'Unexpected path step $(ID) after association $(NAME) in "EXISTS"');
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
347
|
+
const isPrefixedWithTableAlias = firstLinkIsEntityOrQuerySource(exprPath.concat(i));
|
|
348
|
+
const base = getBase(queryBase, isPrefixedWithTableAlias, current, exprPath.concat(i));
|
|
349
|
+
const { root, ref } = getFirstAssoc(current, exprPath.concat(i));
|
|
134
350
|
|
|
135
351
|
if (!root.target) {
|
|
136
|
-
error(null,
|
|
137
|
-
return [];
|
|
352
|
+
error(null, exprPath.concat(i), { type: root.type }, '“EXISTS” can only be used with associations/compositions, found $(TYPE)');
|
|
353
|
+
return { result: [], leftovers: [] };
|
|
138
354
|
}
|
|
139
355
|
|
|
140
356
|
const subselect = getSubselect(root.target, ref, sources);
|
|
@@ -152,18 +368,24 @@ function handleExists(csn, error) {
|
|
|
152
368
|
subselect.SELECT.where.push(...[ 'and', ...remapExistingWhere(target, ref.where) ]);
|
|
153
369
|
|
|
154
370
|
newExpr.push(subselect);
|
|
371
|
+
toContinue.push([ exprPath.concat(newExpr.length - 1), exprPath.concat([ newExpr.length - 1, 'SELECT', 'where' ]) ]);
|
|
155
372
|
}
|
|
156
373
|
else { // Drill down into other places that might contain a `EXISTS <assoc>`
|
|
157
|
-
if (expr[i].xpr)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
374
|
+
if (expr[i].xpr) {
|
|
375
|
+
const { result, leftovers } = processExists(queryPath, exprPath.concat([ i, 'xpr' ]));
|
|
376
|
+
expr[i].xpr = result;
|
|
377
|
+
toContinue.push(...leftovers);
|
|
378
|
+
}
|
|
379
|
+
if (expr[i].args && Array.isArray(expr[i].args)) {
|
|
380
|
+
const { result, leftovers } = processExists(queryPath, exprPath.concat([ i, 'args' ]));
|
|
381
|
+
expr[i].args = result;
|
|
382
|
+
toContinue.push(...leftovers);
|
|
383
|
+
}
|
|
162
384
|
newExpr.push(expr[i]);
|
|
163
385
|
}
|
|
164
386
|
}
|
|
165
387
|
|
|
166
|
-
return newExpr;
|
|
388
|
+
return { result: newExpr, leftovers: toContinue };
|
|
167
389
|
}
|
|
168
390
|
|
|
169
391
|
/**
|
|
@@ -229,23 +451,35 @@ function handleExists(csn, error) {
|
|
|
229
451
|
function translateUnmanagedAssocToWhere(root, target, subselect, isPrefixedWithTableAlias, base, current) {
|
|
230
452
|
for (let j = 0; j < root.on.length; j++) {
|
|
231
453
|
const part = root.on[j];
|
|
454
|
+
|
|
455
|
+
// we can only resolve stuff on refs - skip literals like =
|
|
456
|
+
// but also keep along stuff like null and undefined, so compiler
|
|
457
|
+
// can have a chance to complain/ we can fail later nicely maybe
|
|
458
|
+
if (!(part && part.ref)) {
|
|
459
|
+
subselect.SELECT.where.push(part);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// root.$path should be safe - we can only reference things in exists that exist when we enrich
|
|
464
|
+
// so all of them should have a $path.
|
|
465
|
+
const { art, links } = inspectRef(root.$path.concat([ 'on', j ]));
|
|
232
466
|
// Dollar Self Backlink
|
|
233
|
-
if (isValidDollarSelf(root.on[j], root.on[j + 1], root.on[j + 2])) {
|
|
467
|
+
if (isValidDollarSelf(root.on[j], root.$path.concat([ 'on', j ]), root.on[j + 1], root.on[j + 2], root.$path.concat([ 'on', j + 2 ]))) {
|
|
234
468
|
if (root.on[j].ref[0] === '$self' && root.on[j].ref.length === 1)
|
|
235
|
-
subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j + 2]));
|
|
469
|
+
subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j + 2], root.$path.concat([ 'on', j + 2 ])));
|
|
236
470
|
else
|
|
237
|
-
subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j]));
|
|
471
|
+
subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j], root.$path.concat([ 'on', j ])));
|
|
238
472
|
|
|
239
473
|
j += 2;
|
|
240
474
|
}
|
|
241
|
-
else if (
|
|
475
|
+
else if (links && links[0].art === root) { // target side
|
|
242
476
|
subselect.SELECT.where.push({ ref: [ target, ...part.ref.slice(1) ] });
|
|
243
477
|
}
|
|
244
478
|
else if (part.$scope === '$self') { // source side - "absolute" scope
|
|
245
479
|
// cut off the $self, as we prefix the entity name now
|
|
246
480
|
subselect.SELECT.where.push({ ref: [ base, ...part.ref.slice(1) ] });
|
|
247
481
|
}
|
|
248
|
-
else if (
|
|
482
|
+
else if (art) { // source side - with local scope
|
|
249
483
|
if (isPrefixedWithTableAlias)
|
|
250
484
|
subselect.SELECT.where.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
|
|
251
485
|
else
|
|
@@ -260,43 +494,82 @@ function handleExists(csn, error) {
|
|
|
260
494
|
* Check that an expression triple is a valid $self
|
|
261
495
|
*
|
|
262
496
|
* @param {Token} leftSide
|
|
497
|
+
* @param {CSN.Path} pathLeft
|
|
263
498
|
* @param {Token} middle
|
|
264
499
|
* @param {Token} rightSide
|
|
500
|
+
* @param {CSN.Path} pathRight
|
|
265
501
|
* @returns {boolean}
|
|
266
502
|
*/
|
|
267
|
-
function isValidDollarSelf(leftSide, middle, rightSide) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
503
|
+
function isValidDollarSelf(leftSide, pathLeft, middle, rightSide, pathRight) {
|
|
504
|
+
if (leftSide && leftSide.ref && rightSide && rightSide.ref && middle === '=') {
|
|
505
|
+
const right = inspectRef(pathRight);
|
|
506
|
+
const left = inspectRef(pathLeft);
|
|
507
|
+
|
|
508
|
+
if (!right || !left)
|
|
509
|
+
return false;
|
|
510
|
+
|
|
511
|
+
const rightSideArt = right.art;
|
|
512
|
+
const leftSideArt = left.art;
|
|
513
|
+
|
|
514
|
+
return leftSide.ref[0] === '$self' && leftSide.ref.length === 1 && rightSideArt && rightSideArt.target ||
|
|
515
|
+
rightSide.ref[0] === '$self' && rightSide.ref.length === 1 && leftSideArt && leftSideArt.target;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return false;
|
|
275
519
|
}
|
|
276
520
|
}
|
|
277
521
|
|
|
278
522
|
/**
|
|
279
|
-
* From the given expression (having
|
|
523
|
+
* From the given expression (having inspectRef -> links), find the first association.
|
|
280
524
|
*
|
|
281
525
|
* @param {object} xprPart
|
|
282
|
-
* @
|
|
526
|
+
* @param {CSN.Path} path
|
|
527
|
+
* @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).
|
|
283
528
|
*/
|
|
284
|
-
function getFirstAssoc(xprPart) {
|
|
529
|
+
function getFirstAssoc(xprPart, path) {
|
|
530
|
+
const { links, art } = inspectRef(path);
|
|
285
531
|
for (let i = 0; i < xprPart.ref.length - 1; i++) {
|
|
286
|
-
if (
|
|
287
|
-
return {
|
|
532
|
+
if (links[i].art && links[i].art.target) {
|
|
533
|
+
return {
|
|
534
|
+
head: (i === 0 ? [] : xprPart.ref.slice(0, i)), root: links[i].art, ref: xprPart.ref[i], tail: xprPart.ref.slice(i + 1),
|
|
535
|
+
};
|
|
536
|
+
}
|
|
288
537
|
}
|
|
289
|
-
return {
|
|
538
|
+
return {
|
|
539
|
+
head: (xprPart.ref.length === 1 ? [] : xprPart.ref.slice(0, xprPart.ref.length - 1)), root: art, ref: xprPart.ref[xprPart.ref.length - 1], tail: [],
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Get the last association from the expression part - similar to getFirstAssoc
|
|
545
|
+
*
|
|
546
|
+
* @param {object} xprPart
|
|
547
|
+
* @param {CSN.Path} path
|
|
548
|
+
* @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).
|
|
549
|
+
*/
|
|
550
|
+
function getLastAssoc(xprPart, path) {
|
|
551
|
+
const { links, art } = inspectRef(path);
|
|
552
|
+
for (let i = xprPart.ref.length - 1; i > -1; i--) {
|
|
553
|
+
if (links[i].art && links[i].art.target) {
|
|
554
|
+
return {
|
|
555
|
+
head: (i === 0 ? [] : xprPart.ref.slice(0, i)), root: links[i].art, ref: xprPart.ref[i], tail: xprPart.ref.slice(i + 1),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
head: (xprPart.ref.length === 1 ? [] : xprPart.ref.slice(0, xprPart.ref.length - 1)), root: art, ref: xprPart.ref[xprPart.ref.length - 1], tail: [],
|
|
561
|
+
};
|
|
290
562
|
}
|
|
291
563
|
|
|
292
564
|
/**
|
|
293
|
-
* Check (using
|
|
565
|
+
* Check (using inspectRef -> links), wether the first path step is an entity or query source
|
|
294
566
|
*
|
|
295
|
-
* @param {
|
|
567
|
+
* @param {CSN.Path} path
|
|
296
568
|
* @returns {boolean}
|
|
297
569
|
*/
|
|
298
|
-
function firstLinkIsEntityOrQuerySource(
|
|
299
|
-
|
|
570
|
+
function firstLinkIsEntityOrQuerySource(path) {
|
|
571
|
+
const { links } = inspectRef(path);
|
|
572
|
+
return links && (links[0].art.kind === 'entity' || links[0].art.query || links[0].art.from);
|
|
300
573
|
}
|
|
301
574
|
|
|
302
575
|
/**
|
|
@@ -316,13 +589,14 @@ function handleExists(csn, error) {
|
|
|
316
589
|
* we can be sure that resolving the ref requires $env information.
|
|
317
590
|
*
|
|
318
591
|
* @param {object} xpr
|
|
592
|
+
* @param {CSN.Path} path
|
|
319
593
|
* @returns {string|undefined} undefined in case of errors
|
|
320
594
|
* @throws {Error} Throws if xpr.ref but no xpr.$env
|
|
321
595
|
* @todo $env is going to be removed from CSN, but csnRefs will provide it
|
|
322
596
|
*/
|
|
323
597
|
// eslint-disable-next-line consistent-return
|
|
324
|
-
function getParent(xpr) {
|
|
325
|
-
if (firstLinkIsEntityOrQuerySource(
|
|
598
|
+
function getParent(xpr, path) {
|
|
599
|
+
if (firstLinkIsEntityOrQuerySource(path)) {
|
|
326
600
|
return xpr.ref[0];
|
|
327
601
|
}
|
|
328
602
|
else if (xpr.$env) {
|
|
@@ -366,7 +640,7 @@ function handleExists(csn, error) {
|
|
|
366
640
|
};
|
|
367
641
|
// Because the generated things don't have _links, _art etc. set
|
|
368
642
|
// We could also make getParent more robust to calculate the links JIT if they are missing
|
|
369
|
-
|
|
643
|
+
generatedExists.set(subselect, true);
|
|
370
644
|
|
|
371
645
|
const nonEnumElements = Object.create(null);
|
|
372
646
|
nonEnumElements.dummy = {
|
|
@@ -384,14 +658,15 @@ function handleExists(csn, error) {
|
|
|
384
658
|
* @param {string|null} queryBase
|
|
385
659
|
* @param {boolean} isPrefixedWithTableAlias
|
|
386
660
|
* @param {CSN.Column} current
|
|
661
|
+
* @param {CSN.Path} path
|
|
387
662
|
* @returns {string}
|
|
388
663
|
*/
|
|
389
|
-
function getBase(queryBase, isPrefixedWithTableAlias, current) {
|
|
664
|
+
function getBase(queryBase, isPrefixedWithTableAlias, current, path) {
|
|
390
665
|
if (queryBase)
|
|
391
666
|
return getRealName(csn, queryBase);
|
|
392
667
|
else if (isPrefixedWithTableAlias)
|
|
393
668
|
return current.ref[0];
|
|
394
|
-
return getParent(current);
|
|
669
|
+
return getParent(current, path);
|
|
395
670
|
}
|
|
396
671
|
|
|
397
672
|
|
|
@@ -423,31 +698,34 @@ function handleExists(csn, error) {
|
|
|
423
698
|
* @param {string} base The source entity/query source name
|
|
424
699
|
* @param {string} target The target entity/query source name
|
|
425
700
|
* @param {CSN.Element} assoc The association element - the "not-$self" side of the comparison
|
|
701
|
+
* @param {CSN.Path} path
|
|
426
702
|
* @returns {TokenStream} The WHERE representing the $self comparison
|
|
427
703
|
*/
|
|
428
|
-
function translateDollarSelfToWhere(base, target, assoc) {
|
|
704
|
+
function translateDollarSelfToWhere(base, target, assoc, path) {
|
|
429
705
|
const where = [];
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
706
|
+
const { art } = inspectRef(path);
|
|
707
|
+
if (art.keys) {
|
|
708
|
+
for (let i = 0; i < art.keys.length; i++) {
|
|
709
|
+
const lop = { ref: [ target, ...assoc.ref.slice(1), ...art.keys[i].ref ] }; // target side
|
|
710
|
+
const rop = { ref: [ base, ...art.keys[i].ref ] }; // source side
|
|
434
711
|
if (i > 0)
|
|
435
712
|
where.push('and');
|
|
436
713
|
|
|
437
714
|
where.push(...[ lop, '=', rop ]);
|
|
438
715
|
}
|
|
439
716
|
}
|
|
440
|
-
else if (
|
|
441
|
-
for (let i = 0; i <
|
|
442
|
-
const part =
|
|
443
|
-
|
|
717
|
+
else if (art.on) {
|
|
718
|
+
for (let i = 0; i < art.on.length; i++) {
|
|
719
|
+
const part = art.on[i];
|
|
720
|
+
const partInspect = inspectRef(art.$path.concat([ 'on', i ]));
|
|
721
|
+
if (partInspect.links && partInspect.links[0].art === art) { // target side
|
|
444
722
|
where.push({ ref: [ base, ...part.ref.slice(1) ] });
|
|
445
723
|
}
|
|
446
724
|
else if (part.$scope === '$self') { // source side - "absolute" scope
|
|
447
725
|
// Same message as in forHanaNew/transformDollarSelfComparisonWithUnmanagedAssoc
|
|
448
726
|
error(null, part.$path, 'An association that uses "$self" in its ON-condition can\'t be compared to "$self"');
|
|
449
727
|
}
|
|
450
|
-
else if (
|
|
728
|
+
else if (partInspect.art) { // source side - with local scope
|
|
451
729
|
where.push({ ref: [ target, ...assoc.ref.slice(1, -1), ...part.ref ] });
|
|
452
730
|
}
|
|
453
731
|
else { // operator - or any other leftover
|