@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
@@ -9,6 +9,9 @@ options {
9
9
  const { XsnSource, XsnArtifact, XsnName } = require( '../compiler/xsn-model' );
10
10
  const AstBuildingParser = require('../parsers/AstBuildingParser');
11
11
  }
12
+ @footer{
13
+ module.exports = { CdlParser }; // make it work with lazyload()
14
+ }
12
15
 
13
16
  // Content:
14
17
  // - top-level: USING, NAMESPACE, artifactDefOrExtend (start rule: start)
@@ -213,23 +216,17 @@ aspectDef[ art, outer ]
213
216
  { this.docComment( $art ); } annoAssignMid[ $art ]*
214
217
  (
215
218
  elementsBlock[ $art ]
216
- actionsBlock[ $art ]?
217
219
  |
218
- ':' aspectColonSpec[ $art ]
219
- )?
220
- ;
221
-
222
- aspectColonSpec[ art ]
223
- options{ minTokensMatched = 1 }
224
- :
225
- (
226
- incl=simplePath { $art.includes ??= []; $art.includes.push( $incl ); }
227
- ( ',' | <exitLoop> )
228
- )*
229
- (
220
+ <exitRule>
221
+ |
222
+ ':'
223
+ (
224
+ incl=simplePath { $art.includes ??= []; $art.includes.push( $incl ); }
225
+ ( ',' | <exitLoop> | <exitRule> )
226
+ )*
230
227
  elementsBlock[ $art ]
231
- actionsBlock[ $art ]?
232
- )? // TODO: no rule end after ','
228
+ )
229
+ actionsBlock[ $art ]?
233
230
  ;
234
231
 
235
232
  entityDef[ art, outer ]
@@ -419,7 +416,7 @@ elementDef[ outer, art = undefined ]
419
416
  { this.docComment( $art ); } annoAssignStd[ $art ]*
420
417
  ( VIRTUAL { $art.virtual = this.valueWithLocation( true ); } )?
421
418
  ( KEY { $art.key = this.valueWithLocation( true ); } )?
422
- ( MASKED { $art.masked = this.valueWithLocation( true ); }
419
+ ( <hide> MASKED { $art.masked = this.valueWithLocation( true ); }
423
420
  { this.message( 'syntax-unsupported-masked', this.lb(), { keyword: 'masked' } ); } )?
424
421
  ( ELEMENT { $art.$syntax = 'element'; } )?
425
422
  Id['Element'] <prepare=elementRestriction, arg=elem>
@@ -429,7 +426,7 @@ elementDef[ outer, art = undefined ]
429
426
  elementsBlock[ $art ]
430
427
  nullability[ $art ]?
431
428
  |
432
- ':' typeExpression[ $art ] // was elementType, with NOT? NULL / DEFAULT
429
+ ':' typeExpression[ $art ] // includes DEFAULT
433
430
  )?
434
431
  (
435
432
  <cond=elementRestriction, arg=calc> '='
@@ -440,9 +437,7 @@ elementDef[ outer, art = undefined ]
440
437
  // TODO: why have `stored` as property of the value?
441
438
  { if (this.elementRestriction( true, 'anno' )) this.docComment( $art ); }
442
439
  ( <cond=elementRestriction, arg=anno> annoAssignStd[ $art ] )*
443
- |
444
- { this.docComment( $art, 'type' ); } // delayed from type expression
445
- )
440
+ )?
446
441
  ;
447
442
 
448
443
  enumSymbolsBlock[ art ]
@@ -464,10 +459,10 @@ enumSymbolDef[ outer ] locals[ art = new XsnArtifact() ]
464
459
  { this.docComment( $art ); } annoAssignStd[ $art ]*
465
460
  ( '='
466
461
  (
467
- <priority> String
462
+ <prefer> String
468
463
  { $art.value = this.quotedLiteral(); }
469
464
  |
470
- <priority> Number
465
+ <prefer> Number
471
466
  { $art.value = this.numberLiteral(); }
472
467
  |
473
468
  sign='+'/'-' Number
@@ -803,7 +798,7 @@ typeOrIncludesSpec[ art ]
803
798
  (
804
799
  typeExpression[ $art ]
805
800
  |
806
- <priority>
801
+ <prefer>
807
802
  ref=simplePath { $art.type = $ref; }
808
803
  (
809
804
  // <default> does not work here
@@ -830,122 +825,103 @@ typeOrIncludesSpec[ art ]
830
825
  )
831
826
  ;
832
827
 
833
- // Type expression (after the `:`) and “final” annotation assignment. Type
834
- // expressions include `null`/`not null` and `default`, the latter is forbidden in
835
- // the `returns` type.
828
+ // Type expression (after the `:`), including `null`/`not null` and `default`;
829
+ // the latter is forbidden in the `returns` type.
830
+ //
831
+ // This rule also parses annotation assignments and doc comments after the
832
+ // type/target reference and after each type property; exceptions are:
833
+ // - not after element and `enum` blocks (would interfere with optional `;`)
834
+ // - no further type property after `many …` @assignment`, because the
835
+ // annotations are attached to the element, the type properties to the line type
836
836
  //
837
837
  // If used in a definition with additional clauses (currently just `= expr` for
838
838
  // elements), these clauses must be guarded with <cond=…>.
839
839
  //
840
- // No annotation assignments are allowed after element and enum blocks, because
841
- // these would conflict with the optional `;` after those blocks. To lower the
842
- // impact with enums, the “final” annotation assignments are actually moved before
843
- // the keyword `enum` (only allowed if not after `many`/`array of`).
844
- //
845
- // This rule is not used when the type expression is restricted: CDL-style cast in
846
- // `select` items, `cast` function, `mixin` definition.
840
+ // This rule is for element, type, (input and `returns`) parameter and annotation
841
+ // definitions. It is not used when the type expression is restricted: CDL-style
842
+ // cast in `select` items, `cast` function, `mixin` definition.
847
843
 
848
844
  typeExpression[ art ]
849
- // TODO: really introduce <exitRule>
850
845
  :
851
- elementsBlock[ $art ] <prepare=elementRestriction, arg=calc>
852
- nullability[ $art ]?
853
- |
854
846
  ( typeRefOptArgs[ $art ] | typeTypeOf[ $art ] )
855
847
  (<altRuleStart>)
856
- nullability[ $art ]?
857
- (
858
- nullabilityAndDefault[ $art ]
859
- // TODO TOOL: with <exitRule>, we could move the following to the end
860
- (
861
- { this.elementRestriction( false, 'calc' ); }
862
- // <prepare=elementRestriction, arg=calc>
863
- { this.docComment( $art ); } annoAssignStd[ $art ]+
864
- |
865
- // We could still have the calc expression after this → delay
866
- { this.docComment( $art, 'elem' ); }
867
- )
868
- |
869
- { this.docComment( $art, 'elem' ); }
870
- |
871
- { this.docComment( $art ); } annoAssignStd[ $art ]+ // +!
872
- ( enumSymbolsBlock[ $art ] <prepare=elementRestriction, arg=anno>
873
- // TODO TOOL: written?
874
- nullabilityAndDefault[ $art ]?
848
+ { this.docComment( $art ); } annoAssignStd[ $art ]*
849
+ ( <cond=elementRestriction, arg=notNull> nullability[ $art ]
850
+ { this.docComment( $art ); } annoAssignStd[ $art ]*
851
+ )?
852
+ ( enumSymbolsBlock[ $art ] <prepare=elementRestriction, arg=anno>
853
+ ( <cond=elementRestriction, arg=notNull> nullability[ $art ] )?
854
+ ( <cond=elementRestriction, arg=default>
855
+ DEFAULT expr=expression { $art.default = $expr; }
875
856
  )?
876
- |
877
- { this.docComment( $art ); }
878
- enumSymbolsBlock[ $art ] <prepare=elementRestriction, arg=anno>
879
- nullabilityAndDefault[ $art ]?
880
- )
857
+ ( <cond=elementRestriction, arg=notNull> nullability[ $art ] )?
858
+ | typeProperties[ $art ]
859
+ )?
881
860
  |
882
- LOCALIZED
883
- { $art.localized = this.valueWithLocation( true ); }
884
- typeRefOptArgs[ $art ] // TODO: why no TYPE OF ?
885
- nullabilityAndDefault[ $art ]?
886
- // TODO TOOL: with <exitRule>, we could move the following to the end
887
- (
888
- { this.elementRestriction( false, 'calc' ); }
889
- // <prepare=elementRestriction, arg=calc>
890
- { this.docComment( $art ); } annoAssignStd[ $art ]+
891
- |
892
- { this.docComment( $art, 'elem' ); }
893
- )
861
+ LOCALIZED { $art.localized = this.valueWithLocation( true ); }
862
+ typeRefOptArgs[ $art ] // no TYPE OF
863
+ { this.docComment( $art ); }
864
+ typeProperties[ $art ]?
894
865
  |
895
866
  assoc=ASSOCIATION <prepare=elementRestriction, arg=calc>
896
867
  cardinality[ $art ]? TO card=ONE/MANY?
897
- target=simplePath { this.setAssocAndComposition( $art, $assoc, $card, $target ); }
898
- ( ON cond=condition { $art.on = $cond; }
899
- | foreignKeysBlock[ $art ]?
900
- nullabilityAndDefault[ $art ]?
901
- // scalar default ? - hm..., what about calc expression?
902
- )
903
- // final anno assignments allowed also with fkeys (no auto-`;` after them):
904
- // (the "TODO TOOL" code for doc/annos would also be fine)
905
- { this.docComment( $art ); } annoAssignStd[ $art ]*
868
+ typeAssocProperties[ $art, $assoc, $card ]
906
869
  |
907
870
  assoc=COMPOSITION <prepare=elementRestriction, arg=calc>
908
871
  cardinality[ $art ]? OF card=ONE/MANY?
909
- (
910
- target=simplePath { this.setAssocAndComposition( $art, $assoc, $card, $target ); }
911
- ( ON cond=condition { $art.on = $cond; }
912
- | foreignKeysBlock[ $art ]?
913
- nullabilityAndDefault[ $art ]?
914
- )
915
- // final anno assignments allowed also with fkeys (no auto-`;` after them):
916
- // (the "TODO TOOL" code for doc/annos would also be fine)
917
- { this.docComment( $art ); } annoAssignStd[ $art ]*
918
- |
919
- { $target = { location: this.startLocation( this.la() ) };
920
- this.setAssocAndComposition( $art, $assoc, $card, $target ); }
921
- elementsBlock[ $target ]
922
- { $target.location = $target.elements[Symbol.for('cds.$location')]; }
872
+ ( typeAssocProperties[ $art, $assoc, $card ]
873
+ | elementsBlock[ this.setAssocAndComposition( $art, $assoc, $card ) ]
874
+ { $art.target.location = $art.target.elements[Symbol.for('cds.$location')]; }
923
875
  )
924
876
  |
925
877
  ( ARRAY <prepare=elementRestriction, arg=calc>
926
878
  OF { $art.items = { location: this.locationOfPrevTokens( 2 ) }; }
927
879
  | MANY <prepare=elementRestriction, arg=calc>
928
880
  { $art.items = { location: this.lb().location }; }
929
- )
881
+ ) // no anno assignments, except to end type expression
930
882
  (
931
- elementsBlock[ $art.items ]
932
- nullability[ $art.items ]?
933
- |
934
883
  ( typeRefOptArgs[ $art.items ] | typeTypeOf[ $art.items ] )
935
- nullability[ $art.items ]?
936
- ( enumSymbolsBlock[ $art.items ]
937
- nullability[ $art.items ]?
938
- |
939
- // TODO TOOL: with <exitRule>, we could move the following to the end
940
- { this.elementRestriction( false, 'calc' ); }
941
- // <prepare=elementRestriction, arg=calc>
942
- { this.docComment( $art ); } annoAssignStd[ $art ]+
943
- |
944
- { this.docComment( $art, 'elem' ); }
884
+ ( <cond=elementRestriction, arg=notNull> nullability[ $art.items ] )?
885
+ ( { this.docComment( $art ); } annoAssignStd[ $art ]*
886
+ { ; } <exitRule> // TODO TOOL: make it work without workaround { ; }
887
+ // TODO TOOL: investigate why simply `{} <exitRule>` is ignored
888
+ | enumSymbolsBlock[ $art.items ]
945
889
  )
890
+ |
891
+ elementsBlock[ $art.items ]
946
892
  )
893
+ ( <cond=elementRestriction, arg=notNull> nullability[ $art.items ] )?
894
+ |
895
+ elementsBlock[ $art ] <prepare=elementRestriction, arg=calc>
896
+ nullability[ $art ]?
897
+ ;
898
+
899
+ typeAssocProperties[ art, assoc, card ]
900
+ :
901
+ target=simplePath { this.setAssocAndComposition( $art, $assoc, $card, $target ); }
902
+ { this.docComment( $art ); } annoAssignStd[ $art ]*
903
+ ( ON cond=condition { $art.on = $cond; }
904
+ { this.docComment( $art ); } annoAssignStd[ $art ]*
905
+ | foreignKeysBlock[ $art ] { this.docComment( $art ); } typeProperties[ $art ]?
906
+ // remark: no auto-`;` after foreign keys → anno assignment after it possible
907
+ | typeProperties[ $art ]
908
+ )?
909
+ ;
910
+
911
+ typeProperties[ art ]
912
+ :
913
+ (
914
+ annoAssignStd[ $art ]
915
+ |
916
+ <cond=elementRestriction, arg=notNull>
917
+ nullability[ $art ] { this.docComment( $art ); }
918
+ |
919
+ <cond=elementRestriction, arg=default>
920
+ DEFAULT expr=expression { $art.default = $expr; this.docComment( $art ); }
921
+ )+
947
922
  ;
948
923
 
924
+
949
925
  typeTypeOf[ art ] locals[ location ]
950
926
  :
951
927
  TYPE OF { location = this.locationOfPrevTokens( 2 ); }
@@ -1049,7 +1025,7 @@ targetCardinality[ card, atAlt = false ]
1049
1025
  '*' { $card.targetMax = this.valueWithLocation(); }
1050
1026
  |
1051
1027
  Number { $card.targetMax = this.unsignedIntegerLiteral(); }
1052
- ( <altRuleStart> ) // TODO TOOL: robust error when moved to after '('
1028
+ (<altRuleStart>) // TODO TOOL: robust error when moved to after '('
1053
1029
  (
1054
1030
  '..' { $card.targetMin = $card.targetMax; }
1055
1031
  ( '*' { $card.targetMax = this.valueWithLocation(); }
@@ -1059,20 +1035,6 @@ targetCardinality[ card, atAlt = false ]
1059
1035
  )
1060
1036
  ;
1061
1037
 
1062
- nullabilityAndDefault[ art ]
1063
- :
1064
- nullability[ $art ]
1065
- ( <cond=elementRestriction, arg=default> DEFAULT expr=expression
1066
- { $art.default = $expr; }
1067
- )?
1068
- |
1069
- <cond=elementRestriction, arg=default> DEFAULT expr=expression
1070
- { $art.default = $expr; }
1071
- nullability[ $art ]?
1072
- // TODO TOOL: when `followUnion` does not contain `Id`, `RuleEnd_` does not
1073
- // need to induce prediction (here for `default`).
1074
- ;
1075
-
1076
1038
  nullability[ art ]
1077
1039
  :
1078
1040
  NULL { this.setNullability( $art, false ); }
@@ -1209,7 +1171,7 @@ tableExpression returns[ default expr = {} ] // TableOrJoin
1209
1171
  tableOrQueryParens returns[ default expr ]
1210
1172
  :
1211
1173
  '(' <prepare=queryOnLeft>
1212
- ( <priority> tableOrQueryParens[ ...$ ]
1174
+ ( <prefer> tableOrQueryParens[ ...$ ]
1213
1175
  ( tableExpression[ ...$ ]<atAltStart, prepare=queryOnLeft, arg=table>
1214
1176
  | <cond=queryOnLeft> queryExpression[ ...$ ]<atAltStart>
1215
1177
  )?
@@ -1498,8 +1460,6 @@ condition returns[ default expr ]
1498
1460
  expression[ ...$ ]
1499
1461
  ;
1500
1462
 
1501
- // TODO TOOL: sort rules if a rule with <altRuleStart> uses rule with <altRuleStart>
1502
- // at least issue error if no user-sorted
1503
1463
  valuePath returns[ default expr = { path: [] } ] locals[ pathItem ]
1504
1464
  @finally{ this.attachLocation( $expr ); }
1505
1465
  :
@@ -1619,7 +1579,7 @@ expression returns[ default expr = {} ]
1619
1579
  expressionOrQueryParens returns[ default expr ]
1620
1580
  :
1621
1581
  '(' <prepare=queryOnLeft>
1622
- ( <priority> expressionOrQueryParens[ ...$ ]
1582
+ ( <prefer> expressionOrQueryParens[ ...$ ]
1623
1583
  ( expression[ ...$ ]<atAltStart, prepare=queryOnLeft, arg=expr>
1624
1584
  continueExpressionslist[ ...$ ]?
1625
1585
  | continueExpressionslist[ ...$ ] <prepare=queryOnLeft, arg=expr>
@@ -2000,7 +1960,8 @@ annoValue returns[ default value = {} ]
2000
1960
  }
2001
1961
  ( ',' | <exitLoop> )
2002
1962
  )*
2003
- // ( <cond=TODO> '}' ) // TODO TOOL - workaround:
1963
+ // TODO TOOL: allow:
1964
+ // ( <cond=arrayAnno, …> '}' | <error> ) // TODO TOOL - workaround:
2004
1965
  { this.ec( 'arrayAnno', 'orNotEmpty' ); } '}'
2005
1966
  // Do NOT use <prepare=afterBrace> here!
2006
1967
  |
@@ -0,0 +1,123 @@
1
+ // Entry point to parsers
2
+
3
+ 'use strict';
4
+
5
+ const lazyload = require('../base/lazyload')( module );
6
+ const { CompilerAssertion } = require( '../base/error' );
7
+ const { createMessageFunctions } = require( '../base/messages' );
8
+ const { XsnSource } = require('../compiler/xsn-model');
9
+
10
+ const parseWithAntlr = lazyload('../language/antlrParser');
11
+ const CdlLexer = require( './Lexer' );
12
+ const gen = lazyload( '../gen/CdlParser' );
13
+
14
+ const rules = {
15
+ cdl: { func: 'start', returns: 'source', $frontend: 'cdl' },
16
+ query: { func: 'queryEOF', returns: 'query' },
17
+ expr: { func: 'conditionEOF', returns: 'cond' }, // yes, condition
18
+ };
19
+
20
+ function parseCdl( source, filename = '<undefined>.cds',
21
+ options = {}, messageFunctions = null,
22
+ rule = 'cdl' ) {
23
+ const rulespec = rules[rule];
24
+ if (!options.newParser)
25
+ return parseWithAntlr( source, filename, options, messageFunctions, rulespec );
26
+ const { CdlParser } = gen;
27
+ if (CdlParser.tracingParser) // tracing → direct console output of message
28
+ messageFunctions = createMessageFunctions( {}, 'parse', {} );
29
+
30
+ const lexer = new CdlLexer( filename, source );
31
+ const parser = new CdlParser( lexer, options, messageFunctions ).init();
32
+ parser.filename = filename; // LSP compatibility
33
+
34
+ // For LSP:
35
+ const { parseListener, attachTokens } = options;
36
+ if (parseListener || attachTokens)
37
+ setTokenStream( parser, lexer );
38
+ if (parseListener)
39
+ setParseListener( parser, parseListener );
40
+
41
+ const result = {};
42
+ try {
43
+ parser[rulespec.func]( result );
44
+ }
45
+ catch (e) {
46
+ if (!(e instanceof RangeError && /Maximum.*exceeded$/i.test( e.message )))
47
+ throw e;
48
+ messageFunctions.error( 'syntax-invalid-source', { file: filename },
49
+ { '#': 'cdl-stackoverflow' } );
50
+ result[rulespec.returns] = undefined;
51
+ }
52
+ const ast = result[rulespec?.returns] || (rule === 'cdl' ? new XsnSource( 'cdl' ) : {} );
53
+ ast.options = options;
54
+ if (attachTokens === true || attachTokens === filename)
55
+ ast.tokenStream = parser._input;
56
+ return ast;
57
+ }
58
+
59
+ function setTokenStream( parser, lexer ) {
60
+ const combined = [];
61
+ const { tokens, comments, docComments } = parser;
62
+ const length = tokens.length + comments.length + docComments.length;
63
+ let tokenIdx = 0;
64
+ let commentIdx = 0;
65
+ let docCommentIdx = 0;
66
+ for (let index = 0; index < length; ++index) {
67
+ if (tokens[tokenIdx].location.tokenIndex === index) // EOF has largest tokenIndex
68
+ combined.push( tokens[tokenIdx++] );
69
+ else if (comments[commentIdx]?.location.tokenIndex === index)
70
+ combined.push( comments[commentIdx++] );
71
+ else
72
+ combined.push( docComments[docCommentIdx++] );
73
+ }
74
+ if (!combined.at( -1 ))
75
+ throw new CompilerAssertion( 'Invalid values for `tokenIndex`' );
76
+ for (const tok of combined)
77
+ tok.start = lexer.characterPos( tok.location.line, tok.location.col );
78
+
79
+ parser._input = { tokens: combined, lexer }; // lexer for characterPos() in cdshi.js
80
+ parser.getTokenStream = function getTokenStream() {
81
+ return this._input;
82
+ };
83
+ }
84
+
85
+ function setParseListener( parser, parseListener ) {
86
+ const { CdlParser } = gen;
87
+ parser.rule_ = function rule_( ...args ) {
88
+ // TODO: can we use `super` here?
89
+ CdlParser.prototype.rule_.apply( this, args );
90
+ let state = this.s;
91
+ while (typeof this.table[--state] !== 'string')
92
+ ;
93
+ const $ctx = { // ANTLR-like context, TODO LSP: more to add?
94
+ parser: this, // set in generated ANTLR parser for each rule context
95
+ ruleName: this.table[state], // instead of ruleIndex
96
+ start: this.la(), // set in Parser#enterRule
97
+ stop: null,
98
+ };
99
+ parser.stack.at( -1 ).$ctx = $ctx;
100
+ parseListener.enterEveryRule( $ctx );
101
+ };
102
+ parser.exit_ = function exit_( ...args ) {
103
+ const { $ctx } = parser.stack.at( -1 );
104
+ // TODO: what should we do in case of errors?
105
+ $ctx.stop = this.lb();
106
+ parseListener.exitEveryRule( $ctx );
107
+ return CdlParser.prototype.exit_.apply( this, args );
108
+ };
109
+ parser.c = function c( ...args ) { // consume
110
+ const symbol = this.la();
111
+ const result = CdlParser.prototype.c.apply( this, args );
112
+ if (result)
113
+ parseListener.visitTerminal( { symbol } );
114
+ return result;
115
+ };
116
+ parser.skipToken_ = function skipToken_( ...args ) { // skip token in error recovery
117
+ const symbol = this.la();
118
+ CdlParser.prototype.skipToken_.apply( this, args ); // = `++this.tokenIdx`
119
+ parseListener.visitErrorNode( { symbol } );
120
+ };
121
+ }
122
+
123
+ module.exports = { parseCdl };
@@ -559,13 +559,19 @@ function toSqlDdl( csn, options, messageFunctions ) {
559
559
  const add = def.new.target
560
560
  ? render.addAssociations(artifactName, { [eltName]: def.new }, env)
561
561
  : render.addColumnsFromElementsObj(artifactName, { [eltName]: def.new }, env);
562
- addMigration(resultObj, artifactName, true, render.concat(...drop, ...add).map(s => (def.lossy && options.src !== 'hdi' ? `-- [WARNING] this statement could be lossy\n${s}` : s)));
562
+ addMigration(resultObj, artifactName, true, render.concat(...drop, ...add).map(s => (def.lossy !== undefined && options.src !== 'hdi' ? `-- [WARNING] this statement could ${def.lossy ? 'be lossy' : 'fail'}: ${def.details}\n${s}` : s)));
563
563
  }
564
564
  else { // Lossless change: no associations directly affected, no size reduction.
565
- addMigration(resultObj, artifactName, false, render.alterColumns(artifactName, sqlId, def, eltStrNew, eltName, activateAlterMode(env, 'migration')).map(s => (def.lossy ? `-- [WARNING] this statement could be lossy\n${s}` : s)));
565
+ addMigration(resultObj, artifactName, false, render.alterColumns(artifactName, sqlId, def, eltStrNew, eltName, activateAlterMode(env, 'migration')).map(s => (def.lossy !== undefined ? `-- [WARNING] this statement could ${def.lossy ? 'be lossy' : 'fail'}: ${def.details}\n${s}` : s)));
566
566
  }
567
567
  }
568
568
  }
569
+
570
+ if (render.getConsolidatedAlterColumn) {
571
+ const consolidated = render.getConsolidatedAlterColumn(artifactName);
572
+ if (consolidated)
573
+ addMigration(resultObj, artifactName, false, consolidated);
574
+ }
569
575
  }
570
576
 
571
577
  /**
@@ -72,6 +72,15 @@ class DeltaRenderer {
72
72
  * Render column modifications as SQL.
73
73
  */
74
74
  alterColumns(artifactName, columnName, delta, definitionsStr, _eltName, _env) {
75
+ if (Array.isArray(definitionsStr)) {
76
+ const prefix = `ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER (`;
77
+ let padding = '';
78
+ for (let i = 0; i < prefix.length; i++)
79
+ padding += ' ';
80
+ const body = definitionsStr.map(s => padding + s).join(',\n').slice(padding.length); // no padding for first part
81
+ const postfix = ');';
82
+ return [ prefix + body + postfix ];
83
+ }
75
84
  return [ `ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER (${definitionsStr});` ];
76
85
  }
77
86
 
@@ -123,6 +132,29 @@ class DeltaRenderer {
123
132
  }
124
133
 
125
134
  class DeltaRendererHana extends DeltaRenderer {
135
+ #alters = [];
136
+ #details = [];
137
+
138
+ getConsolidatedAlterColumn(artifactName) {
139
+ if (this.#alters.length === 0)
140
+ return null;
141
+ const result = [ ...this.#details, ...super.alterColumns(artifactName, null, null, this.#alters) ];
142
+ this.#alters = [];
143
+ this.#details = [];
144
+ return result;
145
+ }
146
+
147
+ /**
148
+ * Render column modifications as SQL.
149
+ */
150
+ alterColumns(artifactName, columnName, delta, definitionsStr, _eltName, _env) {
151
+ if (delta.details)
152
+ this.#details.push(`-- [WARNING] this statement could ${delta.lossy ? 'be lossy' : 'fail'}: ${delta.details}`);
153
+
154
+ this.#alters.push(definitionsStr);
155
+ return [];
156
+ }
157
+
126
158
  /**
127
159
  * Render column additions as HANA SQL. Checks for duplicate elements.
128
160
  */
@@ -230,7 +262,7 @@ class DeltaRendererH2 extends DeltaRenderer {
230
262
  /**
231
263
  * Render column modifications as H2 SQL - no ().
232
264
  */
233
- alterColumns(artifactName, columnName, delta, definitionsStr) {
265
+ alterColumns(artifactName, columnName, delta, definitionsStr, _eltName, _env) {
234
266
  return [ `ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER ${definitionsStr};` ];
235
267
  }
236
268
  }