@sap/cds-compiler 3.5.4 → 3.6.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 (84) hide show
  1. package/CHANGELOG.md +65 -2
  2. package/bin/cdsc.js +14 -6
  3. package/doc/CHANGELOG_ARCHIVE.md +10 -10
  4. package/doc/CHANGELOG_DEPRECATED.md +2 -2
  5. package/lib/api/main.js +32 -55
  6. package/lib/api/options.js +3 -2
  7. package/lib/api/validate.js +5 -0
  8. package/lib/base/message-registry.js +104 -32
  9. package/lib/base/messages.js +277 -212
  10. package/lib/base/optionProcessorHelper.js +9 -2
  11. package/lib/base/shuffle.js +50 -0
  12. package/lib/checks/actionsFunctions.js +37 -20
  13. package/lib/checks/foreignKeys.js +13 -6
  14. package/lib/checks/nonexpandableStructured.js +1 -2
  15. package/lib/checks/onConditions.js +21 -19
  16. package/lib/checks/parameters.js +1 -1
  17. package/lib/checks/queryNoDbArtifacts.js +2 -0
  18. package/lib/checks/types.js +16 -22
  19. package/lib/compiler/assert-consistency.js +31 -28
  20. package/lib/compiler/builtins.js +20 -4
  21. package/lib/compiler/checks.js +72 -63
  22. package/lib/compiler/define.js +396 -314
  23. package/lib/compiler/extend.js +55 -49
  24. package/lib/compiler/index.js +5 -0
  25. package/lib/compiler/populate.js +28 -11
  26. package/lib/compiler/propagator.js +2 -1
  27. package/lib/compiler/resolve.js +28 -13
  28. package/lib/compiler/shared.js +15 -10
  29. package/lib/compiler/utils.js +7 -7
  30. package/lib/edm/annotations/genericTranslation.js +51 -46
  31. package/lib/edm/annotations/preprocessAnnotations.js +37 -40
  32. package/lib/edm/csn2edm.js +69 -21
  33. package/lib/edm/edm.js +2 -2
  34. package/lib/edm/edmInboundChecks.js +6 -8
  35. package/lib/edm/edmPreprocessor.js +88 -80
  36. package/lib/edm/edmUtils.js +6 -15
  37. package/lib/gen/Dictionary.json +81 -13
  38. package/lib/gen/language.checksum +1 -1
  39. package/lib/gen/language.interp +2 -1
  40. package/lib/gen/languageParser.js +4680 -4484
  41. package/lib/inspect/inspectModelStatistics.js +2 -1
  42. package/lib/inspect/inspectPropagation.js +2 -1
  43. package/lib/json/from-csn.js +131 -78
  44. package/lib/json/to-csn.js +39 -23
  45. package/lib/language/antlrParser.js +0 -3
  46. package/lib/language/docCommentParser.js +7 -3
  47. package/lib/language/errorStrategy.js +3 -2
  48. package/lib/language/genericAntlrParser.js +96 -41
  49. package/lib/language/language.g4 +112 -128
  50. package/lib/language/multiLineStringParser.js +2 -1
  51. package/lib/main.d.ts +115 -2
  52. package/lib/main.js +16 -3
  53. package/lib/model/csnRefs.js +32 -4
  54. package/lib/model/csnUtils.js +109 -179
  55. package/lib/model/enrichCsn.js +13 -8
  56. package/lib/model/revealInternalProperties.js +4 -3
  57. package/lib/optionProcessor.js +22 -3
  58. package/lib/render/manageConstraints.js +11 -15
  59. package/lib/render/toCdl.js +144 -47
  60. package/lib/render/toHdbcds.js +22 -22
  61. package/lib/render/toRename.js +3 -4
  62. package/lib/render/toSql.js +31 -22
  63. package/lib/render/utils/delta.js +3 -1
  64. package/lib/render/utils/sql.js +2 -14
  65. package/lib/transform/db/associations.js +6 -6
  66. package/lib/transform/db/cdsPersistence.js +3 -3
  67. package/lib/transform/db/constraints.js +4 -6
  68. package/lib/transform/db/expansion.js +4 -4
  69. package/lib/transform/db/flattening.js +12 -15
  70. package/lib/transform/db/temporal.js +4 -3
  71. package/lib/transform/db/transformExists.js +13 -7
  72. package/lib/transform/draft/db.js +7 -7
  73. package/lib/transform/forOdataNew.js +15 -4
  74. package/lib/transform/forRelationalDB.js +59 -41
  75. package/lib/transform/odata/toFinalBaseType.js +106 -82
  76. package/lib/transform/odata/typesExposure.js +26 -17
  77. package/lib/transform/odata/utils.js +1 -1
  78. package/lib/transform/parseExpr.js +1 -1
  79. package/lib/transform/transformUtilsNew.js +33 -10
  80. package/lib/transform/translateAssocsToJoins.js +8 -7
  81. package/lib/transform/universalCsn/coreComputed.js +7 -5
  82. package/lib/transform/universalCsn/universalCsnEnricher.js +12 -4
  83. package/lib/utils/timetrace.js +2 -2
  84. package/package.json +1 -2
@@ -119,7 +119,8 @@
119
119
 
120
120
  'use strict';
121
121
 
122
- const { forEachGeneric, forEachInOrder } = require('../base/model');
122
+ const { forEachGeneric, forEachInOrder, isBetaEnabled } = require('../base/model');
123
+ const shuffleGen = require('../base/shuffle');
123
124
  const {
124
125
  dictAdd, dictAddArray, dictForEach, pushToDict,
125
126
  } = require('../base/dictionaries');
@@ -161,9 +162,12 @@ function define( model ) {
161
162
  resolveUncheckedPath,
162
163
  checkAnnotate,
163
164
  } = model.$functions;
165
+ const { shuffleDict, shuffleArray } = shuffleGen( options.testMode );
164
166
 
165
167
  const extensionsDict = Object.create(null);
166
168
  Object.assign( model.$functions, {
169
+ shuffleDict,
170
+ shuffleArray,
167
171
  initArtifact,
168
172
  initMembers,
169
173
  extensionsDict, // a dictionary - TODO: put directly into model?
@@ -176,6 +180,8 @@ function define( model ) {
176
180
  return art._subArtifacts || Object.create(null);
177
181
  };
178
182
 
183
+ let boundSelfParamType = true; // special `$self` for binding param must still be initialised
184
+
179
185
  return doDefine();
180
186
 
181
187
  /**
@@ -199,15 +205,16 @@ function define( model ) {
199
205
  model.$lateExtensions = Object.create(null); // for generated artifacts
200
206
 
201
207
  initBuiltins( model );
202
- for (const name in model.sources)
208
+ const sourceNames = shuffleArray( Object.keys( model.sources ) );
209
+ for (const name of sourceNames)
203
210
  addSource( model.sources[name] );
204
- for (const name in model.sources)
211
+ for (const name of sourceNames)
205
212
  initNamespaceAndUsing( model.sources[name] );
206
213
  dictForEach( model.definitions, initArtifact );
207
214
  dictForEach( model.vocabularies, initVocabulary );
208
215
  dictForEach( extensionsDict, initExtension );
209
216
 
210
- mergeI18nBlocks();
217
+ addI18nBlocks();
211
218
  }
212
219
 
213
220
  // Phase 1: ----------------------------------------------------------------
@@ -232,25 +239,31 @@ function define( model ) {
232
239
  }
233
240
  if (src.$frontend !== 'json') { // CDL input
234
241
  // TODO: set _block to builtin
235
- if (src.artifacts)
242
+ if (src.artifacts) {
243
+ // addArtifact() adds usings to src.artifacts: shuffleDict must be assigned first
244
+ src.artifacts = shuffleDict( src.artifacts );
236
245
  addPathPrefixes( src.artifacts, prefix ); // before addUsing
237
- else if (src.usings || src.namespace)
246
+ }
247
+ else if (src.usings || src.namespace) {
238
248
  src.artifacts = Object.create(null);
249
+ }
239
250
  if (src.usings)
240
- src.usings.forEach( u => addUsing( u, src ) );
251
+ shuffleArray( src.usings ).forEach( u => addUsing( u, src ) );
241
252
  if (namespace)
242
253
  addNamespace( namespace, src );
243
- if (src.artifacts) // addArtifact needs usings for context extensions
254
+ if (src.artifacts) { // addArtifact needs usings for context extensions
255
+ src.artifacts = shuffleDict( src.artifacts );
244
256
  dictForEach( src.artifacts, a => addArtifact( a, src, prefix ) );
257
+ }
245
258
  }
246
259
  else if (src.definitions) { // CSN input
247
260
  prefix = '';
248
- dictForEach( src.definitions, v => addDefinition( v, src ) );
261
+ dictForEach( shuffleDict( src.definitions ), v => addDefinition( v, src ) );
249
262
  }
250
263
  if (src.vocabularies) {
251
264
  if (!model.vocabularies)
252
265
  model.vocabularies = Object.create(null);
253
- dictForEach( src.vocabularies, v => addVocabulary( v, src, prefix ) );
266
+ dictForEach( shuffleDict( src.vocabularies ), v => addVocabulary( v, src, prefix ) );
254
267
  }
255
268
  if (src.extensions) { // requires using to be known!
256
269
  src.extensions.forEach( e => addExtension( e, src ) );
@@ -341,6 +354,7 @@ function define( model ) {
341
354
  dictAddArray( src.artifacts, name, decl );
342
355
  }
343
356
 
357
+ // must be called after addUsing().
344
358
  function addNamespace( path, src ) {
345
359
  const absolute = pathName( path );
346
360
  if (path.broken) // parsing may have failed
@@ -405,10 +419,56 @@ function define( model ) {
405
419
  dictAdd( model.vocabularies, name.absolute, vocab );
406
420
  }
407
421
 
408
- // Phase 2 ("init") --------------------------------------------------------
422
+ /**
423
+ * Add (optional) translations into the XSN model.
424
+ */
425
+ function addI18nBlocks() {
426
+ // TODO: the sequence should be in sync with extend / annotate / future $sources
427
+ const sortedSources = Object.keys(model.sources)
428
+ .filter(name => !!model.sources[name].i18n)
429
+ .sort( (a, b) => compareLayer( model.sources[a], model.sources[b] ) );
430
+
431
+ if (sortedSources.length === 0)
432
+ return;
433
+
434
+ if (!model.i18n)
435
+ model.i18n = Object.create( null );
436
+
437
+ for (const name of sortedSources)
438
+ addI18nFromSource( model.sources[name] );
439
+ }
440
+
441
+ /**
442
+ * Add the source's translations to the model. Warns if the source's translations
443
+ * do not match the ones from previous sources.
444
+ *
445
+ * @param {XSN.SourceAst} src
446
+ */
447
+ function addI18nFromSource( src ) {
448
+ for (const langKey of Object.keys( src.i18n )) {
449
+ if (!model.i18n[langKey])
450
+ model.i18n[langKey] = Object.create( null );
451
+
452
+ for (const textKey of Object.keys( src.i18n[langKey] )) {
453
+ const sourceVal = src.i18n[langKey][textKey];
454
+ const modelVal = model.i18n[langKey][textKey];
455
+ if (!modelVal) {
456
+ model.i18n[langKey][textKey] = sourceVal;
457
+ }
458
+ else if (modelVal.val !== sourceVal.val) {
459
+ // TODO: behave like annotation assignments? message-id?
460
+ warning( 'i18n-different-value', sourceVal.location,
461
+ { prop: textKey, otherprop: langKey } );
462
+ }
463
+ }
464
+ }
465
+ }
466
+
467
+ // Phase 2 ("init"), top-level & main -----------------------------------------
409
468
  // Functions called from top-level: initNamespaceAndUsing(), initArtifact(),
410
469
  // initVocabulary(), initExtension()
411
470
 
471
+ // TODO: message ids
412
472
  function checkRedefinition( art ) {
413
473
  if (!art.$duplicates || art.$errorReported === 'syntax-duplicate-extend' ||
414
474
  art.$errorReported === 'syntax-duplicate-annotate')
@@ -443,7 +503,7 @@ function define( model ) {
443
503
  // TODO: make it possible to have no location
444
504
  const ns = { kind: 'namespace', name: { absolute, location }, location };
445
505
  model.definitions[absolute] = ns;
446
- initParentLink( ns, model.definitions );
506
+ initArtifactParentLink( ns, model.definitions );
447
507
  }
448
508
  const builtin = model.$builtins[id];
449
509
  if (builtin && !builtin.internal &&
@@ -471,7 +531,7 @@ function define( model ) {
471
531
 
472
532
  function initArtifact( art, reInit = false ) {
473
533
  if (!reInit)
474
- initParentLink( art, model.definitions );
534
+ initArtifactParentLink( art, model.definitions );
475
535
  const block = art._block;
476
536
  checkRedefinition( art );
477
537
  initAnnotations( art, block );
@@ -489,27 +549,22 @@ function define( model ) {
489
549
  if (!setLink( art, '_leadingQuery', initQueryExpression( art.query, art ) ) )
490
550
  return; // null or undefined in case of parse error
491
551
  setLink( art._leadingQuery, '_$next', art );
492
- // the following may be removed soon if we have:
493
- // view elements as proxies to elements of leading query
494
552
  if (art.elements) { // specified element via compilation of client-style CSN
553
+ // TODO: consider this part of a revamped on-demand 'extend' functionality
495
554
  setLink( art, 'elements$', art.elements );
496
555
  delete art.elements;
497
556
  }
498
- if (art.enum) { // specified enum via compilation of client-style CSN
499
- setLink( art, 'enum$', art.enum );
500
- delete art.enum;
501
- }
502
557
  }
503
558
 
504
559
  function initVocabulary( art ) {
505
- initParentLink( art, model.vocabularies );
560
+ initArtifactParentLink( art, model.vocabularies );
506
561
  checkRedefinition( art );
507
562
  const block = art._block;
508
563
  initAnnotations( art, block );
509
564
  initMembers( art, art, block );
510
565
  }
511
566
 
512
- function initParentLink( art, definitions ) {
567
+ function initArtifactParentLink( art, definitions ) {
513
568
  setLink( art, '_parent', null );
514
569
  const { absolute } = art.name;
515
570
  const dot = absolute.lastIndexOf('.');
@@ -522,7 +577,7 @@ function define( model ) {
522
577
  const { location } = art.name; // TODO: make it possible to have no location
523
578
  parent = { kind: 'namespace', name: { absolute: prefix, location }, location };
524
579
  definitions[prefix] = parent;
525
- initParentLink( parent, definitions );
580
+ initArtifactParentLink( parent, definitions );
526
581
  }
527
582
  setLink( art, '_parent', parent );
528
583
  if (!parent._subArtifacts)
@@ -540,7 +595,7 @@ function define( model ) {
540
595
  * - for members in compile(): init annotations via extendMembers/annotateMembers
541
596
  * - for members in parse.cdl(): init annotation via initMembers
542
597
  *
543
- * In the future:
598
+ * In the future (after name cleanup):
544
599
  *
545
600
  * - also initialize members and member extensions/annotations here
546
601
  * - we might also do other things, like calculating whether an `extend` is
@@ -561,7 +616,7 @@ function define( model ) {
561
616
  // no _block: define, _block: annotate/extend/edmx
562
617
  // would fit with extending defs with props like length
563
618
  for (const prop in art) {
564
- if (prop.charAt(0) === '@') {
619
+ if (prop.charAt(0) === '@' || prop === 'doc') {
565
620
  const anno = art[prop];
566
621
  // TODO: make anno never be an array, see addAnnotation() in genericAntlrParser
567
622
  if (Array.isArray( anno ))
@@ -582,11 +637,7 @@ function define( model ) {
582
637
  }
583
638
  }
584
639
 
585
- // From here til EOF, reexamine code ---------------------------------------
586
- // See populate:
587
- // - userQuery() or _query property?
588
- // - initFromColumns()
589
- // - ensureColumnName()
640
+ // Init special things: -------------------------------------------------------
590
641
 
591
642
  function initDollarSelf( art ) {
592
643
  const selfname = '$self';
@@ -628,132 +679,76 @@ function define( model ) {
628
679
  art.$tableAliases.$parameters = parameters;
629
680
  }
630
681
 
631
- function initSubQuery( query ) {
632
- if (query.on)
633
- initExprForQuery( query.on, query );
634
- // TODO: MIXIN with name = ...subquery (not yet supported anyway)
635
- initSelectItems( query, query.columns, query );
636
- if (query.where)
637
- initExprForQuery( query.where, query );
638
- if (query.having)
639
- initExprForQuery( query.having, query );
640
- initMembers( query, query, query._block );
641
- }
642
-
643
- function initSelectItems( parent, columns, user ) {
644
- // TODO: forbid expand/inline with :param, global:true, in ref-where, outside queries (CSN), ...
645
- let wildcard = null;
646
- let hasItems = false;
647
- for (const col of columns || parent.expand || parent.inline || []) {
648
- if (!col) // parse error
649
- continue;
650
- hasItems = true;
651
- if (!columns) {
652
- if (parent.value)
653
- setLink( col, '_pathHead', parent ); // also set for '*' in expand/inline
654
- else if (parent._pathHead)
655
- setLink( col, '_pathHead', parent._pathHead );
656
- }
657
- if (col.val === '*') {
658
- if (!wildcard) {
659
- wildcard = col;
660
- }
661
- else {
662
- // a late syntax error (this code also runs with parse-cdl), i.e.
663
- // no semantic loc (wouldn't be available for expand/inline anyway)
664
- error( 'syntax-duplicate-clause', [ col.location, null ],
665
- { prop: '*', line: wildcard.location.line, col: wildcard.location.col },
666
- 'You have provided a $(PROP) already at line $(LINE), column $(COL)' );
667
- // TODO: extra text variants for expand/inline? - probably not
668
- col.val = null; // do not consider it for expandWildcard()
669
- }
670
- }
671
- // Either expression (value), expand or new association (target && type)
672
- else if (col.value || col.expand || (col.target && col.type)) {
673
- setLink( col, '_block', parent._block );
674
- initAnnotations( col, parent._block );
675
- if (col.inline) { // `@anno elem.{ * }` does not work
676
- if (col.doc) {
677
- warning( 'syntax-ignoring-anno', [ col.doc.location, col ],
678
- { '#': 'doc', code: '.{ ‹inline› }' } );
679
- }
680
- // col.$annotations no available for CSN input, have to search.
681
- // Warning about first annotation should be enough to avoid spam.
682
- const firstAnno = Object.keys(col).find(key => key.startsWith('@'));
683
- if (firstAnno) {
684
- warning( 'syntax-ignoring-anno', [ col[firstAnno].name.location, col ],
685
- { code: '.{ ‹inline› }' } );
686
- }
687
- }
688
- // TODO: allow sub queries? at least in top-level expand without parallel ref
689
- if (columns)
690
- initExprForQuery( col.value, parent );
691
- initSelectItems( col, null, user );
692
- }
693
- }
682
+ // From here til EOF, reexamine code ---------------------------------------
683
+ // See populate:
684
+ // - userQuery() or _query property?
685
+ // - initFromColumns()
686
+ // - ensureColumnName()
694
687
 
695
- if (hasItems && !wildcard && parent.excludingDict) {
696
- // TODO: Better way to get source file?
697
- let block = parent;
698
- while (block._block)
699
- block = block._block;
688
+ // Init queries: --------------------------------------------------------------
700
689
 
701
- if (block.$frontend === 'cdl') {
702
- warning('query-ignoring-exclude', [ parent.excludingDict[$location], user ],
703
- { prop: '*' },
704
- 'Excluding elements without wildcard $(PROP) has no effect');
690
+ // art is:
691
+ // - entity for top-level queries (including UNION args)
692
+ // - $tableAlias for sub query in FROM - TODO: what about UNION there?
693
+ // - $query for real sub query (in columns, WHERE, ...), again: what about UNION there?
694
+ function initQueryExpression( query, art ) {
695
+ if (!query) // parse error
696
+ return query;
697
+ if (query.from) { // select
698
+ initQuery();
699
+ initTableExpression( query.from, query, [] );
700
+ if (query.mixin)
701
+ initMixins( query, art );
702
+ if (!query.$tableAliases.$self) { // same as $projection
703
+ const self = {
704
+ name: { alias: '$self', query: query.name.select, absolute: art.name.absolute },
705
+ kind: '$self',
706
+ location: query.location,
707
+ };
708
+ setLink( self, '_origin', query );
709
+ setLink( self, '_parent', query );
710
+ setLink( self, '_main', query._main );
711
+ setLink( self, '_effectiveType', query ); // TODO: remove
712
+ query.$tableAliases.$self = self;
713
+ query.$tableAliases.$projection = self;
705
714
  }
715
+ initSubQuery( query ); // check for SELECT clauses after from / mixin
706
716
  }
707
- }
708
-
709
- function initExprForQuery( expr, query ) {
710
- if (Array.isArray(expr)) { // TODO: old-style $parens ?
711
- expr.forEach( e => initExprForQuery( e, query ) );
712
- }
713
- else if (!expr) {
714
- return;
715
- }
716
- else if (expr.query) {
717
- initQueryExpression( expr.query, query );
718
- }
719
- else if (expr.args) {
720
- const args = Array.isArray(expr.args) ? expr.args : Object.values( expr.args );
721
- args.forEach( e => initExprForQuery( e, query ) );
717
+ else if (query.args) { // UNION, INTERSECT, ..., query in parens
718
+ const leading = initQueryExpression( query.args[0], art );
719
+ for (const q of query.args.slice(1))
720
+ initQueryExpression( q, art );
721
+ setLink( query, '_leadingQuery', leading );
722
+ if (leading && query.orderBy) {
723
+ if (leading.$orderBy)
724
+ leading.$orderBy.push( ...query.orderBy );
725
+ else
726
+ leading.$orderBy = [ ...query.orderBy ];
727
+ }
728
+ // ORDER BY to be evaluated in leading query (LIMIT is literal)
722
729
  }
723
- else if (expr.path && expr.$expected === 'exists') {
724
- expr.$expected = 'approved-exists';
725
- approveExistsInChildren(expr);
730
+ else { // with parse error (`select from <EOF>`, `select from E { *, ( select }`)
731
+ return undefined;
726
732
  }
727
- }
733
+ return query._leadingQuery || query;
728
734
 
729
- /**
730
- * If we have a valid top-level exists, exists in filters of sub-expressions can be translated,
731
- * since we will have a top-level subquery after exists-processing in the forRelationalDB.
732
- *
733
- * Recursively drill down into:
734
- * - the .path
735
- * - the .args
736
- * - the .where.args
737
- *
738
- * Any $expected === 'exists' encountered along the way are turned into 'approved-exists'
739
- *
740
- * working: exists toE[exists toE] -> select from E where exists toE
741
- * not working: toE[exists toE] -> we don't support subqueries in filters
742
- *
743
- * @param {object} exprOrPathElement starts w/ an expr but then subelem from .path or .where.args
744
- */
745
- function approveExistsInChildren( exprOrPathElement ) {
746
- if (!exprOrPathElement) // may be null in case of parse error
747
- return;
748
- if (exprOrPathElement.$expected === 'exists')
749
- exprOrPathElement.$expected = 'approved-exists';
750
- // Drill down
751
- if (exprOrPathElement.args)
752
- exprOrPathElement.args.forEach(elem => approveExistsInChildren(elem));
753
- else if (exprOrPathElement.where && exprOrPathElement.where.args)
754
- exprOrPathElement.where.args.forEach(elem => approveExistsInChildren(elem));
755
- else if (exprOrPathElement.path)
756
- exprOrPathElement.path.forEach(elem => approveExistsInChildren(elem));
735
+ function initQuery() {
736
+ const main = art._main || art;
737
+ setLink( query, '_$next',
738
+ // if art is $tableAlias, set to embedding query
739
+ (!art._main || art.kind === 'select' || art.kind === '$join')
740
+ ? art : art._parent ); // TODO: check with name resolution change
741
+ setLink( query, '_block', art._block );
742
+ query.kind = 'select';
743
+ query.name = { location: query.location };
744
+ setMemberParent( query, main.$queries.length + 1, main );
745
+ // console.log(art.kind,art.name,query.name,query._$next.name)
746
+ // if (query.name.query === 1 && query.name.absolute === 'S') throw new CompilerAssertion();
747
+ main.$queries.push( query );
748
+ setLink( query, '_parent', art ); // _parent should point to alias/main/query
749
+ query.$tableAliases = Object.create( null ); // table aliases and mixin definitions
750
+ dependsOnSilent( main, query );
751
+ }
757
752
  }
758
753
 
759
754
  // table is table expression in FROM, becomes an alias
@@ -766,8 +761,8 @@ function define( model ) {
766
761
  return;
767
762
  if (!table.name) {
768
763
  const last = table.path[table.path.length - 1];
769
- const dot = last.id.lastIndexOf('.');
770
- const id = (dot < 0) ? last.id : last.id.substring( dot + 1 );
764
+ const dot = last?.id?.lastIndexOf('.');
765
+ const id = (dot >= 0) ? last.id.substring( dot + 1 ) : last.id || '';
771
766
  // TODO: if we have too much time, we can calculate the real location with '.'
772
767
  table.name = { $inferred: 'as', id, location: last.location };
773
768
  }
@@ -847,117 +842,180 @@ function define( model ) {
847
842
  }
848
843
  }
849
844
 
850
- // art is:
851
- // - entity for top-level queries (including UNION args)
852
- // - $tableAlias for sub query in FROM - TODO: what about UNION there?
853
- // - $query for real sub query (in columns, WHERE, ...), again: what about UNION there?
854
- function initQueryExpression( query, art ) {
855
- if (!query) // parse error
856
- return query;
857
- if (query.from) { // select
858
- initQuery();
859
- initTableExpression( query.from, query, [] );
860
- if (query.mixin)
861
- addMixin();
862
- if (!query.$tableAliases.$self) { // same as $projection
863
- const self = {
864
- name: { alias: '$self', query: query.name.select, absolute: art.name.absolute },
865
- kind: '$self',
866
- location: query.location,
867
- };
868
- setLink( self, '_origin', query );
869
- setLink( self, '_parent', query );
870
- setLink( self, '_main', query._main );
871
- setLink( self, '_effectiveType', query ); // TODO: remove
872
- query.$tableAliases.$self = self;
873
- query.$tableAliases.$projection = self;
874
- }
875
- initSubQuery( query ); // check for SELECT clauses after from / mixin
845
+ function initSubQuery( query ) {
846
+ if (query.on)
847
+ initExprForQuery( query.on, query );
848
+ // TODO: MIXIN with name = ...subquery (not yet supported anyway)
849
+ initSelectItems( query, query.columns, query );
850
+ if (query.where)
851
+ initExprForQuery( query.where, query );
852
+ if (query.having)
853
+ initExprForQuery( query.having, query );
854
+ initMembers( query, query, query._block );
855
+ }
856
+
857
+ function initExprForQuery( expr, query ) {
858
+ if (Array.isArray(expr)) { // TODO: old-style $parens ?
859
+ expr.forEach( e => initExprForQuery( e, query ) );
876
860
  }
877
- else if (query.args) { // UNION, INTERSECT, ..., query in parens
878
- const leading = initQueryExpression( query.args[0], art );
879
- for (const q of query.args.slice(1))
880
- initQueryExpression( q, art );
881
- setLink( query, '_leadingQuery', leading );
882
- if (leading && query.orderBy) {
883
- if (leading.$orderBy)
884
- leading.$orderBy.push( ...query.orderBy );
885
- else
886
- leading.$orderBy = [ ...query.orderBy ];
887
- }
888
- // ORDER BY to be evaluated in leading query (LIMIT is literal)
861
+ else if (!expr) {
862
+ return;
889
863
  }
890
- else { // with parse error (`select from <EOF>`, `select from E { *, ( select }`)
891
- return undefined;
864
+ else if (expr.query) {
865
+ initQueryExpression( expr.query, query );
892
866
  }
893
- return query._leadingQuery || query;
867
+ else if (expr.args) {
868
+ const args = Array.isArray(expr.args) ? expr.args : Object.values( expr.args );
869
+ args.forEach( e => initExprForQuery( e, query ) );
870
+ }
871
+ else if (expr.path && expr.$expected === 'exists') {
872
+ // TODO: does really the parser has to set $expected?
873
+ expr.$expected = 'approved-exists';
874
+ approveExistsInChildren(expr);
875
+ }
876
+ }
894
877
 
895
- function initQuery() {
896
- const main = art._main || art;
897
- setLink( query, '_$next',
898
- // if art is $tableAlias, set to embedding query
899
- (!art._main || art.kind === 'select' || art.kind === '$join')
900
- ? art : art._parent ); // TODO: check with name resolution change
901
- setLink( query, '_block', art._block );
902
- query.kind = 'select';
903
- query.name = { location: query.location };
904
- setMemberParent( query, main.$queries.length + 1, main );
905
- // console.log(art.kind,art.name,query.name,query._$next.name)
906
- // if (query.name.query === 1 && query.name.absolute === 'S') throw Error();
907
- main.$queries.push( query );
908
- setLink( query, '_parent', art ); // _parent should point to alias/main/query
909
- query.$tableAliases = Object.create( null ); // table aliases and mixin definitions
910
- dependsOnSilent( main, query );
878
+ function initMixins( query, art ) {
879
+ // TODO: re-check if mixins have already duplicates
880
+ for (const name in query.mixin) {
881
+ const mixin = query.mixin[name];
882
+ if (!(mixin.$duplicates)) {
883
+ setMemberParent( mixin, name, query );
884
+ mixin.name.alias = mixin.name.id;
885
+ setLink( mixin, '_block', art._block );
886
+ // TODO: do some initMembers() ? If people had annotation
887
+ // assignments on the mixin... (also for future mixin definitions
888
+ // with generated values)
889
+ dictAdd( query.$tableAliases, name, query.mixin[name], ( dupName, loc ) => {
890
+ error( 'duplicate-definition', [ loc, query ], { name: dupName, '#': 'alias' } );
891
+ } );
892
+ if (mixin.name.id[0] === '$') {
893
+ warning( 'name-invalid-dollar-alias', [ mixin.name.location, mixin ],
894
+ { '#': 'mixin', name: '$' } );
895
+ }
896
+ }
911
897
  }
898
+ }
912
899
 
913
- function addMixin() {
914
- // TODO: re-check if mixins have already duplicates
915
- for (const name in query.mixin) {
916
- const mixin = query.mixin[name];
917
- if (!(mixin.$duplicates)) {
918
- setMemberParent( mixin, name, query );
919
- mixin.name.alias = mixin.name.id;
920
- setLink( mixin, '_block', art._block );
921
- // TODO: do some initMembers() ? If people had annotation
922
- // assignments on the mixin... (also for future mixin definitions
923
- // with generated values)
924
- dictAdd( query.$tableAliases, name, query.mixin[name], ( dupName, loc ) => {
925
- error( 'duplicate-definition', [ loc, query ], { name: dupName, '#': 'alias' } );
926
- } );
927
- if (mixin.name.id[0] === '$') {
928
- warning( 'name-invalid-dollar-alias', [ mixin.name.location, mixin ],
929
- { '#': 'mixin', name: '$' } );
900
+ function initSelectItems( parent, columns, user ) {
901
+ // TODO: forbid expand/inline with :param, global:true, in ref-where, outside queries (CSN), ...
902
+ let wildcard = null;
903
+ let hasItems = false;
904
+ for (const col of columns || parent.expand || parent.inline || []) {
905
+ if (!col) // parse error
906
+ continue;
907
+ hasItems = true;
908
+ if (!columns) {
909
+ if (parent.value)
910
+ setLink( col, '_pathHead', parent ); // also set for '*' in expand/inline
911
+ else if (parent._pathHead)
912
+ setLink( col, '_pathHead', parent._pathHead );
913
+ }
914
+ if (col.val === '*') {
915
+ if (!wildcard) {
916
+ wildcard = col;
917
+ }
918
+ else {
919
+ // a late syntax error (this code also runs with parse-cdl), i.e.
920
+ // no semantic loc (wouldn't be available for expand/inline anyway)
921
+ error( 'syntax-duplicate-wildcard', [ col.location, null ],
922
+ {
923
+ '#': (wildcard.location.col ? 'col' : 'std'),
924
+ prop: '*',
925
+ line: wildcard.location.line,
926
+ col: wildcard.location.col,
927
+ }, {
928
+ std: 'You have provided a $(PROP) already in line $(LINE)',
929
+ col: 'You have provided a $(PROP) already at line $(LINE), column $(COL)',
930
+ } );
931
+ // TODO: extra text variants for expand/inline? - probably not
932
+ col.val = null; // do not consider it for expandWildcard()
933
+ }
934
+ }
935
+ // Either expression (value), expand or new association (target && type)
936
+ else if (col.value || col.expand || (col.target && col.type)) {
937
+ setLink( col, '_block', parent._block );
938
+ initAnnotations( col, parent._block );
939
+ if (col.inline) { // `@anno elem.{ * }` does not work
940
+ if (col.doc) {
941
+ warning( 'syntax-ignoring-anno', [ col.doc.location, col ],
942
+ { '#': 'doc', code: '.{ ‹inline› }' } );
943
+ }
944
+ // col.$annotations no available for CSN input, have to search.
945
+ // Warning about first annotation should be enough to avoid spam.
946
+ const firstAnno = Object.keys(col).find(key => key.startsWith('@'));
947
+ if (firstAnno) {
948
+ warning( 'syntax-ignoring-anno', [ col[firstAnno].name.location, col ],
949
+ { code: '.{ ‹inline› }' } );
930
950
  }
931
951
  }
952
+ // TODO: allow sub queries? at least in top-level expand without parallel ref
953
+ if (columns)
954
+ initExprForQuery( col.value, parent );
955
+ initSelectItems( col, null, user );
932
956
  }
933
957
  }
934
- }
935
958
 
936
- // Return whether the `target` is actually a `targetAspect`
937
- // TODO: really do that here and not in kick-start.js?
938
- function targetIsTargetAspect( elem ) {
939
- const { target } = elem;
940
- if (target.elements) {
941
- // TODO: error if CSN has both target.elements and targetAspect.elements -> delete target
942
- return true;
959
+ if (hasItems && !wildcard && parent.excludingDict) {
960
+ // TODO: Better way to get source file?
961
+ let block = parent;
962
+ while (block._block)
963
+ block = block._block;
964
+
965
+ if (block.$frontend === 'cdl') {
966
+ warning('query-ignoring-exclude', [ parent.excludingDict[$location], user ],
967
+ { prop: '*' },
968
+ 'Excluding elements without wildcard $(PROP) has no effect');
969
+ }
943
970
  }
944
- if (elem.targetAspect || options.parseCdl || !isDirectComposition( elem ))
945
- return false;
946
- const name = resolveUncheckedPath( target, 'compositionTarget', elem );
947
- const aspect = name && model.definitions[name];
948
- return aspect && (aspect.kind === 'aspect' || aspect.kind === 'type'); // type is sloppy
949
971
  }
950
972
 
973
+ /**
974
+ * If we have a valid top-level exists, exists in filters of sub-expressions can be translated,
975
+ * since we will have a top-level subquery after exists-processing in the forRelationalDB.
976
+ *
977
+ * Recursively drill down into:
978
+ * - the .path
979
+ * - the .args
980
+ * - the .where.args
981
+ *
982
+ * Any $expected === 'exists' encountered along the way are turned into 'approved-exists'
983
+ *
984
+ * working: exists toE[exists toE] -> select from E where exists toE
985
+ * not working: toE[exists toE] -> we don't support subqueries in filters
986
+ *
987
+ * @param {object} exprOrPathElement starts w/ an expr but then subelem from .path or .where.args
988
+ */
989
+ function approveExistsInChildren( exprOrPathElement ) {
990
+ if (!exprOrPathElement) // may be null in case of parse error
991
+ return;
992
+ if (exprOrPathElement.$expected === 'exists')
993
+ exprOrPathElement.$expected = 'approved-exists';
994
+ // Drill down
995
+ if (exprOrPathElement.args)
996
+ exprOrPathElement.args.forEach(elem => approveExistsInChildren(elem));
997
+ else if (exprOrPathElement.where && exprOrPathElement.where.args)
998
+ exprOrPathElement.where.args.forEach(elem => approveExistsInChildren(elem));
999
+ else if (exprOrPathElement.path)
1000
+ exprOrPathElement.path.forEach(elem => approveExistsInChildren(elem));
1001
+ }
1002
+ // TODO: we might issue 'expr-unexpected-exists' and 'expr-no-subquery' already in
1003
+ // define.js (using a to-be-written expression traversal function in utils.js)
1004
+
1005
+ // Members (elements, enum, actions, params): ---------------------------------
1006
+
951
1007
  /**
952
1008
  * Set property `_parent` for all elements in `parent` to `parent` and do so
953
1009
  * recursively for all sub elements.
954
1010
  *
955
1011
  * If not for extensions: construct === parent
1012
+ *
1013
+ * TODO: separate extension!
956
1014
  */
957
1015
  function initMembers( construct, parent, block, initExtensions = false ) {
958
1016
  // TODO: split extend from init
959
- const isQueryExtension = kindProperties[construct.kind].isExtension &&
960
- (parent._main || parent).query;
1017
+ const main = parent._main || parent;
1018
+ const isQueryExtension = kindProperties[construct.kind].isExtension && main.query;
961
1019
  let obj = construct;
962
1020
  let { items } = obj;
963
1021
  while (items) {
@@ -986,6 +1044,7 @@ function define( model ) {
986
1044
  delete obj.on; // continuation semantics: not specified
987
1045
  }
988
1046
  if (targetAspect.elements) {
1047
+ // TODO: main?
989
1048
  const inEntity = parent._main && parent._main.kind === 'entity';
990
1049
  // TODO: also allow indirectly (component in component in entity)?
991
1050
  setLink( targetAspect, '_outer', obj );
@@ -1025,16 +1084,8 @@ function define( model ) {
1025
1084
  obj = targetAspect;
1026
1085
  }
1027
1086
  }
1028
- if (obj !== parent && obj.elements && parent.enum) {
1029
- // in extensions, extended enums are represented as elements
1030
- for (const n in obj.elements) {
1031
- const e = obj.elements[n];
1032
- if (e.kind === 'element')
1033
- e.kind = 'enum';
1034
- }
1035
- // obj = Object.assign( { enum: obj.elements}, obj );
1036
- // delete obj.elements; // No extra syntax for EXTEND enum
1037
- forEachGeneric( { enum: obj.elements }, 'enum', init );
1087
+ if (obj !== parent && obj.elements && parent.enum) { // applying the extension
1088
+ initElementsAsEnum();
1038
1089
  }
1039
1090
  else {
1040
1091
  if (checkDefinitions( construct, parent, 'elements', obj.elements || false ))
@@ -1056,6 +1107,28 @@ function define( model ) {
1056
1107
  }
1057
1108
  return;
1058
1109
 
1110
+ function initElementsAsEnum() {
1111
+ // in extensions, extended enums are represented as elements
1112
+ for (const n in obj.elements) {
1113
+ const e = obj.elements[n];
1114
+ if (e.kind === 'element') {
1115
+ // An "element" has `$syntax: 'enum'` if it could also be an enum
1116
+ if (e.$syntax === 'enum') { // TODO: what about "just name"? (current forbidden)
1117
+ e.kind = 'enum';
1118
+ }
1119
+ else {
1120
+ // We do not want to complain separately about all element properties:
1121
+ error( 'ext-unexpected-element', [ e.location, construct ],
1122
+ { name: e.name.id, code: 'extend … with enum' },
1123
+ // eslint-disable-next-line max-len
1124
+ 'Unexpected elements like $(NAME) in an extension for an enum. Additionally, use $(CODE) when extending enums' );
1125
+ return;
1126
+ }
1127
+ }
1128
+ }
1129
+ forEachGeneric( { enum: obj.elements }, 'enum', init );
1130
+ }
1131
+
1059
1132
  function init( elem, name, prop ) {
1060
1133
  if (!elem.kind) // wrong CSN input
1061
1134
  elem.kind = dictKinds[prop];
@@ -1083,45 +1156,75 @@ function define( model ) {
1083
1156
 
1084
1157
  const bl = elem._block || block;
1085
1158
  setLink( elem, '_block', bl );
1086
- setMemberParent( elem, name, parent, construct !== parent && prop );
1159
+ const existing = parent[prop]?.[name];
1160
+ const add = construct !== parent && (!existing || elem.$inferred !== 'include');
1161
+ setMemberParent( elem, name, parent, add && prop );
1087
1162
  // console.log(message( null, elem.location, elem, {}, 'Info', 'INIT').toString())
1088
1163
  checkRedefinition( elem );
1089
1164
  if (elem.kind === 'annotate' || elem.kind === 'extend')
1090
1165
  checkAnnotate( elem, elem );
1091
1166
  initAnnotations( elem, bl );
1092
1167
  initMembers( elem, elem, bl, initExtensions );
1168
+ if (boundSelfParamType && (elem.kind === 'action' || elem.kind === 'function'))
1169
+ initBoundSelfParam( elem.params );
1093
1170
 
1094
1171
  // for a correct home path, setMemberParent needed to be called
1095
1172
 
1096
- if (elem.value && elem.kind === 'element' ) {
1097
- // For enums in extensions, `elem.kind` is only changed to `enum` for non-parseCdl
1098
- // mode *and* if the referenced artifact is found.
1099
- // This means that for non-applicable extensions and parseCdl mode, this check
1100
- // is not used.
1101
- //
1102
- // `parent` is the extended entity/type/enum/... and *not* the "extend: ..."
1103
- // itself (which is `construct`).
1104
- if ( parent.kind !== 'select' && parent.kind !== 'extend' ) {
1105
- error( 'unexpected-val', [ elem.value.location, elem ],
1106
- { '#': construct.kind },
1107
- {
1108
- std: 'Elements can\'t have a value',
1109
- entity: 'Entity elements can\'t have a value',
1110
- type: 'Type elements can\'t have a value',
1111
- extend: 'Can\'t extend type/entity elements with values',
1112
- });
1113
- return;
1173
+ if (!elem.value || elem.kind !== 'element' ||
1174
+ elem.$syntax === 'enum' && parent.kind === 'extend') // ambiguous in parse-cdl
1175
+ return;
1176
+ // -> it's a calculated element
1177
+ checkCalculatedElement(elem);
1178
+ elem.$syntax = 'calc';
1179
+ }
1180
+ }
1181
+
1182
+ function checkCalculatedElement( elem ) {
1183
+ const loc = [ elem.value.location, elem ];
1184
+ if (elem._main.kind !== 'entity' && elem._main.kind !== 'aspect' &&
1185
+ elem._main.kind !== 'extend') {
1186
+ error( 'syntax-invalid-calc-elem', loc, { '#': elem._main.kind } );
1187
+ }
1188
+ else if (!isBetaEnabled( options, 'calculatedElements' )) {
1189
+ error( 'def-unsupported-calc-elem', loc,
1190
+ 'Calculated elements are not supported' );
1191
+ }
1192
+ else {
1193
+ const noTruthyAllowed = [ 'localized', 'key', 'virtual' ];
1194
+ for (const prop of noTruthyAllowed) {
1195
+ if (elem[prop]?.val) {
1196
+ // probably better than a parse error (which is good for DEFAULT vs calc),
1197
+ // also appears with parse-cdl:
1198
+ error('syntax-invalid-calc-elem', loc, { '#': prop });
1199
+ return; // one error is enough
1114
1200
  }
1115
1201
  }
1202
+ }
1203
+ }
1116
1204
 
1117
- if (parent.enum && elem.type) {
1118
- // already rejected by from-csn, can only happen in extensions
1119
- error( 'unexpected-type', [ elem.type.location, elem ], {},
1120
- 'Enum values can\'t have a custom type' );
1205
+ function initBoundSelfParam( params ) {
1206
+ if (!params)
1207
+ return;
1208
+ if (boundSelfParamType === true) { // first try
1209
+ const def = model.definitions.$self;
1210
+ if (def) {
1211
+ // TODO v4: bring this always, probably even as error
1212
+ warning( 'name-deprecated-for-artifacts', [ def.location, def ], { name: '$self' },
1213
+ 'Do not use $(NAME) as name for an artifact definition' );
1214
+ boundSelfParamType = false;
1215
+ return;
1121
1216
  }
1217
+ boundSelfParamType = { name: { absolute: '$self' } };
1122
1218
  }
1219
+ const first = params[Object.keys( params )[0] || ''];
1220
+ const type = first?.type || first?.items?.type; // this sequence = no derived type
1221
+ if (type?.path?.length === 1 && type?.path[0]?.id === '$self') // TODO: no where: ?
1222
+ setLink( type, '_artifact', boundSelfParamType );
1123
1223
  }
1124
1224
 
1225
+ // To be reworked -------------------------------------------------------------
1226
+
1227
+ // TODO: make special for extend/annotate
1125
1228
  function checkDefinitions( construct, parent, prop, dict = construct[prop] ) {
1126
1229
  // TODO: do differently, see also annotateMembers() in resolver
1127
1230
  // To have been checked by parsers:
@@ -1161,8 +1264,15 @@ function define( model ) {
1161
1264
  }
1162
1265
  }
1163
1266
  else if (feature) { // allowed in principle, but not with extend
1164
- error( 'extend-type', [ location, construct ], {},
1165
- 'Only structures or enum types can be extended with elements/enums' );
1267
+ if (parent.$inferred === 'include') { // special case for better error message
1268
+ const variant = (construct.enum || construct.elements) ? 'elements' : 'std';
1269
+ error( 'ref-expected-direct-structure', [ location, construct ],
1270
+ { '#': variant, art: parent });
1271
+ }
1272
+ else {
1273
+ error( 'extend-type', [ location, construct ], {},
1274
+ 'Only structures or enum types can be extended with elements/enums' );
1275
+ }
1166
1276
  }
1167
1277
  else if (prop === 'elements') {
1168
1278
  error( 'unexpected-elements', [ location, construct ], {},
@@ -1178,47 +1288,19 @@ function define( model ) {
1178
1288
  return construct === parent;
1179
1289
  }
1180
1290
 
1181
- /**
1182
- * Merge (optional) translations into the XSN model.
1183
- */
1184
- function mergeI18nBlocks() {
1185
- const sortedSources = Object.keys(model.sources)
1186
- .filter(name => !!model.sources[name].i18n)
1187
- .sort( (a, b) => compareLayer( model.sources[a], model.sources[b] ) );
1188
-
1189
- if (sortedSources.length === 0)
1190
- return;
1191
-
1192
- if (!model.i18n)
1193
- model.i18n = Object.create( null );
1194
-
1195
- for (const name of sortedSources)
1196
- initI18nFromSource( model.sources[name] );
1197
- }
1198
-
1199
- /**
1200
- * Add the source's translations to the model. Warns if the source's translations
1201
- * do not match the ones from previous sources.
1202
- *
1203
- * @param {XSN.SourceAst} src
1204
- */
1205
- function initI18nFromSource( src ) {
1206
- for (const langKey of Object.keys( src.i18n )) {
1207
- if (!model.i18n[langKey])
1208
- model.i18n[langKey] = Object.create( null );
1209
-
1210
- for (const textKey of Object.keys( src.i18n[langKey] )) {
1211
- const sourceVal = src.i18n[langKey][textKey];
1212
- const modelVal = model.i18n[langKey][textKey];
1213
- if (!modelVal) {
1214
- model.i18n[langKey][textKey] = sourceVal;
1215
- }
1216
- else if (modelVal.val !== sourceVal.val) {
1217
- warning( 'i18n-different-value', sourceVal.location,
1218
- { prop: textKey, otherprop: langKey } );
1219
- }
1220
- }
1291
+ // Return whether the `target` is actually a `targetAspect`
1292
+ // TODO: really do that here and not in kick-start.js?
1293
+ function targetIsTargetAspect( elem ) {
1294
+ const { target } = elem;
1295
+ if (target.elements) {
1296
+ // TODO: error if CSN has both target.elements and targetAspect.elements -> delete target
1297
+ return true;
1221
1298
  }
1299
+ if (elem.targetAspect || options.parseCdl || !isDirectComposition( elem ))
1300
+ return false;
1301
+ const name = resolveUncheckedPath( target, 'compositionTarget', elem );
1302
+ const aspect = name && model.definitions[name];
1303
+ return aspect && (aspect.kind === 'aspect' || aspect.kind === 'type'); // type is sloppy
1222
1304
  }
1223
1305
  }
1224
1306