@sap/cds-compiler 5.1.2 → 5.3.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 (68) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/bin/cdsc.js +7 -2
  3. package/bin/cdshi.js +24 -17
  4. package/bin/cdsse.js +17 -18
  5. package/doc/CHANGELOG_BETA.md +9 -4
  6. package/lib/api/main.js +19 -2
  7. package/lib/api/options.js +4 -1
  8. package/lib/api/validate.js +5 -0
  9. package/lib/base/builtins.js +1 -0
  10. package/lib/base/message-registry.js +40 -3
  11. package/lib/base/messages.js +1 -1
  12. package/lib/base/model.js +0 -11
  13. package/lib/checks/actionsFunctions.js +0 -12
  14. package/lib/checks/structuredAnnoExpressions.js +10 -14
  15. package/lib/compiler/assert-consistency.js +21 -13
  16. package/lib/compiler/builtins.js +2 -2
  17. package/lib/compiler/checks.js +25 -6
  18. package/lib/compiler/define.js +27 -31
  19. package/lib/compiler/extend.js +16 -18
  20. package/lib/compiler/generate.js +3 -3
  21. package/lib/compiler/populate.js +22 -16
  22. package/lib/compiler/propagator.js +3 -2
  23. package/lib/compiler/resolve.js +87 -94
  24. package/lib/compiler/shared.js +12 -13
  25. package/lib/compiler/tweak-assocs.js +390 -86
  26. package/lib/compiler/utils.js +41 -33
  27. package/lib/compiler/xpr-rewrite.js +45 -58
  28. package/lib/edm/annotations/genericTranslation.js +17 -13
  29. package/lib/edm/csn2edm.js +28 -4
  30. package/lib/edm/edm.js +68 -28
  31. package/lib/edm/edmInboundChecks.js +5 -8
  32. package/lib/edm/edmPreprocessor.js +66 -40
  33. package/lib/edm/edmUtils.js +1 -1
  34. package/lib/gen/BaseParser.js +778 -0
  35. package/lib/gen/CdlParser.js +4477 -0
  36. package/lib/gen/language.checksum +1 -1
  37. package/lib/gen/language.interp +1 -1
  38. package/lib/gen/languageParser.js +4072 -4024
  39. package/lib/inspect/inspectPropagation.js +1 -1
  40. package/lib/json/from-csn.js +5 -3
  41. package/lib/json/to-csn.js +7 -10
  42. package/lib/language/antlrParser.js +96 -0
  43. package/lib/language/errorStrategy.js +1 -1
  44. package/lib/language/genericAntlrParser.js +32 -4
  45. package/lib/language/multiLineStringParser.js +1 -1
  46. package/lib/main.d.ts +23 -0
  47. package/lib/model/cloneCsn.js +22 -13
  48. package/lib/model/csnUtils.js +2 -0
  49. package/lib/model/revealInternalProperties.js +2 -0
  50. package/lib/modelCompare/utils/filter.js +70 -42
  51. package/lib/optionProcessor.js +16 -10
  52. package/lib/parsers/AstBuildingParser.js +1290 -0
  53. package/lib/parsers/CdlGrammar.g4 +2013 -0
  54. package/lib/parsers/Lexer.js +249 -0
  55. package/lib/render/toCdl.js +46 -45
  56. package/lib/render/toSql.js +5 -5
  57. package/lib/transform/addTenantFields.js +4 -4
  58. package/lib/transform/db/applyTransformations.js +54 -16
  59. package/lib/transform/draft/odata.js +10 -11
  60. package/lib/transform/effective/flattening.js +10 -14
  61. package/lib/transform/forRelationalDB.js +7 -6
  62. package/lib/transform/odata/flattening.js +42 -31
  63. package/lib/transform/odata/toFinalBaseType.js +7 -6
  64. package/lib/transform/universalCsn/universalCsnEnricher.js +1 -0
  65. package/lib/utils/moduleResolve.js +1 -1
  66. package/package.json +2 -2
  67. package/share/messages/redirected-to-ambiguous.md +5 -4
  68. package/share/messages/redirected-to-complex.md +6 -3
@@ -36,7 +36,7 @@ function inspectPropagation( xsn, options, artifactName ) {
36
36
 
37
37
  if (!artifactXsn) {
38
38
  error(null, null, { name: artifactName },
39
- // eslint-disable-next-line max-len
39
+ // eslint-disable-next-line @stylistic/js/max-len
40
40
  'Artifact $(NAME) not found, only top-level artifacts and their elements are supported for now');
41
41
  return null;
42
42
  }
@@ -1134,7 +1134,7 @@ function elementsDict( def, spec, xsn ) {
1134
1134
  return elements;
1135
1135
  warning( 'syntax-expecting-returns', elements[$location],
1136
1136
  { prop: 'elements', parentprop: 'returns' },
1137
- // eslint-disable-next-line max-len
1137
+ // eslint-disable-next-line @stylistic/js/max-len
1138
1138
  'Expecting property $(PROP) to be put into an object for property $(PARENTPROP) when annotating action return structures' );
1139
1139
  xsn.returns = { kind: 'annotate', elements, location: elements[$location] };
1140
1140
  return undefined;
@@ -2076,8 +2076,10 @@ function toXsn( csn, filename, options, messageFunctions ) {
2076
2076
 
2077
2077
  const xsn = new XsnSource( 'json' ); // TODO: 'csn'? LSP does not use $frontend
2078
2078
 
2079
- // eslint-disable-next-line object-curly-newline
2080
- ({ message, error, warning, info } = messageFunctions);
2079
+
2080
+ ({
2081
+ message, error, warning, info,
2082
+ } = messageFunctions);
2081
2083
 
2082
2084
  if (csnVersionZero) {
2083
2085
  warning( 'syntax-deprecated-csn-version', location(true), {},
@@ -123,7 +123,7 @@ const transformers = {
123
123
  value: enumValueOrCalc, // do not list for select items as elements
124
124
  query,
125
125
  elements,
126
- actions, // TODO: just normal dictionary
126
+ actions: sortedDict, // TODO: just normal dictionary
127
127
  returns, // storing the return type of actions
128
128
  // special: top-level, cardinality -----------------------------------------
129
129
  sources,
@@ -609,6 +609,10 @@ function addLocation( loc, csn ) {
609
609
  // is often not the reason for an error or warning. So we gain little benefit for
610
610
  // two more properties. It is also an indication that the location is not exact.
611
611
  const val = { file: loc.file, line: loc.line, col: loc.col };
612
+ if (withLocations === 'withEndPosition' && loc.endLine) {
613
+ val.endLine = loc.endLine;
614
+ val.endCol = loc.endCol;
615
+ }
612
616
  Object.defineProperty( csn, '$location', {
613
617
  value: val, configurable: true, writable: true, enumerable: withLocations,
614
618
  } );
@@ -621,18 +625,11 @@ function insertOrderDict( dict ) {
621
625
  return dictionary( dict, keys );
622
626
  }
623
627
 
624
- function sortedDict( dict ) {
628
+ function sortedDict( dict, _csn, _node, prop ) {
625
629
  const keys = Object.keys( dict );
626
630
  if (strictMode)
627
631
  keys.sort();
628
- return dictionary( dict, keys );
629
- }
630
-
631
- function actions( dict, _csn, node ) {
632
- const keys = Object.keys( dict );
633
- if (strictMode && node.kind === 'annotate')
634
- keys.sort(); // TODO: always sort with --test-mode ?
635
- return dictionary( dict, keys, 'actions' );
632
+ return dictionary( dict, keys, prop );
636
633
  }
637
634
 
638
635
  function params( dict ) {
@@ -17,6 +17,11 @@ const { XsnSource } = require('../compiler/xsn-model');
17
17
  const Parser = require('../gen/languageParser').default;
18
18
  const Lexer = require('../gen/languageLexer').default;
19
19
 
20
+ const CdlLexer = require( '../parsers/Lexer' );
21
+ const CdlParser = require( '../gen/CdlParser' );
22
+ const { createMessageFunctions } = require( '../base/messages' );
23
+ const { CompilerAssertion } = require( '../base/error' );
24
+
20
25
  // Error listener used for ANTLR4-generated parser
21
26
  class ErrorListener extends antlr4.error.ErrorListener {
22
27
  // method which is called by generated parser with --trace-parser[-amg]:
@@ -129,6 +134,9 @@ const rules = {
129
134
  function parse( source, filename = '<undefined>.cds',
130
135
  options = {}, messageFunctions = null,
131
136
  rule = 'cdl' ) {
137
+ if (options.newParser)
138
+ return parseWithNewParser( source, filename, options, messageFunctions, rule );
139
+
132
140
  const lexer = new Lexer( new antlr4.InputStream(source) );
133
141
  const tokenStream = new RewriteTypeTokenStream(lexer);
134
142
  /** @type {object} */
@@ -211,4 +219,92 @@ function parse( source, filename = '<undefined>.cds',
211
219
  return ast;
212
220
  }
213
221
 
222
+ function parseWithNewParser( source, filename, options, messageFunctions, rule ) {
223
+ if (CdlParser.tracingParser) // tracing → direct console output of message
224
+ messageFunctions = createMessageFunctions( {}, 'parse', {} );
225
+ const lexer = new CdlLexer( filename, source );
226
+ const parser = new CdlParser( lexer, options, messageFunctions ).init();
227
+ parser.filename = filename; // LSP compatibility
228
+
229
+ const { parseListener, attachTokens } = options;
230
+ if (parseListener || attachTokens) {
231
+ const combined = [];
232
+ const { tokens, comments, docComments } = parser;
233
+ const length = tokens.length + comments.length + docComments.length;
234
+ let tokenIdx = 0;
235
+ let commentIdx = 0;
236
+ let docCommentIdx = 0;
237
+ for (let index = 0; index < length; ++index) {
238
+ if (tokens[tokenIdx].location.tokenIndex === index) // EOF has largest tokenIndex
239
+ combined.push( tokens[tokenIdx++] );
240
+ else if (comments[commentIdx]?.location.tokenIndex === index)
241
+ combined.push( comments[commentIdx++] );
242
+ else
243
+ combined.push( docComments[docCommentIdx++] );
244
+ }
245
+ if (!combined.at( -1 ))
246
+ throw new CompilerAssertion( 'Invalid values for `tokenIndex`' );
247
+ for (const tok of combined)
248
+ tok.start = lexer.characterPos( tok.location.line, tok.location.col );
249
+
250
+ parser._input = { tokens: combined, lexer }; // lexer for characterPos() in cdshi.js
251
+ parser.getTokenStream = function getTokenStream() {
252
+ return this._input;
253
+ };
254
+ }
255
+ // LSP feature: provide parse listener with ANTLR-like context:
256
+ if (parseListener) {
257
+ // TODO LSP: we could also call different listener methods: then LSP could
258
+ // have dedicated methods for ANTLR-based and new parser
259
+ parser.rule_ = function rule_( ...args ) {
260
+ CdlParser.prototype.rule_.apply( this, args );
261
+ let state = this.s;
262
+ while (typeof this.table[--state] !== 'string')
263
+ ;
264
+ const $ctx = { // TODO LSP: more to add?
265
+ parser: this, // set in generated ANTLR parser for each rule context
266
+ ruleName: this.table[state], // instead of ruleIndex
267
+ start: this.la(), // set in Parser#enterRule
268
+ stop: null,
269
+ };
270
+ parser.stack.at( -1 ).$ctx = $ctx;
271
+ parseListener.enterEveryRule( $ctx );
272
+ };
273
+ parser.exit_ = function exit_( ...args ) {
274
+ const { $ctx } = parser.stack.at( -1 );
275
+ // TODO: what should we do in case of errors?
276
+ $ctx.stop = this.lb();
277
+ parseListener.exitEveryRule( $ctx );
278
+ CdlParser.prototype.exit_.apply( this, args );
279
+ };
280
+ parser.c = function c( ...args ) { // consume
281
+ CdlParser.prototype.c.apply( this, args );
282
+ parseListener.visitTerminal( { symbol: this.lb() } );
283
+ };
284
+ parser.skipToken_ = function skipToken_( ...args ) { // skip token in error recovery
285
+ CdlParser.prototype.skipToken_.apply( this, args ); // = `++this.tokenIdx`
286
+ parseListener.visitErrorNode( { symbol: this.lb() } );
287
+ };
288
+ }
289
+ const result = {};
290
+ const rulespec = rules[rule];
291
+ if (rulespec) {
292
+ try {
293
+ parser[rulespec.func]( result );
294
+ }
295
+ catch (e) {
296
+ if (!(e instanceof RangeError && /Maximum.*exceeded$/i.test( e.message )))
297
+ throw e;
298
+ messageFunctions.error('syntax-invalid-source', { file: filename },
299
+ { '#': 'cdl-stackoverflow' } );
300
+ result[rulespec.returns] = undefined;
301
+ }
302
+ }
303
+ const ast = result[rulespec?.returns] || (rule === 'cdl' ? new XsnSource( 'cdl' ) : {} );
304
+ ast.options = options;
305
+ if (attachTokens === true || attachTokens === filename)
306
+ ast.tokenStream = parser._input;
307
+ return ast;
308
+ }
309
+
214
310
  module.exports = parse;
@@ -337,7 +337,7 @@ function reportIgnoredWith( recognizer, t ) {
337
337
  const expecting = this.getExpectedTokensForMessage( recognizer, t );
338
338
  const m = recognizer.warning( 'syntax-unexpected-semicolon', t,
339
339
  { offending: "';'", expecting, keyword: 'with' },
340
- // eslint-disable-next-line max-len
340
+ // eslint-disable-next-line @stylistic/js/max-len
341
341
  'Unexpected $(OFFENDING), expecting $(EXPECTING) - ignored previous $(KEYWORD)' );
342
342
  m.expectedTokens = expecting;
343
343
  }
@@ -101,6 +101,8 @@ Object.assign(GenericAntlrParser.prototype, {
101
101
  warnIfColonFollows,
102
102
  fragileAlias,
103
103
  identAst,
104
+ reportPathNamedManyOrOne,
105
+ reportMissingSemicolon,
104
106
  pushXprToken,
105
107
  pushOpToken,
106
108
  argsExpression,
@@ -276,8 +278,9 @@ function markAsSkippedUntilEOF() {
276
278
  function noAssignmentInSameLine() {
277
279
  const t = this.getCurrentToken();
278
280
  if (t.text === '@' && t.line <= this._input.LT(-1).line) {
281
+ // TODO: use 'syntax-missing-newline'
279
282
  this.warning( 'syntax-missing-semicolon', t, { code: ';' },
280
- // eslint-disable-next-line max-len
283
+ // eslint-disable-next-line @stylistic/js/max-len
281
284
  'Add a $(CODE) and/or newline before the annotation assignment to indicate that it belongs to the next statement' );
282
285
  }
283
286
  }
@@ -682,7 +685,7 @@ function warnIfColonFollows( anno ) {
682
685
  if (t.text === ':') {
683
686
  this.warning( 'syntax-missing-parens', anno.name.location,
684
687
  { code: '@‹anno›', op: ':', newcode: '@(‹anno›…)' },
685
- // eslint-disable-next-line max-len
688
+ // eslint-disable-next-line @stylistic/js/max-len
686
689
  'When $(CODE) is followed by $(OP), use $(NEWCODE) for annotation assignments at this position' );
687
690
  }
688
691
  }
@@ -793,7 +796,7 @@ function identAst( token, category, noTokenTypeCheck = false ) {
793
796
  }
794
797
  else {
795
798
  this.message( 'syntax-deprecated-ident', token, { delimited: id },
796
- // eslint-disable-next-line max-len
799
+ // eslint-disable-next-line @stylistic/js/max-len
797
800
  'Deprecated delimited identifier syntax, use $(DELIMITED) - strings are delimited by single quotes' );
798
801
  }
799
802
  const ast = { id, $delimited: true, location: this.tokenLocation( token ) };
@@ -801,6 +804,31 @@ function identAst( token, category, noTokenTypeCheck = false ) {
801
804
  return ast;
802
805
  }
803
806
 
807
+ function reportPathNamedManyOrOne( { path } ) {
808
+ if (path.length === 1 && !path[0].$delimited &&
809
+ [ 'many', 'one' ].includes( path[0].id.toLowerCase() )) {
810
+ this.message( 'syntax-unexpected-many-one', path[0].location,
811
+ { code: path[0].id, delimited: path[0].id } );
812
+ }
813
+ }
814
+
815
+ function reportMissingSemicolon() {
816
+ const next = this._input.LT(1);
817
+ if (next.text !== ';' && next.text !== '' && // ';' by insertSemicolon()
818
+ next.text !== '}' && next.type !== antlr4.Token.EOF &&
819
+ this._input.LT(-1).text !== '}') {
820
+ const offending = this.literalNames[next.type] || this.symbolicNames[next.type];
821
+ const loc = this.tokenLocation( this._input.LT(-1) );
822
+ // better location after the previous token:
823
+ const location = new Location( loc.file, loc.endLine, loc.endCol );
824
+ // it would be nicer to mention the doc comment if present, but not worth the
825
+ // effort; 'syntax-missing-semicolon' already used
826
+ this.warning( 'syntax-missing-proj-semicolon', location,
827
+ { expecting: [ "';'" ], offending },
828
+ 'Missing $(EXPECTING) before $(OFFENDING)');
829
+ }
830
+ }
831
+
804
832
  function pushXprToken( args ) {
805
833
  const token = this._input.LT(-1);
806
834
  args.push( {
@@ -1021,7 +1049,7 @@ function assignAnnotationValue( anno, value ) {
1021
1049
  {
1022
1050
  std: 'Annotation number $(RAWVALUE) is put as $(VALUE) into the CSN',
1023
1051
  rounded: 'Annotation number $(RAWVALUE) is rounded to $(VALUE)',
1024
- // eslint-disable-next-line max-len
1052
+ // eslint-disable-next-line @stylistic/js/max-len
1025
1053
  infinite: 'Annotation value $(RAWVALUE) is infinite as number and put as string into the CSN',
1026
1054
  } );
1027
1055
  }
@@ -77,7 +77,7 @@ class MultiLineStringParser {
77
77
  this.str = token.text; // Copy because .text is a getter
78
78
 
79
79
  if (this.str[0] !== '`' || this.str[this.str.length - 1] !== '`')
80
- // eslint-disable-next-line max-len
80
+ // eslint-disable-next-line @stylistic/js/max-len
81
81
  throw new CompilerAssertion('Invalid multi-line string sequence: Require string to be surrounded by back-ticks!');
82
82
 
83
83
  this.output = [];
package/lib/main.d.ts CHANGED
@@ -154,6 +154,29 @@ declare namespace compiler {
154
154
  * @since v2.12.1
155
155
  */
156
156
  $xsnObjects?: boolean
157
+ /**
158
+ * If `true`, the CSN will have an enumerable property `$locations`.
159
+ * with values for `line` and `col`, i.e. there is only a start position,
160
+ * but no end position.
161
+ *
162
+ * If `false`, the property will be non-enumerable, i.e. it won't be
163
+ * serialized when using `JSON.stringify()`.
164
+ *
165
+ * With value `"withEndPosition"`, the property will be enumerable and
166
+ * will contain values for the end-position. Other string values
167
+ * are not allowed. This value was introduced in v5.3.0.
168
+ *
169
+ * $location is not set on all artifacts, and it only indicates the position
170
+ * of the _name_ of the artifact.
171
+ */
172
+ withLocations?: boolean|string
173
+ /**
174
+ * Use the new non-ANTLR based parser for compilation.
175
+ * Experimental flag!
176
+ *
177
+ * @since v5.2.0
178
+ */
179
+ newParser?: boolean
157
180
  /**
158
181
  * Internal option for LSP only!
159
182
  * If set, each AST gets a `tokenStream` property containing all lexed tokens.
@@ -5,16 +5,23 @@ const { ModelError } = require('../base/error');
5
5
  const { setHidden } = require('../utils/objectUtils');
6
6
  const { isAnnotationExpression } = require('../base/builtins');
7
7
 
8
- const csnDictionaries = [
9
- 'args',
10
- 'params',
11
- 'enum',
12
- 'mixin',
13
- 'elements',
14
- 'actions',
15
- 'definitions',
16
- 'vocabularies',
17
- ];
8
+ const csnDictionaries = {
9
+ __proto__: null,
10
+ args: 1,
11
+ params: 1,
12
+ enum: 1,
13
+ mixin: 1,
14
+ elements: 1,
15
+ actions: 1,
16
+ definitions: 1,
17
+ vocabularies: 1,
18
+ };
19
+
20
+ const sortedCsnDictionaries = {
21
+ __proto__: null,
22
+ definitions: 1,
23
+ actions: 1,
24
+ };
18
25
 
19
26
  function shallowCopy( val, _options, _sort ) {
20
27
  return val;
@@ -74,9 +81,9 @@ function cloneCsn( csn, options, sort ) {
74
81
  else if (!val || typeof val !== 'object') {
75
82
  r[n] = val;
76
83
  }
77
- else if (csnDictionaries.includes(n) && !Array.isArray(val)) {
78
- const sortDict = n === 'definitions' &&
79
- (!options || options.testMode || options.testSortCsn);
84
+ else if (csnDictionaries[n] && !Array.isArray(val)) {
85
+ const sortDict = (!options || options.testMode || options.testSortCsn) &&
86
+ sortedCsnDictionaries[n];
80
87
  // Array check for property `args` which may either be a dictionary or an array.
81
88
  r[n] = cloneCsnDict(val, options, sort, sortDict);
82
89
  }
@@ -122,6 +129,8 @@ function hasNonEnumerable( object, property ) {
122
129
  * @param {object} csn
123
130
  * @param {CSN.Options} options Only cloneOptions.dictionaryPrototype is
124
131
  * used and cloneOptions are passed to sortCsn().
132
+ * @param {boolean} sortProps Whether to sort CSN properties.
133
+ * @param {boolean} sortDict Whether to sort CSN dictionary entries.
125
134
  */
126
135
  function cloneCsnDict( csn, options, sortProps, sortDict ) {
127
136
  const proto = options?.dictionaryPrototype;
@@ -3,6 +3,7 @@
3
3
  const { csnRefs, implicitAs, pathId } = require('./csnRefs');
4
4
  const {
5
5
  transformExpression,
6
+ transformAnnotationExpression,
6
7
  applyTransformations,
7
8
  applyTransformationsOnNonDictionary,
8
9
  applyTransformationsOnDictionary,
@@ -1419,6 +1420,7 @@ module.exports = {
1419
1420
  getUnderscoredName,
1420
1421
  getElementDatabaseNameOf,
1421
1422
  transformExpression,
1423
+ transformAnnotationExpression,
1422
1424
  applyTransformations,
1423
1425
  applyTransformationsOnNonDictionary,
1424
1426
  applyTransformationsOnDictionary,
@@ -306,6 +306,8 @@ function revealInternalProperties( model, nameOrPath ) {
306
306
  }
307
307
 
308
308
  function array( node, fn ) {
309
+ if (!Array.isArray( node ))
310
+ return node;
309
311
  const r = node.map( n => fn( n, node ) );
310
312
  if (node[$location])
311
313
  r.push( { '[$location]': locationString( node[$location] ) } );
@@ -10,46 +10,57 @@ function isKey( element ) {
10
10
  return element.key;
11
11
  }
12
12
 
13
- module.exports = {
14
- sqlite: getFilterObject(
15
- 'sqlite',
16
- function forEachExtension(extend, name, elementOrConstraint, { error, warning }) {
17
- if (isKey(elementOrConstraint)) { // Key must not be extended
18
- error('type-unsupported-key-sqlite', [ 'definitions', extend, 'elements', name ], { id: name, name: 'sqlite', '#': 'std' } );
19
- return false;
20
- }
21
- else if (elementOrConstraint.parentTable) { // constraints have a .parentTable
22
- warning('def-unsupported-constraint-add', [ 'definitions', elementOrConstraint.parentTable, 'elements', elementOrConstraint.paths ? name : name.slice(elementOrConstraint.parentTable.length + 1) ], { id: elementOrConstraint.identifier || name, name: 'sqlite' },
23
- 'Ignoring add of constraint $(ID), as this is not supported for dialect $(NAME); you need to manually resolve this via shadow tables and data copy');
13
+ function getFilter(options) {
14
+ const filters = {
15
+ sqlite: getFilterObject(
16
+ options,
17
+ 'sqlite',
18
+ function forEachExtension(extend, name, elementOrConstraint, { error, warning }) {
19
+ if (isKey(elementOrConstraint)) { // Key must not be extended
20
+ error('type-unsupported-key-sqlite', [ 'definitions', extend, 'elements', name ], { id: name, name: 'sqlite', '#': 'std' } );
21
+ return false;
22
+ }
23
+ else if (elementOrConstraint.parentTable) { // constraints have a .parentTable
24
+ warning('def-unsupported-constraint-add', [ 'definitions', elementOrConstraint.parentTable, 'elements', elementOrConstraint.paths ? name : name.slice(elementOrConstraint.parentTable.length + 1) ], { id: elementOrConstraint.identifier || name, name: 'sqlite' },
25
+ 'Ignoring add of constraint $(ID), as this is not supported for dialect $(NAME); you need to manually resolve this via shadow tables and data copy');
26
+ return false;
27
+ }
28
+
29
+ return true;
30
+ },
31
+ function forEachMigration(migrate, name, migration, change, error) {
32
+ const newIsKey = isKey(migration.new);
33
+ const oldIsKey = isKey(migration.old);
34
+ if ((newIsKey || oldIsKey) && oldIsKey !== newIsKey) // Turned into key or key was removed
35
+ error('type-unsupported-key-sqlite', [ 'definitions', migrate, 'elements', name ], { id: name, name: 'sqlite', '#': 'changed' } );
36
+ else
37
+ delete change[name];
38
+ },
39
+ function forEachConstraintRemoval(constraintRemovals, name, constraint, warning) {
40
+ warning('def-unsupported-constraint-drop', [ 'definitions', constraint.parentTable, 'elements', constraint.paths ? name : name.slice(constraint.parentTable.length + 1) ], { id: constraint.identifier || name, name: 'sqlite' },
41
+ 'Ignoring drop of constraint $(ID), as this is not supported for dialect $(NAME); you need to manually resolve this via shadow tables and data copy');
42
+ delete constraintRemovals[name];
43
+ },
44
+ function primaryKey() {
24
45
  return false;
25
46
  }
47
+ ),
48
+ postgres: getFilterObject(options, 'postgres'),
49
+ h2: getFilterObject(options, 'h2'),
50
+ hana: getFilterObject(options, 'hana'),
51
+ };
26
52
 
27
- return true;
28
- },
29
- function forEachMigration(migrate, name, migration, change, error) {
30
- const newIsKey = isKey(migration.new);
31
- const oldIsKey = isKey(migration.old);
32
- if ((newIsKey || oldIsKey) && oldIsKey !== newIsKey) // Turned into key or key was removed
33
- error('type-unsupported-key-sqlite', [ 'definitions', migrate, 'elements', name ], { id: name, name: 'sqlite', '#': 'changed' } );
34
- else
35
- delete change[name];
36
- },
37
- function forEachConstraintRemoval(constraintRemovals, name, constraint, warning) {
38
- warning('def-unsupported-constraint-drop', [ 'definitions', constraint.parentTable, 'elements', constraint.paths ? name : name.slice(constraint.parentTable.length + 1) ], { id: constraint.identifier || name, name: 'sqlite' },
39
- 'Ignoring drop of constraint $(ID), as this is not supported for dialect $(NAME); you need to manually resolve this via shadow tables and data copy');
40
- delete constraintRemovals[name];
41
- },
42
- function primaryKey() {
43
- return false;
44
- }
45
- ),
46
- postgres: getFilterObject('postgres'),
47
- h2: getFilterObject('h2'),
48
- hana: getFilterObject('hana'),
53
+ return filters[options.sqlDialect];
54
+ }
55
+
56
+ module.exports = {
49
57
  csn: filterCsn,
58
+ getFilter,
50
59
  };
51
60
 
52
- function getFilterObject( dialect, extensionCallback, migrationCallback, removeConstraintsCallback, primaryKeyCallback ) {
61
+ function getFilterObject( options, dialect, extensionCallback, migrationCallback, removeConstraintsCallback, primaryKeyCallback ) {
62
+ const context = { hasLossyChanges: false };
63
+ const raiseErrorOrMarkAsLossy = getSafeguardManager(context, options);
53
64
  return {
54
65
  // will be called with a simple Array.filter, as we need to filter constraint `ADD` for SQLite
55
66
  extension: ({
@@ -70,24 +81,28 @@ function getFilterObject( dialect, extensionCallback, migrationCallback, removeC
70
81
  },
71
82
  // will be called with a Array.forEach
72
83
  migration: (migrations, { error, warning, message }) => {
73
- forEach(migrations.remove, (name) => {
74
- error('def-unsupported-element-drop', [ 'definitions', migrations.migrate, 'elements', name ], {}, 'Dropping elements is not supported');
84
+ forEach(migrations.remove, (name, migration) => {
85
+ raiseErrorOrMarkAsLossy(migration, () => error('def-unsupported-element-drop', [ 'definitions', migrations.migrate, 'elements', name ], {}, 'Dropping elements is not supported'));
75
86
  });
87
+
76
88
  forEach(migrations.change, (name, migration) => {
77
89
  const loc = [ 'definitions', migrations.migrate, 'elements', name ];
78
90
  if (migration.new.type === migration.old.type && migration.new.length < migration.old.length)
79
- error('type-unsupported-length-change', loc, { id: name }, 'Changed element $(ID) is a length reduction and is not supported');
91
+ raiseErrorOrMarkAsLossy(migration, () => error('type-unsupported-length-change', loc, { id: name }, 'Changed element $(ID) is a length reduction and is not supported'));
80
92
  else if (migration.new.type === migration.old.type && migration.new.scale !== migration.old.scale)
81
- error('type-unsupported-scale-change', loc, { id: name }, 'Changed element $(ID) is a scale change and is not supported');
93
+ raiseErrorOrMarkAsLossy(migration, () => error('type-unsupported-scale-change', loc, { id: name }, 'Changed element $(ID) is a scale change and is not supported'));
82
94
  else if (migration.new.type === migration.old.type && migration.new.precision !== migration.old.scale)
83
- error('type-unsupported-precision-change', loc, { id: name }, 'Changed element $(ID) is a precision change and is not supported');
95
+ raiseErrorOrMarkAsLossy(migration, () => error('type-unsupported-precision-change', loc, { id: name }, 'Changed element $(ID) is a precision change and is not supported'));
84
96
  else if (migration.new.type !== migration.old.type && typeChangeIsNotCompatible(dialect, migration.old.type, migration.new.type))
85
- error('type-unsupported-change', loc, { id: name, name: migration.old.type, type: migration.new.type }, 'Changed element $(ID) is a lossy type change from $(NAME) to $(TYPE) and is not supported');
97
+ raiseErrorOrMarkAsLossy(migration, () => error('type-unsupported-change', loc, { id: name, name: migration.old.type, type: migration.new.type }, 'Changed element $(ID) is a lossy type change from $(NAME) to $(TYPE) and is not supported'));
86
98
  else if (dialect !== 'sqlite' && isKey(migration.new) && !isKey(migration.old)) // key added/changed - pg, hana and sqlite do not support it, h2 probably also - issues when data is in the table already
87
- message('type-unsupported-key-change', [ 'definitions', migrations.migrate, 'elements', name ], { id: name, '#': 'changed' } );
99
+ raiseErrorOrMarkAsLossy(migration, () => message('type-unsupported-key-change', [ 'definitions', migrations.migrate, 'elements', name ], { id: name, '#': 'changed' } ));
88
100
  else if (migrationCallback)
89
101
  migrationCallback(migrations.migrate, name, migration, migrations.change, error);
90
102
 
103
+ if (options.script && migration.lossy && migrationCallback)
104
+ migrationCallback(migrations.migrate, name, migration, migrations.change, error);
105
+
91
106
  // TODO: precision/scale growth
92
107
  });
93
108
 
@@ -102,7 +117,7 @@ function getFilterObject( dialect, extensionCallback, migrationCallback, removeC
102
117
  },
103
118
  deletion: ([ artifactName, artifact ], error) => {
104
119
  if (isPersistedAsTable(artifact))
105
- error('def-unsupported-table-drop', [ 'definitions', artifactName ], 'Dropping tables is not supported');
120
+ raiseErrorOrMarkAsLossy(artifact, () => error('def-unsupported-table-drop', [ 'definitions', artifactName ], 'Dropping tables is not supported'));
106
121
  },
107
122
  changedPrimaryKeys: (changedPrimaryKeyArtifactName) => {
108
123
  if (primaryKeyCallback)
@@ -110,6 +125,7 @@ function getFilterObject( dialect, extensionCallback, migrationCallback, removeC
110
125
 
111
126
  return true;
112
127
  },
128
+ hasLossyChanges: () => context.hasLossyChanges,
113
129
  };
114
130
  }
115
131
 
@@ -173,3 +189,15 @@ function filterCsn( csn ) {
173
189
 
174
190
  return csn;
175
191
  }
192
+
193
+ function getSafeguardManager( context, options ) {
194
+ return function raiseErrorOrMarkAsLossy(migration, raiseError) {
195
+ if (!options.script) {
196
+ raiseError();
197
+ }
198
+ else {
199
+ migration.lossy = true;
200
+ context.hasLossyChanges = true;
201
+ }
202
+ };
203
+ }