@sap/cds-compiler 2.5.2 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +235 -9
- package/bin/cdsc.js +44 -27
- package/bin/cdsse.js +1 -0
- package/doc/CHANGELOG_BETA.md +37 -3
- package/lib/api/.eslintrc.json +2 -0
- package/lib/api/main.js +37 -123
- package/lib/api/options.js +27 -15
- package/lib/api/validate.js +34 -9
- package/lib/backends.js +9 -89
- package/lib/base/dictionaries.js +2 -1
- package/lib/base/keywords.js +32 -2
- package/lib/base/message-registry.js +73 -11
- package/lib/base/messages.js +86 -30
- package/lib/base/model.js +6 -6
- package/lib/base/optionProcessorHelper.js +56 -22
- 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 +29 -2
- package/lib/checks/types.js +26 -2
- package/lib/checks/unknownMagic.js +41 -0
- package/lib/checks/utils.js +61 -0
- package/lib/checks/validator.js +60 -7
- package/lib/compiler/assert-consistency.js +23 -7
- package/lib/compiler/base.js +65 -0
- package/lib/compiler/builtins.js +30 -1
- package/lib/compiler/checks.js +8 -5
- package/lib/compiler/definer.js +157 -133
- package/lib/compiler/index.js +89 -31
- package/lib/compiler/propagator.js +5 -2
- package/lib/compiler/resolver.js +375 -185
- package/lib/compiler/shared.js +49 -202
- package/lib/compiler/utils.js +173 -0
- package/lib/edm/annotations/genericTranslation.js +183 -187
- package/lib/edm/csn2edm.js +104 -108
- package/lib/edm/edm.js +18 -21
- package/lib/edm/edmPreprocessor.js +388 -146
- package/lib/edm/edmUtils.js +104 -34
- package/lib/gen/Dictionary.json +22 -0
- 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 +5330 -4300
- package/lib/json/from-csn.js +110 -52
- package/lib/json/to-csn.js +434 -120
- package/lib/language/antlrParser.js +15 -3
- package/lib/language/errorStrategy.js +1 -0
- package/lib/language/genericAntlrParser.js +93 -26
- package/lib/language/language.g4 +172 -31
- package/lib/main.d.ts +216 -19
- package/lib/main.js +32 -7
- package/lib/model/api.js +78 -0
- package/lib/model/csnRefs.js +413 -149
- package/lib/model/csnUtils.js +286 -75
- package/lib/model/enrichCsn.js +50 -6
- package/lib/model/revealInternalProperties.js +22 -5
- package/lib/modelCompare/compare.js +39 -21
- package/lib/optionProcessor.js +35 -18
- package/lib/render/.eslintrc.json +4 -1
- package/lib/render/DuplicateChecker.js +9 -6
- package/lib/render/toCdl.js +121 -36
- package/lib/render/toHdbcds.js +148 -98
- package/lib/render/toSql.js +114 -43
- 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 +281 -106
- package/lib/transform/db/draft.js +11 -8
- package/lib/transform/db/expansion.js +584 -0
- package/lib/transform/db/flattening.js +341 -0
- package/lib/transform/db/groupByOrderBy.js +2 -2
- package/lib/transform/db/transformExists.js +345 -65
- package/lib/transform/db/views.js +438 -0
- package/lib/transform/forHanaNew.js +131 -793
- package/lib/transform/forOdataNew.js +30 -24
- package/lib/transform/localized.js +39 -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 +144 -78
- package/lib/transform/translateAssocsToJoins.js +22 -27
- package/lib/transform/universalCsnEnricher.js +67 -0
- package/lib/utils/file.js +5 -14
- package/lib/utils/moduleResolve.js +6 -8
- package/lib/utils/term.js +65 -42
- package/lib/utils/timetrace.js +48 -26
- 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,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}
|
|
116
243
|
*/
|
|
117
|
-
function
|
|
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}
|
|
301
|
+
*/
|
|
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,15 @@ 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
|
|
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));
|
|
129
350
|
|
|
130
|
-
if (
|
|
131
|
-
error(null,
|
|
132
|
-
|
|
351
|
+
if (!root.target) {
|
|
352
|
+
error(null, exprPath.concat(i), { type: root.type }, '“EXISTS” can only be used with associations/compositions, found $(TYPE)');
|
|
353
|
+
return { result: [], leftovers: [] };
|
|
133
354
|
}
|
|
134
355
|
|
|
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
356
|
const subselect = getSubselect(root.target, ref, sources);
|
|
139
357
|
|
|
140
358
|
const target = subselect.SELECT.from.as; // use subquery alias as target - prevent shadowing
|
|
@@ -150,18 +368,24 @@ function handleExists(csn, error) {
|
|
|
150
368
|
subselect.SELECT.where.push(...[ 'and', ...remapExistingWhere(target, ref.where) ]);
|
|
151
369
|
|
|
152
370
|
newExpr.push(subselect);
|
|
371
|
+
toContinue.push([ exprPath.concat(newExpr.length - 1), exprPath.concat([ newExpr.length - 1, 'SELECT', 'where' ]) ]);
|
|
153
372
|
}
|
|
154
373
|
else { // Drill down into other places that might contain a `EXISTS <assoc>`
|
|
155
|
-
if (expr[i].xpr)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
+
}
|
|
160
384
|
newExpr.push(expr[i]);
|
|
161
385
|
}
|
|
162
386
|
}
|
|
163
387
|
|
|
164
|
-
return newExpr;
|
|
388
|
+
return { result: newExpr, leftovers: toContinue };
|
|
165
389
|
}
|
|
166
390
|
|
|
167
391
|
/**
|
|
@@ -227,23 +451,35 @@ function handleExists(csn, error) {
|
|
|
227
451
|
function translateUnmanagedAssocToWhere(root, target, subselect, isPrefixedWithTableAlias, base, current) {
|
|
228
452
|
for (let j = 0; j < root.on.length; j++) {
|
|
229
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 ]));
|
|
230
466
|
// Dollar Self Backlink
|
|
231
|
-
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 ]))) {
|
|
232
468
|
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]));
|
|
469
|
+
subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j + 2], root.$path.concat([ 'on', j + 2 ])));
|
|
234
470
|
else
|
|
235
|
-
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 ])));
|
|
236
472
|
|
|
237
473
|
j += 2;
|
|
238
474
|
}
|
|
239
|
-
else if (
|
|
475
|
+
else if (links && links[0].art === root) { // target side
|
|
240
476
|
subselect.SELECT.where.push({ ref: [ target, ...part.ref.slice(1) ] });
|
|
241
477
|
}
|
|
242
478
|
else if (part.$scope === '$self') { // source side - "absolute" scope
|
|
243
479
|
// cut off the $self, as we prefix the entity name now
|
|
244
480
|
subselect.SELECT.where.push({ ref: [ base, ...part.ref.slice(1) ] });
|
|
245
481
|
}
|
|
246
|
-
else if (
|
|
482
|
+
else if (art) { // source side - with local scope
|
|
247
483
|
if (isPrefixedWithTableAlias)
|
|
248
484
|
subselect.SELECT.where.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
|
|
249
485
|
else
|
|
@@ -258,43 +494,82 @@ function handleExists(csn, error) {
|
|
|
258
494
|
* Check that an expression triple is a valid $self
|
|
259
495
|
*
|
|
260
496
|
* @param {Token} leftSide
|
|
497
|
+
* @param {CSN.Path} pathLeft
|
|
261
498
|
* @param {Token} middle
|
|
262
499
|
* @param {Token} rightSide
|
|
500
|
+
* @param {CSN.Path} pathRight
|
|
263
501
|
* @returns {boolean}
|
|
264
502
|
*/
|
|
265
|
-
function isValidDollarSelf(leftSide, middle, rightSide) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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;
|
|
273
519
|
}
|
|
274
520
|
}
|
|
275
521
|
|
|
276
522
|
/**
|
|
277
|
-
* From the given expression (having
|
|
523
|
+
* From the given expression (having inspectRef -> links), find the first association.
|
|
278
524
|
*
|
|
279
525
|
* @param {object} xprPart
|
|
280
|
-
* @
|
|
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).
|
|
281
528
|
*/
|
|
282
|
-
function getFirstAssoc(xprPart) {
|
|
529
|
+
function getFirstAssoc(xprPart, path) {
|
|
530
|
+
const { links, art } = inspectRef(path);
|
|
283
531
|
for (let i = 0; i < xprPart.ref.length - 1; i++) {
|
|
284
|
-
if (
|
|
285
|
-
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
|
+
}
|
|
286
537
|
}
|
|
287
|
-
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
|
+
};
|
|
288
562
|
}
|
|
289
563
|
|
|
290
564
|
/**
|
|
291
|
-
* Check (using
|
|
565
|
+
* Check (using inspectRef -> links), wether the first path step is an entity or query source
|
|
292
566
|
*
|
|
293
|
-
* @param {
|
|
567
|
+
* @param {CSN.Path} path
|
|
294
568
|
* @returns {boolean}
|
|
295
569
|
*/
|
|
296
|
-
function firstLinkIsEntityOrQuerySource(
|
|
297
|
-
|
|
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);
|
|
298
573
|
}
|
|
299
574
|
|
|
300
575
|
/**
|
|
@@ -314,13 +589,14 @@ function handleExists(csn, error) {
|
|
|
314
589
|
* we can be sure that resolving the ref requires $env information.
|
|
315
590
|
*
|
|
316
591
|
* @param {object} xpr
|
|
592
|
+
* @param {CSN.Path} path
|
|
317
593
|
* @returns {string|undefined} undefined in case of errors
|
|
318
594
|
* @throws {Error} Throws if xpr.ref but no xpr.$env
|
|
319
595
|
* @todo $env is going to be removed from CSN, but csnRefs will provide it
|
|
320
596
|
*/
|
|
321
597
|
// eslint-disable-next-line consistent-return
|
|
322
|
-
function getParent(xpr) {
|
|
323
|
-
if (firstLinkIsEntityOrQuerySource(
|
|
598
|
+
function getParent(xpr, path) {
|
|
599
|
+
if (firstLinkIsEntityOrQuerySource(path)) {
|
|
324
600
|
return xpr.ref[0];
|
|
325
601
|
}
|
|
326
602
|
else if (xpr.$env) {
|
|
@@ -364,7 +640,7 @@ function handleExists(csn, error) {
|
|
|
364
640
|
};
|
|
365
641
|
// Because the generated things don't have _links, _art etc. set
|
|
366
642
|
// We could also make getParent more robust to calculate the links JIT if they are missing
|
|
367
|
-
|
|
643
|
+
generatedExists.set(subselect, true);
|
|
368
644
|
|
|
369
645
|
const nonEnumElements = Object.create(null);
|
|
370
646
|
nonEnumElements.dummy = {
|
|
@@ -382,14 +658,15 @@ function handleExists(csn, error) {
|
|
|
382
658
|
* @param {string|null} queryBase
|
|
383
659
|
* @param {boolean} isPrefixedWithTableAlias
|
|
384
660
|
* @param {CSN.Column} current
|
|
661
|
+
* @param {CSN.Path} path
|
|
385
662
|
* @returns {string}
|
|
386
663
|
*/
|
|
387
|
-
function getBase(queryBase, isPrefixedWithTableAlias, current) {
|
|
664
|
+
function getBase(queryBase, isPrefixedWithTableAlias, current, path) {
|
|
388
665
|
if (queryBase)
|
|
389
666
|
return getRealName(csn, queryBase);
|
|
390
667
|
else if (isPrefixedWithTableAlias)
|
|
391
668
|
return current.ref[0];
|
|
392
|
-
return getParent(current);
|
|
669
|
+
return getParent(current, path);
|
|
393
670
|
}
|
|
394
671
|
|
|
395
672
|
|
|
@@ -421,31 +698,34 @@ function handleExists(csn, error) {
|
|
|
421
698
|
* @param {string} base The source entity/query source name
|
|
422
699
|
* @param {string} target The target entity/query source name
|
|
423
700
|
* @param {CSN.Element} assoc The association element - the "not-$self" side of the comparison
|
|
701
|
+
* @param {CSN.Path} path
|
|
424
702
|
* @returns {TokenStream} The WHERE representing the $self comparison
|
|
425
703
|
*/
|
|
426
|
-
function translateDollarSelfToWhere(base, target, assoc) {
|
|
704
|
+
function translateDollarSelfToWhere(base, target, assoc, path) {
|
|
427
705
|
const where = [];
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
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
|
|
432
711
|
if (i > 0)
|
|
433
712
|
where.push('and');
|
|
434
713
|
|
|
435
714
|
where.push(...[ lop, '=', rop ]);
|
|
436
715
|
}
|
|
437
716
|
}
|
|
438
|
-
else if (
|
|
439
|
-
for (let i = 0; i <
|
|
440
|
-
const part =
|
|
441
|
-
|
|
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
|
|
442
722
|
where.push({ ref: [ base, ...part.ref.slice(1) ] });
|
|
443
723
|
}
|
|
444
724
|
else if (part.$scope === '$self') { // source side - "absolute" scope
|
|
445
725
|
// Same message as in forHanaNew/transformDollarSelfComparisonWithUnmanagedAssoc
|
|
446
726
|
error(null, part.$path, 'An association that uses "$self" in its ON-condition can\'t be compared to "$self"');
|
|
447
727
|
}
|
|
448
|
-
else if (
|
|
728
|
+
else if (partInspect.art) { // source side - with local scope
|
|
449
729
|
where.push({ ref: [ target, ...assoc.ref.slice(1, -1), ...part.ref ] });
|
|
450
730
|
}
|
|
451
731
|
else { // operator - or any other leftover
|