@sap/cds-compiler 5.4.4 → 5.5.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 (40) hide show
  1. package/CHANGELOG.md +22 -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 +47 -11
  10. package/lib/base/messages.js +17 -3
  11. package/lib/checks/cdsMap.js +27 -0
  12. package/lib/checks/{dbFeatureFlags.js → featureFlags.js} +1 -1
  13. package/lib/checks/parameters.js +61 -4
  14. package/lib/checks/validator.js +17 -7
  15. package/lib/compiler/define.js +1 -0
  16. package/lib/compiler/index.js +7 -7
  17. package/lib/gen/BaseParser.js +345 -235
  18. package/lib/gen/CdlParser.js +4438 -4492
  19. package/lib/gen/Dictionary.json +2 -2
  20. package/lib/language/antlrParser.js +2 -111
  21. package/lib/main.js +16 -37
  22. package/lib/model/cloneCsn.js +1 -5
  23. package/lib/modelCompare/utils/filter.js +47 -21
  24. package/lib/parsers/AstBuildingParser.js +92 -73
  25. package/lib/parsers/CdlGrammar.g4 +110 -137
  26. package/lib/parsers/index.js +123 -0
  27. package/lib/render/toSql.js +8 -2
  28. package/lib/render/utils/delta.js +33 -1
  29. package/lib/transform/db/{transformExists.js → assocsToQueries/transformExists.js} +12 -407
  30. package/lib/transform/db/assocsToQueries/utils.js +440 -0
  31. package/lib/transform/db/expansion.js +2 -2
  32. package/lib/transform/draft/db.js +14 -3
  33. package/lib/transform/effective/annotations.js +3 -3
  34. package/lib/transform/effective/main.js +5 -7
  35. package/lib/transform/featureFlags.js +5 -0
  36. package/lib/transform/forRelationalDB.js +125 -192
  37. package/lib/transform/transformUtils.js +0 -51
  38. package/lib/utils/objectUtils.js +13 -0
  39. package/package.json +2 -2
  40. package/lib/transform/db/featureFlags.js +0 -5
@@ -1089,8 +1089,7 @@
1089
1089
  "Type": "Core.Tag",
1090
1090
  "AppliesTo": [
1091
1091
  "EntityContainer"
1092
- ],
1093
- "$experimental": true
1092
+ ]
1094
1093
  },
1095
1094
  "Common.mediaUploadLink": {
1096
1095
  "Type": "Edm.String",
@@ -4475,6 +4474,7 @@
4475
4474
  "$kind": "ComplexType",
4476
4475
  "BaseType": "UI.DataFieldAbstract",
4477
4476
  "Properties": {
4477
+ "ID": "Edm.String",
4478
4478
  "Actions": "Collection(UI.DataFieldForActionAbstract)",
4479
4479
  "Label": "Edm.String",
4480
4480
  "Criticality": "UI.CriticalityType",
@@ -17,11 +17,6 @@ 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
-
25
20
  // Error listener used for ANTLR4-generated parser
26
21
  class ErrorListener extends antlr4.error.ErrorListener {
27
22
  // method which is called by generated parser with --trace-parser[-amg]:
@@ -125,18 +120,7 @@ function tokenTypeOf( recognizer, literalName ) {
125
120
  // the AST locations and error messages. If provided, `options` are compile
126
121
  // options.
127
122
 
128
- const rules = {
129
- cdl: { func: 'start', returns: 'source', $frontend: 'cdl' },
130
- query: { func: 'queryEOF', returns: 'query' },
131
- expr: { func: 'conditionEOF', returns: 'cond' }, // yes, condition
132
- };
133
-
134
- function parse( source, filename = '<undefined>.cds',
135
- options = {}, messageFunctions = null,
136
- rule = 'cdl' ) {
137
- if (options.newParser)
138
- return parseWithNewParser( source, filename, options, messageFunctions, rule );
139
-
123
+ function parse( source, filename, options, messageFunctions, rulespec ) {
140
124
  const lexer = new Lexer( new antlr4.InputStream(source) );
141
125
  const tokenStream = new RewriteTypeTokenStream(lexer);
142
126
  /** @type {object} */
@@ -179,10 +163,9 @@ function parse( source, filename = '<undefined>.cds',
179
163
  parser.addParseListener(options.parseListener);
180
164
 
181
165
 
182
- const rulespec = rules[rule];
183
166
  let tree;
184
167
  try {
185
- tree = rule && parser[rulespec.func]();
168
+ tree = parser[rulespec.func]();
186
169
  }
187
170
  catch (e) {
188
171
  if (e instanceof RangeError && e.message.match(/Maximum.*exceeded$/i)) {
@@ -219,96 +202,4 @@ function parse( source, filename = '<undefined>.cds',
219
202
  return ast;
220
203
  }
221
204
 
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
- return CdlParser.prototype.exit_.apply( this, args );
279
- };
280
- parser.c = function c( ...args ) { // consume
281
- const symbol = this.la();
282
- const result = CdlParser.prototype.c.apply( this, args );
283
- if (result)
284
- parseListener.visitTerminal( { symbol } );
285
- return result;
286
- };
287
- parser.skipToken_ = function skipToken_( ...args ) { // skip token in error recovery
288
- const symbol = this.la();
289
- CdlParser.prototype.skipToken_.apply( this, args ); // = `++this.tokenIdx`
290
- parseListener.visitErrorNode( { symbol } );
291
- };
292
- }
293
- const result = {};
294
- const rulespec = rules[rule];
295
- if (rulespec) {
296
- try {
297
- parser[rulespec.func]( result );
298
- }
299
- catch (e) {
300
- if (!(e instanceof RangeError && /Maximum.*exceeded$/i.test( e.message )))
301
- throw e;
302
- messageFunctions.error('syntax-invalid-source', { file: filename },
303
- { '#': 'cdl-stackoverflow' } );
304
- result[rulespec.returns] = undefined;
305
- }
306
- }
307
- const ast = result[rulespec?.returns] || (rule === 'cdl' ? new XsnSource( 'cdl' ) : {} );
308
- ast.options = options;
309
- if (attachTokens === true || attachTokens === filename)
310
- ast.tokenStream = parser._input;
311
- return ast;
312
- }
313
-
314
205
  module.exports = parse;
package/lib/main.js CHANGED
@@ -1,19 +1,23 @@
1
- // Main entry point for the CDS Compiler
1
+ // Main entry point for the CDS Compiler (API)
2
2
  //
3
- // File for external usage = which is read in other modules with
4
- // require('cdsv');
3
+ // Other NPM modules must not require any other files than this one.
5
4
 
6
5
  // Proposed intra-module lib dependencies:
7
6
  // - lib/base/<file>.js: can be required by all others, requires no other
8
- // of this project
7
+ // of this project, except a lib/base/<other-file>.js
8
+ // - lib/util/<file>.js: TODO - clarify diff to lib/base/
9
9
  // - lib/<dir>/<file>.js: can be required by other files lib/<dir>/,
10
- // can require other files lib/<dir>/ and lib/base/<file>.js
10
+ // can require other files in lib/<dir>/ and lib/base/<file>.js,
11
+ // and lib/<other-dir>/ (the index.js in <other-dir>).
11
12
  // - lib/main.js (this file): can be required by none in lib/ (only in
12
13
  // bin/ and test/), can require any other
13
14
 
14
15
  'use strict';
15
16
 
17
+ const lazyload = require('./base/lazyload')( module );
18
+
16
19
  const { traceApi } = require('./api/trace');
20
+
17
21
  const snapi = lazyload('./api/main');
18
22
  const csnUtils = lazyload('./model/csnUtils');
19
23
  const model_api = lazyload('./model/api');
@@ -22,7 +26,7 @@ const sqlIdentifier = lazyload('./sql-identifier');
22
26
  const keywords = lazyload( './base/keywords' );
23
27
  const toCdl = lazyload('./render/toCdl');
24
28
 
25
- const parseLanguage = lazyload('./language/antlrParser');
29
+ const parsers = lazyload('./parsers');
26
30
  const compiler = lazyload('./compiler');
27
31
  const shared = lazyload('./compiler/shared');
28
32
  const define = lazyload('./compiler/define');
@@ -43,8 +47,8 @@ function parseCdl( cdlSource, filename, options = {} ) {
43
47
  const messageFunctions = messages.createMessageFunctions( options, 'parse', model );
44
48
  model.$messageFunctions = messageFunctions;
45
49
 
46
- const xsn = parseLanguage( cdlSource, filename, Object.assign( { parseOnly: true }, options ),
47
- messageFunctions );
50
+ const xsn = parsers.parseCdl( cdlSource, filename, Object.assign( { parseOnly: true }, options ),
51
+ messageFunctions );
48
52
  sources[filename] = xsn;
49
53
  shared.fns( model );
50
54
  define( model );
@@ -55,16 +59,16 @@ function parseCdl( cdlSource, filename, options = {} ) {
55
59
 
56
60
  function parseCql( cdlSource, filename = '<query>.cds', options = {} ) {
57
61
  const messageFunctions = messages.createMessageFunctions( options, 'parse' );
58
- const xsn = parseLanguage( cdlSource, filename, Object.assign( { parseOnly: true }, options ),
59
- messageFunctions, 'query' );
62
+ const xsn = parsers.parseCdl( cdlSource, filename, Object.assign( { parseOnly: true }, options ),
63
+ messageFunctions, 'query' );
60
64
  messageFunctions.throwWithError();
61
65
  return toCsn.compactQuery( xsn );
62
66
  }
63
67
 
64
68
  function parseExpr( cdlSource, filename = '<expr>.cds', options = {} ) {
65
69
  const messageFunctions = messages.createMessageFunctions( options, 'parse' );
66
- const xsn = parseLanguage( cdlSource, filename, Object.assign( { parseOnly: true }, options ),
67
- messageFunctions, 'expr' );
70
+ const xsn = parsers.parseCdl( cdlSource, filename, Object.assign( { parseOnly: true }, options ),
71
+ messageFunctions, 'expr' );
68
72
  messageFunctions.throwWithError();
69
73
  return toCsn.compactExpr( xsn );
70
74
  }
@@ -191,28 +195,3 @@ module.exports = {
191
195
  isInReservedNamespace: (...args) => builtins.isInReservedNamespace(...args),
192
196
  },
193
197
  };
194
-
195
- /**
196
- * Load the module on-demand and not immediately.
197
- *
198
- * @param {string} moduleName Name of the module to load - like with require
199
- * @returns {object} A Proxy that handles the on-demand loading
200
- */
201
- function lazyload(moduleName) {
202
- let module;
203
- return new Proxy(((...args) => {
204
- if (!module)
205
- module = require(moduleName);
206
-
207
- if (module.apply && typeof module.apply === 'function')
208
- return module.apply(this, args);
209
- return module; // for destructured calls
210
- }), {
211
- get(target, name) {
212
- if (!module)
213
- module = require(moduleName);
214
-
215
- return module[name];
216
- },
217
- });
218
- }
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { csnPropertyOrder } = require('../json/to-csn');
4
4
  const { ModelError } = require('../base/error');
5
- const { setHidden } = require('../utils/objectUtils');
5
+ const { setHidden, hasNonEnumerable } = require('../utils/objectUtils');
6
6
  const { isAnnotationExpression } = require('../base/builtins');
7
7
 
8
8
  const csnDictionaries = {
@@ -110,10 +110,6 @@ function cloneCsn( csn, options, sort ) {
110
110
  return r;
111
111
  }
112
112
 
113
- function hasNonEnumerable( object, property ) {
114
- return Object.prototype.hasOwnProperty.call( object, property ) &&
115
- !Object.prototype.propertyIsEnumerable.call( object, property );
116
- }
117
113
 
118
114
  /**
119
115
  * Deeply clone the given CSN dictionary and return it.
@@ -61,6 +61,7 @@ module.exports = {
61
61
  function getFilterObject( options, dialect, extensionCallback, migrationCallback, removeConstraintsCallback, primaryKeyCallback ) {
62
62
  const context = { hasLossyChanges: false };
63
63
  const raiseErrorOrMarkAsLossy = getSafeguardManager(context, options);
64
+ const messageVariant = options.script ? 'script' : 'std';
64
65
  return {
65
66
  // will be called with a simple Array.filter, as we need to filter constraint `ADD` for SQLite
66
67
  extension: ({
@@ -69,7 +70,7 @@ function getFilterObject( options, dialect, extensionCallback, migrationCallback
69
70
  let returnValue = true;
70
71
  forEach(elements, (name, element) => {
71
72
  if (dialect !== 'sqlite' && isKey(element))
72
- message('type-unsupported-key-change', [ 'definitions', extend, 'elements', name ], { id: name, '#': 'std' } );
73
+ message('migration-unsupported-key-change', [ 'definitions', extend, 'elements', name ], { id: name, '#': 'std' } );
73
74
  else if (extensionCallback && !extensionCallback(extend, name, element, { error, warning }))
74
75
  returnValue = false;
75
76
  });
@@ -82,23 +83,31 @@ function getFilterObject( options, dialect, extensionCallback, migrationCallback
82
83
  // will be called with a Array.forEach
83
84
  migration: (migrations, { error, warning, message }) => {
84
85
  forEach(migrations.remove, (name, migration) => {
85
- raiseErrorOrMarkAsLossy(migration, () => error('def-unsupported-element-drop', [ 'definitions', migrations.migrate, 'elements', name ], {}, 'Dropping elements is not supported'));
86
+ raiseErrorOrMarkAsLossy(name, migration, 'migration-unsupported-element-drop', id => message(id, [ 'definitions', migrations.migrate, 'elements', name ], { '#': messageVariant }));
86
87
  });
87
88
 
88
89
  forEach(migrations.change, (name, migration) => {
89
90
  const loc = [ 'definitions', migrations.migrate, 'elements', name ];
90
- if (migration.new.type === migration.old.type && migration.new.length < migration.old.length)
91
- raiseErrorOrMarkAsLossy(migration, () => error('type-unsupported-length-change', loc, { id: name }, 'Changed element $(ID) is a length reduction and is not supported'));
92
- else if (migration.new.type === migration.old.type && migration.new.scale !== migration.old.scale)
93
- raiseErrorOrMarkAsLossy(migration, () => error('type-unsupported-scale-change', loc, { id: name }, 'Changed element $(ID) is a scale change and is not supported'));
94
- else if (migration.new.type === migration.old.type && migration.new.precision !== migration.old.scale)
95
- raiseErrorOrMarkAsLossy(migration, () => error('type-unsupported-precision-change', loc, { id: name }, 'Changed element $(ID) is a precision change and is not supported'));
96
- else if (migration.new.type !== migration.old.type && typeChangeIsNotCompatible(dialect, migration.old.type, migration.new.type))
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'));
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
99
- raiseErrorOrMarkAsLossy(migration, () => message('type-unsupported-key-change', [ 'definitions', migrations.migrate, 'elements', name ], { id: name, '#': 'changed' } ));
100
- else if (migrationCallback)
91
+ if (migration.new.type === migration.old.type && migration.new.length < migration.old.length) {
92
+ raiseErrorOrMarkAsLossy(name, migration, 'migration-unsupported-length-change', id => message(id, loc, { '#': messageVariant, id: name }));
93
+ }
94
+ else if (migration.new.type === migration.old.type && migration.new.scale !== migration.old.scale) {
95
+ raiseErrorOrMarkAsLossy(name, migration, 'migration-unsupported-scale-change', id => message(id, loc, { '#': messageVariant, id: name }));
96
+ }
97
+ else if (migration.new.type === migration.old.type && migration.new.precision !== migration.old.scale) {
98
+ raiseErrorOrMarkAsLossy(name, migration, 'migration-unsupported-precision-change', id => message(id, loc, { '#': messageVariant, id: name }));
99
+ }
100
+ else if (migration.new.type !== migration.old.type && typeChangeIsNotCompatible(dialect, migration.old.type, migration.new.type)) {
101
+ raiseErrorOrMarkAsLossy(name, migration, 'migration-unsupported-change', id => message(id, loc, {
102
+ '#': messageVariant, id: name, name: migration.old.type, type: migration.new.type,
103
+ }));
104
+ }
105
+ 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
106
+ raiseErrorOrMarkAsLossy(name, migration, 'migration-unsupported-key-change', id => message( id, [ 'definitions', migrations.migrate, 'elements', name ], { id: name, '#': 'changed' } ));
107
+ }
108
+ else if (migrationCallback) {
101
109
  migrationCallback(migrations.migrate, name, migration, migrations.change, error);
110
+ }
102
111
 
103
112
  if (options.script && migration.lossy && migrationCallback)
104
113
  migrationCallback(migrations.migrate, name, migration, migrations.change, error);
@@ -115,9 +124,9 @@ function getFilterObject( options, dialect, extensionCallback, migrationCallback
115
124
  });
116
125
  }
117
126
  },
118
- deletion: ([ artifactName, artifact ], error) => {
127
+ deletion: ([ artifactName, artifact ], { message }) => {
119
128
  if (isPersistedAsTable(artifact))
120
- raiseErrorOrMarkAsLossy(artifact, () => error('def-unsupported-table-drop', [ 'definitions', artifactName ], 'Dropping tables is not supported'));
129
+ raiseErrorOrMarkAsLossy(artifactName, artifact, 'migration-unsupported-table-drop', id => message(id, [ 'definitions', artifactName ], { '#': messageVariant }));
121
130
  },
122
131
  changedPrimaryKeys: (changedPrimaryKeyArtifactName) => {
123
132
  if (primaryKeyCallback)
@@ -191,13 +200,30 @@ function filterCsn( csn ) {
191
200
  }
192
201
 
193
202
  function getSafeguardManager( context, options ) {
194
- return function raiseErrorOrMarkAsLossy(migration, raiseError) {
195
- if (!options.script) {
196
- raiseError();
197
- }
198
- else {
199
- migration.lossy = true;
203
+ return function raiseErrorOrMarkAsLossy(name, migration, id, raiseMessage) {
204
+ raiseMessage(id);
205
+
206
+ if (options.script) {
207
+ migration.details = getDetails(id, name);
208
+ migration.lossy = id !== 'migration-unsupported-key-change';
200
209
  context.hasLossyChanges = true;
201
210
  }
202
211
  };
203
212
  }
213
+
214
+ const details = {
215
+ 'migration-unsupported-element-drop': 'drop of element',
216
+ 'migration-unsupported-length-change': 'length reduction of element',
217
+ 'migration-unsupported-scale-change': 'scale reduction of element',
218
+ 'migration-unsupported-precision-change': 'precision reduction of element',
219
+ 'migration-unsupported-change': 'incompatible type change of element',
220
+ 'migration-unsupported-key-change': 'key property change of element',
221
+ 'migration-unsupported-table-drop': 'drop of entity',
222
+ };
223
+
224
+ function getDetails(id, name) {
225
+ if (details[id])
226
+ return `${details[id] } "${ name }" - check warnings for details`;
227
+
228
+ return null;
229
+ }