@sap/cds-compiler 5.0.6 → 5.1.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 +35 -0
  2. package/bin/cdsc.js +34 -8
  3. package/bin/cdshi.js +2 -1
  4. package/lib/api/main.js +10 -1
  5. package/lib/api/options.js +2 -3
  6. package/lib/base/message-registry.js +14 -0
  7. package/lib/base/messages.js +2 -1
  8. package/lib/base/meta.js +10 -0
  9. package/lib/base/optionProcessorHelper.js +11 -0
  10. package/lib/checks/dbFeatureFlags.js +5 -0
  11. package/lib/compiler/assert-consistency.js +1 -0
  12. package/lib/compiler/define.js +1 -1
  13. package/lib/compiler/extend.js +43 -7
  14. package/lib/compiler/index.js +4 -4
  15. package/lib/compiler/populate.js +58 -11
  16. package/lib/compiler/propagator.js +9 -4
  17. package/lib/compiler/resolve.js +115 -79
  18. package/lib/compiler/shared.js +2 -1
  19. package/lib/compiler/tweak-assocs.js +29 -5
  20. package/lib/edm/edm.js +8 -0
  21. package/lib/edm/edmPreprocessor.js +7 -3
  22. package/lib/gen/Dictionary.json +37 -0
  23. package/lib/json/to-csn.js +2 -0
  24. package/lib/main.js +2 -5
  25. package/lib/model/cloneCsn.js +1 -0
  26. package/lib/model/csnRefs.js +2 -1
  27. package/lib/model/csnUtils.js +0 -12
  28. package/lib/model/revealInternalProperties.js +0 -1
  29. package/lib/modelCompare/compare.js +12 -10
  30. package/lib/optionProcessor.js +2 -0
  31. package/lib/render/toCdl.js +8 -7
  32. package/lib/render/toHdbcds.js +1 -2
  33. package/lib/render/toSql.js +44 -8
  34. package/lib/transform/db/backlinks.js +20 -5
  35. package/lib/transform/db/killAnnotations.js +3 -0
  36. package/lib/transform/db/processSqlServices.js +63 -0
  37. package/lib/transform/draft/odata.js +6 -1
  38. package/lib/transform/forRelationalDB.js +9 -0
  39. package/lib/utils/file.js +77 -4
  40. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,31 @@
7
7
  Note: `beta` fixes, changes and features are usually not listed in this ChangeLog but [here](doc/CHANGELOG_BETA.md).
8
8
  The compiler behavior concerning `beta` features can change at any time without notice.
9
9
 
10
+ ## Version 5.1.0 - 2024-07-25
11
+
12
+ ### Added
13
+
14
+ - cdsc: Option `--stdin` was added to support input via standard input, e.g. `cat file.cds | cdsc --stdin`
15
+ - Allow to refer to draft state element `IsActiveEntity` via magic variable `$draft.IsActiveEntity` in annotation path expressions.
16
+ + for.odata: During draft augmentation `$draft.IsActiveEntity` is rewritten to `$self.IsActiveEntity` for all draft enabled
17
+ entities (root and sub nodes but not for named types or entity parameters).
18
+ + to.edm(x): (V4 only) Allow to refer to an entity element in a bound action via `$self` and not only via explicit binding parameter
19
+ in an annotation path expression. The API generator will prefix the path with the actual binding parameter name (explicit, annotation or
20
+ default).
21
+
22
+ ### Changed
23
+
24
+ - Update OData vocabularies: 'Common', 'Core', 'HTML5', 'UI'.
25
+ - to.cdl|hdbcds|hdi|sql: Remove `generated by` comment.
26
+
27
+ ### Fixed
28
+
29
+ - compiler: checks for associations now work for nested projections of the form `association.{ id }`
30
+ - to.edm(x): No `Nullable` attribute for `$ReturnType` of `Collection(<entity type>)` [OData V4 CSDL, section 12.8 Return Type](https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#sec_ReturnType)
31
+ - to.sql|hdi|hdbcds: Detect and error on "cross-eyed" backlinks, where we cannot construct a valid on-condition.
32
+ - to.sql|hdi.migration: Correctly detect that a view was dropped - this was previously just silently ignored.
33
+
34
+
10
35
  ## Version 5.0.6 - 2024-07-10
11
36
 
12
37
  ### Fixed
@@ -73,6 +98,16 @@ This is a preview version for the major release and contains breaking changes. I
73
98
  - API: Deprecated functions `preparedCsnToEdmx` and `preparedCsnToEdm` were removed.
74
99
  Use `to.edm(x)` instead.
75
100
 
101
+
102
+ ## Version 4.9.6 - 2024-07-15
103
+
104
+ ### Fixed
105
+
106
+ - for.seal: Don't generate DRAFT artifacts.
107
+ - for.odata: Propagate all `@odata { Type, MaxLength, Precision, Scale, SRID }` to generated foreign keys.
108
+ - to.edm(x): Respect `AppliesTo` specification in term definitions for actions and functions.
109
+ - to.sql: Conditions inside filters in combination with foreign key aliases were not properly translated in rare cases.
110
+
76
111
  ## Version 4.9.4 - 2024-05-21
77
112
 
78
113
  ### Fixed
package/bin/cdsc.js CHANGED
@@ -35,6 +35,7 @@ const { addLocalizationViews } = require('../lib/transform/localized');
35
35
  const { addTenantFields } = require('../lib/transform/addTenantFields');
36
36
  const { availableBetaFlags } = require('../lib/base/model');
37
37
  const { alterConstraintsWithCsn } = require('../lib/render/manageConstraints');
38
+ const { tmpFilePath, readStream } = require('../lib/utils/file');
38
39
 
39
40
  // Note: Instead of throwing ProcessExitError, we would rather just call process.exit(exitCode),
40
41
  // but that might truncate the output of stdout and stderr, both of which are async (or rather,
@@ -85,9 +86,12 @@ function remapCmdOptions( options, command ) {
85
86
  }
86
87
 
87
88
  function cdsc_main() {
88
- // Parse the command line and translate it into options
89
+ if (process.argv.some(arg => arg === '-i' || arg === '--stdin'))
90
+ optionProcessor.makePositionalArgumentsOptional();
89
91
 
92
+ // Parse the command line and translate it into options
90
93
  const cmdLine = optionProcessor.processCmdLine(process.argv);
94
+
91
95
  // Deal with '--version' explicitly
92
96
  if (cmdLine.options.version) {
93
97
  process.stdout.write(`${main.version()}\n`);
@@ -210,6 +214,9 @@ function cdsc_main() {
210
214
  if (typeof cmdLine.options.moduleLookupDirectories === 'string')
211
215
  cmdLine.options.moduleLookupDirectories = cmdLine.options.moduleLookupDirectories.split(',');
212
216
 
217
+ if (cmdLine.options.stdin)
218
+ cmdLine.options.fallbackParser ??= 'auto!';
219
+
213
220
  parseSeverityOptions(cmdLine);
214
221
 
215
222
  // Do the work for the selected command
@@ -230,12 +237,12 @@ function validateDirectBackendOption( command, options, args ) {
230
237
  displayUsage(`Option '--direct-backend' can't be used with command '${command}'`,
231
238
  optionProcessor.helpText, 2);
232
239
  }
233
- if (!args.files || args.files.length !== 1) {
234
- displayUsage(`Option '--direct-backend' expects exactly one JSON file, but ${args.files.length} given`,
240
+ if (!options.stdin && (!args.files || args.files.length !== 1)) {
241
+ displayUsage(`Option '--direct-backend' expects exactly one JSON file, but ${args.files?.length || 'none'} given`,
235
242
  optionProcessor.helpText, 2);
236
243
  }
237
- const filename = args.files[0];
238
- if (!filename.endsWith('.csn') && !filename.endsWith('.json')) {
244
+ const filename = args.files?.[0];
245
+ if (filename && !filename.endsWith('.csn') && !filename.endsWith('.json')) {
239
246
  displayUsage('Option \'--direct-backend\' expects a filename with a *.csn or *.json suffix',
240
247
  optionProcessor.helpText, 2);
241
248
  }
@@ -259,8 +266,21 @@ function displayUsage( error, helpText, code ) {
259
266
  throw new ProcessExitError(code);
260
267
  }
261
268
 
269
+ /**
270
+ * As the compiler is file-based and will always at least try to call `realpath()` on a file,
271
+ * we fake a "stdin" file for it.
272
+ *
273
+ * @returns {Promise<string>}
274
+ */
275
+ async function createTemporaryFileFromStdin() {
276
+ const contents = await readStream(process.stdin);
277
+ const file = tmpFilePath('cds-compiler-stdin', 'cds');
278
+ await fs.promises.writeFile(file, contents);
279
+ return file;
280
+ }
281
+
262
282
  // Executes a command line that has been translated to 'command' (what to do), 'options' (how) and 'args' (which files)
263
- function executeCommandLine( command, options, args ) {
283
+ async function executeCommandLine( command, options, args ) {
264
284
  const normalizeFilename = options.testMode && process.platform === 'win32';
265
285
  const messageLevels = {
266
286
  Error: 0, Warning: 1, Info: 2, Debug: 3,
@@ -304,17 +324,22 @@ function executeCommandLine( command, options, args ) {
304
324
  }
305
325
 
306
326
  options.messages = [];
327
+ args.files ??= [];
328
+
329
+ // Load a file from stdin if no explicit file is given and stdin is not a TTY.
330
+ if (options.stdin)
331
+ args.files.push(await createTemporaryFileFromStdin());
307
332
 
308
333
  const fileCache = Object.create(null);
309
334
  const compiled = options.directBackend
310
335
  ? util.promisify(fs.readFile)( args.files[0], 'utf-8' ).then(str => JSON.parse( str ))
311
336
  : compiler.compileX( args.files, undefined, options, fileCache );
312
337
 
313
- compiled.then( commands[command] )
338
+ await compiled.then( commands[command] )
314
339
  .then( displayMessages, displayErrors )
315
340
  .catch( catchErrors );
316
341
 
317
- return; // below are only command implementations.
342
+ // below are only command implementations.
318
343
 
319
344
  // Execute the command line option '--to-cdl' and display the results.
320
345
  // Return the original model (for chaining)
@@ -724,3 +749,4 @@ function parseSeverityOptions({ options }) {
724
749
  }
725
750
  }
726
751
  }
752
+
package/bin/cdshi.js CHANGED
@@ -13,7 +13,8 @@
13
13
 
14
14
  const compiler = require('../lib/compiler');
15
15
  const fs = require('fs');
16
- fs.readFile( '/dev/stdin', 'utf8', highlight );
16
+ const stdinFd = 0;
17
+ fs.readFile( stdinFd, 'utf8', highlight );
17
18
 
18
19
  const categoryChars = { // default: first char of category name
19
20
  // first char lowercase = reference other than via extend/annotate:
package/lib/api/main.js CHANGED
@@ -596,6 +596,15 @@ function sqlMigration( csn, options, messageFunctions, beforeImage ) {
596
596
  if (constraintDeletions)
597
597
  Object.values(constraintDeletions).forEach(constraint => dropSqls.push(constraint));
598
598
 
599
+ if (Object.keys(drops.final).length > 0) {
600
+ const order = sortViews({ sql: {}, csn: beforeImage });
601
+
602
+ for (const { name } of order) {
603
+ if (drops.final[name])
604
+ dropSqls.push(drops.final[name]);
605
+ }
606
+ }
607
+
599
608
  // We need to drop the things without dependants first - so inversely sorted
600
609
  dropSqls.reverse();
601
610
 
@@ -679,7 +688,7 @@ function createSqlDefinitions( hdbkinds, afterImage ) {
679
688
  */
680
689
  function createSqlDeletions( deletions, beforeImage ) {
681
690
  const result = [];
682
- objectUtils.forEach(deletions, name => result.push({ name: getFileName(name, beforeImage), suffix: '.hdbtable' }));
691
+ objectUtils.forEach(deletions, name => result.push({ name: getFileName(name, beforeImage), suffix: beforeImage.definitions[name].query ? '.hdbview' : '.hdbtable' }));
683
692
  return result;
684
693
  }
685
694
  /**
@@ -29,7 +29,6 @@ const publicOptionsNewAPI = [
29
29
  'magicVars', // deprecated, not removed in v3 as we have specific error messages for it
30
30
  'variableReplacements',
31
31
  'pre2134ReferentialConstraintNames',
32
- 'generatedByComment',
33
32
  'betterSqliteSessionVariables',
34
33
  'fewerLocalizedViews',
35
34
  'withHanaAssociations',
@@ -156,7 +155,7 @@ module.exports = {
156
155
  sql: (options) => {
157
156
  const hardOptions = { src: 'sql', toSql: true, forHana: true };
158
157
  const defaultOptions = {
159
- sqlMapping: 'plain', sqlDialect: 'plain', generatedByComment: true, withHanaAssociations: true,
158
+ sqlMapping: 'plain', sqlDialect: 'plain', withHanaAssociations: true,
160
159
  };
161
160
  const processed = translateOptions(options, defaultOptions, hardOptions, undefined, [ 'sql-dialect-and-naming' ], 'to.sql');
162
161
 
@@ -166,7 +165,7 @@ module.exports = {
166
165
  const hardOptions = { src: 'hdi', toSql: true, forHana: true };
167
166
  // TODO: sqlDialect should be a hard option!
168
167
  const defaultOptions = {
169
- sqlMapping: 'plain', sqlDialect: 'hana', generatedByComment: false, withHanaAssociations: true,
168
+ sqlMapping: 'plain', sqlDialect: 'hana', withHanaAssociations: true,
170
169
  };
171
170
  return translateOptions(options, defaultOptions, hardOptions, { sqlDialect: generateStringValidator([ 'hana' ]) }, undefined, 'to.hdi');
172
171
  },
@@ -97,6 +97,7 @@ const centralMessages = {
97
97
  'ref-expecting-foreign-key': { severity: 'Error' },
98
98
  'ref-invalid-source': { severity: 'Error' },
99
99
  'ref-invalid-target': { severity: 'Error' },
100
+ 'ref-missing-self-counterpart': { severity: 'Error', configurableFor: true },
100
101
  'ref-sloppy-target': { severity: 'Error', configurableFor: 'v4' },
101
102
 
102
103
  'extend-repeated-intralayer': { severity: 'Warning' },
@@ -556,6 +557,10 @@ const centralMessageTexts = {
556
557
  // Messages for erroneous references -----------------------------------------
557
558
  // location at erroneous reference (if possible)
558
559
  'ref-deprecated-orderby': 'Replace source element reference $(ID) by $(NEWCODE); auto-corrected',
560
+ 'ref-missing-self-counterpart' : {
561
+ std: 'Expected to find a matching element in $self-comparison for foreign key $(PROP) of association $(NAME)',
562
+ unmanaged: 'Expected to find a matching element in $self-comparison for $(PROP) of association $(NAME)'
563
+ },
559
564
  'ref-unexpected-self': {
560
565
  std: 'Unexpected $(ID) reference; is valid only in ON-conditions of unmanaged associations',
561
566
  on: 'Unexpected $(ID) reference; is valid only if compared to be equal to an association of the target side',
@@ -965,6 +970,15 @@ const centralMessageTexts = {
965
970
  'managed': 'Ignoring managed association $(NAME) that is published in a UNION',
966
971
  'std': 'Ignoring association $(NAME) that is published in a UNION'
967
972
  },
973
+ 'query-missing-element': {
974
+ std: 'Element $(ID) is missing in specified elements',
975
+ enum: 'Enum $(ID) is missing in specified enum values',
976
+ foreignKeys: 'Foreign key $(ID) is missing in specified foreign keys',
977
+ },
978
+ 'query-unspecified-element': {
979
+ std: 'Element $(ID) does not result from the query',
980
+ foreignKeys: 'Foreign key $(ID) does not result from the query',
981
+ },
968
982
 
969
983
  'ref-sloppy-target': 'An entity or an aspect (not type) is expected here',
970
984
 
@@ -13,6 +13,7 @@ const { analyseCsnPath, traverseQuery } = require('../model/csnRefs');
13
13
  const { CompilerAssertion } = require('./error');
14
14
  const { getArtifactName } = require('../compiler/base');
15
15
  const { cdlNewLineRegEx } = require('../language/textUtils');
16
+ const meta = require('./meta');
16
17
 
17
18
  const fs = require('fs');
18
19
  const path = require('path');
@@ -117,7 +118,7 @@ class CompilationError extends Error {
117
118
  // no proper message about _what_ the root cause of the exception was.
118
119
  // To mitigate that, we serialize the first error in the message as well.
119
120
  const firstError = messages.find( m => m.severity === 'Error' )?.toString() || '';
120
- super( `CDS compilation failed\n${firstError}` );
121
+ super( `CDS compilation failed (@sap/cds-compiler v${ meta.version() })\n${firstError}` );
121
122
 
122
123
  /** @since v4.0.0 */
123
124
  this.code = 'ERR_CDS_COMPILATION_FAILURE';
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ // Metadata, e.g. version.
4
+
5
+ /** The compiler version (taken from package.json) */
6
+ function version() {
7
+ return require('../../package.json').version;
8
+ }
9
+
10
+ module.exports = { version };
@@ -42,6 +42,7 @@ function createOptionProcessor() {
42
42
  },
43
43
  help,
44
44
  processCmdLine,
45
+ makePositionalArgumentsOptional,
45
46
  };
46
47
  return optionProcessor;
47
48
 
@@ -291,6 +292,16 @@ function createOptionProcessor() {
291
292
  };
292
293
  }
293
294
 
295
+ function makePositionalArgumentsOptional() {
296
+ for (const arg of optionProcessor.positionalArguments || [])
297
+ arg.required = false;
298
+
299
+ for (const cmd in optionProcessor.commands) {
300
+ for (const arg of optionProcessor.commands[cmd].positionalArguments || [])
301
+ arg.required = false;
302
+ }
303
+ }
304
+
294
305
  // API: Let the option processor digest a command line 'argv'
295
306
  // The expectation is to get a commandline like this:
296
307
  // $ node cdsc.js -x 1 --foo toXyz -y --bar-wiz bla arg1 arg2
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { setProp } = require('../base/model');
4
4
  const { featureFlags } = require('../transform/db/featureFlags');
5
+ const { isSqlService } = require('../transform/db/processSqlServices');
5
6
 
6
7
  /**
7
8
  *
@@ -25,4 +26,8 @@ module.exports = {
25
26
  value: setFeatureFlag('$calculatedElements'),
26
27
  expand: setFeatureFlag('$expandInline'),
27
28
  inline: setFeatureFlag('$expandInline'),
29
+ kind: function setFeatureFlagForSqlService( artifact ) {
30
+ if (isSqlService(artifact))
31
+ setFeatureFlag( '$sqlService' ).call(this);
32
+ },
28
33
  };
@@ -255,6 +255,7 @@ function assertConsistency( model, stage ) {
255
255
  elements: { kind: true, inherits: 'definitions', also: [ 0 ] }, // 0 for cyclic expansions
256
256
  // specified elements in query entities (TODO: introduce real "specified elements" instead):
257
257
  elements$: { kind: true, enumerable: false, test: TODO },
258
+ foreignKeys$: { kind: true, enumerable: false, test: TODO },
258
259
  enum$: { kind: true, enumerable: false, test: TODO },
259
260
  typeProps$: { kind: true, enumerable: false, test: TODO },
260
261
  // helper property for faster processing:
@@ -216,7 +216,7 @@ function define( model ) {
216
216
 
217
217
  const { $self } = model.definitions;
218
218
  if ($self) {
219
- message( 'name-deprecated-$self', [ $self.location, $self ], { name: '$self' },
219
+ message( 'name-deprecated-$self', [ $self.name.location, $self ], { name: '$self' },
220
220
  'Do not use $(NAME) as name for an artifact definition' );
221
221
  }
222
222
  }
@@ -27,6 +27,7 @@ const {
27
27
  const layers = require('./moduleLayers');
28
28
  const { CompilerAssertion } = require('../base/error');
29
29
  const { Location } = require('../base/location');
30
+ const { typeParameters } = require('./builtins');
30
31
 
31
32
  const $location = Symbol.for( 'cds.$location' );
32
33
 
@@ -69,6 +70,7 @@ function extend( model ) {
69
70
  createRemainingAnnotateStatements,
70
71
  extendArtifactBefore,
71
72
  extendArtifactAfter,
73
+ extendForeignKeys,
72
74
  applyIncludes, // TODO: re-check
73
75
  } );
74
76
 
@@ -91,7 +93,8 @@ function extend( model ) {
91
93
  //-----------------------------------------------------------------------------
92
94
  // Extensions: general algorithm
93
95
  //-----------------------------------------------------------------------------
94
- // extendArtifactBefore, extendArtifactAfter, createRemainingAnnotateStatements
96
+ // extendArtifactBefore, extendArtifactAfter, createRemainingAnnotateStatements,
97
+ // extendForeignKeys
95
98
 
96
99
  /**
97
100
  * Goes through all (applied) annotations in the given artifact and chooses one
@@ -179,22 +182,53 @@ function extend( model ) {
179
182
  else {
180
183
  let elementsProp = 'elements';
181
184
  if (art.kind !== 'annotate')
182
- elementsProp = art.enum && 'enum' || art.foreignKeys && 'foreignKeys' || 'elements';
183
- moveDictExtensions( art, extensionsMap, elementsProp, 'elements' );
185
+ elementsProp = art.enum && 'enum' || art.target && 'foreignKeys' || 'elements';
186
+
187
+ // keys are handled in tweak-assocs.js; don't push them down; see extendForeignKeys()
188
+ if (elementsProp !== 'foreignKeys')
189
+ moveDictExtensions( art, extensionsMap, elementsProp, 'elements' );
184
190
  moveDictExtensions( art, extensionsMap, 'enum' );
185
191
  }
186
192
  }
187
193
 
194
+ /**
195
+ * Apply foreign key extensions. Because foreign keys are handled late in the compiler
196
+ * (in tweak-assocs.js), we can't apply them in effectiveType(), yet.
197
+ * Instead, we postpone applying them until all foreign keys were generated.
198
+ *
199
+ * @param art
200
+ */
201
+ function extendForeignKeys( art ) {
202
+ // See extendArtifactAfter() for targetAspect/items handling.
203
+ const sub = art.items || art.targetAspect?.elements && art.targetAspect;
204
+ if (!art._extensions || sub)
205
+ return;
206
+
207
+ // push down foreign keys
208
+ moveDictExtensions( art, art._extensions, 'foreignKeys', 'elements' );
209
+ if (!art.foreignKeys)
210
+ return;
211
+
212
+ forEachGeneric(art, 'foreignKeys', (key) => {
213
+ if (!key._effectiveType)
214
+ throw new CompilerAssertion('foreign key should have been processed');
215
+ extendArtifactBefore( key );
216
+ extendArtifactAfter( key );
217
+ });
218
+ }
219
+
188
220
  /**
189
221
  * Applying extensions is handled in extendArtifactAfter(). And only afterward,
190
222
  * an effective sequence number is set. Meaning that if a sub-artifact already
191
223
  * has a sequence number, then extensions would be lost.
224
+ *
225
+ * A special case are foreign keys, see extendForeignKeys().
192
226
  */
193
227
  function ensureArtifactNotProcessed( art ) {
194
228
  if (!model.options.testMode)
195
229
  return;
196
230
 
197
- if (art.$effectiveSeqNo !== 0 && art.$effectiveSeqNo !== undefined) {
231
+ if (art.kind !== 'key' && art.$effectiveSeqNo !== 0 && art.$effectiveSeqNo !== undefined) {
198
232
  // if the artifact already has a sequence number, then
199
233
  // extendArtifactAfter() was already called -> annotations would be lost.
200
234
  throw new CompilerAssertion('artifact already processed; extensions would be lost');
@@ -357,7 +391,7 @@ function extend( model ) {
357
391
 
358
392
  function extensionOverwrites( ext, prop ) {
359
393
  return (prop.charAt(0) !== '@')
360
- ? [ 'doc', 'length', 'precision', 'scale', 'srid' ].includes( prop )
394
+ ? (prop === 'doc' || typeParameters.list.includes(prop))
361
395
  : !annotationHasEllipsis( ext[prop] );
362
396
  }
363
397
 
@@ -437,7 +471,7 @@ function extend( model ) {
437
471
  query.columns.push( ...ext.columns );
438
472
  initSelectItems( query, ext.columns, query, true );
439
473
  }
440
- else if ([ 'length', 'precision', 'scale', 'srid' ].includes( prop )) {
474
+ else if (typeParameters.list.includes( prop )) {
441
475
  const typeExts = art.$typeExts || (art.$typeExts = {});
442
476
  typeExts[prop] = ext;
443
477
  }
@@ -647,6 +681,7 @@ function extend( model ) {
647
681
  const extensions = extensionsMap[extProp];
648
682
  if (!extensions)
649
683
  return;
684
+
650
685
  const artDict = art[artProp] || annotateFor( art, extProp ); // no auto-correction in annotate
651
686
 
652
687
  for (const ext of extensions) {
@@ -762,10 +797,11 @@ function extend( model ) {
762
797
  const dict = parent[prop];
763
798
  if (!dict) {
764
799
  // TODO: check - for each name? - better locations
765
- const location = ext._parent[prop][$location] || ext.name.location;
800
+ const location = ext._parent[prop]?.[$location] || ext.name.location;
766
801
  // Remark: no `elements` dict location with `annotate Main:elem`
767
802
  switch (prop) {
768
803
  // TODO: change texts, somehow similar to checkDefinitions() ?
804
+ case 'foreignKeys':
769
805
  case 'elements':
770
806
  case 'enum': // TODO: extra?
771
807
  warning( 'anno-unexpected-elements', [ location, ext._parent ],
@@ -34,7 +34,7 @@ const { Location, emptyWeakLocation } = require('../base/location');
34
34
  const { createMessageFunctions, deduplicateMessages } = require('../base/messages');
35
35
  const { checkRemovedDeprecatedFlags } = require('../base/model');
36
36
  const { promiseAllDoNotRejectImmediately } = require('../base/node-helpers');
37
- const { cdsFs } = require('../utils/file');
37
+ const { cdsFs, fileExtension } = require('../utils/file');
38
38
 
39
39
  const fs = require('fs');
40
40
  const path = require('path');
@@ -82,7 +82,7 @@ class ArgumentError extends Error {
82
82
  function parseX( source, filename, options = {}, messageFunctions = null ) {
83
83
  if (!messageFunctions)
84
84
  messageFunctions = createMessageFunctions( options, 'parse' );
85
- const ext = path.extname( filename ).slice(1).toLowerCase();
85
+ const ext = fileExtension( filename );
86
86
  const parser = parserForFile( source, ext, options );
87
87
  if (parser)
88
88
  return parser( source, filename, options, messageFunctions );
@@ -166,10 +166,10 @@ function compileX( filenames, dir = '', options = {}, fileCache = Object.create(
166
166
  .then( () => promiseAllDoNotRejectImmediately( input.files.map( readAndParse ) ) )
167
167
  .then( testInvocation, (reason) => {
168
168
  // do not reject with PromiseAllError, use InvocationError:
169
- const errs = reason.valuesOrErrors.filter( e => e instanceof Error );
169
+ const errs = reason.valuesOrErrors?.filter( e => e instanceof Error ) || [ reason ];
170
170
  // internal error if no file IO error (has property `path`)
171
171
  return Promise.reject( errs.find( e => !e.path ) ||
172
- new InvocationError( [ ...input.repeated, ...errs ]) );
172
+ new InvocationError( [ ...(input?.repeated || []), ...errs ]) );
173
173
  } );
174
174
 
175
175
  if (!options.parseOnly && !options.parseCdl)
@@ -83,6 +83,7 @@ function populate( model ) {
83
83
  effectiveType,
84
84
  getOrigin,
85
85
  getInheritedProp,
86
+ mergeSpecifiedForeignKeys,
86
87
  } );
87
88
  // let depth = 100;
88
89
 
@@ -115,7 +116,10 @@ function populate( model ) {
115
116
 
116
117
  /** Make sure that effectiveType() is called on all members and items */
117
118
  function traverseElementEnvironments( art ) {
118
- // TODO: we could leave out foreign keys (but they are traversed via forEachMember)
119
+ // We leave out foreign keys (as they are traversed via forEachMember).
120
+ // Keys are handled in tweak-assocs.js
121
+ if (art.kind === 'key')
122
+ return;
119
123
  let type = effectiveType( art );
120
124
  while (type?.items)
121
125
  type = effectiveType( type.items );
@@ -529,9 +533,6 @@ function populate( model ) {
529
533
  if (!selem) {
530
534
  info( 'query-missing-element', [ ielem.name.location, art ], {
531
535
  '#': ielem.kind === 'enum' ? 'enum' : 'std', id,
532
- }, {
533
- std: 'Element $(ID) is missing in specified elements',
534
- enum: 'Enum $(ID) is missing in specified enum values',
535
536
  } );
536
537
  }
537
538
  else {
@@ -560,6 +561,8 @@ function populate( model ) {
560
561
  setLink( ielem, 'elements$', selem.elements );
561
562
  if (selem.enum)
562
563
  setLink( ielem, 'enum$', selem.enum );
564
+ if (selem.foreignKeys)
565
+ setLink( ielem, 'foreignKeys$', selem.foreignKeys );
563
566
  }
564
567
  }
565
568
 
@@ -574,8 +577,57 @@ function populate( model ) {
574
577
  specifiedElement.$isSpecifiedElement = true;
575
578
  if (!specifiedElement.$replacement) {
576
579
  const loc = [ specifiedElement.name.location, specifiedElement ];
577
- error( 'query-unspecified-element', loc, { id },
578
- 'Element $(ID) does not result from the query' );
580
+ error( 'query-unspecified-element', loc, { id } );
581
+ }
582
+ }
583
+ }
584
+
585
+ /**
586
+ * Merge _specified_ foreign keys with _inferred_ foreign keys in the given view/element,
587
+ * where specified elements can appear through CSN.
588
+ *
589
+ * We only copy annotations.
590
+ *
591
+ * This is important to ensure re-compilability.
592
+ *
593
+ * TODO: make this part of a revamped on-demand 'extend' functionality.
594
+ *
595
+ * @param art
596
+ */
597
+ function mergeSpecifiedForeignKeys( art ) {
598
+ if (!art.foreignKeys)
599
+ return; // TODO: Warn if there are no foreign keys?
600
+
601
+ let wasAnnotated = false;
602
+
603
+ for (const id in art.foreignKeys) {
604
+ const ielem = art.foreignKeys[id]; // inferred element
605
+ const selem = art.foreignKeys$[id]; // specified element
606
+ if (!selem) {
607
+ info( 'query-missing-element', [ ielem.name.location, art ], { '#': 'foreignKeys', id } );
608
+ }
609
+ else {
610
+ for (const prop in selem) {
611
+ // just annotation assignments and doc comments for foreign keys
612
+ if (prop.charAt(0) === '@' || prop === 'doc') {
613
+ ielem[prop] = selem[prop];
614
+ // required for gensrc mode of to-csn.js, otherwise the annotation
615
+ // may be lost during recompilation.
616
+ ielem[prop].$priority = 'annotate';
617
+ wasAnnotated = true;
618
+ }
619
+ }
620
+ selem.$replacement = true;
621
+ }
622
+ }
623
+ if (wasAnnotated)
624
+ setExpandStatusAnnotate( art, 'annotate' );
625
+
626
+ for (const id in art.foreignKeys$) {
627
+ const specifiedElement = art.foreignKeys$[id];
628
+ if (!specifiedElement.$replacement) {
629
+ const loc = [ specifiedElement.name.location, specifiedElement ];
630
+ error( 'query-unspecified-element', loc, { '#': 'foreignKeys', id } );
579
631
  }
580
632
  }
581
633
  }
@@ -816,15 +868,10 @@ function populate( model ) {
816
868
  const excludingDict = (colParent || query).excludingDict || Object.create( null );
817
869
 
818
870
  const envParent = wildcard._pathHead; // TODO: rename _pathHead to _columnParent
819
- // console.log('S1:',location.line,location.col,
820
- // envParent&&!!envParent._origin&&envParent._origin.name)
821
871
  const env = wildcardColumnEnv( wildcard, query );
822
872
  if (!env)
823
873
  return;
824
874
 
825
- // if (envParent) console.log('S2:',location.line,location.col,
826
- // envParent?.name,envParent?._origin?.name,
827
- // Object.keys(env),Object.keys(elements))
828
875
  for (const name in env) {
829
876
  const navElem = env[name];
830
877
  // TODO: remove all access to masked (use 'grep')
@@ -49,12 +49,17 @@ function propagate( model ) {
49
49
  targetAspect,
50
50
  cardinality: notWithExpand,
51
51
  on: notWithExpand,
52
- foreignKeys: expensive, // includes "notWithExpand", dictionary copy
52
+ // "expensive" includes "notWithExpand"
53
+ // required for places where we don't handle associations, such as in parameters;
54
+ // otherwise already expanded and rewritten.
55
+ foreignKeys: expensive,
53
56
  items,
57
+ // required for propagation in targetAspect; otherwise already expanded
54
58
  elements: expensive,
55
- enum: expensive,
56
- params: expensive, // actually only with parent action
57
- returns,
59
+ // already expanded if necessary
60
+ // enum: expensive,
61
+ // params: expensive, // actually only with parent action
62
+ // returns,
58
63
  $enclosed: annotation,
59
64
  };
60
65
  const ruleToFunction = {