@sap/cds-compiler 5.4.2 → 5.5.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/bin/cds_remove_invalid_whitespace.js +4 -4
  3. package/bin/cds_update_annotations.js +3 -3
  4. package/bin/cds_update_identifiers.js +3 -3
  5. package/lib/api/main.js +18 -30
  6. package/lib/api/validate.js +6 -1
  7. package/lib/base/lazyload.js +28 -0
  8. package/lib/base/location.js +1 -0
  9. package/lib/base/message-registry.js +53 -11
  10. package/lib/base/messages.js +17 -3
  11. package/lib/checks/{dbFeatureFlags.js → featureFlags.js} +1 -1
  12. package/lib/checks/parameters.js +61 -4
  13. package/lib/checks/validator.js +14 -6
  14. package/lib/compiler/index.js +7 -7
  15. package/lib/compiler/shared.js +29 -13
  16. package/lib/gen/BaseParser.js +345 -235
  17. package/lib/gen/CdlParser.js +4434 -4492
  18. package/lib/gen/Dictionary.json +2 -2
  19. package/lib/json/to-csn.js +3 -1
  20. package/lib/language/antlrParser.js +2 -111
  21. package/lib/main.js +16 -37
  22. package/lib/modelCompare/utils/filter.js +47 -21
  23. package/lib/parsers/AstBuildingParser.js +59 -49
  24. package/lib/parsers/CdlGrammar.g4 +91 -130
  25. package/lib/parsers/index.js +123 -0
  26. package/lib/render/toSql.js +8 -2
  27. package/lib/render/utils/delta.js +33 -1
  28. package/lib/transform/db/{transformExists.js → assocsToQueries/transformExists.js} +12 -407
  29. package/lib/transform/db/assocsToQueries/utils.js +440 -0
  30. package/lib/transform/db/expansion.js +2 -2
  31. package/lib/transform/draft/db.js +14 -3
  32. package/lib/transform/effective/annotations.js +3 -3
  33. package/lib/transform/effective/main.js +5 -7
  34. package/lib/transform/featureFlags.js +5 -0
  35. package/lib/transform/forRelationalDB.js +125 -192
  36. package/lib/transform/odata/createForeignKeys.js +1 -1
  37. package/lib/transform/odata/flattening.js +1 -1
  38. package/lib/transform/transformUtils.js +0 -51
  39. package/package.json +2 -2
  40. package/lib/transform/db/featureFlags.js +0 -5
@@ -1,9 +1,8 @@
1
1
  'use strict';
2
2
 
3
- const { forAllQueries, forEachDefinition, walkCsnPath } = require('../../model/csnUtils');
4
- const { setProp } = require('../../base/model');
5
- const { getRealName } = require('../../render/utils/common');
6
- const { ModelError } = require('../../base/error');
3
+ const { forAllQueries, forEachDefinition, walkCsnPath } = require('../../../model/csnUtils');
4
+ const { setProp } = require('../../../base/model');
5
+ const { getHelpers } = require('./utils');
7
6
 
8
7
  /**
9
8
  * Turn a `exists assoc[filter = 100]` into a `exists (select 1 as dummy from assoc.target where <assoc on condition> and assoc.target.filter = 100)`.
@@ -51,6 +50,14 @@ const { ModelError } = require('../../base/error');
51
50
  * @param {Function} dropDefinitionCache
52
51
  */
53
52
  function handleExists( csn, options, error, inspectRef, initDefinition, dropDefinitionCache ) {
53
+ const {
54
+ getBase,
55
+ firstLinkIsEntityOrQuerySource,
56
+ getFirstAssoc,
57
+ translateManagedAssocToWhere,
58
+ getQuerySources,
59
+ translateUnmanagedAssocToWhere,
60
+ } = getHelpers(csn, inspectRef, error);
54
61
  const generatedExists = new WeakMap();
55
62
  forEachDefinition(csn, (artifact, artifactName) => {
56
63
  // drop cache: Otherwise, the projection/query hack below won't work, because csnRefs
@@ -106,48 +113,6 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
106
113
  }
107
114
  });
108
115
 
109
- /**
110
- * Get the source aliases from a join
111
- *
112
- * @param {Array} args Join args
113
- * @returns {object}
114
- */
115
- function getJoinSources( args ) {
116
- let sources = Object.create(null);
117
- for (const join of args) {
118
- if (join.as) {
119
- sources[join.as] = join.as;
120
- }
121
- else if (join.args) {
122
- const subSources = getJoinSources(join.args);
123
- sources = Object.assign(sources, subSources);
124
- }
125
- else if (join.ref) {
126
- sources[join.ref[join.ref.length - 1]] = join.ref[join.ref.length - 1];
127
- }
128
- }
129
-
130
- return sources;
131
- }
132
-
133
- /**
134
- * Get the source aliases from a query - drill down somewhat into joins (is that correct?)
135
- *
136
- * @param {CSN.Query} query
137
- * @returns {object}
138
- */
139
- function getQuerySources( query ) {
140
- const sources = Object.create(null);
141
- if (query.from.as)
142
- sources[query.from.as] = query.from.as;
143
- else if (query.from.args)
144
- return Object.assign(sources, getJoinSources(query.from.args));
145
- else if (query.from.ref)
146
- sources[query.from.ref[query.from.ref.length - 1]] = query.from.ref[query.from.ref.length - 1];
147
-
148
- return sources;
149
- }
150
-
151
116
  /**
152
117
  * Get the index of the first association that is found - starting the
153
118
  * search at the given startIndex.
@@ -300,7 +265,7 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
300
265
  if (i < expr.length - 1 && expr[i] === 'exists' && expr[i + 1].ref) {
301
266
  i++;
302
267
  const current = expr[i];
303
- const isPrefixedWithTableAlias = firstLinkIsEntityOrQuerySource(exprPath.concat(i));
268
+ const isPrefixedWithTableAlias = firstLinkIsEntityOrQuerySource({}, exprPath.concat(i));
304
269
  const base = getBase(queryBase, isPrefixedWithTableAlias, current, exprPath.concat(i));
305
270
  const { root, ref } = getFirstAssoc(current, exprPath.concat(i));
306
271
 
@@ -358,300 +323,6 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
358
323
  return { result: newExpr, leftovers: toContinue };
359
324
  }
360
325
 
361
- /**
362
- * Translate an `EXISTS <managed assoc>` into a part of a WHERE condition.
363
- *
364
- * For each of the foreign keys, do:
365
- * + build the target side by prefixing `target` in front of the ref
366
- * + build the source side by prefixing `base` (if not already part of `current`)
367
- * and the assoc name itself (current) in front of the ref
368
- * + Compare source and target with `=`
369
- *
370
- * If there is more than one foreign key, join with `and`.
371
- *
372
- * The new tokens are immediately added to the WHERE of the subselect
373
- *
374
- * @param {CSN.Element} root
375
- * @param {string} target
376
- * @param {boolean} isPrefixedWithTableAlias
377
- * @param {string} base
378
- * @param {Token} current
379
- * @returns {object[]} The stuff to add to the where
380
- */
381
- function translateManagedAssocToWhere( root, target, isPrefixedWithTableAlias, base, current ) {
382
- if (current.$scope === '$self') {
383
- error('ref-unexpected-self', current.$path, { '#': 'exists', id: current.ref[0], name: 'exists' });
384
- return [];
385
- }
386
-
387
- const whereExtension = [];
388
- for (let j = 0; j < root.keys.length; j++) {
389
- const lop = { ref: [ target, ...root.keys[j].ref ] }; // target side
390
- const rop = { ref: (isPrefixedWithTableAlias ? [] : [ base ]).concat([ ...toRawRef(current.ref), ...root.keys[j].ref ]) }; // source side
391
-
392
- if (j > 0)
393
- whereExtension.push('and');
394
-
395
- whereExtension.push(...[ lop, '=', rop ]);
396
- }
397
-
398
- return whereExtension;
399
- }
400
-
401
- /**
402
- * Turn a ref-array into an array of strings.
403
- *
404
- * @param {Array} ref Array of strings or objects with `id`
405
- * @returns {string[]}
406
- */
407
- function toRawRef( ref ) {
408
- return ref.map(r => (r.id ? r.id : r));
409
- }
410
-
411
- /**
412
- *
413
- * Translate an `EXISTS <unmanaged assoc>` into a part of a WHERE condition.
414
- *
415
- * A valid $self-backlink is handled in translateDollarSelfToWhere.
416
- *
417
- * For an ordinary unmanaged association, we do the following for each part of the on-condition:
418
- * - target side: We prefix the real target and cut off the assoc-name from the ref
419
- * - source side w/ leading $self: We remove the $self and add the source side entity/query source
420
- * - source side w/o leading $self: We simply add the source side entity/query source in front of the ref
421
- * - all other: Leave intact, usually operators
422
- *
423
- * @param {CSN.Element} root
424
- * @param {string} target
425
- * @param {boolean} isPrefixedWithTableAlias
426
- * @param {string} base
427
- * @param {Token} current
428
- * @returns {object[]} The stuff to add to the where
429
- */
430
- function translateUnmanagedAssocToWhere( root, target, isPrefixedWithTableAlias, base, current ) {
431
- const whereExtension = [];
432
-
433
- for (let j = 0; j < root.on.length; j++)
434
- j = processExpressionPart(root.on, root.$path.concat('on'), j, whereExtension);
435
-
436
- return whereExtension;
437
-
438
- /**
439
- * Process the given expression and apply the steps described above.
440
- *
441
- * @param {Array} expression Expression we are processing
442
- * @param {CSN.Path} path Path to the expression
443
- * @param {number} expressionIndex Index in the current expression, imporant for paths and stuff
444
- * @param {Array} collector Array to collect the processed expressionparts into
445
- * @returns {number} How far along expression we have processed - so the main loop can jump ahead
446
- */
447
- function processExpressionPart(expression, path, expressionIndex, collector) {
448
- const part = expression[expressionIndex];
449
-
450
- if (part?.xpr) {
451
- const xpr = { xpr: [] };
452
- for (let i = 0; i < part.xpr.length; i++)
453
- i = processExpressionPart(part.xpr, path.concat(expressionIndex, 'xpr'), i, xpr.xpr);
454
-
455
- collector.push(xpr);
456
- return expressionIndex;
457
- }
458
-
459
- // we can only resolve stuff on refs - skip literals like =
460
- // but also keep along stuff like null and undefined, so compiler
461
- // can have a chance to complain/ we can fail later nicely maybe
462
- if (!(part && part.ref)) {
463
- collector.push(part);
464
- return expressionIndex;
465
- }
466
-
467
- // root.$path should be safe - we can only reference things in exists that exist when we enrich
468
- // so all of them should have a $path.
469
- const { art, links } = inspectRef(path.concat(expressionIndex));
470
- // Dollar Self Backlink
471
- if (isValidDollarSelf(expression[expressionIndex], path.concat(expressionIndex), expression[expressionIndex + 1], expression[expressionIndex + 2], path.concat(expressionIndex + 2 ))) {
472
- if (expression[expressionIndex].ref[0] === '$self' && expression[expressionIndex].ref.length === 1)
473
- collector.push(...translateDollarSelfToWhere(base, target, expression[expressionIndex + 2], path.concat(expressionIndex + 2 )));
474
- else
475
- collector.push(...translateDollarSelfToWhere(base, target, expression[expressionIndex], path.concat(expressionIndex)));
476
-
477
- return expressionIndex + 2;
478
- }
479
- else if (links && links[0].art === root) { // target side
480
- collector.push({ ref: [ target, ...part.ref.slice(1) ] });
481
- }
482
- else if (part.$scope === '$self') { // source side - "absolute" scope
483
- const column = part._art._column;
484
- if (column && column.as) { // Replace with the "original" expression (the .ref, .xpr etc.)
485
- collector.push(translateToSourceSide(column));
486
- }
487
- else {
488
- collector.push(assignAndDeleteAsAndKey({}, part, { ref: [ base, ...part.ref.slice(1) ] }));
489
- }
490
- }
491
- else if (art) { // source side - with local scope
492
- if (isPrefixedWithTableAlias || part.$scope === 'alias')
493
- collector.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
494
- else
495
- collector.push({ ref: [ base, ...current.ref.slice(0, -1), ...part.ref ] });
496
- }
497
- else { // operator - or any other leftover
498
- collector.push(part);
499
- }
500
-
501
- return expressionIndex;
502
- }
503
-
504
-
505
- /**
506
- * Run Object.assign on all of the passed in parameters and delete a .as and .key at the end
507
- *
508
- * @param {...any} args
509
- * @returns {object} The merged args without an .as and .key property
510
- */
511
- function assignAndDeleteAsAndKey( ...args ) {
512
- const obj = Object.assign.apply(null, args);
513
- delete obj.as;
514
- delete obj.key;
515
- return obj;
516
- }
517
- /**
518
- * Translate the given obj (a column-like thing) into an expression that we can use in the WHERE.
519
- * - Strip off $self/$projection and correctly replace with source expression
520
- * - Drill further down into .xpr
521
- * - Correctly set table alias in front of ref
522
- *
523
- * @param {object} obj
524
- * @returns {object}
525
- */
526
- function translateToSourceSide( obj ) {
527
- if (obj.ref) {
528
- if (obj.$scope === '$self') { // TODO: Check with this way down, do we keep the links?
529
- const column = obj._art._column;
530
- if (column && column.as)
531
- return translateToSourceSide(column);
532
- return assignAndDeleteAsAndKey({}, obj, { ref: [ base, ...obj.ref.slice(1) ] });
533
- }
534
- else if (typeof obj.$env === 'string') {
535
- return assignAndDeleteAsAndKey({}, obj, { ref: [ obj.$env, ...obj.ref ] });
536
- }
537
-
538
- return assignAndDeleteAsAndKey({}, obj, { ref: [ ...obj.ref ] });
539
- }
540
- else if (obj.xpr) { // we need to drill further down into .xpr
541
- return assignAndDeleteAsAndKey({}, obj, { xpr: obj.xpr.map(translateToSourceSide) });
542
- }
543
- else if (obj.args) {
544
- return assignAndDeleteAsAndKey({}, obj, { args: obj.args.map(translateToSourceSide) });
545
- }
546
-
547
- return obj;
548
- }
549
-
550
- /**
551
- * Check that an expression triple is a valid $self
552
- *
553
- * @param {Token} leftSide
554
- * @param {CSN.Path} pathLeft
555
- * @param {Token} middle
556
- * @param {Token} rightSide
557
- * @param {CSN.Path} pathRight
558
- * @returns {boolean}
559
- */
560
- function isValidDollarSelf( leftSide, pathLeft, middle, rightSide, pathRight ) {
561
- if (leftSide && leftSide.ref && rightSide && rightSide.ref && middle === '=') {
562
- const right = inspectRef(pathRight);
563
- const left = inspectRef(pathLeft);
564
-
565
- if (!right || !left)
566
- return false;
567
-
568
- const rightSideArt = right.art;
569
- const leftSideArt = left.art;
570
-
571
- return leftSide.ref[0] === '$self' && leftSide.ref.length === 1 && rightSideArt && rightSideArt.target ||
572
- rightSide.ref[0] === '$self' && rightSide.ref.length === 1 && leftSideArt && leftSideArt.target;
573
- }
574
-
575
- return false;
576
- }
577
- }
578
-
579
- /**
580
- * From the given expression (having inspectRef -> links), find the first association.
581
- *
582
- * @param {object} xprPart
583
- * @param {CSN.Path} path
584
- * @returns {{head: Array, root: CSN.Element, ref: string|object, tail: Array}} The first assoc (root), the corresponding ref (ref), anything before the ref (head) and the rest of the ref (tail).
585
- */
586
- function getFirstAssoc( xprPart, path ) {
587
- const { links, art } = inspectRef(path);
588
- for (let i = 0; i < xprPart.ref.length - 1; i++) {
589
- if (links[i].art && links[i].art.target) {
590
- return {
591
- head: (i === 0 ? [] : xprPart.ref.slice(0, i)), root: links[i].art, ref: xprPart.ref[i], tail: xprPart.ref.slice(i + 1),
592
- };
593
- }
594
- }
595
- return {
596
- head: (xprPart.ref.length === 1 ? [] : xprPart.ref.slice(0, xprPart.ref.length - 1)), root: art, ref: xprPart.ref[xprPart.ref.length - 1], tail: [],
597
- };
598
- }
599
-
600
- /**
601
- * Check (using inspectRef -> links), whether the first path step is an entity or query source
602
- *
603
- * @param {CSN.Path} path
604
- * @returns {boolean}
605
- */
606
- function firstLinkIsEntityOrQuerySource( path ) {
607
- const { links } = inspectRef(path);
608
- return links && (links[0].art.kind === 'entity' || links[0].art.query || links[0].art.from);
609
- }
610
-
611
- /**
612
- * For a given xpr, check in which entity/query source the ref "is".
613
- *
614
- * If the ref already starts with an entity/query source, simply return the first ref step.
615
- * Otherwise, use $env to figure it out:
616
- * - $env=<string> -> the string is the source
617
- * - $env=<number> && $scope='mixin' -> the current query is the source
618
- * - $env=<number> && $scope!=='mixin' -> such refs start with entity/query source, are already handled
619
- * - $env=true -> does not apply for "EXISTS" handling, only happens in ORDER BY or explicit on-cond redirection
620
- *
621
- * If we have a ref but no $env, throw to trigger recompile - but such cases should have already led to a recompile with
622
- * the validator/enricher.
623
- *
624
- * Since we only call this function when it is not just a simple SELECT FROM X,
625
- * we can be sure that resolving the ref requires $env information.
626
- *
627
- * @param {object} xpr
628
- * @param {CSN.Path} path
629
- * @returns {string|undefined} undefined in case of errors
630
- * @throws {Error} Throws if xpr.ref but no xpr.$env
631
- * @todo $env is going to be removed from CSN, but csnRefs will provide it
632
- */
633
- // eslint-disable-next-line consistent-return
634
- function getParent( xpr, path ) {
635
- if (firstLinkIsEntityOrQuerySource(path)) {
636
- return xpr.ref[0];
637
- }
638
- else if (xpr.$env) {
639
- if (typeof xpr.$env === 'string') {
640
- return xpr.$env;
641
- }
642
- else if (typeof xpr.$env === 'number') {
643
- if (xpr.$scope === 'mixin')
644
- return '';
645
- return error(null, xpr.$path, '$env with number is not handled yet - report this error!');
646
- }
647
-
648
- return error(null, xpr.$path, 'Boolean $env is not handled yet - report this error!');
649
- }
650
- else if (xpr.ref) {
651
- throw new ModelError('Missing $env and missing leading artifact ref - throwing to trigger recompilation!');
652
- }
653
- }
654
-
655
326
  /**
656
327
  * Build an initial subselect for the final `EXISTS <subselect>`.
657
328
  *
@@ -692,25 +363,6 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
692
363
  return subselect;
693
364
  }
694
365
 
695
- /**
696
- * Get the name of the source-side query source
697
- *
698
- * @param {string | Array | null} queryBase
699
- * @param {boolean} isPrefixedWithTableAlias
700
- * @param {CSN.Column} current
701
- * @param {CSN.Path} path
702
- * @returns {string}
703
- */
704
- function getBase( queryBase, isPrefixedWithTableAlias, current, path ) {
705
- if (typeof queryBase === 'string') // alias
706
- return queryBase;
707
- else if (queryBase) // ref
708
- return queryBase.length > 1 ? queryBase[queryBase.length - 1] : getRealName(csn, queryBase[0]);
709
- else if (isPrefixedWithTableAlias)
710
- return current.ref[0];
711
- return getParent(current, path);
712
- }
713
-
714
366
 
715
367
  /**
716
368
  * If the assoc-base for EXISTS <assoc> has a filter, we need to merge this filter into the WHERE-clause of the subquery.
@@ -739,53 +391,6 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
739
391
  return part;
740
392
  });
741
393
  }
742
-
743
- /**
744
- * Turn the would-be on-condition of a $self backlink into a WHERE condition.
745
- *
746
- * Prefix the target/source side base accordingly and build the source = target comparisons.
747
- *
748
- * @param {string} base The source entity/query source name
749
- * @param {string} target The target entity/query source name
750
- * @param {object} assoc The association element - the "not-$self" side of the comparison
751
- * @param {CSN.Path} path
752
- * @returns {TokenStream} The WHERE representing the $self comparison
753
- */
754
- function translateDollarSelfToWhere( base, target, assoc, path ) {
755
- const where = [];
756
- const { art } = inspectRef(path);
757
- if (art.keys) {
758
- for (let i = 0; i < art.keys.length; i++) {
759
- const lop = { ref: [ target, ...assoc.ref.slice(1), ...art.keys[i].ref ] }; // target side
760
- const rop = { ref: [ base, ...art.keys[i].ref ] }; // source side
761
- if (i > 0)
762
- where.push('and');
763
-
764
- where.push(...[ lop, '=', rop ]);
765
- }
766
- }
767
- else if (art.on) {
768
- for (let i = 0; i < art.on.length; i++) {
769
- const part = art.on[i];
770
- const partInspect = inspectRef(art.$path.concat([ 'on', i ]));
771
- if (partInspect.links && partInspect.links[0].art === art) { // target side
772
- where.push({ ref: [ base, ...part.ref.slice(1) ] });
773
- }
774
- else if (part.$scope === '$self') { // source side - "absolute" scope
775
- // Same message as in forRelationalDB/transformDollarSelfComparisonWithUnmanagedAssoc
776
- error(null, part.$path, { name: '$self' },
777
- 'An association that uses $(NAME) in its ON-condition can\'t be compared to "$self"');
778
- }
779
- else if (partInspect.art) { // source side - with local scope
780
- where.push({ ref: [ target, ...assoc.ref.slice(1, -1), ...part.ref ] });
781
- }
782
- else { // operator - or any other leftover
783
- where.push(part);
784
- }
785
- }
786
- }
787
- return where;
788
- }
789
394
  }
790
395
 
791
396