@sap/cds-compiler 5.1.2 → 5.3.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 +58 -0
- package/bin/cdsc.js +7 -2
- package/bin/cdshi.js +24 -17
- package/bin/cdsse.js +17 -18
- package/doc/CHANGELOG_BETA.md +9 -4
- package/lib/api/main.js +19 -2
- package/lib/api/options.js +4 -1
- package/lib/api/validate.js +5 -0
- package/lib/base/builtins.js +1 -0
- package/lib/base/message-registry.js +40 -3
- package/lib/base/messages.js +1 -1
- package/lib/base/model.js +0 -11
- package/lib/checks/actionsFunctions.js +0 -12
- package/lib/checks/structuredAnnoExpressions.js +10 -14
- package/lib/compiler/assert-consistency.js +21 -13
- package/lib/compiler/builtins.js +2 -2
- package/lib/compiler/checks.js +25 -6
- package/lib/compiler/define.js +27 -31
- package/lib/compiler/extend.js +16 -18
- package/lib/compiler/generate.js +3 -3
- package/lib/compiler/populate.js +22 -16
- package/lib/compiler/propagator.js +3 -2
- package/lib/compiler/resolve.js +87 -94
- package/lib/compiler/shared.js +12 -13
- package/lib/compiler/tweak-assocs.js +390 -86
- package/lib/compiler/utils.js +41 -33
- package/lib/compiler/xpr-rewrite.js +45 -58
- package/lib/edm/annotations/genericTranslation.js +17 -13
- package/lib/edm/csn2edm.js +28 -4
- package/lib/edm/edm.js +68 -28
- package/lib/edm/edmInboundChecks.js +5 -8
- package/lib/edm/edmPreprocessor.js +66 -40
- package/lib/edm/edmUtils.js +1 -1
- package/lib/gen/BaseParser.js +778 -0
- package/lib/gen/CdlParser.js +4477 -0
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +1 -1
- package/lib/gen/languageParser.js +4072 -4024
- package/lib/inspect/inspectPropagation.js +1 -1
- package/lib/json/from-csn.js +5 -3
- package/lib/json/to-csn.js +7 -10
- package/lib/language/antlrParser.js +96 -0
- package/lib/language/errorStrategy.js +1 -1
- package/lib/language/genericAntlrParser.js +32 -4
- package/lib/language/multiLineStringParser.js +1 -1
- package/lib/main.d.ts +23 -0
- package/lib/model/cloneCsn.js +22 -13
- package/lib/model/csnUtils.js +2 -0
- package/lib/model/revealInternalProperties.js +2 -0
- package/lib/modelCompare/utils/filter.js +70 -42
- package/lib/optionProcessor.js +16 -10
- package/lib/parsers/AstBuildingParser.js +1290 -0
- package/lib/parsers/CdlGrammar.g4 +2013 -0
- package/lib/parsers/Lexer.js +249 -0
- package/lib/render/toCdl.js +46 -45
- package/lib/render/toSql.js +5 -5
- package/lib/transform/addTenantFields.js +4 -4
- package/lib/transform/db/applyTransformations.js +54 -16
- package/lib/transform/draft/odata.js +10 -11
- package/lib/transform/effective/flattening.js +10 -14
- package/lib/transform/forRelationalDB.js +7 -6
- package/lib/transform/odata/flattening.js +42 -31
- package/lib/transform/odata/toFinalBaseType.js +7 -6
- package/lib/transform/universalCsn/universalCsnEnricher.js +1 -0
- package/lib/utils/moduleResolve.js +1 -1
- package/package.json +2 -2
- package/share/messages/redirected-to-ambiguous.md +5 -4
- package/share/messages/redirected-to-complex.md +6 -3
|
@@ -41,10 +41,13 @@ function tweakAssocs( model ) {
|
|
|
41
41
|
extendForeignKeys,
|
|
42
42
|
createRemainingAnnotateStatements,
|
|
43
43
|
mergeSpecifiedForeignKeys,
|
|
44
|
+
navigationEnv,
|
|
45
|
+
redirectionChain,
|
|
44
46
|
} = model.$functions;
|
|
45
47
|
|
|
46
48
|
Object.assign(model.$functions, {
|
|
47
|
-
|
|
49
|
+
findRewriteTarget,
|
|
50
|
+
cachedRedirectionChain,
|
|
48
51
|
});
|
|
49
52
|
|
|
50
53
|
// Phase 5: rewrite associations
|
|
@@ -73,7 +76,6 @@ function tweakAssocs( model ) {
|
|
|
73
76
|
// Only top-level queries and sub queries in FROM
|
|
74
77
|
|
|
75
78
|
function rewriteArtifact( art ) {
|
|
76
|
-
// return;
|
|
77
79
|
if (!art.query) {
|
|
78
80
|
rewriteAssociation( art );
|
|
79
81
|
}
|
|
@@ -126,7 +128,7 @@ function tweakAssocs( model ) {
|
|
|
126
128
|
// ID published! Used in stakeholder project; if renamed, add to oldMessageIds
|
|
127
129
|
info( 'assoc-outside-service', loc, { '#': text, target, service: main._service }, {
|
|
128
130
|
std: 'Association target $(TARGET) is outside any service',
|
|
129
|
-
// eslint-disable-next-line max-len
|
|
131
|
+
// eslint-disable-next-line @stylistic/js/max-len
|
|
130
132
|
exposed: 'If association is published in service $(SERVICE), its target $(TARGET) is outside any service',
|
|
131
133
|
} );
|
|
132
134
|
}
|
|
@@ -143,7 +145,7 @@ function tweakAssocs( model ) {
|
|
|
143
145
|
if (assoc && assoc.foreignKeys) {
|
|
144
146
|
error( 'rewrite-key-for-unmanaged', [ elem.on.location, elem ],
|
|
145
147
|
{ keyword: 'on', art: assocWithExplicitSpec( assoc ) },
|
|
146
|
-
// eslint-disable-next-line max-len
|
|
148
|
+
// eslint-disable-next-line @stylistic/js/max-len
|
|
147
149
|
'Do not specify an $(KEYWORD) condition when redirecting the managed association $(ART)' );
|
|
148
150
|
}
|
|
149
151
|
checkIgnoredFilter( elem );
|
|
@@ -333,27 +335,62 @@ function tweakAssocs( model ) {
|
|
|
333
335
|
|
|
334
336
|
// TODO: split this function: create foreign keys without `targetElement`
|
|
335
337
|
// already in Phase 2: redirectImplicitly()
|
|
336
|
-
// console.log(message( null, elem.location, elem, {art:assoc,target:assoc.target},
|
|
337
|
-
// 'Info','FK').toString())
|
|
338
338
|
elem.foreignKeys = Object.create(null); // set already here (also for zero foreign keys)
|
|
339
339
|
forEachInOrder( assoc, 'foreignKeys', ( orig, name ) => {
|
|
340
340
|
const location = weakRefLocation( elem.target );
|
|
341
341
|
const fk = linkToOrigin( orig, name, elem, 'foreignKeys', location );
|
|
342
342
|
fk.$inferred = 'rewrite'; // Override existing value; TODO: other $inferred value?
|
|
343
343
|
setLink( fk, '_effectiveType', fk );
|
|
344
|
-
|
|
345
|
-
if (elem._redirected)
|
|
346
|
-
|
|
347
|
-
const state = rewriteItem( elem, i, i.id, elem, true );
|
|
348
|
-
if (state && state !== true && te.path.length === 1)
|
|
349
|
-
setArtifactLink( te, state );
|
|
350
|
-
}
|
|
351
|
-
fk.targetElement = te;
|
|
344
|
+
fk.targetElement = copyExpr( orig.targetElement, location );
|
|
345
|
+
if (elem._redirected)
|
|
346
|
+
rewriteKey( elem, fk.targetElement );
|
|
352
347
|
} );
|
|
353
348
|
if (elem.foreignKeys) // Possibly no fk was set
|
|
354
349
|
elem.foreignKeys[$inferred] = 'rewrite';
|
|
355
350
|
}
|
|
356
351
|
|
|
352
|
+
function rewriteKey( elem, targetElement ) {
|
|
353
|
+
let projectedKey = null;
|
|
354
|
+
// rewrite along redirection chain
|
|
355
|
+
for (const alias of elem._redirected) {
|
|
356
|
+
if (alias.kind !== '$tableAlias')
|
|
357
|
+
continue;
|
|
358
|
+
|
|
359
|
+
projectedKey = firstProjectionForPath( targetElement.path, 0, alias, null );
|
|
360
|
+
if (projectedKey.elem) {
|
|
361
|
+
const item = targetElement.path[projectedKey.index];
|
|
362
|
+
item.id = projectedKey.elem.name.id;
|
|
363
|
+
if (projectedKey.index > 0)
|
|
364
|
+
targetElement.path.splice(0, projectedKey.index);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
setArtifactLink( targetElement.path[0], null );
|
|
368
|
+
setArtifactLink( targetElement, null );
|
|
369
|
+
|
|
370
|
+
const culprit = !elem.target.$inferred && elem.target ||
|
|
371
|
+
elem.value?.path?.[elem.value.path.length - 1] ||
|
|
372
|
+
elem;
|
|
373
|
+
// TODO: probably better to collect the non-projected foreign keys
|
|
374
|
+
// and have one message for all
|
|
375
|
+
error('rewrite-undefined-key', [ weakLocation( culprit.location ), elem ], {
|
|
376
|
+
'#': 'std',
|
|
377
|
+
id: targetElement.path.map(p => p.id).join('.'),
|
|
378
|
+
target: alias._main,
|
|
379
|
+
name: elem.name.id,
|
|
380
|
+
});
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (projectedKey?.elem) {
|
|
386
|
+
const item = targetElement.path[0];
|
|
387
|
+
setArtifactLink( item, projectedKey.elem );
|
|
388
|
+
setArtifactLink( targetElement, projectedKey.elem );
|
|
389
|
+
return projectedKey.elem;
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
357
394
|
// TODO: there is no need to rewrite the on condition of non-leading queries,
|
|
358
395
|
// i.e. we could just have on = {…}
|
|
359
396
|
// TODO: re-check $self rewrite (with managed composition of aspects),
|
|
@@ -364,11 +401,18 @@ function tweakAssocs( model ) {
|
|
|
364
401
|
// same (TODO later: set status whether rewrite changes anything),
|
|
365
402
|
// especially problematic are refs starting with $self:
|
|
366
403
|
setExpandStatus( elem, 'target' );
|
|
404
|
+
|
|
405
|
+
// There were previous issues in resolving the target artifact.
|
|
406
|
+
// Avoid further compiler messages.
|
|
407
|
+
if (!elem.target._artifact)
|
|
408
|
+
return;
|
|
409
|
+
|
|
367
410
|
if (elem._parent?.kind === 'element') {
|
|
368
411
|
// managed association as sub element not supported yet
|
|
369
412
|
// TODO: Only report once for multi-include chains, see
|
|
370
413
|
// Associations/SubElements/UnmanagedInSubElement.err.cds
|
|
371
414
|
error( 'type-unsupported-rewrite', [ elem.location, elem ], { '#': 'sub-element' } );
|
|
415
|
+
removeArtifactLinks();
|
|
372
416
|
return;
|
|
373
417
|
}
|
|
374
418
|
const nav = (elem._main?.query && elem.value)
|
|
@@ -380,31 +424,45 @@ function tweakAssocs( model ) {
|
|
|
380
424
|
elem.on.$inferred = 'copy';
|
|
381
425
|
|
|
382
426
|
const { navigation } = nav;
|
|
383
|
-
if (!navigation) // TODO: what about $projection.assoc as myAssoc ?
|
|
427
|
+
if (!navigation) { // TODO: what about $projection.assoc as myAssoc ?
|
|
428
|
+
if (elem._columnParent) {
|
|
429
|
+
error( 'rewrite-not-supported', [ elem.target.location, elem ], { '#': 'inline-expand' } );
|
|
430
|
+
removeArtifactLinks();
|
|
431
|
+
}
|
|
384
432
|
return; // should not happen: $projection, $magic, or ref to const
|
|
385
|
-
|
|
386
|
-
// Currently, having an unmanaged association inside a struct is not
|
|
387
|
-
// supported by this function:
|
|
388
|
-
if (navigation !== assoc && navigation._origin !== assoc) { // TODO: re-check
|
|
389
|
-
// For "assoc1.assoc2" and "struct.elem1.assoc2"
|
|
390
|
-
if (elem._redirected !== null) // null = already reported
|
|
391
|
-
error( 'rewrite-not-supported', [ elem.target.location, elem ] );
|
|
392
433
|
}
|
|
393
|
-
|
|
434
|
+
|
|
435
|
+
if (!nav.tableAlias || nav.tableAlias.path) {
|
|
436
|
+
const navEnv = followNavigationPath( elem.value?.path, nav ) || nav.tableAlias;
|
|
394
437
|
traverseExpr( elem.on, 'rewrite-on', elem,
|
|
395
|
-
expr => rewriteExpr( expr, elem, nav.tableAlias ) );
|
|
438
|
+
expr => rewriteExpr( expr, elem, nav.tableAlias, navEnv ) );
|
|
396
439
|
}
|
|
397
|
-
else if (elem.
|
|
398
|
-
error( 'rewrite-not-supported', [ elem.target.location, elem ] );
|
|
440
|
+
else if (elem._columnParent) {
|
|
441
|
+
error( 'rewrite-not-supported', [ elem.target.location, elem ], { '#': 'inline-expand' } );
|
|
442
|
+
removeArtifactLinks();
|
|
443
|
+
return;
|
|
399
444
|
}
|
|
400
445
|
else {
|
|
401
446
|
// TODO: support that, now that the ON condition is rewritten in the right order
|
|
402
447
|
error( null, [ elem.value.location, elem ], {},
|
|
403
448
|
'Selecting unmanaged associations from a sub query is not supported' );
|
|
449
|
+
removeArtifactLinks();
|
|
450
|
+
return;
|
|
404
451
|
}
|
|
405
452
|
|
|
406
453
|
addConditionFromAssocPublishing( elem, assoc, nav );
|
|
407
454
|
elem.on.$inferred = 'rewrite';
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Clear all `_artifact` links in the ON-condition to avoid follow-up
|
|
458
|
+
* issues during ON-condition rewriting of associations that inherit
|
|
459
|
+
* the ON-condition.
|
|
460
|
+
*/
|
|
461
|
+
function removeArtifactLinks() {
|
|
462
|
+
traverseExpr( elem.on, 'rewrite-on', elem, (expr) => {
|
|
463
|
+
setArtifactLink( expr, null );
|
|
464
|
+
} );
|
|
465
|
+
}
|
|
408
466
|
}
|
|
409
467
|
|
|
410
468
|
/**
|
|
@@ -495,6 +553,7 @@ function tweakAssocs( model ) {
|
|
|
495
553
|
function filterToCondition( assocPathStep, elem, nav ) {
|
|
496
554
|
const cond = copyExpr( assocPathStep.where );
|
|
497
555
|
cond.$parens = [ assocPathStep.location ];
|
|
556
|
+
const navEnv = nav && followNavigationPath( elem.value?.path, nav ) || nav?.tableAlias;
|
|
498
557
|
traverseExpr( cond, 'rewrite-filter', elem, (expr) => {
|
|
499
558
|
if (!expr.path || expr.path.length === 0)
|
|
500
559
|
return;
|
|
@@ -518,7 +577,7 @@ function tweakAssocs( model ) {
|
|
|
518
577
|
setLink( expr.path[0], '_navigation', assocPathStep._navigation );
|
|
519
578
|
}
|
|
520
579
|
// up to here, filter is relative to original association
|
|
521
|
-
rewriteExpr( expr, elem, nav?.tableAlias );
|
|
580
|
+
rewriteExpr( expr, elem, nav?.tableAlias, navEnv );
|
|
522
581
|
}
|
|
523
582
|
} );
|
|
524
583
|
|
|
@@ -528,7 +587,7 @@ function tweakAssocs( model ) {
|
|
|
528
587
|
|
|
529
588
|
// Caller must ensure ON-condition correctness via rewriteExpr()!
|
|
530
589
|
function foreignKeysToOnCondition( elem, assoc, nav ) {
|
|
531
|
-
if (model.options.testMode && !nav.tableAlias && !elem.
|
|
590
|
+
if (model.options.testMode && !nav.tableAlias && !elem._columnParent && elem.$syntax !== 'calc')
|
|
532
591
|
throw new CompilerAssertion('rewriting keys to cond: no tableAlias but not inline/calc');
|
|
533
592
|
|
|
534
593
|
if ((!nav.tableAlias && elem.$syntax !== 'calc') || elem._parent?.kind === 'element' ||
|
|
@@ -571,8 +630,11 @@ function tweakAssocs( model ) {
|
|
|
571
630
|
setLink( rhs.path[0], '_artifact', assoc );
|
|
572
631
|
setLink( rhs, '_artifact', rhs.path[rhs.path.length - 1]._artifact );
|
|
573
632
|
|
|
574
|
-
if (elem.$syntax !== 'calc') {
|
|
575
|
-
|
|
633
|
+
if (elem.$syntax !== 'calc') {
|
|
634
|
+
// Not passing an element, as we don't want to use our own filtered association here!
|
|
635
|
+
// That's done for lhs.
|
|
636
|
+
const projectedFk = firstProjectionForPath( rhs.path, 0, nav.tableAlias, null );
|
|
637
|
+
// different to lhs!
|
|
576
638
|
rewritePath( rhs, projectedFk.item, elem, projectedFk.elem, elem.value.location );
|
|
577
639
|
}
|
|
578
640
|
|
|
@@ -607,33 +669,33 @@ function tweakAssocs( model ) {
|
|
|
607
669
|
return cond;
|
|
608
670
|
}
|
|
609
671
|
|
|
610
|
-
|
|
672
|
+
/**
|
|
673
|
+
* @param expr
|
|
674
|
+
* @param assoc
|
|
675
|
+
* @param tableAlias
|
|
676
|
+
* @param navEnv Navigation element / table alias, used to traverse/rewrite the path.
|
|
677
|
+
*/
|
|
678
|
+
function rewriteExpr( expr, assoc, tableAlias, navEnv = tableAlias ) {
|
|
611
679
|
// Rewrite ON condition (resulting in outside perspective) for association
|
|
612
680
|
// 'assoc' in query or including entity from ON cond of mixin element /
|
|
613
681
|
// element in included structure / element in source ref/d by table alias.
|
|
614
682
|
|
|
615
683
|
// TODO: complain about $self (unclear semantics)
|
|
616
|
-
// console.log( info(null, [assoc.name.location, assoc],
|
|
617
|
-
// { art: expr._artifact, names: expr.path.map(i=>i.id) }, 'A').toString(), expr.path)
|
|
618
684
|
|
|
619
685
|
if (!expr.path || !expr._artifact)
|
|
620
686
|
return;
|
|
621
687
|
if (!assoc._main)
|
|
622
688
|
return;
|
|
623
|
-
if (
|
|
624
|
-
const source = tableAlias._origin;
|
|
689
|
+
if (navEnv) { // from ON cond of element in source ref/d by table alias
|
|
625
690
|
const root = expr.path[0]._navigation || expr.path[0]._artifact;
|
|
626
|
-
if (!root || root.
|
|
627
|
-
return;
|
|
691
|
+
if (!root || root.kind === 'builtin')
|
|
692
|
+
return; // not $self or source element, e.g. builtin
|
|
693
|
+
|
|
694
|
+
// parameters are not allowed in ON-conditions; error emitted elsewhere already
|
|
628
695
|
if (expr.scope === 'param' || root.kind === '$parameters')
|
|
629
|
-
return;
|
|
630
|
-
const startIndex = (root.kind === '$self' ? 1 : 0);
|
|
631
|
-
const result = firstProjectionForPath( expr.path, startIndex, tableAlias, assoc );
|
|
632
|
-
// For `assoc[…]`, ensure that we don't rewrite to another projection on `assoc`.
|
|
633
|
-
if (result.item && assoc._origin === result.item._artifact)
|
|
634
|
-
result.elem = assoc;
|
|
696
|
+
return;
|
|
635
697
|
|
|
636
|
-
|
|
698
|
+
rewritePathForEnv( expr, navEnv, assoc );
|
|
637
699
|
}
|
|
638
700
|
else if (assoc._main.query) { // from ON cond of mixin element in query
|
|
639
701
|
const root = expr.path[0]._navigation || expr.path[0]._artifact;
|
|
@@ -641,15 +703,16 @@ function tweakAssocs( model ) {
|
|
|
641
703
|
if (assoc.$errorReported !== 'assoc-unexpected-scope') {
|
|
642
704
|
error( 'assoc-unexpected-scope', [ assoc.value.location, assoc ],
|
|
643
705
|
{ id: assoc.value._artifact.name.id },
|
|
644
|
-
// eslint-disable-next-line max-len
|
|
706
|
+
// eslint-disable-next-line @stylistic/js/max-len
|
|
645
707
|
'Association $(ID) can\'t be projected because its ON-condition refers to a parameter' );
|
|
646
708
|
assoc.$errorReported = 'assoc-unexpected-scope';
|
|
647
709
|
}
|
|
648
710
|
return;
|
|
649
711
|
}
|
|
650
|
-
|
|
651
|
-
|
|
712
|
+
if (expr.path[0]._navigation) { // rewrite src elem, mixin, $self[.elem]
|
|
713
|
+
const nav = pathNavigation( expr );
|
|
652
714
|
const elem = (assoc._origin === root) ? assoc : navProjection( nav.navigation, assoc );
|
|
715
|
+
// TODO: Use rewritePathForEnv(); make it handle mixins
|
|
653
716
|
rewritePath( expr, nav.item, assoc, elem,
|
|
654
717
|
nav.item ? nav.item.location : expr.path[0].location );
|
|
655
718
|
}
|
|
@@ -673,11 +736,11 @@ function tweakAssocs( model ) {
|
|
|
673
736
|
if (!(elem === item._artifact || // redirection for explicit def
|
|
674
737
|
elem._origin === item._artifact)) {
|
|
675
738
|
const art = assoc._origin;
|
|
676
|
-
// eslint-disable-next-line max-len
|
|
739
|
+
// eslint-disable-next-line @stylistic/js/max-len
|
|
677
740
|
warning( 'rewrite-shadowed', [ elem.name.location, elem ], { art: art && effectiveType( art ) }, {
|
|
678
|
-
// eslint-disable-next-line max-len
|
|
741
|
+
// eslint-disable-next-line @stylistic/js/max-len
|
|
679
742
|
std: 'This element is not originally referred to in the ON-condition of association $(ART)',
|
|
680
|
-
// eslint-disable-next-line max-len
|
|
743
|
+
// eslint-disable-next-line @stylistic/js/max-len
|
|
681
744
|
element: 'This element is not originally referred to in the ON-condition of association $(MEMBER) of $(ART)',
|
|
682
745
|
} );
|
|
683
746
|
}
|
|
@@ -685,17 +748,147 @@ function tweakAssocs( model ) {
|
|
|
685
748
|
}
|
|
686
749
|
}
|
|
687
750
|
|
|
751
|
+
/**
|
|
752
|
+
* Rewrite the given reference by using projected elements of the given
|
|
753
|
+
* navigation environment.
|
|
754
|
+
*
|
|
755
|
+
* @param {XSN.Expression} ref
|
|
756
|
+
* @param {object} navEnv
|
|
757
|
+
* @param {XSN.Artifact} user
|
|
758
|
+
*/
|
|
759
|
+
function rewritePathForEnv( ref, navEnv, user ) {
|
|
760
|
+
// TODO: combine with rewriteGenericAnnoPath() of xpr-rewrite
|
|
761
|
+
|
|
762
|
+
// reset artifact link; we'll set it again if there are no errors
|
|
763
|
+
setArtifactLink( ref, null );
|
|
764
|
+
|
|
765
|
+
const rootItem = ref.path[0];
|
|
766
|
+
const root = ref.path[0]._navigation || ref.path[0]._artifact;
|
|
767
|
+
const startIndex = (root.kind === '$self' ? 1 : 0);
|
|
768
|
+
|
|
769
|
+
if (root.kind === '$self') {
|
|
770
|
+
let rootEnv = navEnv;
|
|
771
|
+
while (rootEnv?.kind === '$navElement') {
|
|
772
|
+
if (rootEnv._origin?.target?._artifact === root._origin)
|
|
773
|
+
break;
|
|
774
|
+
rootEnv = rootEnv._parent;
|
|
775
|
+
}
|
|
776
|
+
navEnv = rootEnv;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Store the original artifact, so that we can use it to
|
|
780
|
+
// calculate a redirection chain later on.
|
|
781
|
+
ref.path.forEach((item) => {
|
|
782
|
+
if (item._artifact)
|
|
783
|
+
setLink( item, '_originalArtifact', item._artifact );
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
let env = navEnv;
|
|
787
|
+
let art = rootItem._artifact;
|
|
788
|
+
let isTargetSide = null;
|
|
789
|
+
|
|
790
|
+
for (let i = startIndex; i < ref.path.length; ++i) {
|
|
791
|
+
if (i > startIndex && art.target) {
|
|
792
|
+
// if the current artifact is an association, we need to respect the redirection
|
|
793
|
+
// chain from original target to new one.
|
|
794
|
+
// FIXME: Won't work with associations in projected structures.
|
|
795
|
+
const origTarget = ref.path[i - 1]?._originalArtifact?.target?._artifact;
|
|
796
|
+
const chain = cachedRedirectionChain( art, origTarget );
|
|
797
|
+
if (!chain) {
|
|
798
|
+
missingProjection( ref, i, user, false );
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
for (const alias of chain) {
|
|
802
|
+
art = rewritePathItemForEnv( ref, alias, i, user );
|
|
803
|
+
isTargetSide ??= (art === user);
|
|
804
|
+
if (!art) {
|
|
805
|
+
missingProjection( ref, i, user, isTargetSide );
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
art = rewritePathItemForEnv( ref, env, i, user );
|
|
812
|
+
isTargetSide ??= (art === user);
|
|
813
|
+
if (!art) {
|
|
814
|
+
missingProjection( ref, i, user, isTargetSide );
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
env = navigationEnv( art, null, null, 'nav' );
|
|
818
|
+
}
|
|
819
|
+
setArtifactLink( ref, art );
|
|
820
|
+
|
|
821
|
+
if (startIndex === 0 && rootItem.id.startsWith('$')) {
|
|
822
|
+
// TODO: What about filters? Also rewritten there?
|
|
823
|
+
// After rewriting, if an element starts with `$` -> add root prefix
|
|
824
|
+
// FIXME: "user" not correct for association inside sub-element,
|
|
825
|
+
// because `user._parent` is assumed to be the query
|
|
826
|
+
prependSelfToPath( ref.path, user );
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function rewritePathItemForEnv( ref, navEnv, index, user ) {
|
|
831
|
+
const rewriteTarget = findRewriteTarget( ref, index, navEnv, user );
|
|
832
|
+
const found = rewriteTarget[0];
|
|
833
|
+
if (!found) {
|
|
834
|
+
setArtifactLink( ref.path[index], found );
|
|
835
|
+
return found;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (rewriteTarget[1] > index) {
|
|
839
|
+
// we keep the last segment, in case it has non-enumerable properties
|
|
840
|
+
ref.path[index] = ref.path[rewriteTarget[1]];
|
|
841
|
+
ref.path.splice(index + 1, rewriteTarget[1] - index);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const item = ref.path[index];
|
|
845
|
+
if (item.id !== found.name.id || (rewriteTarget[1] - index) !== 0)
|
|
846
|
+
item.id = found.name.id;
|
|
847
|
+
|
|
848
|
+
return setArtifactLink( ref.path[index], found );
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* @param {XSN.Path} ref
|
|
853
|
+
* @param {number} index
|
|
854
|
+
* @param {XSN.Artifact} user
|
|
855
|
+
* @param {boolean} isTargetSide
|
|
856
|
+
*/
|
|
857
|
+
function missingProjection( ref, index, user, isTargetSide ) {
|
|
858
|
+
const item = ref.path[index];
|
|
859
|
+
if (!isTargetSide) {
|
|
860
|
+
const { location } = user.value;
|
|
861
|
+
const rootItem = ref.path[0];
|
|
862
|
+
const elemref = rootItem._navigation?.kind === '$self' ? ref.path.slice(1) : ref.path;
|
|
863
|
+
// TODO: Fix message for sub-elements: `s: { a: Association on x=1, x: Integer};` for x
|
|
864
|
+
error( 'rewrite-not-projected', [ location, user ], {
|
|
865
|
+
name: user.name.id,
|
|
866
|
+
art: item._artifact || item._originalArtifact,
|
|
867
|
+
elemref: { ref: elemref },
|
|
868
|
+
} );
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
const isExplicit = user.target && !user.target.$inferred;
|
|
872
|
+
const loc = isExplicit ? user.target.location : item.location;
|
|
873
|
+
error( 'query-undefined-element', [ loc, user ], {
|
|
874
|
+
'#': isExplicit ? 'redirected' : 'std',
|
|
875
|
+
id: item.id,
|
|
876
|
+
name: user.name.id,
|
|
877
|
+
target: user.target._artifact,
|
|
878
|
+
keyword: 'redirected to',
|
|
879
|
+
} );
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
688
883
|
function rewritePath( ref, item, assoc, elem, location ) {
|
|
689
884
|
const { path } = ref;
|
|
690
885
|
const root = path[0];
|
|
691
886
|
if (!elem) {
|
|
692
887
|
if (location) {
|
|
693
888
|
const elemref = root._navigation?.kind === '$self' ? path.slice(1) : path;
|
|
889
|
+
// TODO: Fix message for sub-elements: `s: { a: Association on x=1, x: Integer};` for x
|
|
694
890
|
error( 'rewrite-not-projected', [ location, assoc ], {
|
|
695
|
-
name: assoc.name.id, art:
|
|
696
|
-
}, {
|
|
697
|
-
std: 'Projected association $(NAME) uses non-projected element $(ELEMREF)',
|
|
698
|
-
element: 'Projected association $(NAME) uses non-projected element $(ELEMREF) of $(ART)',
|
|
891
|
+
name: assoc.name.id, art: elemref[0]._artifact, elemref: { ref: elemref },
|
|
699
892
|
} );
|
|
700
893
|
}
|
|
701
894
|
delete root._navigation;
|
|
@@ -729,14 +922,11 @@ function tweakAssocs( model ) {
|
|
|
729
922
|
if (i === item)
|
|
730
923
|
state = setArtifactLink( i, elem );
|
|
731
924
|
}
|
|
732
|
-
else
|
|
733
|
-
state = rewriteItem( state, i,
|
|
925
|
+
else {
|
|
926
|
+
state = rewriteItem( state, i, assoc );
|
|
734
927
|
if (!state || state === true)
|
|
735
928
|
break;
|
|
736
929
|
}
|
|
737
|
-
else {
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
740
930
|
}
|
|
741
931
|
if (state !== true)
|
|
742
932
|
setArtifactLink( ref, state );
|
|
@@ -749,9 +939,15 @@ function tweakAssocs( model ) {
|
|
|
749
939
|
path.unshift( root );
|
|
750
940
|
}
|
|
751
941
|
|
|
752
|
-
|
|
942
|
+
/**
|
|
943
|
+
* @param elem "Navigation environment" (element) for `item`.
|
|
944
|
+
* @param item Path segment to rewrite.
|
|
945
|
+
* @param assoc Published association of query.
|
|
946
|
+
*/
|
|
947
|
+
function rewriteItem( elem, item, assoc ) {
|
|
753
948
|
if (!elem._redirected)
|
|
754
949
|
return true;
|
|
950
|
+
let name = item.id;
|
|
755
951
|
for (const alias of elem._redirected) {
|
|
756
952
|
// TODO: a message for the same situation as msg 'rewrite-shadowed'?
|
|
757
953
|
if (alias.kind === '$tableAlias') { // _redirected also contains structures for includes
|
|
@@ -760,21 +956,8 @@ function tweakAssocs( model ) {
|
|
|
760
956
|
// but its origins, too.
|
|
761
957
|
const proj = navProjection( alias.elements[name], assoc );
|
|
762
958
|
name = proj?.name?.id;
|
|
763
|
-
if (!name)
|
|
764
|
-
|
|
765
|
-
break;
|
|
766
|
-
setArtifactLink( item, null );
|
|
767
|
-
const culprit = elem.target && !elem.target.$inferred && elem.target ||
|
|
768
|
-
elem.value?.path?.[elem.value.path.length - 1] ||
|
|
769
|
-
elem;
|
|
770
|
-
// TODO: probably better to collect the non-projected foreign keys
|
|
771
|
-
// and have one message for all
|
|
772
|
-
error( 'rewrite-undefined-key', [ weakLocation( culprit.location ), assoc ], {
|
|
773
|
-
'#': 'std', id: item.id, target: alias._main, name: assoc.name.id,
|
|
774
|
-
});
|
|
775
|
-
// ''
|
|
776
|
-
return null;
|
|
777
|
-
}
|
|
959
|
+
if (!name)
|
|
960
|
+
break;
|
|
778
961
|
item.id = name;
|
|
779
962
|
// TODO: Why not break here? Test test3/scenarios/AFC/db/view/consumption/C_ScopedRole.cds
|
|
780
963
|
}
|
|
@@ -798,20 +981,98 @@ function tweakAssocs( model ) {
|
|
|
798
981
|
} );
|
|
799
982
|
return null;
|
|
800
983
|
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Get the redirection chain between the element's target and the original target.
|
|
987
|
+
* Returns `null` if there is no valid chain.
|
|
988
|
+
* Uses `_redirected` if valid.
|
|
989
|
+
*
|
|
990
|
+
* @param {XSN.Artifact} elem
|
|
991
|
+
* @param {XSN.Artifact} origTarget
|
|
992
|
+
* @returns {null|XSN.Artifact[]}
|
|
993
|
+
*/
|
|
994
|
+
function cachedRedirectionChain( elem, origTarget ) {
|
|
995
|
+
const target = elem.target?._artifact;
|
|
996
|
+
if (!target || !origTarget)
|
|
997
|
+
return null;
|
|
998
|
+
if (target === origTarget)
|
|
999
|
+
return [];
|
|
1000
|
+
|
|
1001
|
+
if (elem._redirected === null) {
|
|
1002
|
+
// means: "don't touch paths after assoc"
|
|
1003
|
+
// TODO: figure out if we can assume that here as well
|
|
1004
|
+
return [];
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (elem._redirected) {
|
|
1008
|
+
// No need to recalculate if the original target is already in '_redirected'.
|
|
1009
|
+
const i = elem._redirected.findIndex(ta => ta._origin === origTarget);
|
|
1010
|
+
if (i > -1)
|
|
1011
|
+
return elem._redirected.slice(i); // TODO: check if it is always "i===0".
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
return redirectionChain( elem, target, origTarget, true );
|
|
1015
|
+
}
|
|
801
1016
|
}
|
|
802
1017
|
|
|
803
1018
|
function navProjection( navigation, preferred ) {
|
|
804
1019
|
// TODO: Info if more than one possibility?
|
|
805
|
-
// console.log(navigation,navigation._projections)
|
|
806
1020
|
if (!navigation)
|
|
807
1021
|
return {};
|
|
808
|
-
|
|
1022
|
+
|
|
1023
|
+
if (!navigation._projections && !navigation._complexProjections)
|
|
809
1024
|
return null;
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1025
|
+
|
|
1026
|
+
// _complexProjections contains projections that are not "simple",
|
|
1027
|
+
// i.e. contain a filter or arguments. Only used if it contains our
|
|
1028
|
+
// preferred association.
|
|
1029
|
+
if (preferred && ( navigation._complexProjections?.includes( preferred ) ||
|
|
1030
|
+
navigation._projections?.includes( preferred )))
|
|
1031
|
+
return preferred;
|
|
1032
|
+
|
|
1033
|
+
return navigation._projections?.[0] || null;
|
|
813
1034
|
}
|
|
814
1035
|
|
|
1036
|
+
function findRewriteTarget( expr, index, env, user ) {
|
|
1037
|
+
if (env.kind === '$navElement' || env.kind === '$tableAlias') {
|
|
1038
|
+
const r = firstProjectionForPath( expr.path, index, env, user );
|
|
1039
|
+
return [ r.elem, r.index ];
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const item = expr.path[index];
|
|
1043
|
+
// If the artifact is already in the same definition, we must not check the query.
|
|
1044
|
+
// Or if it is not a query -> no $navElement -> use `elements`
|
|
1045
|
+
if (item._artifact?._main === env || !env.query && env.kind !== 'select') {
|
|
1046
|
+
if (env.elements?.[item.id])
|
|
1047
|
+
return [ env.elements[item.id], index ];
|
|
1048
|
+
return [ null, expr.path.length ];
|
|
1049
|
+
}
|
|
1050
|
+
const items = (env._leadingQuery || env)._combined?.[item.id];
|
|
1051
|
+
const allNavs = !items || Array.isArray(items) ? items : [ items ];
|
|
1052
|
+
|
|
1053
|
+
// If the annotation target itself has a table alias, require projections of that
|
|
1054
|
+
// table alias. Of course, that only works if we're talking about the same query.
|
|
1055
|
+
const tableAlias = (user._main?._origin === item._artifact?._main &&
|
|
1056
|
+
user.value?.path[0]?._navigation?.kind === '$tableAlias')
|
|
1057
|
+
? user.value.path[0]._navigation : null;
|
|
1058
|
+
|
|
1059
|
+
// Look at all table aliase that could project `item` and only select
|
|
1060
|
+
// those that have actual projections.
|
|
1061
|
+
const navs = allNavs?.filter(p => p._origin === item._artifact &&
|
|
1062
|
+
(!tableAlias || tableAlias === p._parent));
|
|
1063
|
+
if (!navs || navs.length === 0)
|
|
1064
|
+
return [ null, expr.path.length ];
|
|
1065
|
+
|
|
1066
|
+
// If there are multiple navigations for the element, just use the first that matches.
|
|
1067
|
+
// In case of table aliases, it's just one.
|
|
1068
|
+
for (const nav of navs) {
|
|
1069
|
+
const r = firstProjectionForPath( expr.path, index, nav._parent, user );
|
|
1070
|
+
if (r.elem)
|
|
1071
|
+
return [ r.elem, r.index ];
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
return [ null, expr.path.length ];
|
|
1075
|
+
}
|
|
815
1076
|
|
|
816
1077
|
/**
|
|
817
1078
|
* For a path `a.b.c.d`, return a projection for the first path item that is projected,
|
|
@@ -824,6 +1085,9 @@ function navProjection( navigation, preferred ) {
|
|
|
824
1085
|
* The returned object `ret` has `ret.item`, which is the path item at index `ret.index`
|
|
825
1086
|
* that is projected. `ret.elem` is the element projection.
|
|
826
1087
|
*
|
|
1088
|
+
* If nothing was found, `ret.elem` is null, and `ret.item` is the last segment for which
|
|
1089
|
+
* there was a $navElement.
|
|
1090
|
+
*
|
|
827
1091
|
* @param {any[]} path
|
|
828
1092
|
* @param {number} startIndex
|
|
829
1093
|
* @param {object} nav
|
|
@@ -844,32 +1108,72 @@ function firstProjectionForPath( path, startIndex, nav, elem ) {
|
|
|
844
1108
|
|
|
845
1109
|
let proj = null;
|
|
846
1110
|
let navItem = nav;
|
|
847
|
-
|
|
848
|
-
|
|
1111
|
+
let navIndex = startIndex;
|
|
1112
|
+
for (; navIndex < path.length; ++navIndex) {
|
|
1113
|
+
const item = path[navIndex];
|
|
849
1114
|
navItem = item?.id && navItem.elements?.[item.id];
|
|
850
1115
|
if (!navItem) {
|
|
851
1116
|
break;
|
|
852
1117
|
}
|
|
853
|
-
else if (navItem._projections) {
|
|
1118
|
+
else if (navItem._projections || navItem._complexProjections) {
|
|
854
1119
|
const projElem = navProjection( navItem, elem );
|
|
855
1120
|
if (projElem && projElem === elem) {
|
|
856
1121
|
// in case the specified association is found, _always_ use it.
|
|
857
|
-
return { index:
|
|
1122
|
+
return { index: navIndex, item, elem };
|
|
858
1123
|
}
|
|
859
1124
|
else if (projElem) {
|
|
860
1125
|
const queryIndex = selectedElements.indexOf(projElem);
|
|
861
1126
|
if (!proj || queryIndex < proj.queryIndex) {
|
|
862
1127
|
proj = {
|
|
863
|
-
index:
|
|
1128
|
+
index: navIndex, item, elem: projElem, queryIndex,
|
|
864
1129
|
};
|
|
865
1130
|
}
|
|
866
1131
|
}
|
|
867
1132
|
}
|
|
868
1133
|
}
|
|
1134
|
+
if (proj)
|
|
1135
|
+
return proj;
|
|
869
1136
|
|
|
870
|
-
|
|
1137
|
+
const index = (navIndex - 1) <= startIndex ? startIndex : (navIndex - 1);
|
|
1138
|
+
return { index, item: path[index], elem: null };
|
|
871
1139
|
}
|
|
872
1140
|
|
|
1141
|
+
/**
|
|
1142
|
+
* Follow the navigation along the given path to its N-1 path step, so
|
|
1143
|
+
* that the last step can be resolved against the returned navigation like
|
|
1144
|
+
* `returnValue.elements[last.id]`.
|
|
1145
|
+
*
|
|
1146
|
+
* @param {XSN.Path} path
|
|
1147
|
+
* @param {object} nav
|
|
1148
|
+
* @returns {object|null}
|
|
1149
|
+
*/
|
|
1150
|
+
function followNavigationPath( path, nav ) {
|
|
1151
|
+
if (!nav.item || !path || path.length === 1)
|
|
1152
|
+
return nav.tableAlias;
|
|
1153
|
+
|
|
1154
|
+
const startIndex = path.indexOf(nav.item);
|
|
1155
|
+
if (startIndex === -1)
|
|
1156
|
+
return null;
|
|
1157
|
+
|
|
1158
|
+
// navigation is already at last path step
|
|
1159
|
+
if (startIndex === path.length - 1) {
|
|
1160
|
+
return nav.navigation?.kind === '$navElement'
|
|
1161
|
+
? nav.navigation._parent
|
|
1162
|
+
: nav.tableAlias;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
let navItem = nav.navigation || nav.tableAlias;
|
|
1166
|
+
for (let i = startIndex + 1; i < path.length - 1; ++i) {
|
|
1167
|
+
const item = path[i];
|
|
1168
|
+
navItem = item?.id && navItem.elements?.[item.id];
|
|
1169
|
+
if (!navItem)
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
return navItem;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
|
|
873
1177
|
/**
|
|
874
1178
|
* Return condensed info about reference in select item
|
|
875
1179
|
* - tableAlias.elem -> { navigation: navElem, item: path[1], tableAlias }
|