@sap/cds-compiler 3.9.4 → 4.0.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.
Files changed (95) hide show
  1. package/CHANGELOG.md +107 -4
  2. package/README.md +0 -1
  3. package/bin/cdsc.js +11 -23
  4. package/bin/cdsse.js +3 -3
  5. package/doc/API.md +5 -0
  6. package/doc/CHANGELOG_ARCHIVE.md +1 -1
  7. package/doc/CHANGELOG_BETA.md +17 -1
  8. package/doc/CHANGELOG_DEPRECATED.md +28 -0
  9. package/lib/api/.eslintrc.json +1 -1
  10. package/lib/api/main.js +55 -9
  11. package/lib/api/options.js +2 -0
  12. package/lib/base/error.js +2 -0
  13. package/lib/base/message-registry.js +143 -64
  14. package/lib/base/messages.js +213 -107
  15. package/lib/base/model.js +11 -11
  16. package/lib/checks/.eslintrc.json +1 -1
  17. package/lib/checks/annotationsOData.js +2 -2
  18. package/lib/checks/elements.js +1 -1
  19. package/lib/checks/enricher.js +26 -3
  20. package/lib/checks/onConditions.js +67 -12
  21. package/lib/checks/queryNoDbArtifacts.js +106 -105
  22. package/lib/checks/sql-snippets.js +2 -0
  23. package/lib/checks/types.js +12 -6
  24. package/lib/checks/validator.js +2 -2
  25. package/lib/compiler/assert-consistency.js +10 -8
  26. package/lib/compiler/builtins.js +8 -2
  27. package/lib/compiler/checks.js +52 -35
  28. package/lib/compiler/define.js +31 -26
  29. package/lib/compiler/extend.js +120 -65
  30. package/lib/compiler/finalize-parse-cdl.js +12 -43
  31. package/lib/compiler/generate.js +16 -5
  32. package/lib/compiler/index.js +8 -5
  33. package/lib/compiler/kick-start.js +4 -3
  34. package/lib/compiler/populate.js +96 -95
  35. package/lib/compiler/propagator.js +7 -8
  36. package/lib/compiler/resolve.js +377 -103
  37. package/lib/compiler/shared.js +794 -517
  38. package/lib/compiler/tweak-assocs.js +8 -6
  39. package/lib/compiler/utils.js +44 -0
  40. package/lib/edm/annotations/genericTranslation.js +41 -5
  41. package/lib/edm/csn2edm.js +34 -32
  42. package/lib/edm/edm.js +34 -31
  43. package/lib/edm/edmAnnoPreprocessor.js +0 -23
  44. package/lib/edm/edmInboundChecks.js +7 -2
  45. package/lib/edm/edmPreprocessor.js +25 -18
  46. package/lib/edm/edmUtils.js +8 -4
  47. package/lib/gen/Dictionary.json +18 -0
  48. package/lib/gen/language.checksum +1 -1
  49. package/lib/gen/language.interp +4 -2
  50. package/lib/gen/languageParser.js +5006 -4582
  51. package/lib/json/from-csn.js +157 -112
  52. package/lib/json/to-csn.js +60 -89
  53. package/lib/language/antlrParser.js +17 -13
  54. package/lib/language/docCommentParser.js +11 -1
  55. package/lib/language/genericAntlrParser.js +13 -10
  56. package/lib/language/language.g4 +168 -97
  57. package/lib/main.d.ts +128 -36
  58. package/lib/main.js +1 -1
  59. package/lib/model/csnRefs.js +24 -5
  60. package/lib/model/csnUtils.js +9 -8
  61. package/lib/model/revealInternalProperties.js +7 -12
  62. package/lib/model/sortViews.js +4 -2
  63. package/lib/modelCompare/compare.js +1 -1
  64. package/lib/modelCompare/utils/filter.js +40 -2
  65. package/lib/optionProcessor.js +0 -3
  66. package/lib/render/toCdl.js +247 -214
  67. package/lib/render/toHdbcds.js +197 -181
  68. package/lib/render/toSql.js +325 -289
  69. package/lib/render/utils/common.js +42 -4
  70. package/lib/render/utils/delta.js +1 -1
  71. package/lib/render/utils/sql.js +3 -3
  72. package/lib/transform/braceExpression.js +2 -2
  73. package/lib/transform/db/.eslintrc.json +1 -1
  74. package/lib/transform/db/applyTransformations.js +3 -3
  75. package/lib/transform/db/associations.js +24 -12
  76. package/lib/transform/db/expansion.js +17 -18
  77. package/lib/transform/db/flattening.js +17 -21
  78. package/lib/transform/db/rewriteCalculatedElements.js +171 -64
  79. package/lib/transform/db/views.js +3 -4
  80. package/lib/transform/draft/db.js +21 -12
  81. package/lib/transform/draft/odata.js +4 -0
  82. package/lib/transform/forOdataNew.js +62 -47
  83. package/lib/transform/forRelationalDB.js +12 -7
  84. package/lib/transform/localized.js +4 -2
  85. package/lib/transform/odata/toFinalBaseType.js +5 -5
  86. package/lib/transform/odata/typesExposure.js +3 -3
  87. package/lib/transform/parseExpr.js +3 -0
  88. package/lib/transform/transformUtilsNew.js +43 -23
  89. package/lib/transform/translateAssocsToJoins.js +7 -6
  90. package/lib/transform/universalCsn/.eslintrc.json +1 -1
  91. package/lib/transform/universalCsn/coreComputed.js +7 -5
  92. package/lib/transform/universalCsn/universalCsnEnricher.js +12 -12
  93. package/package.json +2 -2
  94. package/share/messages/{duplicate-autoexposed.md → def-duplicate-autoexposed.md} +5 -1
  95. package/share/messages/message-explanations.json +1 -1
@@ -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
- /** @type {any} may also be a boolean */
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
- // addImplicitForeignKeys() in populate.js, as we might need to know the
104
- // foreign keys in populate.js (foreign key access w/o JOINs).
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 exaclty.)
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) { // in foreign keys
410
- const target = parent && parent.target;
411
- if (target && target._artifact) {
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, 'expr', art, undefined, art.expand || art.inline );
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 && art.virtual.val ||
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
- if (art.type)
497
- error( 'type-unexpected-structure', [ art.type.location, art ], { '#': 'calc' } );
498
- else
499
- error( 'ref-unexpected-structured', [ art.value.location, art ], { '#': 'expr' } );
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, undefined, true );
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, query._combined );
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, query._combined );
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-union', query.elements, query._parent );
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', query.elements );
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, 'expr', query, query._combined );
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 seem to be similar, but different in SQLite 3.22.0: ORDER BY seems
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, mode, pathDict, q ) {
842
+ function resolveBy( array, refMode, exprMode ) {
617
843
  for (const value of array ) {
618
844
  if (value)
619
- resolveExpr( value, mode, q || query, value.path && pathDict );
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 && obj.$inferred !== 'REDIRECTED') {
626
- message( 'assoc-in-array', [ obj.on.location, art ], {},
627
- // TODO: also check parameter parent, two messages?
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 if (obj.$inferred !== 'REDIRECTED') {
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.$inferred === 'REDIRECTED' || obj.type?._artifact?.internal)
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 } = art.target;
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` may point to `artWithType` itself, if the type is structured.
915
- // Also: For enums, it points to the enum type, which is why this trick is needed.
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
- const cyclic = new Set();
1212
+ // TODO: check relationship with resolveTypeArgumentsUnchecked()
921
1213
  let effectiveTypeArt = effectiveType( typeArt );
922
- while (effectiveTypeArt && effectiveTypeArt.enum && !cyclic.has(effectiveTypeArt)) {
923
- cyclic.add(effectiveTypeArt);
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, expected, user, extDict, expandOrInline ) {
974
- // TODO: when we have rewritten the resolvePath functions,
975
- // define a traverseExpr() in ./utils.js
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
- // supported (avoid that word if you do not want to get tickets when it
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
- if (expected instanceof Function) {
997
- expected( expr, user, extDict );
998
- return;
999
- }
1000
- resolvePath( expr, expected, user, extDict );
1274
+ resolvePath( expr, expected, user );
1001
1275
 
1002
- const last = !expandOrInline && expr.path[expr.path.length - 1];
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, extDict, step === last );
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, extDict, isLast ) {
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 = (art.kind === 'entity') &&
1034
- (!isLast || [ 'from', 'exists', 'approved-exists' ].includes( expected )) && art;
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, extDict, step.location );
1306
+ resolveParams( step.args, art, entity, expected, user, step.location );
1037
1307
  if (entity) {
1038
- if (step.where)
1039
- resolveExpr( step.where, 'filter', user, environment( type ) );
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?.location || step.cardinality ) {
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, extDict, stepLocation ) {
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, extDict );
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
- resolveExpr( a, exp, user, extDict );
1366
+ // TODO: Also for other parameters?
1367
+ resolveExpr( a, (expected === 'from') ? 'param-only' : exp, user );
1094
1368
  }
1095
1369
  }
1096
1370
  }