@sap/cds-compiler 3.3.2 → 3.4.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 (74) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/bin/cdsc.js +3 -1
  3. package/doc/CHANGELOG_BETA.md +17 -0
  4. package/lib/api/main.js +147 -18
  5. package/lib/api/validate.js +8 -3
  6. package/lib/base/dictionaries.js +6 -6
  7. package/lib/base/keywords.js +104 -0
  8. package/lib/base/message-registry.js +136 -67
  9. package/lib/base/messages.js +59 -48
  10. package/lib/base/model.js +1 -0
  11. package/lib/checks/actionsFunctions.js +1 -1
  12. package/lib/checks/cdsPersistence.js +1 -1
  13. package/lib/checks/checkForTypes.js +13 -8
  14. package/lib/checks/defaultValues.js +3 -1
  15. package/lib/checks/elements.js +1 -1
  16. package/lib/checks/parameters.js +4 -2
  17. package/lib/checks/queryNoDbArtifacts.js +1 -1
  18. package/lib/checks/sql-snippets.js +12 -10
  19. package/lib/checks/validator.js +14 -4
  20. package/lib/compiler/assert-consistency.js +8 -7
  21. package/lib/compiler/checks.js +30 -20
  22. package/lib/compiler/define.js +89 -25
  23. package/lib/compiler/extend.js +21 -18
  24. package/lib/compiler/finalize-parse-cdl.js +14 -9
  25. package/lib/compiler/populate.js +30 -8
  26. package/lib/compiler/propagator.js +4 -2
  27. package/lib/compiler/resolve.js +11 -5
  28. package/lib/compiler/shared.js +66 -48
  29. package/lib/compiler/tweak-assocs.js +2 -3
  30. package/lib/compiler/utils.js +11 -0
  31. package/lib/edm/annotations/genericTranslation.js +7 -4
  32. package/lib/edm/csn2edm.js +1 -1
  33. package/lib/gen/language.checksum +1 -1
  34. package/lib/gen/language.interp +1 -1
  35. package/lib/gen/languageParser.js +3565 -3544
  36. package/lib/json/csnVersion.js +13 -13
  37. package/lib/json/from-csn.js +140 -158
  38. package/lib/json/to-csn.js +23 -5
  39. package/lib/language/.eslintrc.json +4 -0
  40. package/lib/language/antlrParser.js +7 -10
  41. package/lib/language/docCommentParser.js +1 -2
  42. package/lib/language/errorStrategy.js +54 -27
  43. package/lib/language/genericAntlrParser.js +115 -84
  44. package/lib/language/language.g4 +29 -25
  45. package/lib/language/multiLineStringParser.js +75 -63
  46. package/lib/main.js +1 -0
  47. package/lib/model/csnRefs.js +4 -3
  48. package/lib/model/csnUtils.js +39 -7
  49. package/lib/model/sortViews.js +7 -3
  50. package/lib/modelCompare/compare.js +49 -15
  51. package/lib/modelCompare/filter.js +83 -0
  52. package/lib/optionProcessor.js +5 -1
  53. package/lib/render/manageConstraints.js +9 -5
  54. package/lib/render/toCdl.js +120 -62
  55. package/lib/render/toHdbcds.js +1 -1
  56. package/lib/render/toSql.js +6 -2
  57. package/lib/render/utils/common.js +7 -0
  58. package/lib/sql-identifier.js +7 -0
  59. package/lib/transform/db/assertUnique.js +27 -38
  60. package/lib/transform/db/expansion.js +11 -4
  61. package/lib/transform/db/temporal.js +3 -1
  62. package/lib/transform/db/transformExists.js +7 -1
  63. package/lib/transform/db/views.js +42 -13
  64. package/lib/transform/draft/db.js +2 -2
  65. package/lib/transform/forRelationalDB.js +12 -6
  66. package/lib/transform/localized.js +1 -1
  67. package/lib/transform/odata/typesExposure.js +2 -1
  68. package/lib/transform/parseExpr.js +245 -0
  69. package/lib/transform/transformUtilsNew.js +23 -14
  70. package/lib/transform/translateAssocsToJoins.js +12 -12
  71. package/lib/utils/term.js +5 -5
  72. package/package.json +2 -2
  73. package/share/messages/message-explanations.json +1 -1
  74. package/share/messages/{syntax-expected-integer.md → syntax-expecting-integer.md} +1 -1
@@ -25,9 +25,9 @@ const {
25
25
  * of whitespace characters removed.
26
26
  */
27
27
  function stripIndentation(str) {
28
- if (str === '') {
29
- return ['', 0];
30
- }
28
+ if (str === '')
29
+ return [ '', 0 ];
30
+
31
31
 
32
32
  // Note: We have to check all newline characters, as the string is not normalized, yet.
33
33
  const lines = str.split(cdlNewLineRegEx);
@@ -39,20 +39,19 @@ function stripIndentation(str) {
39
39
  // If there is a trailing line break, it means that ``` is on newline and
40
40
  // therefore the indentation to remove is 0.
41
41
  // Remove the last newline, which may be CRLF.
42
- return [lines.slice(0, -1).join('\n'), 0];
42
+ return [ lines.slice(0, -1).join('\n'), 0 ];
43
43
  }
44
44
 
45
45
  const minIndent = lines.reduce((min, line, index) => {
46
46
  // Note: Last line is the line containing ```. There, we always count the indentation,
47
47
  // even if blank. For all other lines, blank lines are ignored.
48
- if (isWhitespaceOrNewLineOnly(line) && index !== (n-1))
48
+ if (isWhitespaceOrNewLineOnly(line) && index !== (n - 1))
49
49
  return min;
50
50
 
51
51
  let count = 0;
52
52
  const length = Math.min(min, line.length);
53
- while (count < length && isWhitespaceCharacterNoNewline(line[count])) {
53
+ while (count < length && isWhitespaceCharacterNoNewline(line[count]))
54
54
  count++;
55
- }
56
55
  return Math.min(min, count);
57
56
  }, Number.MAX_SAFE_INTEGER);
58
57
 
@@ -63,10 +62,10 @@ function stripIndentation(str) {
63
62
  }
64
63
 
65
64
  // Remove trailing last line, if there was nothing else in that line.
66
- if (lines[n-1] === '')
65
+ if (lines[n - 1] === '')
67
66
  lines.pop();
68
67
 
69
- return [lines.join('\n'), minIndent];
68
+ return [ lines.join('\n'), minIndent ];
70
69
  }
71
70
 
72
71
  class MultiLineStringParser {
@@ -75,9 +74,9 @@ class MultiLineStringParser {
75
74
  this.token = token;
76
75
  this.str = token.text; // Copy because .text is a getter
77
76
 
78
- if (this.str[0] !== '`' || this.str[this.str.length-1] !== '`') {
77
+ if (this.str[0] !== '`' || this.str[this.str.length - 1] !== '`')
78
+ // eslint-disable-next-line max-len
79
79
  throw new Error('Invalid multi-line string sequence: Require string to be surrounded by back-ticks!');
80
- }
81
80
 
82
81
  this.output = [];
83
82
  this.isTextBlock = this.str.startsWith('```');
@@ -90,7 +89,8 @@ class MultiLineStringParser {
90
89
  if (this.isTextBlock) {
91
90
  this.i = 3;
92
91
  this.end = this.str.length - 3;
93
- } else {
92
+ }
93
+ else {
94
94
  this.i = 1;
95
95
  this.end = this.str.length - 1;
96
96
  }
@@ -102,9 +102,8 @@ class MultiLineStringParser {
102
102
  * @return {string}
103
103
  */
104
104
  parse() {
105
- if (this.str.length === 2) {
105
+ if (this.str.length === 2)
106
106
  return ''; // Nothing to do: ``
107
- }
108
107
 
109
108
  if (this.isTextBlock) {
110
109
  // If there are no line breaks, emit an error as normal single-back-tick
@@ -112,7 +111,7 @@ class MultiLineStringParser {
112
111
  // there is no text without at least one line break.
113
112
  if (!cdlNewLineRegEx.test(this.str)) {
114
113
  const loc = this._locationForCharacters(this.end, 1);
115
- this.parser.message('syntax-invalid-text-block', loc);
114
+ this.parser.error('syntax-invalid-text-block', loc);
116
115
  return '';
117
116
  }
118
117
  this._skipOptionalLanguageIdentifierLine();
@@ -135,7 +134,7 @@ class MultiLineStringParser {
135
134
  // Note: Index is at first character of string
136
135
 
137
136
  do {
138
- switch(this._current()) {
137
+ switch (this._current()) {
139
138
  case this._matchLineBreakAtCurrentChar():
140
139
  this.output.push('\n');
141
140
  break;
@@ -146,8 +145,8 @@ class MultiLineStringParser {
146
145
  case '$':
147
146
  if (this._lookahead() === '{') {
148
147
  const loc = this._locationForCharacters(this.i, 2);
149
- this.parser.message('syntax-missing-escape', loc,
150
- { '#': 'placeholder', code: '${', newcode: '\\${' });
148
+ this.parser.error('syntax-missing-escape', loc,
149
+ { '#': 'placeholder', code: '${', newcode: '\\${' });
151
150
  }
152
151
  this.output.push(this.str[this.i]);
153
152
  break;
@@ -155,7 +154,7 @@ class MultiLineStringParser {
155
154
  this.output.push(this.str[this.i]);
156
155
  break;
157
156
  }
158
- } while(this._move());
157
+ } while (this._move());
159
158
 
160
159
  return this.output.join('');
161
160
  }
@@ -166,27 +165,27 @@ class MultiLineStringParser {
166
165
  * @private
167
166
  */
168
167
  _innerEscape() {
169
- switch(this._current()) {
168
+ switch (this._current()) {
170
169
  case this._matchLineBreakAtCurrentChar():
171
170
  // Don't add to output -> line break is escaped
172
171
  break;
173
172
  case 'b': // backspace
174
- this.output.push(`\b`);
173
+ this.output.push('\b');
175
174
  break;
176
175
  case 'f': // form feed
177
- this.output.push(`\f`);
176
+ this.output.push('\f');
178
177
  break;
179
178
  case 'v': // vertical tabulator
180
- this.output.push(`\v`);
179
+ this.output.push('\v');
181
180
  break;
182
181
  case 'r': // carriage return
183
- this.output.push(`\r`);
182
+ this.output.push('\r');
184
183
  break;
185
184
  case 'n': // line feed
186
- this.output.push(`\n`);
185
+ this.output.push('\n');
187
186
  break;
188
187
  case 't': // tab
189
- this.output.push(`\t`);
188
+ this.output.push('\t');
190
189
  break;
191
190
  case '\\':
192
191
  case '"':
@@ -206,21 +205,23 @@ class MultiLineStringParser {
206
205
  break;
207
206
  case '0': // null terminator
208
207
  if (!/^\d$/.test(this._lookahead())) {
209
- this.output.push(`\0`);
208
+ this.output.push('\0');
210
209
  break;
211
210
  }
212
211
  // Let the default case handle octal representation.
213
212
  // fallthrough
214
213
  default: {
215
214
  this.output.push(this._current());
216
- const loc = this._locationForCharacters(this.i-1, 2);
215
+ const loc = this._locationForCharacters(this.i - 1, 2);
217
216
  if (/\s/.test(this._current())) {
218
- this.parser.message('syntax-invalid-escape', loc, { '#': 'whitespace' });
219
- } else if (/\d/.test(this._current())) {
220
- this.parser.message('syntax-invalid-escape', loc, { '#': 'octal' });
221
- } else {
222
- const code = this._makeCode('\\' + this._current());
223
- this.parser.message('syntax-unknown-escape', loc, { '#': 'std', code });
217
+ this.parser.error('syntax-invalid-escape', loc, { '#': 'whitespace' });
218
+ }
219
+ else if (/\d/.test(this._current())) {
220
+ this.parser.error('syntax-invalid-escape', loc, { '#': 'octal' });
221
+ }
222
+ else {
223
+ const code = this._makeCode(`\\${ this._current() }`);
224
+ this.parser.message('syntax-unknown-escape', loc, { '#': 'std', code });
224
225
  }
225
226
  break;
226
227
  }
@@ -239,9 +240,9 @@ class MultiLineStringParser {
239
240
  // syntax variant as the first invalid code point is \u{110000}
240
241
  // and an empty `codePoint` is only possible with the braced variant.
241
242
  const reportInvalidCodePoint = () => {
242
- const code = this._makeCode(`\\u{${codePoint}}`);
243
- const loc = this._locationForCharacters(this.i-codePoint.length, codePoint.length);
244
- this.parser.message('syntax-invalid-escape', loc, { '#': 'codepoint', code });
243
+ const code = this._makeCode(`\\u{${ codePoint }}`);
244
+ const loc = this._locationForCharacters(this.i - codePoint.length, codePoint.length);
245
+ this.parser.error('syntax-invalid-escape', loc, { '#': 'codepoint', code });
245
246
  };
246
247
 
247
248
  const n = Number.parseInt(codePoint, 16);
@@ -252,7 +253,8 @@ class MultiLineStringParser {
252
253
 
253
254
  try {
254
255
  this.output.push(String.fromCodePoint(n));
255
- } catch (e) {
256
+ }
257
+ catch (e) {
256
258
  // RangeError is thrown if number isn't a valid code point
257
259
  reportInvalidCodePoint();
258
260
  }
@@ -275,18 +277,20 @@ class MultiLineStringParser {
275
277
  if (!this._eos() && /^[\dA-Fa-f]$/.test(this._lookahead())) {
276
278
  this._move();
277
279
  codePoint += this._current();
278
- } else {
280
+ }
281
+ else {
279
282
  break;
280
283
  }
281
284
  }
282
285
 
283
286
  if (codePoint.length === count) {
284
287
  this._parseHexCodePoint(codePoint);
285
- } else {
286
- const loc = this._locationForCharacters(this.i+1, 1);
287
- const code = this._eos(this.i+1) ? `\\${mode}${codePoint}` : `\\${mode}${codePoint}${this._lookahead()}`;
288
- this.parser.message('syntax-invalid-escape', loc,
289
- { '#': 'hex-count', number: count, code: this._makeCode(code) });
288
+ }
289
+ else {
290
+ const loc = this._locationForCharacters(this.i + 1, 1);
291
+ const code = this._eos(this.i + 1) ? `\\${ mode }${ codePoint }` : `\\${ mode }${ codePoint }${ this._lookahead() }`;
292
+ this.parser.error('syntax-invalid-escape', loc,
293
+ { '#': 'hex-count', number: count, code: this._makeCode(code) });
290
294
  }
291
295
  }
292
296
 
@@ -305,14 +309,17 @@ class MultiLineStringParser {
305
309
  if (/^[\dA-Fa-f]$/.test(this._lookahead())) {
306
310
  this._move();
307
311
  codePoint += this._current();
308
- } else if (this._lookahead() === '}') {
312
+ }
313
+ else if (this._lookahead() === '}') {
309
314
  break;
310
- } else if (!this._eos(this.i+1)) {
311
- const loc = this._locationForCharacters(this.i+1, 1); // Point to the exact character
312
- const code = this._makeCode(`\\u{${codePoint}${this._lookahead()}…}`);
313
- this.parser.message('syntax-invalid-escape', loc, { '#': 'unicode-hex', code });
315
+ }
316
+ else if (!this._eos(this.i + 1)) {
317
+ const loc = this._locationForCharacters(this.i + 1, 1); // Point to the exact character
318
+ const code = this._makeCode(`\\u{${ codePoint }${ this._lookahead() }…}`);
319
+ this.parser.error('syntax-invalid-escape', loc, { '#': 'unicode-hex', code });
314
320
  return;
315
- } else {
321
+ }
322
+ else {
316
323
  break;
317
324
  }
318
325
  }
@@ -320,9 +327,10 @@ class MultiLineStringParser {
320
327
  if (this._lookahead() === '}') {
321
328
  this._move();
322
329
  this._parseHexCodePoint(codePoint);
323
- } else {
330
+ }
331
+ else {
324
332
  const loc = this._locationForCharacters(this.i, 1);
325
- this.parser.message('syntax-invalid-escape', loc, { '#': 'unicode-brace' });
333
+ this.parser.error('syntax-invalid-escape', loc, { '#': 'unicode-brace' });
326
334
  }
327
335
  }
328
336
 
@@ -333,7 +341,7 @@ class MultiLineStringParser {
333
341
  * @private
334
342
  */
335
343
  _skipOptionalLanguageIdentifierLine() {
336
- while(!this._eos()) {
344
+ while (!this._eos()) {
337
345
  switch (this._current()) {
338
346
  case this._matchLineBreakAtCurrentChar():
339
347
  this._move();
@@ -342,7 +350,7 @@ class MultiLineStringParser {
342
350
  // Do not allow an escape in the language identifier. If at the line's end, users
343
351
  // may expect the identifier to span more than the first line, which is _not_ the case.
344
352
  const loc = this._locationForCharacters(this.i, 1);
345
- this.parser.message('syntax-invalid-escape', loc, { '#': 'language-identifier' });
353
+ this.parser.error('syntax-invalid-escape', loc, { '#': 'language-identifier' });
346
354
  this._move();
347
355
  break;
348
356
  }
@@ -367,7 +375,7 @@ class MultiLineStringParser {
367
375
  */
368
376
  _matchLineBreakAtCurrentChar() {
369
377
  // Only increase line number for \n, because ANTLR does the same
370
- switch(this._current()) {
378
+ switch (this._current()) {
371
379
  case '\r':
372
380
  if (this._lookahead() === '\n') {
373
381
  this._move(); // \r\n is normalized
@@ -382,6 +390,7 @@ class MultiLineStringParser {
382
390
  case '\u2028': // LS
383
391
  case '\u2029': // PS
384
392
  return this._current();
393
+ default: break;
385
394
  }
386
395
  return null;
387
396
  }
@@ -419,7 +428,7 @@ class MultiLineStringParser {
419
428
  * @returns {string}
420
429
  */
421
430
  _lookahead() {
422
- return this.str[this.i+1];
431
+ return this.str[this.i + 1];
423
432
  }
424
433
 
425
434
  /**
@@ -439,7 +448,7 @@ class MultiLineStringParser {
439
448
  * @returns {string}
440
449
  */
441
450
  _previous() {
442
- return this.str[this.i-1];
451
+ return this.str[this.i - 1];
443
452
  }
444
453
 
445
454
  /**
@@ -455,8 +464,12 @@ class MultiLineStringParser {
455
464
  file: this.parser.filename,
456
465
  line: this.token.line + this._lineInString,
457
466
  endLine: this.token.line + this._lineInString,
458
- col: this._lineInString > 0 ? i - this._currentLineBreakIndex + this._indentation : this.token.column + i + 1,
459
- endCol: this._lineInString > 0 ? i - this._currentLineBreakIndex + width + this._indentation : this.token.column + i + width + 1,
467
+ col: this._lineInString > 0
468
+ ? i - this._currentLineBreakIndex + this._indentation
469
+ : this.token.column + i + 1,
470
+ endCol: this._lineInString > 0
471
+ ? i - this._currentLineBreakIndex + width + this._indentation
472
+ : this.token.column + i + width + 1,
460
473
  };
461
474
  }
462
475
 
@@ -468,7 +481,7 @@ class MultiLineStringParser {
468
481
  * @param {string} code
469
482
  * @private
470
483
  */
471
- _makeCode(code) {
484
+ _makeCode(code) { // eslint-disable-line class-methods-use-this
472
485
  // For characters that may be rendered as newline,
473
486
  // see <https://www.unicode.org/reports/tr14/tr14-32.html>.
474
487
  //
@@ -484,9 +497,8 @@ class MultiLineStringParser {
484
497
  //
485
498
  // For Visualization, see <https://en.wikipedia.org/wiki/Newline#Unicode>
486
499
  // U+23CE: ⏎
487
-
488
- const allPossibleNewLineCharacters = /[\u{000A}\u{000B}\u{000C}\u{000D}\u{0085}\u{2028}\u{2029}]/ug;
489
- return code.replace(allPossibleNewLineCharacters, '\u{23CE}');
500
+ const allNewLineCharacters = /[\u{000A}\u{000B}\u{000C}\u{000D}\u{0085}\u{2028}\u{2029}]/ug;
501
+ return code.replace(allNewLineCharacters, '\u{23CE}');
490
502
  }
491
503
  }
492
504
 
package/lib/main.js CHANGED
@@ -115,6 +115,7 @@ module.exports = {
115
115
  functions: Object.freeze([ ...keywords.cdl_functions ] ),
116
116
  }),
117
117
  sql: Object.assign((...args) => snapi.sql(...args), {
118
+ migration: (...args) => snapi.sql.migration(...args),
118
119
  sqlite: {
119
120
  keywords: Object.freeze([ ...keywords.sqlite ] )
120
121
  },
@@ -924,14 +924,15 @@ function startCsnPath( csnPath, csn ) {
924
924
  index: 1, main, parent, art,
925
925
  };
926
926
  }
927
- if (csnPath.length < 2 || csnPath[0] !== 'definitions')
928
- throw new CompilerAssertion( 'References outside definitions not supported yet');
929
- const art = csn.definitions[csnPath[1]];
927
+ if (csnPath.length < 2 || csnPath[0] !== 'definitions' && csnPath[0] !== 'vocabularies')
928
+ throw new CompilerAssertion( 'References outside definitions and vocabularies not supported yet');
929
+ const art = csn[csnPath[0]][csnPath[1]];
930
930
  return {
931
931
  index: 2, main: art, parent: null, art,
932
932
  };
933
933
  }
934
934
 
935
+
935
936
  /**
936
937
  * @param {CSN.Path} csnPath
937
938
  * @param {CSN.Model} csn
@@ -1106,22 +1106,24 @@ const _dependents = Symbol('_dependents');
1106
1106
  * _dependents: All artifacts that depend on this artifact (because they have a ref that points to it)
1107
1107
  * _dependencies: All artifacts this artifact depends on (because it has a ref to it)
1108
1108
  *
1109
- * @param {object} csn A CSN to enrich in-place
1109
+ * @param {CSN.Model} csn A CSN to enrich in-place
1110
+ * @param {object} refs csnRefs, only used for artifactRef
1110
1111
  * @returns {object} CSN with _dependents/_dependencies set, "cleanup" function, _dependents/_dependencies Symbol used
1111
1112
  */
1112
- function setDependencies( csn ) {
1113
+ function setDependencies( csn, refs = csnRefs(csn) ) {
1113
1114
  const cleanup = [];
1114
- const { artifactRef } = csnRefs(csn);
1115
+ const { artifactRef } = refs;
1115
1116
 
1116
1117
  forEachDefinition(csn, (artifact, artifactName) => {
1117
- if (getNormalizedQuery(artifact).query) {
1118
+ const queries = getNormalizedQuery(artifact).query;
1119
+ if (queries) {
1118
1120
  initDependencies(artifact);
1119
- forAllQueries(getNormalizedQuery(artifact).query, (query) => {
1120
- if (query.SELECT && query.SELECT.from) {
1121
+ forAllQueries(queries, (query) => {
1122
+ if (query.SELECT?.from) {
1121
1123
  if (query.SELECT.from.args)
1122
1124
  handleArgs(artifact, artifactName, query.SELECT.from.args);
1123
1125
 
1124
- else if (typeof query.SELECT.from === 'string' || query.SELECT.from.ref )
1126
+ else if (typeof query.SELECT.from === 'string' || query.SELECT.from.ref)
1125
1127
  handleDependency(artifactRef(query.SELECT.from), artifact, artifactName);
1126
1128
  }
1127
1129
  }, [ 'definitions', artifactName, (artifact.projection ? 'projection' : 'query') ]);
@@ -1172,6 +1174,34 @@ function setDependencies( csn ) {
1172
1174
  function isPersistedOnDatabase(art) {
1173
1175
  return !(art.kind === 'entity' && (art.abstract || hasAnnotationValue(art, '@cds.persistence.skip')));
1174
1176
  }
1177
+ /**
1178
+ * Check if the given artifact will be persisted on the database via `CREATE VIEW`
1179
+ *
1180
+ * @param {CSN.Artifact} artifact
1181
+ * @returns {boolean}
1182
+ */
1183
+ function isPersistedAsView(artifact) {
1184
+ return artifact && artifact.kind === 'entity' &&
1185
+ !artifact._ignore &&
1186
+ !artifact.abstract &&
1187
+ ((artifact.query || artifact.projection) && !hasAnnotationValue(artifact, '@cds.persistence.table')) &&
1188
+ !hasAnnotationValue(artifact, '@cds.persistence.skip') &&
1189
+ !hasAnnotationValue(artifact, '@cds.persistence.exists');
1190
+ }
1191
+ /**
1192
+ * Check if the given artifact will be persisted on the database via `CREATE TABLE`
1193
+ *
1194
+ * @param {CSN.Artifact} artifact
1195
+ * @returns {boolean}
1196
+ */
1197
+ function isPersistedAsTable(artifact) {
1198
+ return artifact.kind === 'entity' &&
1199
+ !artifact._ignore &&
1200
+ !artifact.abstract &&
1201
+ (!artifact.query && !artifact.projection || hasAnnotationValue(artifact, '@cds.persistence.table')) &&
1202
+ !hasAnnotationValue(artifact, '@cds.persistence.skip') &&
1203
+ !hasAnnotationValue(artifact, '@cds.persistence.exists');
1204
+ }
1175
1205
 
1176
1206
  /**
1177
1207
  * Central generated by cds-compiler string generator function without further decoration
@@ -1533,6 +1563,8 @@ module.exports = {
1533
1563
  applyTransformationsOnDictionary,
1534
1564
  setDependencies,
1535
1565
  isPersistedOnDatabase,
1566
+ isPersistedAsView,
1567
+ isPersistedAsTable,
1536
1568
  generatedByCompilerVersion,
1537
1569
  getNormalizedQuery,
1538
1570
  getRootArtifactName,
@@ -51,7 +51,7 @@ function _calculateDepth(definitionsArray, _dependents, _dependencies) {
51
51
  nonZero.push([ artifactName, artifact ]);
52
52
  }
53
53
  else {
54
- artifact.$pointers = 0;
54
+ delete artifact.$pointers;
55
55
  zero.push([ artifactName, artifact ]);
56
56
  }
57
57
  });
@@ -66,10 +66,14 @@ function _findWithXPointers(definitionsArray, x, _dependents, _dependencies) {
66
66
  const nonZero = [];
67
67
 
68
68
  definitionsArray.forEach(([ artifactName, artifact ]) => {
69
- if (artifact.$pointers !== undefined && artifact.$pointers === x)
69
+ if (artifact.$pointers !== undefined && artifact.$pointers === x) {
70
70
  zero.push([ artifactName, artifact ]);
71
- else
71
+ if (artifact.$pointers === 0)
72
+ delete artifact.$pointers;
73
+ }
74
+ else {
72
75
  nonZero.push([ artifactName, artifact ]);
76
+ }
73
77
  });
74
78
 
75
79
  return {
@@ -4,9 +4,12 @@ const { makeMessageFunction } = require('../base/messages');
4
4
  const {
5
5
  forEachDefinition,
6
6
  forEachMember,
7
- hasAnnotationValue
7
+ isPersistedAsTable,
8
+ isPersistedAsView
8
9
  } = require('../model/csnUtils');
9
10
  const { isBetaEnabled } = require('../base/model');
11
+ // used to mark a view as changed so we know to drop-create it
12
+ const isChanged = Symbol('Marks a view as changed');
10
13
 
11
14
  /**
12
15
  * Compares two models, in HANA-transformed CSN format, to each other.
@@ -14,7 +17,7 @@ const { isBetaEnabled } = require('../base/model');
14
17
  * @param beforeModel the before-model
15
18
  * @param afterModel the after-model
16
19
  * @param {HdiOptions|false} options
17
- * @returns {object} the sets of deletions, extensions, and migrations of entities necessary to transform the before-model
20
+ * @returns {ModelDiff} the sets of deletions, extensions, and migrations of entities necessary to transform the before-model
18
21
  * to the after-model, together with all the definitions of the after-model
19
22
  */
20
23
  function compareModels(beforeModel, afterModel, options) {
@@ -46,17 +49,18 @@ function validateCsnVersions(beforeModel, afterModel, options) {
46
49
 
47
50
  if (!beforeVersionParts || beforeVersionParts.length < 2) {
48
51
  const { error, throwWithAnyError } = makeMessageFunction(beforeModel, options, 'modelCompare');
49
- error(null, null, {}, `Invalid CSN version: ${beforeVersion}`);
52
+ error(null, null, { version: beforeVersion }, 'Invalid CSN version: $(VERSION)');
50
53
  throwWithAnyError();
51
54
  }
52
55
  if (!afterVersionParts || afterVersionParts.length < 2) {
53
56
  const { error, throwWithAnyError } = makeMessageFunction(afterModel, options, 'modelCompare');
54
- error(null, null, {}, `Invalid CSN version: ${afterVersion}`);
57
+ error(null, null, { version: afterVersion }, 'Invalid CSN version: $(VERSION)');
55
58
  throwWithAnyError();
56
59
  }
57
60
  if (beforeVersionParts[0] > afterVersionParts[0] && !(options && options.allowCsnDowngrade)) {
58
61
  const { error, throwWithAnyError } = makeMessageFunction(afterModel, options, 'modelCompare');
59
- error(null, null, {}, `Incompatible CSN versions: ${afterVersion} is a major downgrade from ${beforeVersion}. Is @sap/cds-compiler version ${require('../../package.json').version} outdated?`);
62
+ error(null, null, { value: afterVersion, othervalue: beforeVersion, version: require('../../package.json').version },
63
+ 'Incompatible CSN versions: $(VALUE) is a major downgrade from $(OTHERVALUE). Is @sap/cds-compiler version $(VERSION) outdated?');
60
64
  throwWithAnyError();
61
65
  }
62
66
  }
@@ -116,10 +120,18 @@ function getArtifactComparator(otherModel, options, addedEntities, deletedEntiti
116
120
  // Arguments are interchanged in this case: `artifact` from beforeModel and `otherArtifact` from afterModel.
117
121
  if (isPersisted && !isPersistedOther) {
118
122
  deletedEntities[name] = artifact;
123
+ } else if(isPersistedAsView(artifact) && isPersistedOther) { // view turned into table - need to render a drop for the view
124
+ deletedEntities[name] = artifact;
119
125
  }
120
126
  return;
121
127
  }
122
128
 
129
+ // to make it easier to know which views to drop-create
130
+ if(isPersistedAsView(artifact) && isPersistedAsView(otherArtifact)) {
131
+ // TODO: Check only on artifact.query/projection BUT: Need to manually check for sql-snippets then!
132
+ artifact[isChanged] = JSON.stringify(artifact) !== JSON.stringify(otherArtifact);
133
+ }
134
+
123
135
  // Looking for added entities and added/deleted/changed elements.
124
136
  // Parameters: `artifact` from afterModel and `otherArtifact` from beforeModel.
125
137
 
@@ -146,15 +158,6 @@ function getArtifactComparator(otherModel, options, addedEntities, deletedEntiti
146
158
  };
147
159
  }
148
160
 
149
- function isPersistedAsTable(artifact) {
150
- return artifact.kind === 'entity'
151
- && !artifact._ignore
152
- && !artifact.abstract
153
- && (!artifact.query && !artifact.projection || hasAnnotationValue(artifact, '@cds.persistence.table'))
154
- && !hasAnnotationValue(artifact, '@cds.persistence.skip')
155
- && !hasAnnotationValue(artifact, '@cds.persistence.exists');
156
- }
157
-
158
161
  function getElementComparator(otherArtifact, addedElementsDict = null, changedElementsDict = null) {
159
162
  return function compareElements(element, name) {
160
163
  if (element._ignore) {
@@ -244,5 +247,36 @@ function changedElement(element, otherElement) {
244
247
 
245
248
  module.exports = {
246
249
  compareModels,
247
- deepEqual
250
+ deepEqual,
251
+ isChanged
248
252
  };
253
+
254
+ /**
255
+ * A ModelDiff encapsulates the changes between two models ("before" and "after"). It contains information
256
+ * about changes to .elements and removed artifacts.
257
+ *
258
+ * @typedef {object} ModelDiff
259
+ * @property {CSN.Definitions} definitions The artifacts present in the "after" model
260
+ * @property {CSN.Definitions} deletions The artifacts present in the "before", but not in the "after"
261
+ * @property {extension[]} extensions The elements added to artifacts
262
+ * @property {migration[]} migrations Altered or removed elements
263
+ */
264
+
265
+ /**
266
+ * @typedef {object} extension
267
+ * @property {CSN.Elements} elements The elements that where added
268
+ * @property {string} extend Name of the artifact that the .elements need to be added to
269
+ */
270
+
271
+ /**
272
+ * @typedef {object} migration
273
+ * @property {Object.<string, ChangeSet>} change An object of changes - the key being the name of the changed element, the value being the change.
274
+ * @property {string} migrate Name of the artifact that the .change and .remove apply to
275
+ * @property {CSN.Elements} remove An object of removed elements
276
+ */
277
+
278
+ /**
279
+ * @typedef {object} ChangeSet Describes the change of one element
280
+ * @property {CSN.Element} old The old element definition
281
+ * @property {CSN.Element} new The new element definition
282
+ */