@sap/cds-compiler 3.9.4 → 4.0.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 +92 -4
- package/README.md +0 -1
- package/bin/cdsc.js +11 -23
- package/bin/cdsse.js +3 -3
- package/doc/API.md +5 -0
- package/doc/CHANGELOG_ARCHIVE.md +1 -1
- package/doc/CHANGELOG_BETA.md +17 -1
- package/doc/CHANGELOG_DEPRECATED.md +28 -0
- package/lib/api/.eslintrc.json +1 -1
- package/lib/api/main.js +26 -8
- package/lib/api/options.js +2 -0
- package/lib/base/error.js +2 -0
- package/lib/base/message-registry.js +143 -64
- package/lib/base/messages.js +213 -107
- package/lib/base/model.js +11 -11
- package/lib/checks/.eslintrc.json +1 -1
- package/lib/checks/annotationsOData.js +2 -2
- package/lib/checks/elements.js +1 -1
- package/lib/checks/enricher.js +26 -3
- package/lib/checks/onConditions.js +67 -12
- package/lib/checks/queryNoDbArtifacts.js +106 -105
- package/lib/checks/sql-snippets.js +2 -0
- package/lib/checks/types.js +12 -6
- package/lib/checks/validator.js +2 -2
- package/lib/compiler/assert-consistency.js +10 -8
- package/lib/compiler/builtins.js +8 -2
- package/lib/compiler/checks.js +52 -35
- package/lib/compiler/define.js +31 -26
- package/lib/compiler/extend.js +120 -65
- package/lib/compiler/finalize-parse-cdl.js +12 -43
- package/lib/compiler/generate.js +16 -5
- package/lib/compiler/index.js +8 -5
- package/lib/compiler/kick-start.js +4 -3
- package/lib/compiler/populate.js +96 -95
- package/lib/compiler/propagator.js +7 -8
- package/lib/compiler/resolve.js +377 -103
- package/lib/compiler/shared.js +794 -517
- package/lib/compiler/tweak-assocs.js +8 -6
- package/lib/compiler/utils.js +44 -0
- package/lib/edm/annotations/genericTranslation.js +12 -4
- package/lib/edm/csn2edm.js +34 -32
- package/lib/edm/edm.js +34 -31
- package/lib/edm/edmAnnoPreprocessor.js +0 -23
- package/lib/edm/edmInboundChecks.js +7 -2
- package/lib/edm/edmPreprocessor.js +18 -17
- package/lib/edm/edmUtils.js +8 -4
- package/lib/gen/Dictionary.json +18 -0
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +4 -2
- package/lib/gen/languageParser.js +5006 -4582
- package/lib/json/from-csn.js +157 -112
- package/lib/json/to-csn.js +60 -89
- package/lib/language/antlrParser.js +17 -13
- package/lib/language/docCommentParser.js +11 -1
- package/lib/language/genericAntlrParser.js +13 -10
- package/lib/language/language.g4 +168 -97
- package/lib/main.d.ts +128 -36
- package/lib/main.js +1 -1
- package/lib/model/csnRefs.js +24 -5
- package/lib/model/csnUtils.js +9 -8
- package/lib/model/revealInternalProperties.js +7 -12
- package/lib/modelCompare/compare.js +1 -1
- package/lib/modelCompare/utils/filter.js +40 -2
- package/lib/optionProcessor.js +0 -3
- package/lib/render/toCdl.js +247 -214
- package/lib/render/toHdbcds.js +197 -181
- package/lib/render/toSql.js +325 -289
- package/lib/render/utils/common.js +42 -4
- package/lib/render/utils/delta.js +1 -1
- package/lib/render/utils/sql.js +3 -3
- package/lib/transform/braceExpression.js +2 -2
- package/lib/transform/db/.eslintrc.json +1 -1
- package/lib/transform/db/applyTransformations.js +3 -3
- package/lib/transform/db/associations.js +24 -12
- package/lib/transform/db/expansion.js +17 -18
- package/lib/transform/db/flattening.js +17 -21
- package/lib/transform/db/rewriteCalculatedElements.js +171 -64
- package/lib/transform/db/views.js +3 -4
- package/lib/transform/draft/db.js +21 -12
- package/lib/transform/draft/odata.js +4 -0
- package/lib/transform/forOdataNew.js +11 -10
- package/lib/transform/forRelationalDB.js +12 -7
- package/lib/transform/localized.js +4 -2
- package/lib/transform/odata/toFinalBaseType.js +5 -5
- package/lib/transform/odata/typesExposure.js +3 -3
- package/lib/transform/parseExpr.js +3 -0
- package/lib/transform/transformUtilsNew.js +43 -23
- package/lib/transform/translateAssocsToJoins.js +7 -6
- package/lib/transform/universalCsn/.eslintrc.json +1 -1
- package/lib/transform/universalCsn/coreComputed.js +7 -5
- package/lib/transform/universalCsn/universalCsnEnricher.js +12 -12
- package/package.json +2 -2
- package/share/messages/{duplicate-autoexposed.md → def-duplicate-autoexposed.md} +5 -1
- package/share/messages/message-explanations.json +1 -1
package/lib/compiler/resolve.js
CHANGED
|
@@ -43,6 +43,7 @@ const {
|
|
|
43
43
|
forEachMember,
|
|
44
44
|
forEachGeneric,
|
|
45
45
|
forEachInOrder,
|
|
46
|
+
isDeprecatedEnabled,
|
|
46
47
|
} = require('../base/model');
|
|
47
48
|
const { dictAdd } = require('../base/dictionaries');
|
|
48
49
|
const { dictLocation } = require('../base/location');
|
|
@@ -61,6 +62,7 @@ const {
|
|
|
61
62
|
testExpr,
|
|
62
63
|
targetMaxNotOne,
|
|
63
64
|
traverseQueryPost,
|
|
65
|
+
traverseExpr,
|
|
64
66
|
} = require('./utils');
|
|
65
67
|
|
|
66
68
|
const detectCycles = require('./cycle-detector');
|
|
@@ -69,6 +71,10 @@ const $location = Symbol.for('cds.$location');
|
|
|
69
71
|
|
|
70
72
|
const $inferred = Symbol.for('cds.$inferred');
|
|
71
73
|
|
|
74
|
+
// TODO: make this part of specExpected in shared.js
|
|
75
|
+
// (standard: not possible on last if !ref.$expected → ref.scope: '$exists')
|
|
76
|
+
const expWithFilter = [ 'from', 'expand', 'inline' ];
|
|
77
|
+
|
|
72
78
|
// Export function of this file. Resolve type references in augmented CSN
|
|
73
79
|
// `model`. If the model has a property argument `messages`, do not throw
|
|
74
80
|
// exception in case of an error, but push the corresponding error object to
|
|
@@ -87,21 +93,21 @@ function resolve( model ) {
|
|
|
87
93
|
resolveType,
|
|
88
94
|
resolveTypeArgumentsUnchecked,
|
|
89
95
|
} = model.$functions;
|
|
90
|
-
const { environment } = model.$volatileFunctions;
|
|
91
96
|
Object.assign( model.$functions, {
|
|
92
97
|
resolveExpr,
|
|
93
98
|
} );
|
|
94
99
|
|
|
95
|
-
|
|
100
|
+
const ignoreSpecifiedElements
|
|
101
|
+
= isDeprecatedEnabled(model.options, 'ignoreSpecifiedQueryElements');
|
|
96
102
|
|
|
97
103
|
return doResolve();
|
|
98
104
|
|
|
99
105
|
function doResolve() {
|
|
100
|
-
// Phase 1: check paths in usings has been moved to kick-start.js Phase 2:
|
|
106
|
+
// Phase 1: check paths in `usings` has been moved to kick-start.js Phase 2:
|
|
101
107
|
// calculate/init view elements & collect views in order:
|
|
102
108
|
// TODO: It might be that we need to call propagateKeyProps() and
|
|
103
|
-
//
|
|
104
|
-
//
|
|
109
|
+
// addImplicitForeignKeys() in populate.js, as we might need to know the
|
|
110
|
+
// foreign keys in populate.js (foreign key access w/o JOINs).
|
|
105
111
|
|
|
106
112
|
// Phase 2+3: calculate keys along simple queries in collected views:
|
|
107
113
|
model._entities = Object.values( model.definitions )
|
|
@@ -143,7 +149,7 @@ function resolve( model ) {
|
|
|
143
149
|
return;
|
|
144
150
|
// TODO: what about elements where _origin is set without value?
|
|
145
151
|
// TODO: or should we push elems with `expand` sibling to extra list for
|
|
146
|
-
// better messages? (Whatever that means
|
|
152
|
+
// better messages? (Whatever that means exactly.)
|
|
147
153
|
const nav = pathNavigation( elem.value );
|
|
148
154
|
const { path } = elem.value;
|
|
149
155
|
const item = path[path.length - 1];
|
|
@@ -390,9 +396,6 @@ function resolve( model ) {
|
|
|
390
396
|
}
|
|
391
397
|
}
|
|
392
398
|
if (obj.target) {
|
|
393
|
-
// console.log(obj.name,obj._origin?.name,obj)
|
|
394
|
-
if (obj._origin && obj._origin.$inferred === 'REDIRECTED')
|
|
395
|
-
resolveTarget( art, obj._origin );
|
|
396
399
|
// console.log(error( 'test-target', [ obj.location, obj ],
|
|
397
400
|
// { target: obj.target, kind: obj.kind }, 'Target: $(TARGET), Kind $(KIND)'));
|
|
398
401
|
if (!obj.target.$inferred || obj.target.$inferred === 'aspect-composition')
|
|
@@ -406,15 +409,9 @@ function resolve( model ) {
|
|
|
406
409
|
error( 'non-assoc-in-mixin', [ (obj.type || obj.name).location, art ], {},
|
|
407
410
|
'Only unmanaged associations are allowed in mixin clauses' );
|
|
408
411
|
}
|
|
409
|
-
if (art.targetElement)
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
// we just look in target for the path
|
|
413
|
-
// TODO: also check that we do not follow associations? no args, no filter
|
|
414
|
-
resolvePath( art.targetElement, 'targetElement', art,
|
|
415
|
-
environment( target._artifact ), target._artifact );
|
|
416
|
-
}
|
|
417
|
-
}
|
|
412
|
+
if (art.targetElement) // in foreign keys
|
|
413
|
+
resolvePath( art.targetElement, 'targetElement', art );
|
|
414
|
+
|
|
418
415
|
// Resolve projections/views
|
|
419
416
|
// if (art.query)console.log( info( null, [art.query.location,art.query], 'VQ:' ).toString() );
|
|
420
417
|
|
|
@@ -436,7 +433,7 @@ function resolve( model ) {
|
|
|
436
433
|
}
|
|
437
434
|
|
|
438
435
|
resolveExpr( art.default, 'default', art );
|
|
439
|
-
resolveExpr( art.value, '
|
|
436
|
+
resolveExpr( art.value, (art.$syntax === 'calc' ? 'calc' : 'expr'), art );
|
|
440
437
|
if (art.type?.$inferred === 'cast')
|
|
441
438
|
inferTypePropertiesFromCast( art );
|
|
442
439
|
if (art.value) {
|
|
@@ -447,13 +444,229 @@ function resolve( model ) {
|
|
|
447
444
|
}
|
|
448
445
|
|
|
449
446
|
forEachMember( art, resolveRefs, art.targetAspect );
|
|
447
|
+
// After the resolving of foreign keys (and adding implicit ones):
|
|
448
|
+
if (obj.target?.$inferred === '')
|
|
449
|
+
checkRedirectedUserTarget( art );
|
|
450
|
+
|
|
451
|
+
if (!ignoreSpecifiedElements && art.elements$ && art.elements) {
|
|
452
|
+
for (const id in art.elements$) {
|
|
453
|
+
resolveRefs(art.elements$[id]);
|
|
454
|
+
checkSpecifiedElement(art.elements[id], art.elements$[id]);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Check whether the signature of the specified element matches that of the inferred one.
|
|
460
|
+
*
|
|
461
|
+
* TODO:
|
|
462
|
+
* - This function has a lot of quite similar code blocks; it should be refactored to
|
|
463
|
+
* combine them.
|
|
464
|
+
* - Some checks are not performed because of to.sql() backend "bugs", that affect the
|
|
465
|
+
* recompilation, such as flattening removing/not setting "key" where required.
|
|
466
|
+
*
|
|
467
|
+
* @param {XSN.Element} inferredElement
|
|
468
|
+
* @param {XSN.Element} specifiedElement
|
|
469
|
+
* @param {XSN.Element} user Only used for if specifiedElement is actually an `items`
|
|
470
|
+
*/
|
|
471
|
+
function checkSpecifiedElement( inferredElement, specifiedElement, user = specifiedElement ) {
|
|
472
|
+
if (!inferredElement || !specifiedElement)
|
|
473
|
+
return;
|
|
474
|
+
|
|
475
|
+
// Check explicit types: If either side has one, so must the other.
|
|
476
|
+
const sType = specifiedElement.type?._artifact;
|
|
477
|
+
const iType = getInferredPropFromOrigin('type')?._artifact || inferredElement;
|
|
478
|
+
|
|
479
|
+
// xor: could be missing a type;
|
|
480
|
+
// FIXME: The coding above returns incorrect iType for expand on associations
|
|
481
|
+
if (!specifiedElement.type && inferredElement.type) {
|
|
482
|
+
error('query-mismatched-element', [ specifiedElement.location, user ], {
|
|
483
|
+
'#': !specifiedElement.type ? 'missing' : 'extra', name: user.name.id, prop: 'type',
|
|
484
|
+
});
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
// If specified type is `null`, type could not be resolved.
|
|
488
|
+
else if (sType && sType !== iType) {
|
|
489
|
+
const typeName = iType?.name && sType?.name ? 'typeName' : 'type';
|
|
490
|
+
error('query-mismatched-element', [
|
|
491
|
+
specifiedElement.type.location || specifiedElement.location, user,
|
|
492
|
+
], {
|
|
493
|
+
'#': typeName,
|
|
494
|
+
name: user.name.id,
|
|
495
|
+
type: sType,
|
|
496
|
+
othertype: iType,
|
|
497
|
+
});
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// This relies on (element) expansion! Check that both sides have the following properties.
|
|
502
|
+
// On the inferred side, they are likely expanded.
|
|
503
|
+
if (!hasXorPropMismatch('elements') && !hasXorPropMismatch('items') &&
|
|
504
|
+
!hasXorPropMismatch('target') && !hasXorPropMismatch('enum')) {
|
|
505
|
+
// Element are already traversed via elements$ merging.
|
|
506
|
+
|
|
507
|
+
// only check items, if the specified one is not expanded/inferred
|
|
508
|
+
if (specifiedElement.items && !specifiedElement.items.$inferred)
|
|
509
|
+
checkSpecifiedElement(inferredElement.items, specifiedElement.items, specifiedElement);
|
|
510
|
+
|
|
511
|
+
if (specifiedElement.target &&
|
|
512
|
+
specifiedElement.target._artifact !== inferredElement.target._artifact) {
|
|
513
|
+
error('query-mismatched-element', [
|
|
514
|
+
specifiedElement.target.location || specifiedElement.location, user,
|
|
515
|
+
], {
|
|
516
|
+
'#': 'target',
|
|
517
|
+
name: user.name.id,
|
|
518
|
+
target: specifiedElement.target,
|
|
519
|
+
art: inferredElement.target,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (specifiedElement.foreignKeys) {
|
|
524
|
+
const sKeys = Object.keys(specifiedElement.foreignKeys);
|
|
525
|
+
/** @type {any} */
|
|
526
|
+
let iKeys = inferredElement;
|
|
527
|
+
if (inferredElement._effectiveType !== 0) {
|
|
528
|
+
while (iKeys._origin && !iKeys.foreignKeys)
|
|
529
|
+
iKeys = iKeys._origin;
|
|
530
|
+
}
|
|
531
|
+
iKeys = Object.keys(iKeys.foreignKeys || {});
|
|
532
|
+
if (sKeys.length !== iKeys.length || sKeys.some(key => !iKeys.includes(key))) {
|
|
533
|
+
error('query-mismatched-element', [
|
|
534
|
+
specifiedElement.foreignKeys.location || specifiedElement.location, user,
|
|
535
|
+
], {
|
|
536
|
+
'#': 'foreignKeys',
|
|
537
|
+
name: user.name.id,
|
|
538
|
+
target: specifiedElement.target,
|
|
539
|
+
art: inferredElement.target,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (specifiedElement.virtual) {
|
|
545
|
+
const iVirtual = getInferredPropFromOrigin('virtual')?.val || false;
|
|
546
|
+
if (!specifiedElement.virtual.val !== !iVirtual) {
|
|
547
|
+
error('query-mismatched-element', [
|
|
548
|
+
specifiedElement.virtual.location || specifiedElement.location, user,
|
|
549
|
+
], {
|
|
550
|
+
'#': 'prop', prop: 'virtual', name: user.name.id,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// If cardinality is not specified, the compiler uses the inferred one.
|
|
556
|
+
if (specifiedElement.cardinality) {
|
|
557
|
+
const sCardinality = specifiedElement.cardinality;
|
|
558
|
+
const iCardinality = getInferredPropFromOrigin('cardinality');
|
|
559
|
+
if (!iCardinality) {
|
|
560
|
+
error('query-mismatched-element', [
|
|
561
|
+
sCardinality.location || specifiedElement.location, user,
|
|
562
|
+
], {
|
|
563
|
+
'#': 'extra',
|
|
564
|
+
prop: 'cardinality',
|
|
565
|
+
name: user.name.id,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
// Note: Cardinality does not have sourceMin (CSN "srcmin").
|
|
570
|
+
const props = {
|
|
571
|
+
targetMax: 'max',
|
|
572
|
+
targetMin: 'min',
|
|
573
|
+
sourceMax: 'src',
|
|
574
|
+
};
|
|
575
|
+
for (const prop in props) {
|
|
576
|
+
if (sCardinality[prop]?.val === iCardinality[prop]?.val)
|
|
577
|
+
continue;
|
|
578
|
+
error('query-mismatched-element', [
|
|
579
|
+
sCardinality[prop]?.location || sCardinality.location || specifiedElement.location,
|
|
580
|
+
user,
|
|
581
|
+
], {
|
|
582
|
+
// eslint-disable-next-line no-nested-ternary
|
|
583
|
+
'#': !sCardinality[prop] ? 'missing' : (iCardinality[prop] ? 'prop' : 'extra'),
|
|
584
|
+
prop: `cardinality.${ props[prop] }`,
|
|
585
|
+
name: user.name.id,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (specifiedElement.value) {
|
|
592
|
+
error('query-unexpected-property', [
|
|
593
|
+
specifiedElement.value.location || specifiedElement.location, user,
|
|
594
|
+
], {
|
|
595
|
+
'#': 'calculatedElement', prop: 'value', name: user.name.id,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (specifiedElement.key) { // TODO: `|| inferredElement.key?.val`, once to.sql is fixed
|
|
600
|
+
// TODO: Do not use _origin chain for key; has been propagated in propagateKeyProps().
|
|
601
|
+
const iKey = getInferredPropFromOrigin('key')?.val;
|
|
602
|
+
// If "key" is specified or truthy in the inferred element, the values must match.
|
|
603
|
+
if (!iKey !== !specifiedElement.key?.val) {
|
|
604
|
+
error('query-mismatched-element', [
|
|
605
|
+
specifiedElement.key?.location || specifiedElement.location, user,
|
|
606
|
+
], {
|
|
607
|
+
'#': specifiedElement.key ? 'prop' : 'missing', prop: 'key', name: user.name.id,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (specifiedElement.enum) {
|
|
613
|
+
const iEnum = inferredElement.enum || inferredElement.value?.enum;
|
|
614
|
+
forEachGeneric(specifiedElement, 'enum', (sVal, name) => {
|
|
615
|
+
const iVal = iEnum[name];
|
|
616
|
+
if (!iVal) {
|
|
617
|
+
error('query-mismatched-element', [ specifiedElement.location, user ], {
|
|
618
|
+
'#': 'std',
|
|
619
|
+
name: user.name.id,
|
|
620
|
+
prop: 'enum',
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// TODO: Get $expanded enum values
|
|
625
|
+
/* if (iVal.value?.val !== sVal.value?.val || iVal.value?.['#'] !== sVal.value?.['#']) {
|
|
626
|
+
error('query-mismatched-element', [ specifiedElement.location, user ], {
|
|
627
|
+
'#': 'std',
|
|
628
|
+
name: user.name.id,
|
|
629
|
+
prop: 'enum',
|
|
630
|
+
});
|
|
631
|
+
} */
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function hasXorPropMismatch( prop ) {
|
|
637
|
+
// FIXME: `.value` check should be removed after #11183
|
|
638
|
+
// It appears the SQL backends expand a type in cast and add an `enum` property there.
|
|
639
|
+
// This property is directly in the `value` property, but not part of `inferredElement`
|
|
640
|
+
// which has a `type` property, but no `enum`.
|
|
641
|
+
if (!inferredElement[prop] !== !specifiedElement[prop] &&
|
|
642
|
+
!inferredElement.value?.[prop] !== !specifiedElement[prop]) {
|
|
643
|
+
error( 'query-mismatched-element', [ specifiedElement.location, specifiedElement ], {
|
|
644
|
+
'#': specifiedElement[prop] ? 'extra' : 'missing', name: user.name.id, prop,
|
|
645
|
+
});
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function getInferredPropFromOrigin( prop ) {
|
|
652
|
+
// Inferred property via _origin chain (0 === circular).
|
|
653
|
+
let element = inferredElement;
|
|
654
|
+
if (element._effectiveType !== 0) {
|
|
655
|
+
while (getOrigin(element) && !element[prop])
|
|
656
|
+
element = getOrigin(element);
|
|
657
|
+
}
|
|
658
|
+
return element[prop];
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
450
662
|
|
|
451
663
|
// Set '@Core.Computed' in the Core Compiler to have it propagated...
|
|
452
664
|
if (art.kind !== 'element' || art['@Core.Computed'])
|
|
453
665
|
return;
|
|
454
|
-
if (art.virtual
|
|
666
|
+
if (art.virtual?.val ||
|
|
455
667
|
art.value &&
|
|
456
668
|
(!art.value._artifact || !art.value.path || // in localization view: _artifact, but no path
|
|
669
|
+
art.value.stored?.val || // calculated elements on-write are always computed
|
|
457
670
|
art.value._artifact.kind === 'builtin' || art.value._artifact.kind === 'param' )) {
|
|
458
671
|
art['@Core.Computed'] = {
|
|
459
672
|
name: {
|
|
@@ -493,10 +706,23 @@ function resolve( model ) {
|
|
|
493
706
|
error( 'def-invalid-calc-elem', loc, { '#': parent.kind } );
|
|
494
707
|
}
|
|
495
708
|
else if (effectiveType(art)?.elements) {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
709
|
+
// For inferred (e.g. included) calc elements, this error is already emitted at the origin.
|
|
710
|
+
if (!art.$inferred) {
|
|
711
|
+
if (art.type)
|
|
712
|
+
error( 'type-unexpected-structure', [ art.type.location, art ], { '#': 'calc' } );
|
|
713
|
+
else
|
|
714
|
+
error( 'ref-unexpected-structured', [ art.value.location, art ], { '#': 'expr' } );
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
else if (effectiveType(art)?.items) {
|
|
718
|
+
// For inferred (e.g. included) calc elements, this error is already emitted at the origin.
|
|
719
|
+
if (!art.$inferred) {
|
|
720
|
+
const isCast = art.type?.$inferred === 'cast';
|
|
721
|
+
error( 'type-unexpected-many', [ (art.type || art.value).location, art ], {
|
|
722
|
+
'#': (!art.type && 'calc-implicit') || (isCast && 'calc-cast') || 'calc',
|
|
723
|
+
elemref: art.type ? undefined : { ref: art.value.path },
|
|
724
|
+
} );
|
|
725
|
+
}
|
|
500
726
|
}
|
|
501
727
|
else {
|
|
502
728
|
const noTruthyAllowed = [ 'localized', 'key', 'virtual' ];
|
|
@@ -567,7 +793,7 @@ function resolve( model ) {
|
|
|
567
793
|
} );
|
|
568
794
|
// if (!query.$inlines) console.log('RQ:',query)
|
|
569
795
|
for (const col of query.$inlines)
|
|
570
|
-
resolveExpr( col.value, 'expr', col
|
|
796
|
+
resolveExpr( col.value, 'expr', col );
|
|
571
797
|
// for (const col of query.$inlines)
|
|
572
798
|
// if (!col.value.path) throw new CompilerAssertion(col.name.element)
|
|
573
799
|
if (query !== query._main._leadingQuery) // will be done later
|
|
@@ -575,17 +801,17 @@ function resolve( model ) {
|
|
|
575
801
|
if (query.from)
|
|
576
802
|
resolveJoinOn( query.from );
|
|
577
803
|
if (query.where)
|
|
578
|
-
resolveExpr( query.where, 'expr', query
|
|
804
|
+
resolveExpr( query.where, 'expr', query ); // TODO: extra 'where'?
|
|
579
805
|
if (query.groupBy)
|
|
580
|
-
resolveBy( query.groupBy, 'expr' );
|
|
581
|
-
resolveExpr( query.having, 'expr', query
|
|
806
|
+
resolveBy( query.groupBy, 'expr', 'expr' ); // TODO: extra 'groupBy'?
|
|
807
|
+
resolveExpr( query.having, 'expr', query ); // TODO: extra 'having' or 'where'?
|
|
582
808
|
if (query.$orderBy) // ORDER BY from UNION:
|
|
583
809
|
// TODO clarify: can I access the tab alias of outer queries? If not:
|
|
584
810
|
// 4th arg query._main instead query._parent.
|
|
585
|
-
resolveBy( query.$orderBy, 'order-by-
|
|
811
|
+
resolveBy( query.$orderBy, 'order-by-set-ref', 'order-by-set-expr' );
|
|
586
812
|
if (query.orderBy) { // ORDER BY
|
|
587
813
|
// search in `query.elements` after having checked table aliases of the current query
|
|
588
|
-
resolveBy( query.orderBy, 'order-by',
|
|
814
|
+
resolveBy( query.orderBy, 'order-by-ref', 'order-by-expr' );
|
|
589
815
|
// TODO: disallow resulting element ref if in expression!
|
|
590
816
|
// Necessary to check it in the compiler as it might work with other semantics on DB!
|
|
591
817
|
// (we could downgrade it to a warning if name is equal to unique source element name)
|
|
@@ -598,7 +824,7 @@ function resolve( model ) {
|
|
|
598
824
|
for (const j of join.args)
|
|
599
825
|
resolveJoinOn( j );
|
|
600
826
|
if (join.on)
|
|
601
|
-
resolveExpr( join.on, '
|
|
827
|
+
resolveExpr( join.on, 'joinOn', query );
|
|
602
828
|
// TODO: check restrictions according to join "query"
|
|
603
829
|
}
|
|
604
830
|
}
|
|
@@ -609,23 +835,22 @@ function resolve( model ) {
|
|
|
609
835
|
// ORDER BY of an UNION, do not allow any dynamic path in an expression,
|
|
610
836
|
// and only allow the elements of the leading query if it is a path.
|
|
611
837
|
//
|
|
612
|
-
// This
|
|
838
|
+
// This seems to be similar, but different in SQLite 3.22.0: ORDER BY seems
|
|
613
839
|
// to bind stronger than UNION (see <SQLite>/src/parse.y), and the name
|
|
614
840
|
// resolution seems to use select item aliases from all SELECTs of the
|
|
615
841
|
// UNION (see <SQLite>/test/tkt2822.test).
|
|
616
|
-
function resolveBy( array,
|
|
842
|
+
function resolveBy( array, refMode, exprMode ) {
|
|
617
843
|
for (const value of array ) {
|
|
618
844
|
if (value)
|
|
619
|
-
resolveExpr( value,
|
|
845
|
+
resolveExpr( value, (value.path ? refMode : exprMode), query );
|
|
620
846
|
}
|
|
621
847
|
}
|
|
622
848
|
}
|
|
623
849
|
|
|
624
850
|
function resolveTarget( art, obj ) {
|
|
625
|
-
if (art !== obj && obj.on
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
'An association can\'t be used for arrays or parameters' );
|
|
851
|
+
if (art !== obj && obj.on) {
|
|
852
|
+
// Unmanaged assoc inside items. Unmanaged assoc in param handled in resolveRefs()
|
|
853
|
+
message( 'type-invalid-items', [ obj.on.location, art ], { '#': 'assoc', prop: 'items' } );
|
|
629
854
|
setArtifactLink( obj.target, undefined );
|
|
630
855
|
return;
|
|
631
856
|
}
|
|
@@ -653,19 +878,10 @@ function resolve( model ) {
|
|
|
653
878
|
});
|
|
654
879
|
// TODO: also warning if inside structure
|
|
655
880
|
}
|
|
656
|
-
else
|
|
881
|
+
else {
|
|
657
882
|
// TODO: extra with $inferred (to avoid messages)?
|
|
658
|
-
// TODO: in the ON condition of an explicitly provided model entity
|
|
659
|
-
// which is going to be implicitly redirected, we can never navigate
|
|
660
|
-
// along associations, even not to the foreign keys (at least if they
|
|
661
|
-
// are renamed) - introduce extra 'expected' which inspects REDIRECTED
|
|
662
883
|
resolveExpr( obj.on, art.kind === 'mixin' ? 'mixin-on' : 'on', art );
|
|
663
884
|
}
|
|
664
|
-
else {
|
|
665
|
-
const elements = Object.create( art._parent.elements );
|
|
666
|
-
elements[art.name.id] = obj;
|
|
667
|
-
resolveExpr( obj.on, art.kind === 'mixin' ? 'mixin-on' : 'on', art, elements );
|
|
668
|
-
}
|
|
669
885
|
}
|
|
670
886
|
else if (art.kind === 'mixin') {
|
|
671
887
|
error( 'assoc-in-mixin', [ obj.target.location, art ], {},
|
|
@@ -680,7 +896,7 @@ function resolve( model ) {
|
|
|
680
896
|
}
|
|
681
897
|
else if (target && !obj.foreignKeys && target.kind === 'entity') {
|
|
682
898
|
// redirected or explicit type cds.Association, ...
|
|
683
|
-
if (obj
|
|
899
|
+
if (obj.type?._artifact?.internal)
|
|
684
900
|
addImplicitForeignKeys( art, obj, target );
|
|
685
901
|
}
|
|
686
902
|
|
|
@@ -691,11 +907,89 @@ function resolve( model ) {
|
|
|
691
907
|
}
|
|
692
908
|
}
|
|
693
909
|
|
|
910
|
+
|
|
911
|
+
function checkRedirectedUserTarget( art ) {
|
|
912
|
+
const issue = { target: art.target._artifact };
|
|
913
|
+
const tgtPath = art.target.path;
|
|
914
|
+
const modelTarget = tgtPath[tgtPath.length - 1]._artifact; // Array#at comes with node-16.6
|
|
915
|
+
// Check ON condition: no renamed target element
|
|
916
|
+
traverseExpr( art.on, 'on-check', art, (expr) => {
|
|
917
|
+
const { path } = expr;
|
|
918
|
+
if (!expr?._artifact || path?.length < 2 || issue['#'])
|
|
919
|
+
return; // no path or with error or already found issue
|
|
920
|
+
const head = (path[0]._navigation?.kind === '$self') ? 1 : 0;
|
|
921
|
+
if (path[head]._artifact === art)
|
|
922
|
+
checkAutoRedirectedPathItem( path[head + 1], modelTarget, issue );
|
|
923
|
+
} );
|
|
924
|
+
// Check explicit+implicit foreign keys: no renamed target element
|
|
925
|
+
const implicit = art.foreignKeys?.[$inferred];
|
|
926
|
+
forEachGeneric( art, 'foreignKeys', (fkey) => {
|
|
927
|
+
const { targetElement } = fkey;
|
|
928
|
+
if (targetElement._artifact && !issue['#'])
|
|
929
|
+
checkAutoRedirectedPathItem( targetElement.path[0], modelTarget, issue, implicit );
|
|
930
|
+
} );
|
|
931
|
+
// Check implicit foreign keys: same keys in same order
|
|
932
|
+
if (implicit && !issue['#']) {
|
|
933
|
+
const serviceKeys = keyElementNames( issue.target.elements );
|
|
934
|
+
const modelKeys = keyElementNames( modelTarget.elements );
|
|
935
|
+
if (modelKeys.length !== serviceKeys.length) {
|
|
936
|
+
issue.id = modelKeys.find( id => !serviceKeys.includes( id ));
|
|
937
|
+
issue['#'] = 'missing';
|
|
938
|
+
}
|
|
939
|
+
else if (!modelKeys.every( (id, index) => id === serviceKeys[index] )) {
|
|
940
|
+
issue['#'] = 'order';
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (issue['#']) {
|
|
944
|
+
/* eslint-disable max-len */
|
|
945
|
+
message( 'type-expecting-service-target', [ art.target.location, art ], issue, {
|
|
946
|
+
std: 'Expecting service entity $(TARGET)',
|
|
947
|
+
ref: 'Expecting service entity $(TARGET); its element $(ID) referred to at line $(LINE), column $(COL) is not from an element with the same name in the provided model target',
|
|
948
|
+
key: 'Expecting service entity $(TARGET); its key element $(ID) is not from a key element with the same name in the provided model target',
|
|
949
|
+
missing: 'Expecting service entity $(TARGET); it does not have the key element $(ID) of the provided model target',
|
|
950
|
+
order: 'Expecting service entity $(TARGET); its key elements are in a different order than those of the provided model target',
|
|
951
|
+
} );
|
|
952
|
+
/* eslint-enable max-len */
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function keyElementNames( elements ) {
|
|
957
|
+
const names = [];
|
|
958
|
+
for (const name in elements) {
|
|
959
|
+
if (elements[name].key?.val)
|
|
960
|
+
names.push( name );
|
|
961
|
+
}
|
|
962
|
+
return names;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function checkAutoRedirectedPathItem( pathItem, modelTarget, issue, isKey = false ) {
|
|
966
|
+
if (!pathItem) // $self.assoc
|
|
967
|
+
return;
|
|
968
|
+
let targetElem = pathItem._artifact;
|
|
969
|
+
while (targetElem && targetElem._main !== modelTarget)
|
|
970
|
+
targetElem = directOrigin( targetElem );
|
|
971
|
+
if (targetElem?.name.id === pathItem.id && (!isKey || targetElem.key?.val))
|
|
972
|
+
return;
|
|
973
|
+
issue.id = pathItem.id;
|
|
974
|
+
issue.line = pathItem.location.line;
|
|
975
|
+
issue.col = pathItem.location.col;
|
|
976
|
+
issue['#'] = (isKey ? 'key' : 'ref');
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function directOrigin( elem ) {
|
|
980
|
+
if (!elem._main.query || !elem._origin)
|
|
981
|
+
return elem._origin; // included element
|
|
982
|
+
const { path } = elem.value;
|
|
983
|
+
const kind = path[0]._navigation?.kind;
|
|
984
|
+
// TODO: expand/inline (also Alias.*)
|
|
985
|
+
return [ null, '$navElement', '$tableAlias' ][path.length] === (kind || true) && elem._origin;
|
|
986
|
+
}
|
|
987
|
+
|
|
694
988
|
function addImplicitForeignKeys( art, obj, target ) {
|
|
695
989
|
obj.foreignKeys = Object.create(null);
|
|
696
990
|
forEachInOrder( target, 'elements', ( elem, name ) => {
|
|
697
991
|
if (elem.key && elem.key.val) {
|
|
698
|
-
const { location } =
|
|
992
|
+
const { location } = obj.target;
|
|
699
993
|
const key = {
|
|
700
994
|
name: { location, id: elem.name.id, $inferred: 'keys' }, // more by setMemberParent()
|
|
701
995
|
kind: 'key',
|
|
@@ -705,6 +999,7 @@ function resolve( model ) {
|
|
|
705
999
|
};
|
|
706
1000
|
setMemberParent( key, name, art );
|
|
707
1001
|
dictAdd( obj.foreignKeys, name, key );
|
|
1002
|
+
// the following should be done automatically, since we run resolveRefs after that
|
|
708
1003
|
setArtifactLink( key.targetElement, elem );
|
|
709
1004
|
setArtifactLink( key.targetElement.path[0], elem );
|
|
710
1005
|
setLink( key, '_effectiveType', effectiveType(elem) );
|
|
@@ -911,25 +1206,15 @@ function resolve( model ) {
|
|
|
911
1206
|
* If the effective type is an array or structured type, an error is emitted.
|
|
912
1207
|
*/
|
|
913
1208
|
function checkTypeArguments( artWithType, typeArt ) {
|
|
914
|
-
// Note: `_effectiveType`
|
|
915
|
-
//
|
|
916
|
-
// TODO(#8942): May not be necessary if effectiveType() is adapted. Furthermore, the enum
|
|
917
|
-
// trick may be removed if effectiveType() does not stop at enums.
|
|
918
|
-
// TODO: this is wrong - we must check typeArt.enum, not its effectiveType
|
|
1209
|
+
// Note: `_effectiveType` point to `artWithType` itself, if it is an enum type,
|
|
1210
|
+
// descend to the origin in this case.
|
|
919
1211
|
// TODO: this function is not complete(!): parallel `elements` and `length`, … - rework function
|
|
920
|
-
|
|
1212
|
+
// TODO: check relationship with resolveTypeArgumentsUnchecked()
|
|
921
1213
|
let effectiveTypeArt = effectiveType( typeArt );
|
|
922
|
-
while (effectiveTypeArt
|
|
923
|
-
|
|
924
|
-
const underlyingEnumType = getOrigin(effectiveTypeArt);
|
|
925
|
-
if (underlyingEnumType)
|
|
926
|
-
effectiveTypeArt = effectiveType(underlyingEnumType);
|
|
927
|
-
else
|
|
928
|
-
break;
|
|
929
|
-
}
|
|
930
|
-
|
|
1214
|
+
while (effectiveTypeArt?.enum)
|
|
1215
|
+
effectiveTypeArt = effectiveType( getOrigin( effectiveTypeArt ) );
|
|
931
1216
|
if (!effectiveTypeArt)
|
|
932
|
-
return; // e.g. illegal definition references
|
|
1217
|
+
return; // e.g. illegal definition references, cycles, ...
|
|
933
1218
|
|
|
934
1219
|
const params = effectiveTypeArt.parameters &&
|
|
935
1220
|
effectiveTypeArt.parameters.map(p => p.name || p) || [];
|
|
@@ -970,40 +1255,30 @@ function resolve( model ) {
|
|
|
970
1255
|
}
|
|
971
1256
|
}
|
|
972
1257
|
|
|
973
|
-
function resolveExpr( expr,
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
// TODO: extra "expected" 'expand'/'inline' instead o param `expandOrInline`
|
|
977
|
-
if (!expr || typeof expr === 'string') // parse error or keywords in {xpr:...}
|
|
978
|
-
return;
|
|
979
|
-
if (Array.isArray(expr)) {
|
|
980
|
-
expr.forEach( e => resolveExpr( e, expected, user, extDict ) );
|
|
981
|
-
return;
|
|
982
|
-
}
|
|
1258
|
+
function resolveExpr( expr, exprCtx, user ) {
|
|
1259
|
+
traverseExpr( expr, exprCtx, user, (e, c, u) => resolveExprItem( e, c, u ) );
|
|
1260
|
+
}
|
|
983
1261
|
|
|
1262
|
+
function resolveExprItem( expr, expected, user ) {
|
|
984
1263
|
if (expr.type) // e.g. cast( a as Integer )
|
|
985
|
-
resolveTypeExpr( expr, user );
|
|
1264
|
+
resolveTypeExpr( expr, user._user || user );
|
|
986
1265
|
|
|
987
1266
|
if (expr.path) {
|
|
988
1267
|
if (expr.$expected === 'exists') {
|
|
989
1268
|
error( 'expr-unexpected-exists', [ expr.location, user ], {},
|
|
990
1269
|
'An EXISTS predicate is not expected here' );
|
|
991
|
-
// We complain about the EXISTS before, as EXISTS subquery is also not
|
|
992
|
-
//
|
|
993
|
-
// will be supported), TODO: location of EXISTS
|
|
1270
|
+
// We complain about the EXISTS before, as EXISTS subquery is also not supported
|
|
1271
|
+
// TODO: location of EXISTS, TODO: really do this in define.js
|
|
994
1272
|
expr.$expected = 'approved-exists'; // only complain once
|
|
995
1273
|
}
|
|
996
|
-
|
|
997
|
-
expected( expr, user, extDict );
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
resolvePath( expr, expected, user, extDict );
|
|
1274
|
+
resolvePath( expr, expected, user );
|
|
1001
1275
|
|
|
1002
|
-
const last =
|
|
1276
|
+
const last = expr.$expected !== 'approved-exists' && expr.path[expr.path.length - 1];
|
|
1003
1277
|
for (const step of expr.path) {
|
|
1004
1278
|
if (step && (step.args || step.where || step.cardinality) &&
|
|
1005
1279
|
step._artifact && !Array.isArray( step._artifact ) )
|
|
1006
|
-
resolveParamsAndWhere( step, expected, user,
|
|
1280
|
+
resolveParamsAndWhere( step, expected, user, step === last );
|
|
1281
|
+
// TODO: delete 4th arg
|
|
1007
1282
|
}
|
|
1008
1283
|
}
|
|
1009
1284
|
else if (expr.query) {
|
|
@@ -1016,29 +1291,27 @@ function resolve( model ) {
|
|
|
1016
1291
|
'Subqueries are not supported here' );
|
|
1017
1292
|
}
|
|
1018
1293
|
}
|
|
1019
|
-
else if (expr.op && expr.args) {
|
|
1020
|
-
const args = Array.isArray(expr.args) ? expr.args : Object.values( expr.args );
|
|
1021
|
-
args.forEach( e => e && resolveExpr( e, e.$expected || expected, user, extDict ) );
|
|
1022
|
-
}
|
|
1023
|
-
if (expr.suffix)
|
|
1024
|
-
expr.suffix.forEach( s => s && resolveExpr( s, expected, user, extDict ) );
|
|
1025
1294
|
}
|
|
1026
1295
|
|
|
1027
|
-
function resolveParamsAndWhere( step, expected, user,
|
|
1296
|
+
function resolveParamsAndWhere( step, expected, user, isLast ) {
|
|
1028
1297
|
const alias = (step._navigation?.kind === '$tableAlias') ? step._navigation : null;
|
|
1029
1298
|
const type = alias || effectiveType( step._artifact );
|
|
1030
1299
|
const art = (type && type.target) ? type.target._artifact : type;
|
|
1031
1300
|
if (!art)
|
|
1032
1301
|
return;
|
|
1033
|
-
const entity =
|
|
1034
|
-
|
|
1302
|
+
const entity = art.kind === 'entity' &&
|
|
1303
|
+
(!isLast || user.expand || user.inline || expWithFilter.includes( expected )) &&
|
|
1304
|
+
art;
|
|
1035
1305
|
if (step.args)
|
|
1036
|
-
resolveParams( step.args, art, entity, expected, user,
|
|
1306
|
+
resolveParams( step.args, art, entity, expected, user, step.location );
|
|
1037
1307
|
if (entity) {
|
|
1038
|
-
if (step.where)
|
|
1039
|
-
|
|
1308
|
+
if (step.where) {
|
|
1309
|
+
setLink( step, '_user', user._user || user );
|
|
1310
|
+
const inCalc = (expected === 'calc' || expected === 'calc-filter');
|
|
1311
|
+
resolveExpr( step.where, (inCalc ? 'calc-filter' : 'filter'), step );
|
|
1312
|
+
}
|
|
1040
1313
|
}
|
|
1041
|
-
else if (step.where
|
|
1314
|
+
else if (step.where || step.cardinality ) {
|
|
1042
1315
|
const location = combinedLocation( step.where, step.cardinality );
|
|
1043
1316
|
let variant = alias ? 'tableAlias' : 'std';
|
|
1044
1317
|
if (expected === 'from')
|
|
@@ -1054,7 +1327,7 @@ function resolve( model ) {
|
|
|
1054
1327
|
}
|
|
1055
1328
|
}
|
|
1056
1329
|
|
|
1057
|
-
function resolveParams( dict, art, entity, expected, user,
|
|
1330
|
+
function resolveParams( dict, art, entity, expected, user, stepLocation ) {
|
|
1058
1331
|
if (!entity || !entity.params) {
|
|
1059
1332
|
let first = dict[Object.keys(dict)[0]];
|
|
1060
1333
|
if (Array.isArray(first))
|
|
@@ -1077,7 +1350,7 @@ function resolve( model ) {
|
|
|
1077
1350
|
message( 'args-expected-named', [ dict[0] && dict[0].location || stepLocation, user ], {},
|
|
1078
1351
|
'Named parameters must be provided for the entity' );
|
|
1079
1352
|
for (const a of dict)
|
|
1080
|
-
resolveExpr( a, exp, user
|
|
1353
|
+
resolveExpr( a, exp, user );
|
|
1081
1354
|
return;
|
|
1082
1355
|
}
|
|
1083
1356
|
// TODO: allow to specify expected for arguments in in specExpected
|
|
@@ -1090,7 +1363,8 @@ function resolve( model ) {
|
|
|
1090
1363
|
message( 'args-undefined-param', [ a.name.location, user ], { art, id: name },
|
|
1091
1364
|
'Entity $(ART) has no parameter $(ID)' );
|
|
1092
1365
|
}
|
|
1093
|
-
|
|
1366
|
+
// TODO: Also for other parameters?
|
|
1367
|
+
resolveExpr( a, (expected === 'from') ? 'param-only' : exp, user );
|
|
1094
1368
|
}
|
|
1095
1369
|
}
|
|
1096
1370
|
}
|