@sap/cds-compiler 3.6.0 → 3.7.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 +58 -0
- package/README.md +3 -0
- package/bin/cdsc.js +9 -5
- package/doc/CHANGELOG_BETA.md +20 -2
- package/doc/CHANGELOG_DEPRECATED.md +2 -2
- package/lib/api/main.js +2 -1
- package/lib/api/options.js +3 -2
- package/lib/base/dictionaries.js +10 -0
- package/lib/base/message-registry.js +56 -12
- package/lib/base/messages.js +39 -20
- package/lib/base/model.js +1 -0
- package/lib/base/shuffle.js +2 -1
- package/lib/checks/elements.js +29 -1
- package/lib/checks/{emptyOrOnlyVirtual.js → hasPersistedElements.js} +9 -5
- package/lib/checks/nonexpandableStructured.js +1 -1
- package/lib/checks/onConditions.js +8 -5
- package/lib/checks/types.js +6 -1
- package/lib/checks/validator.js +7 -3
- package/lib/compiler/assert-consistency.js +20 -23
- package/lib/compiler/base.js +1 -2
- package/lib/compiler/builtins.js +2 -2
- package/lib/compiler/checks.js +237 -242
- package/lib/compiler/define.js +63 -75
- package/lib/compiler/extend.js +325 -22
- package/lib/compiler/finalize-parse-cdl.js +1 -55
- package/lib/compiler/kick-start.js +6 -7
- package/lib/compiler/populate.js +284 -288
- package/lib/compiler/propagator.js +15 -13
- package/lib/compiler/resolve.js +136 -306
- package/lib/compiler/shared.js +42 -44
- package/lib/compiler/tweak-assocs.js +29 -27
- package/lib/compiler/utils.js +29 -3
- package/lib/edm/annotations/genericTranslation.js +7 -13
- package/lib/edm/annotations/preprocessAnnotations.js +3 -0
- package/lib/edm/csn2edm.js +0 -4
- package/lib/edm/edm.js +6 -4
- package/lib/edm/edmAnnoPreprocessor.js +1 -0
- package/lib/edm/edmPreprocessor.js +1 -5
- package/lib/gen/Dictionary.json +34 -2
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +1 -1
- package/lib/gen/languageParser.js +2429 -2401
- package/lib/inspect/inspectPropagation.js +2 -0
- package/lib/json/from-csn.js +87 -41
- package/lib/json/to-csn.js +47 -16
- package/lib/language/errorStrategy.js +1 -0
- package/lib/language/genericAntlrParser.js +109 -28
- package/lib/language/language.g4 +20 -4
- package/lib/model/csnRefs.js +30 -2
- package/lib/model/csnUtils.js +1 -0
- package/lib/model/revealInternalProperties.js +1 -2
- package/lib/modelCompare/compare.js +2 -1
- package/lib/optionProcessor.js +3 -0
- package/lib/render/manageConstraints.js +5 -2
- package/lib/render/toCdl.js +20 -7
- package/lib/render/toHdbcds.js +2 -8
- package/lib/render/toSql.js +6 -5
- package/lib/render/utils/common.js +9 -5
- package/lib/transform/db/assertUnique.js +2 -1
- package/lib/transform/db/expansion.js +2 -0
- package/lib/transform/db/flattening.js +37 -36
- package/lib/transform/db/rewriteCalculatedElements.js +559 -0
- package/lib/transform/db/transformExists.js +15 -6
- package/lib/transform/db/views.js +40 -37
- package/lib/transform/forRelationalDB.js +44 -30
- package/lib/transform/odata/typesExposure.js +50 -15
- package/lib/transform/parseExpr.js +14 -8
- package/lib/transform/transformUtilsNew.js +6 -5
- package/lib/transform/translateAssocsToJoins.js +49 -33
- package/package.json +1 -1
|
@@ -115,44 +115,7 @@ function getViewTransformer( csn, options, messageFunctions, transformCommon ) {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
-
/**
|
|
119
|
-
* Build a map of the resulting names (i.e. the element name of the column) and references to the respective columns
|
|
120
|
-
*
|
|
121
|
-
* This can later be used to match from elements to columns.
|
|
122
|
-
*
|
|
123
|
-
* @param {CSN.Query} query
|
|
124
|
-
* @returns {object}
|
|
125
|
-
*/
|
|
126
|
-
function getColumnMap( query ) {
|
|
127
|
-
const map = Object.create(null);
|
|
128
|
-
if (query && query.SELECT && query.SELECT.columns) {
|
|
129
|
-
query.SELECT.columns.forEach((col) => {
|
|
130
|
-
if (col === '*') {
|
|
131
|
-
// do nothing
|
|
132
|
-
}
|
|
133
|
-
else if (col.as) {
|
|
134
|
-
if (!map[col.as])
|
|
135
|
-
map[col.as] = col;
|
|
136
|
-
}
|
|
137
|
-
else if (col.ref) {
|
|
138
|
-
// .id on last path step can happen with hdbcds.hdbcds and malicious CSN input - maybe also with params?
|
|
139
|
-
// We made things right in the end with the second add of missing stuff, but why not do it
|
|
140
|
-
// right from the getgo
|
|
141
|
-
const last = getLastRefStepString(col.ref);
|
|
142
|
-
if (!map[last])
|
|
143
|
-
map[last] = col;
|
|
144
|
-
}
|
|
145
|
-
else if (col.func) {
|
|
146
|
-
map[col.func] = col;
|
|
147
|
-
}
|
|
148
|
-
else if (!map[col]) {
|
|
149
|
-
map[col] = col;
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
118
|
|
|
154
|
-
return map;
|
|
155
|
-
}
|
|
156
119
|
/**
|
|
157
120
|
* For things that are not explicitly found in the columns but still present in the elements, add them to the columnMap.
|
|
158
121
|
*
|
|
@@ -513,6 +476,46 @@ function getLastRefStepString( ref ) {
|
|
|
513
476
|
return last;
|
|
514
477
|
}
|
|
515
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Build a map of the resulting names (i.e. the element name of the column) and references to the respective columns
|
|
481
|
+
*
|
|
482
|
+
* This can later be used to match from elements to columns.
|
|
483
|
+
*
|
|
484
|
+
* @param {CSN.Query} query
|
|
485
|
+
* @returns {object}
|
|
486
|
+
*/
|
|
487
|
+
function getColumnMap( query ) {
|
|
488
|
+
const map = Object.create(null);
|
|
489
|
+
if (query && query.SELECT && query.SELECT.columns) {
|
|
490
|
+
query.SELECT.columns.forEach((col) => {
|
|
491
|
+
if (col === '*') {
|
|
492
|
+
// do nothing
|
|
493
|
+
}
|
|
494
|
+
else if (col.as) {
|
|
495
|
+
if (!map[col.as])
|
|
496
|
+
map[col.as] = col;
|
|
497
|
+
}
|
|
498
|
+
else if (col.ref) {
|
|
499
|
+
// .id on last path step can happen with hdbcds.hdbcds and malicious CSN input - maybe also with params?
|
|
500
|
+
// We made things right in the end with the second add of missing stuff, but why not do it
|
|
501
|
+
// right from the getgo
|
|
502
|
+
const last = getLastRefStepString(col.ref);
|
|
503
|
+
if (!map[last])
|
|
504
|
+
map[last] = col;
|
|
505
|
+
}
|
|
506
|
+
else if (col.func) {
|
|
507
|
+
map[col.func] = col;
|
|
508
|
+
}
|
|
509
|
+
else if (!map[col]) {
|
|
510
|
+
map[col] = col;
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return map;
|
|
516
|
+
}
|
|
517
|
+
|
|
516
518
|
module.exports = {
|
|
517
519
|
getViewTransformer,
|
|
520
|
+
getColumnMap,
|
|
518
521
|
};
|
|
@@ -18,6 +18,7 @@ const { timetrace } = require('../utils/timetrace');
|
|
|
18
18
|
const { createReferentialConstraints, assertConstraintIdentifierUniqueness } = require('./db/constraints');
|
|
19
19
|
const { createDict, forEach } = require('../utils/objectUtils');
|
|
20
20
|
const handleExists = require('./db/transformExists');
|
|
21
|
+
const { rewriteCalculatedElementsInViews, processCalculatedElementsInEntities } = require('./db/rewriteCalculatedElements');
|
|
21
22
|
const replaceAssociationsInGroupByOrderBy = require('./db/groupByOrderBy');
|
|
22
23
|
const _forEachDefinition = require('../model/csnUtils').forEachDefinition;
|
|
23
24
|
const flattening = require('./db/flattening');
|
|
@@ -121,6 +122,8 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
|
|
|
121
122
|
let artifactRef,
|
|
122
123
|
inspectRef,
|
|
123
124
|
effectiveType,
|
|
125
|
+
initDefinition,
|
|
126
|
+
dropDefinitionCache,
|
|
124
127
|
get$combined,
|
|
125
128
|
getCsnDef,
|
|
126
129
|
isAssocOrComposition,
|
|
@@ -159,7 +162,7 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
|
|
|
159
162
|
timetrace.stop('Validate');
|
|
160
163
|
|
|
161
164
|
// Needs to happen before tuple expansion, so the newly generated WHERE-conditions have it applied
|
|
162
|
-
handleExists(csn, options, error);
|
|
165
|
+
handleExists(csn, options, error, inspectRef, initDefinition, dropDefinitionCache);
|
|
163
166
|
|
|
164
167
|
// Check if structured elements and managed associations are compared in an expression
|
|
165
168
|
// and expand these structured elements. This tuple expansion allows all other
|
|
@@ -176,14 +179,15 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
|
|
|
176
179
|
const transformCsn = transformUtils.transformModel;
|
|
177
180
|
|
|
178
181
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
forEachDefinition(csn, [
|
|
183
|
+
// (001) Add a temporal where condition to views where applicable before assoc2join
|
|
184
|
+
// assoc2join eventually rewrites the table aliases
|
|
185
|
+
temporal.getViewDecorator(csn, {info}, csnUtils),
|
|
186
|
+
// check unique constraints - further processing is done in rewriteUniqueConstraints
|
|
187
|
+
assertUnique.prepare(csn, options, error, info)
|
|
188
|
+
]);
|
|
184
189
|
|
|
185
|
-
|
|
186
|
-
assertUnique.prepare(csn, options, error, info);
|
|
190
|
+
rewriteCalculatedElementsInViews(csn, options, pathDelimiter, error);
|
|
187
191
|
|
|
188
192
|
if(doA2J) {
|
|
189
193
|
// Expand a structured thing in: keys, columns, order by, group by
|
|
@@ -197,6 +201,11 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
|
|
|
197
201
|
bindCsnReferenceOnly();
|
|
198
202
|
|
|
199
203
|
|
|
204
|
+
// TODO: Instead of 3 separate applyTransformations, we could have each of them just return the "listeners", merge them into
|
|
205
|
+
// one big listener that then gets passed into one single applyTransformations. Each listener would then have to return an array of callbacks to call.
|
|
206
|
+
// With that, we could still ensure the processing order (assuming we don't run into problems with scoping).
|
|
207
|
+
// To analyze: Increased memory vs. saved cycles
|
|
208
|
+
// Looked at it with AFC: This is only a small part of the overall processing time, enrich step of validator is just as expensive
|
|
200
209
|
if(doA2J) {
|
|
201
210
|
const resolved = new WeakMap();
|
|
202
211
|
// No refs with struct-steps exist anymore
|
|
@@ -238,6 +247,8 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
|
|
|
238
247
|
}
|
|
239
248
|
});
|
|
240
249
|
|
|
250
|
+
processCalculatedElementsInEntities(csn, options);
|
|
251
|
+
|
|
241
252
|
timetrace.start('Transform CSN')
|
|
242
253
|
|
|
243
254
|
// (000) Rename primitive types, make UUID a String
|
|
@@ -253,15 +264,15 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
|
|
|
253
264
|
},
|
|
254
265
|
}, true);
|
|
255
266
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
267
|
+
forEachDefinition(csn, [
|
|
268
|
+
// (040) Ignore entities and views that are abstract or implemented
|
|
269
|
+
// or carry the annotation cds.persistence.skip/exists
|
|
270
|
+
// These entities are not removed from the csn, but flagged as "to be ignored"
|
|
271
|
+
cdsPersistence.getAnnoProcessor(),
|
|
272
|
+
// (050) Check @cds.valid.from/to only on entity
|
|
273
|
+
// Views are checked in (001), unbalanced valid.from/to's or mismatching origins
|
|
274
|
+
temporal.getAnnotationHandler(csn, options, pathDelimiter, {error})
|
|
275
|
+
]);
|
|
265
276
|
|
|
266
277
|
// eliminate the doA2J in the functions 'handleManagedAssociationFKs' and 'createForeignKeyElements'
|
|
267
278
|
doA2J && flattening.handleManagedAssociationsAndCreateForeignKeys(csn, options, error, pathDelimiter, true, csnUtils, { skipDict: { actions: true }, allowArtifact: artifact => (artifact.kind === 'entity') });
|
|
@@ -298,18 +309,19 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
|
|
|
298
309
|
}
|
|
299
310
|
});
|
|
300
311
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
312
|
+
forEachDefinition(csn, [
|
|
313
|
+
// For generating DB stuff:
|
|
314
|
+
// - table-entity with parameters: not allowed
|
|
315
|
+
// - view with parameters: ok on HANA, not allowed otherwise
|
|
316
|
+
// (don't complain about action/function with parameters)
|
|
317
|
+
handleChecksForWithParameters,
|
|
318
|
+
// Remove .masked
|
|
319
|
+
// Check that keys are not explicitly nullable
|
|
320
|
+
// Check that Associations are not used in entities/views with parameters
|
|
321
|
+
// (150 b) Strip inheritance
|
|
322
|
+
// Note that this should happen after implicit redirection, because includes are required for that
|
|
323
|
+
handleDBChecks,
|
|
324
|
+
]);
|
|
313
325
|
|
|
314
326
|
// (170) Transform '$self' in backlink associations to appropriate key comparisons
|
|
315
327
|
// Must happen before draft processing because the artificial ON-conditions in generated
|
|
@@ -441,6 +453,8 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
|
|
|
441
453
|
({ artifactRef,
|
|
442
454
|
inspectRef,
|
|
443
455
|
effectiveType,
|
|
456
|
+
initDefinition,
|
|
457
|
+
dropDefinitionCache,
|
|
444
458
|
get$combined,
|
|
445
459
|
getCsnDef,
|
|
446
460
|
isAssocOrComposition,
|
|
@@ -451,7 +465,7 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
|
|
|
451
465
|
|
|
452
466
|
function bindCsnReferenceOnly(){
|
|
453
467
|
// invalidate caches for CSN ref API
|
|
454
|
-
({ artifactRef, inspectRef, effectiveType } = csnRefs(csn));
|
|
468
|
+
({ artifactRef, inspectRef, effectiveType, initDefinition, dropDefinitionCache } = csnRefs(csn));
|
|
455
469
|
}
|
|
456
470
|
|
|
457
471
|
function handleMixinOnConditions(artifact, artifactName) {
|
|
@@ -13,21 +13,66 @@ const { copyAnnotations, getNamespace } = require('../../model/csnUtils');
|
|
|
13
13
|
const { isBetaEnabled } = require('../../base/model.js');
|
|
14
14
|
const { CompilerAssertion } = require('../../base/error');
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* A given CDS model is a set of n definitions D = {v_1, ..., v_n } spanning a type dependency
|
|
18
|
+
* graph T(D) with vertices v_r, v_d (representing the referrer (using) and defining node of a type)
|
|
19
|
+
* and edges e(v_r, v_d).
|
|
20
|
+
*
|
|
21
|
+
* S may be a proper subset of D and is defined by v_s. Up to n S_i may exist.
|
|
22
|
+
* v is element of S_i, if name(v) starts with name(v_s). Therefore any v can only be member
|
|
23
|
+
* of exactly one S_i and v_s is always member of its own S_i.
|
|
24
|
+
*
|
|
25
|
+
* A complete service type dependency graph Tc(S) is defined as a set of vertices v_r, v_d
|
|
26
|
+
* and edges e(v_r, v_d) such that all v_r, v_d are elements of S (v_rs, v_ds) and with that
|
|
27
|
+
* all edges are { e(v_rs, v_ds) }.
|
|
28
|
+
*
|
|
29
|
+
* The input CSN may contain edges e(v_rs, v_dns) with v_r element of S_i (v_rs) and v_d not element
|
|
30
|
+
* of S_i (v_dns).
|
|
31
|
+
*
|
|
32
|
+
* The aim of this algorithm is to produce Tc's for all requested S_i by 'filling' up the missing
|
|
33
|
+
* vertices v_ds and rewriting all e(v_rs, v_dns) to e(v_rs, v_ds).
|
|
34
|
+
*
|
|
35
|
+
* This can be done pretty easily by (recursively) iterating over all requested v_rs and
|
|
36
|
+
* follow e(v_r, v_d) until a v_dns is found. v_dns is cloned and added to S via
|
|
37
|
+
* name(v_ds) = name(v_s) + '.' + name(v_dns). If v_dns is an anonymous definition, an
|
|
38
|
+
* artificial name representing the path to that node is being used.
|
|
39
|
+
*
|
|
40
|
+
* The algorithm has a beneficial side effect: it creates new (sub) schemas on the fly
|
|
41
|
+
* which are required for the construction of the EDM intermediate representation later on.
|
|
42
|
+
*
|
|
43
|
+
* An OData service contains at least one schema. Only (OData) schemas may contain definitions.
|
|
44
|
+
* By default, the CDS service v_s represents the default (OData) schema.
|
|
45
|
+
* However, there are situations where { v_dns } must be partitioned into (sub) schemas in order to
|
|
46
|
+
* maintain their original name prefixes and to be compatible with later service definitions.
|
|
47
|
+
*
|
|
48
|
+
* If name(v_dns) is made up of segments separated by a dot '.', the first n-1 segments represent
|
|
49
|
+
* the sub schema: name(schema) = name(v_s) + '.' + concat(1,n-1, segments(name(v_dns)), '.')
|
|
50
|
+
*
|
|
51
|
+
* If name(v_dns) has no prefix segments, the fallback schema name is prepended instead:
|
|
52
|
+
* name(v_ds) = name(v_s) + '.' + fallbackschema + name(v_dns);
|
|
53
|
+
*
|
|
54
|
+
* @param {CSN.Model} csn
|
|
55
|
+
* @param {function} whatsMyServiceName
|
|
56
|
+
* @param {string[]} requestedServiceNames
|
|
57
|
+
* @param {String} fallBackSchemaName
|
|
58
|
+
* @param {Object} options
|
|
59
|
+
* @param {Object} csnUtils
|
|
60
|
+
* @param {Object} message
|
|
61
|
+
* @returns {Object} schemas dictionary of (sub) schemas for all requested services
|
|
62
|
+
*/
|
|
16
63
|
function typesExposure(csn, whatsMyServiceName, requestedServiceNames, fallBackSchemaName, options, csnUtils, message) {
|
|
17
64
|
const { error } = message;
|
|
18
65
|
const special$self = !csn?.definitions?.$self && '$self';
|
|
19
66
|
// are we working with OData proxies or cross-service refs
|
|
20
67
|
const isMultiSchema = options.odataVersion === 'v4' && (options.odataProxies || options.odataXServiceRefs);
|
|
21
|
-
//
|
|
68
|
+
// service sub schemas as return value
|
|
22
69
|
const schemas = Object.create(null);
|
|
70
|
+
// exposed types register
|
|
23
71
|
const exposedTypes = Object.create(null);
|
|
24
|
-
// walk through the definitions of the given CSN and expose types where needed
|
|
25
72
|
forEachDefinition(csn, (def, defName, propertyName, path) => {
|
|
26
|
-
// we do expose types only for definition from inside services
|
|
27
73
|
const serviceName = whatsMyServiceName(defName, false);
|
|
28
74
|
// run type exposure only on requested services if not in multi schema mode
|
|
29
75
|
// multi schema mode requires a proper type exposure for all services as a prerequisite
|
|
30
|
-
// for the proxy exposure
|
|
31
76
|
if (serviceName && requestedServiceNames.includes(serviceName)) {
|
|
32
77
|
if (def.kind === 'type' || def.kind === 'entity') {
|
|
33
78
|
forEachMember(def, (element, elementName, propertyName, path) => {
|
|
@@ -38,13 +83,9 @@ function typesExposure(csn, whatsMyServiceName, requestedServiceNames, fallBackS
|
|
|
38
83
|
}, path);
|
|
39
84
|
}
|
|
40
85
|
|
|
41
|
-
// For exposed actions and functions that use non-exposed or anonymous structured types, create
|
|
42
|
-
// artificial exposing types.
|
|
43
|
-
// unbound actions
|
|
44
86
|
if (def.kind === 'action' || def.kind === 'function') {
|
|
45
87
|
exposeTypesOfAction(def, defName, defName, serviceName, path);
|
|
46
88
|
}
|
|
47
|
-
// bound actions
|
|
48
89
|
def.actions && Object.entries(def.actions).forEach(([actionName, action]) => {
|
|
49
90
|
exposeTypesOfAction(action, `${defName}_${actionName}`, defName, serviceName, path.concat(['actions', actionName]));
|
|
50
91
|
});
|
|
@@ -53,19 +94,13 @@ function typesExposure(csn, whatsMyServiceName, requestedServiceNames, fallBackS
|
|
|
53
94
|
|
|
54
95
|
if(isBetaEnabled(options, 'odataTerms')) {
|
|
55
96
|
forEachGeneric(csn, 'vocabularies', (def, defName, _propertyName, path) => {
|
|
56
|
-
// we do expose types only for definition from inside services
|
|
57
97
|
const serviceName = whatsMyServiceName(defName, false);
|
|
58
|
-
// run type exposure only on requested services if not in multi schema mode
|
|
59
|
-
// multi schema mode requires a proper type exposure for all services as a prerequisite
|
|
60
|
-
// for the proxy exposure
|
|
61
98
|
if (serviceName && requestedServiceNames.includes(serviceName)) {
|
|
62
99
|
if(csn.definitions[defName]) {
|
|
63
|
-
// error, duplicate definitions not allowed!
|
|
64
|
-
// TODO: Use path as error location as soon as refs outside definitions are supported
|
|
65
100
|
error('odata-definition-exists', ['vocabularies', defName], { anno: defName, '#':'anno' });
|
|
66
101
|
}
|
|
67
102
|
else {
|
|
68
|
-
|
|
103
|
+
// link def into definitions for later use
|
|
69
104
|
def.kind = 'annotation';
|
|
70
105
|
csn.definitions[defName] = def;
|
|
71
106
|
const artificialName = `term_${defName.replace(/\./g, '_')}`;//_${paramName}`;
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* Unary: 'is [not] null', 'not'
|
|
20
20
|
* Conditional: 'case [when then]+ [else]? end', 'and', 'or'
|
|
21
21
|
*
|
|
22
|
-
*
|
|
22
|
+
* stand-alone token: 'new'
|
|
23
23
|
*
|
|
24
24
|
* This is not an optimized LL(1) parser but a token 'sniffer'. A stream is
|
|
25
25
|
* cracked up in sub streams and passed down to the next higher function.
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
* This parser intentionally does no error handling. If a clause is malformed, it is accepted as is.
|
|
34
34
|
*
|
|
35
35
|
* @param {any} xpr A JSON object.
|
|
36
|
-
* @param {Object} state
|
|
37
|
-
* anno: Don't eliminate arrays with single entry in statetations as they are collections
|
|
36
|
+
* @param {Object} state Object
|
|
37
|
+
* anno: Don't eliminate arrays with single entry in statetations (TODO?) as they are collections
|
|
38
38
|
* array: Bias AST representation.
|
|
39
39
|
* nary: return n-ary or binary tree
|
|
40
40
|
*/
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
function parseExpr(xpr, state = { anno: 0, array: true, nary: false }) {
|
|
43
43
|
// Notes:
|
|
44
44
|
// - Variables `s` and `e` are used as index variables into `xpr`s for start and end.
|
|
45
|
-
// - xpr's are our CSN expressions, see <https://
|
|
45
|
+
// - xpr's are our CSN expressions, see <https://cap.cloud.sap/docs/cds/cxn>
|
|
46
46
|
|
|
47
47
|
return parseExprInt(xpr, state);
|
|
48
48
|
|
|
@@ -82,7 +82,7 @@ function parseExpr(xpr, state = { anno: 0, array: true, nary: false }) {
|
|
|
82
82
|
*/
|
|
83
83
|
function rewriteCaseBlock(casePos, endPos) {
|
|
84
84
|
const caseTree = state.array ? [ 'case' ] : { 'case': [] };
|
|
85
|
-
|
|
85
|
+
|
|
86
86
|
let elsePos = endPos;
|
|
87
87
|
let whenPos = casePos;
|
|
88
88
|
|
|
@@ -144,7 +144,7 @@ function parseExpr(xpr, state = { anno: 0, array: true, nary: false }) {
|
|
|
144
144
|
function conditionOR(xpr, s, e, state) {
|
|
145
145
|
return binaryExpr(xpr, ['or'], conditionAnd, s, e, state);
|
|
146
146
|
}
|
|
147
|
-
|
|
147
|
+
|
|
148
148
|
function conditionAnd(xpr, s, e, state) {
|
|
149
149
|
return binaryExpr(xpr, (xpr, s, e) => {
|
|
150
150
|
let a = s-1;
|
|
@@ -283,7 +283,8 @@ function parseExpr(xpr, state = { anno: 0, array: true, nary: false }) {
|
|
|
283
283
|
// return xpr;
|
|
284
284
|
for(let n in xpr) {
|
|
285
285
|
const x = xpr[n];
|
|
286
|
-
|
|
286
|
+
const isAnno = n[0] === '@' && isSimpleAnnoValue(x);
|
|
287
|
+
if(isAnno)
|
|
287
288
|
state.anno++;
|
|
288
289
|
if(Array.isArray(x)) {
|
|
289
290
|
if(csnarray.includes(n) || state.anno !== 0)
|
|
@@ -295,7 +296,7 @@ function parseExpr(xpr, state = { anno: 0, array: true, nary: false }) {
|
|
|
295
296
|
}
|
|
296
297
|
else
|
|
297
298
|
xpr[n] = parseExprInt(x, state);
|
|
298
|
-
if(
|
|
299
|
+
if(isAnno)
|
|
299
300
|
state.anno--;
|
|
300
301
|
}
|
|
301
302
|
}
|
|
@@ -343,6 +344,11 @@ function parseExpr(xpr, state = { anno: 0, array: true, nary: false }) {
|
|
|
343
344
|
|
|
344
345
|
}
|
|
345
346
|
|
|
347
|
+
function isSimpleAnnoValue(val) {
|
|
348
|
+
// Expressions as annotation values always have a `=` and another property.
|
|
349
|
+
return !val?.['='] || Object.keys(val) < 2;
|
|
350
|
+
}
|
|
351
|
+
|
|
346
352
|
module.exports = {
|
|
347
353
|
parseExpr,
|
|
348
354
|
};
|
|
@@ -321,9 +321,7 @@ function getTransformers(model, options, pathDelimiter = '_') {
|
|
|
321
321
|
*/
|
|
322
322
|
function flattenStructStepsInRef(ref, path, links, scope, resolvedLinkTypes=new WeakMap()) {
|
|
323
323
|
// Refs of length 1 cannot contain steps - no need to check
|
|
324
|
-
if (ref.length < 2) {
|
|
325
|
-
return ref;
|
|
326
|
-
} else if(scope === '$self' && ref.length === 2) {
|
|
324
|
+
if (ref.length < 2 || (scope === '$self' && ref.length === 2)) {
|
|
327
325
|
return ref;
|
|
328
326
|
}
|
|
329
327
|
|
|
@@ -1136,7 +1134,7 @@ function getTransformers(model, options, pathDelimiter = '_') {
|
|
|
1136
1134
|
const rhsIsVal = (rhs === 'null' /*|| rhs.val !== undefined*/);
|
|
1137
1135
|
|
|
1138
1136
|
// lhs & rhs must be expandable types (structures or managed associations)
|
|
1139
|
-
// if ever lhs should be
|
|
1137
|
+
// if ever lhs should be allowed to be a value uncomment this
|
|
1140
1138
|
if(!(lhsIsVal /*&& rhsIsVal*/) &&
|
|
1141
1139
|
!(isDollarSelfOrProjectionOperand(lhs) || isDollarSelfOrProjectionOperand(rhs)) &&
|
|
1142
1140
|
RelationalOperators.includes(op) &&
|
|
@@ -1189,6 +1187,7 @@ function getTransformers(model, options, pathDelimiter = '_') {
|
|
|
1189
1187
|
// lhs && rhs are present, consistency checks that affect both ends
|
|
1190
1188
|
else {
|
|
1191
1189
|
// is lhs scalar?
|
|
1190
|
+
// eslint-disable-next-line sonarjs/no-gratuitous-expressions
|
|
1192
1191
|
if(!lhsIsVal && x.lhs && !isScalarOrNoType(x.lhs._art)) {
|
|
1193
1192
|
error(null, location, { prefix, name: `${pathName(x.lhs.ref)}${(xn.length ? '.' + xn : '')}` },
|
|
1194
1193
|
'$(PREFIX): Path $(NAME) must end on a scalar type')
|
|
@@ -1201,6 +1200,7 @@ function getTransformers(model, options, pathDelimiter = '_') {
|
|
|
1201
1200
|
cont = false;
|
|
1202
1201
|
}
|
|
1203
1202
|
// info about type incompatibility if no other errors occurred
|
|
1203
|
+
// eslint-disable-next-line sonarjs/no-gratuitous-expressions
|
|
1204
1204
|
if(!(lhsIsVal || rhsIsVal) && x.lhs && x.rhs && xn && cont) {
|
|
1205
1205
|
const lhst = getType(x.lhs._art);
|
|
1206
1206
|
const rhst = getType(x.rhs._art);
|
|
@@ -1215,6 +1215,7 @@ function getTransformers(model, options, pathDelimiter = '_') {
|
|
|
1215
1215
|
return expr;
|
|
1216
1216
|
|
|
1217
1217
|
// if lhs and rhs are refs set operator from 'like' to '='
|
|
1218
|
+
// eslint-disable-next-line sonarjs/no-gratuitous-expressions
|
|
1218
1219
|
if(op === 'like' && !(lhsIsVal || rhsIsVal)) {
|
|
1219
1220
|
op = '=';
|
|
1220
1221
|
}
|
|
@@ -1419,7 +1420,7 @@ function rewriteBuiltinTypeRef(csn) {
|
|
|
1419
1420
|
const special$self = !csn?.definitions?.$self && '$self';
|
|
1420
1421
|
applyTransformations(csn, {
|
|
1421
1422
|
type: (parent, _prop, type) => {
|
|
1422
|
-
if(type
|
|
1423
|
+
if(type?.ref && (
|
|
1423
1424
|
isBuiltinType(type.ref[0]) ||
|
|
1424
1425
|
type.ref[0] === special$self)
|
|
1425
1426
|
) {
|
|
@@ -17,6 +17,7 @@ function translateAssocsToJoinsCSN(csn, options){
|
|
|
17
17
|
// Do not re-complain about localized
|
|
18
18
|
const compileOptions = { ...options, $skipNameCheck: true };
|
|
19
19
|
delete compileOptions.csnFlavor;
|
|
20
|
+
// console.log('CSN passed by A2J to compiler:',JSON.stringify(csn,null,2))
|
|
20
21
|
const model = recompileX(csn, compileOptions);
|
|
21
22
|
timetrace.stop('A2J: Recompiling model');
|
|
22
23
|
timetrace.start('A2J: Translating associations to joins');
|
|
@@ -251,22 +252,24 @@ function translateAssocsToJoins(model, inputOptions = {})
|
|
|
251
252
|
*/
|
|
252
253
|
function substituteDollarSelf(pathNode)
|
|
253
254
|
{
|
|
254
|
-
let
|
|
255
|
-
|
|
255
|
+
let pathValue = pathNode;
|
|
256
|
+
let [head, ...tail] = pathValue.path;
|
|
257
|
+
while(tail.length && head._navigation?.kind === '$self') {
|
|
256
258
|
const self = head;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if(
|
|
259
|
+
[head, ...tail] = tail;
|
|
260
|
+
if(head) {
|
|
261
|
+
pathValue = self._navigation._origin.elements[head.id].value;
|
|
262
|
+
// core compiler has already caught $self.<assoc>.<postfix> and
|
|
263
|
+
// non-path $self expressions with postfix path
|
|
264
|
+
if(pathValue.path) {
|
|
265
|
+
if(tail.length)
|
|
264
266
|
pathValue = constructPathNode([...pathValue.path, ...tail], pathValue.alias, false);
|
|
265
|
-
|
|
266
|
-
replaceNodeContent(pathNode, pathValue);
|
|
267
|
+
[head, ...tail] = pathValue.path;
|
|
267
268
|
}
|
|
268
269
|
}
|
|
269
270
|
}
|
|
271
|
+
if(head)
|
|
272
|
+
replaceNodeContent(pathNode, pathValue);
|
|
270
273
|
}
|
|
271
274
|
|
|
272
275
|
/*
|
|
@@ -636,7 +639,7 @@ function translateAssocsToJoins(model, inputOptions = {})
|
|
|
636
639
|
}
|
|
637
640
|
|
|
638
641
|
env.assocStack.push(assoc);
|
|
639
|
-
|
|
642
|
+
const onCond = cloneOnCondition(assoc.on);
|
|
640
643
|
env.assocStack.pop();
|
|
641
644
|
return onCond;
|
|
642
645
|
}
|
|
@@ -655,8 +658,8 @@ function translateAssocsToJoins(model, inputOptions = {})
|
|
|
655
658
|
}
|
|
656
659
|
|
|
657
660
|
function cloneOnCondExprStream(expr) {
|
|
658
|
-
|
|
659
|
-
|
|
661
|
+
const args = expr.args;
|
|
662
|
+
const result = { op: { val: expr.op.val }, args: [ ] };
|
|
660
663
|
for(let i = 0; i < args.length; i++)
|
|
661
664
|
{
|
|
662
665
|
if(args[i].op && args[i].op.val === 'xpr')
|
|
@@ -668,7 +671,7 @@ function translateAssocsToJoins(model, inputOptions = {})
|
|
|
668
671
|
else if(i < args.length-2 && args[i].path &&
|
|
669
672
|
args[i+1]?.literal === 'token' && args[i+1]?.val === '=' && args[i+2].path)
|
|
670
673
|
{
|
|
671
|
-
|
|
674
|
+
const fwdAssoc = getForwardAssociation(args[i].path, args[i+2].path);
|
|
672
675
|
if(fwdAssoc)
|
|
673
676
|
{
|
|
674
677
|
//env.assocStack.includes(fwdAssoc) => recursion
|
|
@@ -702,7 +705,7 @@ function translateAssocsToJoins(model, inputOptions = {})
|
|
|
702
705
|
|
|
703
706
|
// If this is a backlink condition, produce the
|
|
704
707
|
// ON cond of the forward assoc with swapped src/tgt aliases
|
|
705
|
-
|
|
708
|
+
const fwdAssoc = getForwardAssociationExpr(expr);
|
|
706
709
|
if(fwdAssoc) {
|
|
707
710
|
if(env.assocStack.length === 2) {
|
|
708
711
|
// reuse (ugly) error message from forHana
|
|
@@ -729,25 +732,38 @@ function translateAssocsToJoins(model, inputOptions = {})
|
|
|
729
732
|
}
|
|
730
733
|
|
|
731
734
|
// The src/tgtAliases need to be swapped for ON Condition of the forward assoc.
|
|
732
|
-
//
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
//
|
|
736
|
-
//
|
|
735
|
+
// If the QAT assoc is a mixin and forward assoc was propagated, the original
|
|
736
|
+
// forward definition must have a target in the query source otherwise the ON cond
|
|
737
|
+
// is not resolvable (exception propagated mixins, as these are defined against the
|
|
738
|
+
// view signature and not a query source). If the target is not part of the query source,
|
|
739
|
+
// raise an error. Swap source and target otherwise.
|
|
737
740
|
function swapTableAliasesForFwdAssoc(fwdAssoc, srcAlias, tgtAlias) {
|
|
738
|
-
|
|
741
|
+
const newSrcAlias = tgtAlias;
|
|
739
742
|
let newTgtAlias = {};
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
743
|
+
|
|
744
|
+
let i = 0;
|
|
745
|
+
let fwdOrigin = fwdAssoc;
|
|
746
|
+
while(fwdOrigin._origin) {
|
|
747
|
+
fwdOrigin = fwdOrigin._origin;
|
|
748
|
+
i++;
|
|
749
|
+
}
|
|
750
|
+
// If fwdAssoc was propagated and the origin is not a mixin itself (which always
|
|
751
|
+
// points to the signature of the current view and ensures that the ON cond is
|
|
752
|
+
// resolvable) make sure that the original assoc target is contained in the local
|
|
753
|
+
// query source
|
|
754
|
+
if(assoc.kind === 'mixin' && i > 0 && fwdOrigin.kind !== 'mixin') {
|
|
755
|
+
const tas = Object.values(env.lead.$tableAliases);
|
|
756
|
+
const i = tas.findIndex(ta => ta._artifact === fwdOrigin.target._artifact);
|
|
757
|
+
if(i >= 0 && tas[i].$QA) {
|
|
758
|
+
newTgtAlias.id = tas[i].$QA.name.id;
|
|
759
|
+
newTgtAlias._artifact = tas[i]._effectiveType;
|
|
760
|
+
newTgtAlias._navigation = tas[i].$QA.path[0]._navigation;
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
error(null, [ assocQAT._origin.location, assocQAT._origin ], { name: fwdOrigin.target._artifact.name.id, art: assoc.name.id },
|
|
764
|
+
'Expected association target $(NAME) of association $(ART) to be a query source');
|
|
765
|
+
newTgtAlias = Object.assign(newTgtAlias, srcAlias);
|
|
766
|
+
}
|
|
751
767
|
}
|
|
752
768
|
else {
|
|
753
769
|
newTgtAlias = Object.assign(newTgtAlias, srcAlias);
|