@sap/cds-compiler 5.3.0 → 5.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 (50) hide show
  1. package/CHANGELOG.md +30 -2
  2. package/bin/cdsc.js +1 -1
  3. package/doc/CHANGELOG_BETA.md +2 -2
  4. package/lib/api/options.js +4 -2
  5. package/lib/base/builtins.js +0 -10
  6. package/lib/base/keywords.js +3 -31
  7. package/lib/base/message-registry.js +23 -5
  8. package/lib/base/messages.js +1 -1
  9. package/lib/checks/existsMustEndInAssoc.js +7 -2
  10. package/lib/checks/foreignKeys.js +12 -7
  11. package/lib/compiler/assert-consistency.js +11 -3
  12. package/lib/compiler/builtins.js +2 -0
  13. package/lib/compiler/checks.js +88 -38
  14. package/lib/compiler/define.js +2 -2
  15. package/lib/compiler/shared.js +9 -10
  16. package/lib/compiler/xpr-rewrite.js +11 -0
  17. package/lib/compiler/xsn-model.js +1 -1
  18. package/lib/edm/csn2edm.js +2 -0
  19. package/lib/edm/edm.js +2 -1
  20. package/lib/edm/edmPreprocessor.js +14 -1
  21. package/lib/edm/edmUtils.js +17 -2
  22. package/lib/gen/BaseParser.js +291 -197
  23. package/lib/gen/CdlParser.js +1631 -1605
  24. package/lib/gen/Dictionary.json +74 -6
  25. package/lib/gen/language.checksum +1 -1
  26. package/lib/gen/language.interp +1 -1
  27. package/lib/gen/languageParser.js +1808 -1804
  28. package/lib/language/antlrParser.js +8 -4
  29. package/lib/language/genericAntlrParser.js +3 -3
  30. package/lib/model/csnUtils.js +6 -1
  31. package/lib/optionProcessor.js +4 -0
  32. package/lib/parsers/AstBuildingParser.js +172 -108
  33. package/lib/parsers/CdlGrammar.g4 +154 -134
  34. package/lib/parsers/Lexer.js +3 -3
  35. package/lib/parsers/identifiers.js +59 -0
  36. package/lib/render/toCdl.js +5 -5
  37. package/lib/render/utils/common.js +5 -0
  38. package/lib/render/utils/delta.js +23 -5
  39. package/lib/transform/db/expansion.js +2 -1
  40. package/lib/transform/db/rewriteCalculatedElements.js +11 -5
  41. package/lib/transform/db/transformExists.js +52 -26
  42. package/lib/transform/effective/annotations.js +147 -0
  43. package/lib/transform/effective/main.js +17 -3
  44. package/lib/transform/forOdata.js +53 -10
  45. package/lib/transform/forRelationalDB.js +8 -1
  46. package/lib/transform/odata/createForeignKeys.js +180 -0
  47. package/lib/transform/odata/flattening.js +135 -19
  48. package/lib/transform/odata/typesExposure.js +4 -3
  49. package/lib/transform/transformUtils.js +6 -6
  50. package/package.json +1 -1
@@ -177,9 +177,9 @@ function string( text, lexer, parser, beg ) {
177
177
  }
178
178
  else { // try with previous date/time/timestamp/x
179
179
  prefix = parser.tokens[parser.tokens.length - 1];
180
- if (prefix.location.endLine !== lexer.location.line ||
180
+ if (prefix && (prefix.location.endLine !== lexer.location.line ||
181
181
  prefix.location.endCol !== lexer.location.col ||
182
- !quotedLiterals.includes( prefix.keyword ))
182
+ !quotedLiterals.includes( prefix.keyword )))
183
183
  prefix = null;
184
184
  while (re.test( lexer.input ) && lexer.input[re.lastIndex] === "'")
185
185
  esc = ++re.lastIndex;
@@ -192,7 +192,7 @@ function string( text, lexer, parser, beg ) {
192
192
  const before = (lastIndex) ? 'string' : 'multi';
193
193
  // eslint-disable-next-line cds-compiler/message-texts
194
194
  parser.error( 'syntax-missing-token-end', lexer.location,
195
- { '#': before, newCode: text }, {
195
+ { '#': before, newcode: text }, {
196
196
  string: 'The string literal must end with $(NEWCODE) before the end of line',
197
197
  multi: 'The multi-line string literal must end with $(NEWCODE)',
198
198
  } );
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ /** RegEx identifying undelimited identifiers in CDL */
4
+ const undelimitedIdentifierRegex = /^[$_\p{ID_Start}][$\p{ID_Continue}\u200C\u200D]*$/u;
5
+
6
+ /**
7
+ * Functions without parentheses in CDL (common standard SQL-92 functions)
8
+ * (do not add more - make it part of the SQL renderer to remove parentheses for
9
+ * other funny SQL functions like CURRENT_UTCTIMESTAMP).
10
+ */
11
+ const functionsWithoutParentheses = [
12
+ 'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP',
13
+ 'CURRENT_USER', 'SESSION_USER', 'SYSTEM_USER',
14
+ ];
15
+
16
+ // CDL reserved keywords, used for automatic quoting in 'toCdl' renderer
17
+ // Keep in sync with reserved keywords in language.g4
18
+ const cdlKeywords = [
19
+ 'ALL',
20
+ 'ANY',
21
+ 'AS',
22
+ 'BY',
23
+ 'CASE',
24
+ 'CAST',
25
+ 'DISTINCT',
26
+ 'EXISTS',
27
+ 'EXTRACT',
28
+ 'FALSE', // boolean
29
+ 'FROM',
30
+ 'IN',
31
+ 'KEY',
32
+ 'NEW',
33
+ 'NOT',
34
+ 'NULL',
35
+ 'OF',
36
+ 'ON',
37
+ 'SELECT',
38
+ 'SOME',
39
+ 'TRIM',
40
+ 'TRUE', // boolean
41
+ 'WHEN',
42
+ 'WHERE',
43
+ 'WITH',
44
+ ];
45
+
46
+ function isSimpleCdlIdentifier( id ) {
47
+ if (undelimitedIdentifierRegex.test(id))
48
+ return true;
49
+ const upperId = id.toUpperCase();
50
+ return !cdlKeywords.includes(upperId) &&
51
+ !functionsWithoutParentheses.includes(upperId);
52
+ }
53
+
54
+ module.exports = {
55
+ undelimitedIdentifierRegex,
56
+ cdlKeywords,
57
+ functionsWithoutParentheses,
58
+ isSimpleCdlIdentifier,
59
+ };
@@ -18,8 +18,8 @@ const {
18
18
  const { isBuiltinType } = require('../base/builtins');
19
19
  const { cloneFullCsn } = require('../model/cloneCsn');
20
20
  const { getKeysDict } = require('../model/csnRefs');
21
+ const { undelimitedIdentifierRegex } = require('../parsers/identifiers');
21
22
 
22
- const identifierRegex = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/;
23
23
  const specialFunctionKeywords = Object.create(null);
24
24
 
25
25
  /**
@@ -2104,7 +2104,7 @@ function csnToCdl( csn, options, msg ) {
2104
2104
  return path.split('.').map((step, index) => {
2105
2105
  if (index === 0)
2106
2106
  return quoteNonIdentifierOrKeyword(step, env);
2107
- else if (!identifierRegex.test(step))
2107
+ else if (!undelimitedIdentifierRegex.test(step))
2108
2108
  return delimitedId(step, env);
2109
2109
  return step;
2110
2110
  }).join('.');
@@ -2142,7 +2142,7 @@ function csnToCdl( csn, options, msg ) {
2142
2142
  * @return {string}
2143
2143
  */
2144
2144
  function quoteNonIdentifier( id, env ) {
2145
- if (!identifierRegex.test(id))
2145
+ if (!undelimitedIdentifierRegex.test(id))
2146
2146
  return delimitedId(id, env);
2147
2147
  return id;
2148
2148
  }
@@ -2262,7 +2262,7 @@ function removeTrailingNewline( str ) {
2262
2262
  * @return {boolean}
2263
2263
  */
2264
2264
  function requiresQuotingForCdl( id, additionalKeywords ) {
2265
- return !identifierRegex.test(id) ||
2265
+ return !undelimitedIdentifierRegex.test(id) ||
2266
2266
  keywords.cdl.includes(id.toUpperCase()) ||
2267
2267
  keywords.cdl_functions.includes(id.toUpperCase()) ||
2268
2268
  additionalKeywords.includes(id.toUpperCase());
@@ -2523,7 +2523,7 @@ function apiSmartId( id, insideFunction = null ) {
2523
2523
  */
2524
2524
  function apiSmartFunctionId( funcName ) {
2525
2525
  const funcId = funcName.toUpperCase();
2526
- const requiresQuoting = !identifierRegex.test(funcName) ||
2526
+ const requiresQuoting = !undelimitedIdentifierRegex.test(funcName) ||
2527
2527
  (keywords.cdl.includes(funcId) && !specialFunctions[funcId]);
2528
2528
  if (requiresQuoting)
2529
2529
  return apiDelimitedId(funcName);
@@ -263,6 +263,7 @@ const cdsToSqlTypes = {
263
263
  'cds.hana.ST_POINT': 'CHAR', // CHAR is implicit fallback used in toSql - make it explicit here
264
264
  'cds.hana.ST_GEOMETRY': 'CHAR', // CHAR is implicit fallback used in toSql - make it explicit here
265
265
  'cds.Vector': 'NVARCHAR', // Not supported; see #11725
266
+ 'cds.Map': 'NCLOB', // Not supported; see #13149
266
267
  },
267
268
  hana: {
268
269
  'cds.hana.SMALLDECIMAL': 'SMALLDECIMAL',
@@ -270,6 +271,7 @@ const cdsToSqlTypes = {
270
271
  'cds.hana.ST_POINT': 'ST_POINT',
271
272
  'cds.hana.ST_GEOMETRY': 'ST_GEOMETRY',
272
273
  'cds.Vector': 'REAL_VECTOR', // FIXME: test me
274
+ 'cds.Map': 'NCLOB',
273
275
  },
274
276
  sqlite: {
275
277
  'cds.Date': 'DATE_TEXT',
@@ -280,6 +282,7 @@ const cdsToSqlTypes = {
280
282
  'cds.hana.BINARY': 'BINARY_BLOB',
281
283
  'cds.hana.SMALLDECIMAL': 'SMALLDECIMAL',
282
284
  'cds.Vector': 'BINARY_BLOB', // Not supported; see #11725
285
+ 'cds.Map': 'JSON',
283
286
  },
284
287
  plain: {
285
288
  'cds.Binary': 'VARBINARY',
@@ -292,6 +295,7 @@ const cdsToSqlTypes = {
292
295
  'cds.DecimalFloat': 'DECFLOAT', // Decimal and Decimal(p) is mapped to cds.DecimalFloat
293
296
  'cds.DateTime': 'TIMESTAMP(0)',
294
297
  'cds.Timestamp': 'TIMESTAMP(7)',
298
+ 'cds.Map': 'JSON',
295
299
  },
296
300
  postgres: {
297
301
  // See <https://www.postgresql.org/docs/current/datatype.html>
@@ -302,6 +306,7 @@ const cdsToSqlTypes = {
302
306
  'cds.Double': 'FLOAT8',
303
307
  'cds.UInt8': 'INTEGER', // Not equivalent
304
308
  'cds.Vector': 'VARCHAR', // Not supported; see #11725
309
+ 'cds.Map': 'JSONB',
305
310
  },
306
311
  };
307
312
 
@@ -179,10 +179,8 @@ class DeltaRendererPostgres extends DeltaRenderer {
179
179
  */
180
180
  alterColumns(artifactName, columnName, delta, definitionsStr, eltName, env) {
181
181
  const sqls = [];
182
- if (delta.new.notNull === true || delta.new.key === true)
183
- definitionsStr = definitionsStr.replace(' NOT NULL', ''); // TODO: Is this robust enough?
184
- else if (delta.new.notNull === false || delta.new.$notNull === false)
185
- definitionsStr = definitionsStr.replace(' NULL', ''); // TODO: Is this robust enough?
182
+
183
+ definitionsStr = this.#removeNullabilityFromElementString(delta, definitionsStr);
186
184
 
187
185
  if (delta.old.default && !delta.old.value) // Drop old default if any exists
188
186
  sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER COLUMN ${columnName} DROP DEFAULT;`);
@@ -190,7 +188,7 @@ class DeltaRendererPostgres extends DeltaRenderer {
190
188
  if (delta.new.default && !delta.new.value ) { // Alter column with default
191
189
  const df = delta.new.default;
192
190
  delete delta.new.default;
193
- const eltStrNoDefault = this.scopedFunctions.renderElement(eltName, delta.new, null, null, env);
191
+ const eltStrNoDefault = this.#removeNullabilityFromElementString(delta, this.scopedFunctions.renderElement(eltName, delta.new, null, null, env));
194
192
  delta.new.default = df;
195
193
  sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER ${eltStrNoDefault};`);
196
194
  sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER COLUMN ${columnName} SET DEFAULT ${this.scopedFunctions.renderExpr(delta.new.default, env.withSubPath('default'))};`);
@@ -206,6 +204,26 @@ class DeltaRendererPostgres extends DeltaRenderer {
206
204
 
207
205
  return sqls;
208
206
  }
207
+
208
+ /**
209
+ * Postgres does not support changing a column AND doing [NOT] NULL things in one statement.
210
+ * So we filter it from the SQL String and render the appropriate SET/DROP for the NOT NULL separately.
211
+ *
212
+ * @param {object} delta
213
+ * @param {string} string
214
+ * @returns {string}
215
+ */
216
+ #removeNullabilityFromElementString(delta, string) {
217
+ if (delta.new.notNull === true || delta.new.key === true)
218
+ string = string.replace(' NOT NULL', ''); // TODO: Is this robust enough?
219
+ else if (delta.new.notNull === false || delta.new.$notNull === false)
220
+ string = string.replace(' NULL', ''); // TODO: Is this robust enough?
221
+ else if (delta.new.notNull === delta.old.notNull)
222
+ string = string.replace( delta.new.notNull ? ' NOT NULL' : ' NULL', ''); // TODO: Is this robust enough?
223
+
224
+
225
+ return string;
226
+ }
209
227
  }
210
228
 
211
229
  class DeltaRendererH2 extends DeltaRenderer {
@@ -596,7 +596,8 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
596
596
  while (stack.length > 0) {
597
597
  const [ current, currentRef, currentAlias ] = stack.pop();
598
598
  if (csnUtils.isStructured(current)) {
599
- const elements = Object.entries(current.elements || csnUtils.effectiveType(current).elements).reverse();
599
+ // `cds.Map` may also be used
600
+ const elements = Object.entries(current.elements || csnUtils.effectiveType(current).elements || {}).reverse();
600
601
  for (const [ name, elem ] of elements)
601
602
  stack.push([ elem, currentRef.concat(name), currentAlias.concat(name) ]);
602
603
  }
@@ -660,11 +660,12 @@ function rewriteCalculatedElementsInViews( csn, options, csnUtils, pathDelimiter
660
660
 
661
661
  /**
662
662
  * @param {CSN.Model} csn
663
+ * @param {CSN.Options} options
663
664
  */
664
- function processCalculatedElementsInEntities( csn ) {
665
+ function processCalculatedElementsInEntities( csn, options ) {
665
666
  forEachDefinition(csn, (artifact, definitionName) => {
666
667
  if (artifact.kind === 'entity' && !(artifact.query || artifact.projection))
667
- removeDummyValueInEntity(artifact, [ 'definitions', definitionName ]);
668
+ removeDummyValueInEntity(artifact, [ 'definitions', definitionName ], options);
668
669
  });
669
670
  }
670
671
 
@@ -674,14 +675,19 @@ function processCalculatedElementsInEntities( csn ) {
674
675
  *
675
676
  * @param {CSN.Artifact} artifact
676
677
  * @param {CSN.Path} path
678
+ * @param {CSN.Options} options
677
679
  * @todo calculated elements that "live" on the database?
678
680
  * @todo error when artifact is empty afterwards? Probably better as a CSN check!
679
681
  */
680
- function removeDummyValueInEntity( artifact, path ) {
682
+ function removeDummyValueInEntity( artifact, path, options ) {
681
683
  applyTransformationsOnDictionary(artifact.elements, {
682
684
  value: (parent, prop, value, p, elements) => {
683
- if (!value.stored)
684
- delete elements[p[p.length - 1]];
685
+ if (!value.stored) {
686
+ if (options.transformation === 'effective' && parent.on)
687
+ delete parent.value;
688
+ else
689
+ delete elements[p.at(-1)];
690
+ }
685
691
  },
686
692
  }, {}, path.concat( 'elements' ));
687
693
  }
@@ -409,6 +409,7 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
409
409
  }
410
410
 
411
411
  /**
412
+ *
412
413
  * Translate an `EXISTS <unmanaged assoc>` into a part of a WHERE condition.
413
414
  *
414
415
  * A valid $self-backlink is handled in translateDollarSelfToWhere.
@@ -428,64 +429,89 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
428
429
  */
429
430
  function translateUnmanagedAssocToWhere( root, target, isPrefixedWithTableAlias, base, current ) {
430
431
  const whereExtension = [];
431
- for (let j = 0; j < root.on.length; j++) {
432
- const part = root.on[j];
432
+
433
+ for (let j = 0; j < root.on.length; j++)
434
+ j = processExpressionPart(root.on, root.$path.concat('on'), j, whereExtension);
435
+
436
+ return whereExtension;
437
+
438
+ /**
439
+ * Process the given expression and apply the steps described above.
440
+ *
441
+ * @param {Array} expression Expression we are processing
442
+ * @param {CSN.Path} path Path to the expression
443
+ * @param {number} expressionIndex Index in the current expression, imporant for paths and stuff
444
+ * @param {Array} collector Array to collect the processed expressionparts into
445
+ * @returns {number} How far along expression we have processed - so the main loop can jump ahead
446
+ */
447
+ function processExpressionPart(expression, path, expressionIndex, collector) {
448
+ const part = expression[expressionIndex];
449
+
450
+ if (part?.xpr) {
451
+ const xpr = { xpr: [] };
452
+ for (let i = 0; i < part.xpr.length; i++)
453
+ i = processExpressionPart(part.xpr, path.concat(expressionIndex, 'xpr'), i, xpr.xpr);
454
+
455
+ collector.push(xpr);
456
+ return expressionIndex;
457
+ }
433
458
 
434
459
  // we can only resolve stuff on refs - skip literals like =
435
460
  // but also keep along stuff like null and undefined, so compiler
436
461
  // can have a chance to complain/ we can fail later nicely maybe
437
462
  if (!(part && part.ref)) {
438
- whereExtension.push(part);
439
- continue;
463
+ collector.push(part);
464
+ return expressionIndex;
440
465
  }
441
466
 
442
467
  // root.$path should be safe - we can only reference things in exists that exist when we enrich
443
468
  // so all of them should have a $path.
444
- const { art, links } = inspectRef(root.$path.concat([ 'on', j ]));
469
+ const { art, links } = inspectRef(path.concat(expressionIndex));
445
470
  // Dollar Self Backlink
446
- if (isValidDollarSelf(root.on[j], root.$path.concat([ 'on', j ]), root.on[j + 1], root.on[j + 2], root.$path.concat([ 'on', j + 2 ]))) {
447
- if (root.on[j].ref[0] === '$self' && root.on[j].ref.length === 1)
448
- whereExtension.push(...translateDollarSelfToWhere(base, target, root.on[j + 2], root.$path.concat([ 'on', j + 2 ])));
471
+ if (isValidDollarSelf(expression[expressionIndex], path.concat(expressionIndex), expression[expressionIndex + 1], expression[expressionIndex + 2], path.concat(expressionIndex + 2 ))) {
472
+ if (expression[expressionIndex].ref[0] === '$self' && expression[expressionIndex].ref.length === 1)
473
+ collector.push(...translateDollarSelfToWhere(base, target, expression[expressionIndex + 2], path.concat(expressionIndex + 2 )));
449
474
  else
450
- whereExtension.push(...translateDollarSelfToWhere(base, target, root.on[j], root.$path.concat([ 'on', j ])));
475
+ collector.push(...translateDollarSelfToWhere(base, target, expression[expressionIndex], path.concat(expressionIndex)));
451
476
 
452
- j += 2;
477
+ return expressionIndex + 2;
453
478
  }
454
479
  else if (links && links[0].art === root) { // target side
455
- whereExtension.push({ ref: [ target, ...part.ref.slice(1) ] });
480
+ collector.push({ ref: [ target, ...part.ref.slice(1) ] });
456
481
  }
457
482
  else if (part.$scope === '$self') { // source side - "absolute" scope
458
483
  const column = part._art._column;
459
484
  if (column && column.as) { // Replace with the "original" expression (the .ref, .xpr etc.)
460
- whereExtension.push(translateToSourceSide(column));
485
+ collector.push(translateToSourceSide(column));
461
486
  }
462
487
  else {
463
- whereExtension.push(assignAndDeleteAs({}, part, { ref: [ base, ...part.ref.slice(1) ] }));
488
+ collector.push(assignAndDeleteAsAndKey({}, part, { ref: [ base, ...part.ref.slice(1) ] }));
464
489
  }
465
490
  }
466
491
  else if (art) { // source side - with local scope
467
492
  if (isPrefixedWithTableAlias || part.$scope === 'alias')
468
- whereExtension.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
493
+ collector.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
469
494
  else
470
- whereExtension.push({ ref: [ base, ...current.ref.slice(0, -1), ...part.ref ] });
495
+ collector.push({ ref: [ base, ...current.ref.slice(0, -1), ...part.ref ] });
471
496
  }
472
497
  else { // operator - or any other leftover
473
- whereExtension.push(part);
498
+ collector.push(part);
474
499
  }
475
- }
476
500
 
477
- return whereExtension;
501
+ return expressionIndex;
502
+ }
478
503
 
479
504
 
480
505
  /**
481
- * Run Object.assign on all of the passed in parameters and delete a .as at the end
506
+ * Run Object.assign on all of the passed in parameters and delete a .as and .key at the end
482
507
  *
483
508
  * @param {...any} args
484
- * @returns {object} The merged args without an .as property
509
+ * @returns {object} The merged args without an .as and .key property
485
510
  */
486
- function assignAndDeleteAs( ...args ) {
511
+ function assignAndDeleteAsAndKey( ...args ) {
487
512
  const obj = Object.assign.apply(null, args);
488
513
  delete obj.as;
514
+ delete obj.key;
489
515
  return obj;
490
516
  }
491
517
  /**
@@ -503,19 +529,19 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
503
529
  const column = obj._art._column;
504
530
  if (column && column.as)
505
531
  return translateToSourceSide(column);
506
- return assignAndDeleteAs({}, obj, { ref: [ base, ...obj.ref.slice(1) ] });
532
+ return assignAndDeleteAsAndKey({}, obj, { ref: [ base, ...obj.ref.slice(1) ] });
507
533
  }
508
534
  else if (typeof obj.$env === 'string') {
509
- return assignAndDeleteAs({}, obj, { ref: [ obj.$env, ...obj.ref ] });
535
+ return assignAndDeleteAsAndKey({}, obj, { ref: [ obj.$env, ...obj.ref ] });
510
536
  }
511
537
 
512
- return assignAndDeleteAs({}, obj, { ref: [ ...obj.ref ] });
538
+ return assignAndDeleteAsAndKey({}, obj, { ref: [ ...obj.ref ] });
513
539
  }
514
540
  else if (obj.xpr) { // we need to drill further down into .xpr
515
- return assignAndDeleteAs({}, obj, { xpr: obj.xpr.map(translateToSourceSide) });
541
+ return assignAndDeleteAsAndKey({}, obj, { xpr: obj.xpr.map(translateToSourceSide) });
516
542
  }
517
543
  else if (obj.args) {
518
- return assignAndDeleteAs({}, obj, { args: obj.args.map(translateToSourceSide) });
544
+ return assignAndDeleteAsAndKey({}, obj, { args: obj.args.map(translateToSourceSide) });
519
545
  }
520
546
 
521
547
  return obj;
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { CompilerAssertion } = require('../../base/error');
4
+ const { forEach } = require('../../utils/objectUtils');
4
5
 
5
6
  const directMappings = {
6
7
  '@Common.IsDayOfCalendarMonth': replace('@Semantics.calendar.dayOfMonth'),
@@ -190,6 +191,152 @@ function remapODataAnnotations( csn ) {
190
191
  };
191
192
  }
192
193
 
194
+ /**
195
+ * Do the .texts anno magic if we can be reasonably sure that we are actually dealing with a .texts entity.
196
+ *
197
+ * @param {CSN.Model} csn
198
+ * @param {string} artifactName
199
+ * @param {CSN.Artifact} artifact
200
+ */
201
+ function sealAnnoMagicForTexts(csn, artifactName, artifact) {
202
+ if (artifactName.endsWith('.texts') && artifact.elements?.locale) {
203
+ const firstNonKey = getFirstNonKeyElement(artifact);
204
+ if (firstNonKey && firstNonKey.type === 'cds.String') {
205
+ artifact['@ObjectModel.supportedCapabilities'] ??= [];
206
+ if (!artifact['@ObjectModel.supportedCapabilities'].find(part => part['#'] === 'LANGUAGE_DEPENDENT_TEXT'))
207
+ artifact['@ObjectModel.supportedCapabilities'].push({ '#': 'LANGUAGE_DEPENDENT_TEXT' });
208
+ if (artifact.elements.locale['@Semantics.language'] === undefined)
209
+ artifact.elements.locale['@Semantics.language'] = true;
210
+ if (firstNonKey['@Semantics.text'] === undefined)
211
+ firstNonKey['@Semantics.text'] = true;
212
+ }
213
+ }
214
+ }
215
+
216
+ /**
217
+ *
218
+ * @param {CSN.Artifact} artifact
219
+ * @returns {CSN.Element|null}
220
+ */
221
+ function getFirstNonKeyElement(artifact) {
222
+ for (const elementName in artifact.elements) {
223
+ if (Object.prototype.hasOwnProperty.call(artifact.elements, elementName)) {
224
+ if (!artifact.elements[elementName].key)
225
+ return artifact.elements[elementName];
226
+ }
227
+ }
228
+
229
+ return null;
230
+ }
231
+
232
+ /**
233
+ *
234
+ * @param {CSN.Model} csn
235
+ * @returns {object} Transfomer object for applyTransformations
236
+ */
237
+ function sealAnnoMagic(csn) {
238
+ return {
239
+ '@ObjectModel.supportedCapabilities': (parent, prop, anno, path) => {
240
+ // Filter only for values we care about
241
+ const filteredAnno = anno.filter(value => value['#'] === 'ANALYTICAL_DIMENSION' || value['#'] === 'LANGUAGE_DEPENDENT_TEXT' || value['#'] === 'ANALYTICAL_PROVIDER');
242
+ if (filteredAnno.filter(value => value['#'] === 'ANALYTICAL_PROVIDER').length > 0 && parent.kind === 'entity' && isPartOfINAService(csn, path[1]) && parent.elements) {
243
+ forEach(parent.elements, (elementName, element) => {
244
+ if (element.target && csn.definitions[element.target]['@ObjectModel.supportedCapabilities']?.filter(value => value['#'] === 'ANALYTICAL_DIMENSION').length > 0) {
245
+ const tuples = getOnConditionAsComparisonTuples(element.on, elementName);
246
+ const targetEntity = csn.definitions[element.target];
247
+ if (element.on.length === 3 && tuples.length > 0 ) {
248
+ tuples.forEach(({ source }) => {
249
+ const sourceElement = parent.elements[source.ref[0]];
250
+ if (!sourceElement.target && sourceElement['@ObjectModel.foreignKey.association'] === undefined)
251
+ sourceElement['@ObjectModel.foreignKey.association'] = { '=': elementName };
252
+ });
253
+ }
254
+
255
+ else if (element.on.length > 3 && tuples.length > 0 && targetEntity['@ObjectModel.representativeKey']) {
256
+ tuples.forEach(({ source, target }) => {
257
+ if (target.ref[1] === targetEntity['@ObjectModel.representativeKey']['=']) {
258
+ const sourceElement = parent.elements[source.ref[0]];
259
+ if (!sourceElement.target && sourceElement['@ObjectModel.foreignKey.association'] === undefined)
260
+ sourceElement['@ObjectModel.foreignKey.association'] = { '=': elementName };
261
+ }
262
+ });
263
+ }
264
+ }
265
+ });
266
+ }
267
+
268
+ if (filteredAnno.filter(value => value['#'] === 'ANALYTICAL_DIMENSION').length > 0 && parent.kind === 'entity' && parent.elements) {
269
+ forEach(parent.elements, (_elementName, element) => {
270
+ if (element['@ObjectModel.text.element'] && parent.elements[element['@ObjectModel.text.element']['=']] && parent.elements[element['@ObjectModel.text.element']['=']]['@Semantics.text'] === undefined)
271
+ parent.elements[element['@ObjectModel.text.element']['=']]['@Semantics.text'] = true;
272
+ });
273
+ }
274
+
275
+ if (filteredAnno.length === 1 && parent.kind && parent['@ObjectModel.modelingPattern'] === undefined) {
276
+ if (filteredAnno[0]['#'] === 'ANALYTICAL_PROVIDER')
277
+ parent['@ObjectModel.modelingPattern'] = { '#': 'ANALYTICAL_CUBE' };
278
+ else
279
+ parent['@ObjectModel.modelingPattern'] = { '#': filteredAnno[0]['#'] };
280
+ }
281
+ },
282
+ };
283
+ }
284
+
285
+ function isPartOfINAService(csn, artifactName) {
286
+ const parts = artifactName.split('.');
287
+ if (parts.length === 1)
288
+ return false; // No dots
289
+ for (let i = 0; i < parts.length; i++) {
290
+ const possibleServiceName = parts.slice(0, i).join('.');
291
+ const possibleDefinition = csn.definitions[possibleServiceName];
292
+ if (possibleDefinition?.kind === 'service')
293
+ return possibleDefinition['@protocol'] === 'ina';
294
+ }
295
+
296
+ return false;
297
+ }
298
+
299
+ /**
300
+ * Split the given on-condition into bite-sized tuples IF
301
+ * - the operator is a =
302
+ * - one of the arguments is of the form <assoc>.<field>
303
+ * - one of the arguments is of the form <field>
304
+ * - there are no braces
305
+ * - each of the comparison tuples is "joined" via "and"
306
+ *
307
+ * Return an empty array if we encounter any tuples/things that do NOT match those criteria
308
+ * @param {CSN.OnCondition} on
309
+ * @param {string} assocName
310
+ * @returns {object[]}
311
+ */
312
+ function getOnConditionAsComparisonTuples(on, assocName) {
313
+ const validTuples = [];
314
+ for (let i = 0; i < on.length - 2; i += 4) {
315
+ let isValid = false;
316
+ const arg1 = on[i];
317
+ const operator = on[i + 1];
318
+ const arg2 = on[i + 2];
319
+ const possibleAnd = i + 3 < on.length ? on[i + 3] : 'and';
320
+ if (possibleAnd === 'and' && operator === '=' && (arg1.ref?.length === 1 && arg2.ref?.length === 2 && arg2.ref[0] === assocName || arg1.ref?.length === 2 && arg1.ref[0] === assocName && arg2.ref?.length === 1 )) { // TODO: Do we care about filters? Filters could cause a crash here?
321
+ if (arg1.ref.length === 1) { // arg1 needs to point to be <field>, arg2 needs to be <assoc>.<field>
322
+ validTuples.push({ source: arg1, target: arg2 });
323
+ isValid = true;
324
+ }
325
+ else { // arg1 needs to point to be <assoc>.<field>, arg2 needs to be <field>
326
+ validTuples.push({ source: arg2, target: arg1 });
327
+ isValid = true;
328
+ }
329
+ }
330
+
331
+ if (!isValid)
332
+ return [];
333
+ }
334
+ return validTuples;
335
+ }
336
+
337
+
193
338
  module.exports = {
194
339
  remapODataAnnotations,
340
+ sealAnnoMagic,
341
+ sealAnnoMagicForTexts,
195
342
  };
@@ -75,15 +75,29 @@ function effectiveCsn( model, options, messageFunctions ) {
75
75
  // ensure getElement works on flattened struct_assoc columns
76
76
  csnUtils = getUtils(csn, 'init-all');
77
77
 
78
- processCalculatedElementsInEntities(csn);
78
+ processCalculatedElementsInEntities(csn, options);
79
79
  associations.managedToUnmanaged(csn, options, csnUtils, messageFunctions);
80
80
  associations.transformBacklinks(csn, options, csnUtils, messageFunctions);
81
- const transformers = mergeTransformers([ options.addCdsPersistenceName ? misc.attachPersistenceName(csn, options, csnUtils) : {}, options.remapOdataAnnotations ? annotations.remapODataAnnotations(csn) : {}, misc.removeDefinitionsAndProperties(csn, options) ], null);
82
- applyTransformations(csn, transformers, [], { skipIgnore: false, processAnnotations: true });
81
+ const transformers = mergeTransformers([
82
+ options.addCdsPersistenceName ? misc.attachPersistenceName(csn, options, csnUtils) : {},
83
+ options.remapOdataAnnotations ? annotations.remapODataAnnotations(csn) : {},
84
+ misc.removeDefinitionsAndProperties(csn, options),
85
+ options.deriveAnalyticalAnnotations ? annotations.sealAnnoMagic(csn) : {},
86
+ ], null);
87
+
88
+ const artifactTransformers = [];
89
+ if (options.deriveAnalyticalAnnotations)
90
+ artifactTransformers.push(annotations.sealAnnoMagicForTexts);
91
+
92
+ applyTransformations(csn, transformers, artifactTransformers, { skipIgnore: false, processAnnotations: true });
83
93
 
84
94
  if (!options.resolveProjections)
85
95
  redoProjections.forEach(fn => fn());
86
96
 
97
+
98
+ // Remove unapplied extensions/annotations
99
+ delete csn.extensions;
100
+
87
101
  messageFunctions.throwWithError();
88
102
 
89
103
  return csn;