@sap/cds-compiler 5.3.2 → 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 (49) hide show
  1. package/CHANGELOG.md +23 -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/transformExists.js +10 -9
  41. package/lib/transform/effective/annotations.js +147 -0
  42. package/lib/transform/effective/main.js +16 -2
  43. package/lib/transform/forOdata.js +53 -10
  44. package/lib/transform/forRelationalDB.js +7 -0
  45. package/lib/transform/odata/createForeignKeys.js +180 -0
  46. package/lib/transform/odata/flattening.js +135 -19
  47. package/lib/transform/odata/typesExposure.js +4 -3
  48. package/lib/transform/transformUtils.js +6 -6
  49. 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
  }
@@ -485,7 +485,7 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
485
485
  collector.push(translateToSourceSide(column));
486
486
  }
487
487
  else {
488
- collector.push(assignAndDeleteAs({}, part, { ref: [ base, ...part.ref.slice(1) ] }));
488
+ collector.push(assignAndDeleteAsAndKey({}, part, { ref: [ base, ...part.ref.slice(1) ] }));
489
489
  }
490
490
  }
491
491
  else if (art) { // source side - with local scope
@@ -503,14 +503,15 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
503
503
 
504
504
 
505
505
  /**
506
- * 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
507
507
  *
508
508
  * @param {...any} args
509
- * @returns {object} The merged args without an .as property
509
+ * @returns {object} The merged args without an .as and .key property
510
510
  */
511
- function assignAndDeleteAs( ...args ) {
511
+ function assignAndDeleteAsAndKey( ...args ) {
512
512
  const obj = Object.assign.apply(null, args);
513
513
  delete obj.as;
514
+ delete obj.key;
514
515
  return obj;
515
516
  }
516
517
  /**
@@ -528,19 +529,19 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
528
529
  const column = obj._art._column;
529
530
  if (column && column.as)
530
531
  return translateToSourceSide(column);
531
- return assignAndDeleteAs({}, obj, { ref: [ base, ...obj.ref.slice(1) ] });
532
+ return assignAndDeleteAsAndKey({}, obj, { ref: [ base, ...obj.ref.slice(1) ] });
532
533
  }
533
534
  else if (typeof obj.$env === 'string') {
534
- return assignAndDeleteAs({}, obj, { ref: [ obj.$env, ...obj.ref ] });
535
+ return assignAndDeleteAsAndKey({}, obj, { ref: [ obj.$env, ...obj.ref ] });
535
536
  }
536
537
 
537
- return assignAndDeleteAs({}, obj, { ref: [ ...obj.ref ] });
538
+ return assignAndDeleteAsAndKey({}, obj, { ref: [ ...obj.ref ] });
538
539
  }
539
540
  else if (obj.xpr) { // we need to drill further down into .xpr
540
- return assignAndDeleteAs({}, obj, { xpr: obj.xpr.map(translateToSourceSide) });
541
+ return assignAndDeleteAsAndKey({}, obj, { xpr: obj.xpr.map(translateToSourceSide) });
541
542
  }
542
543
  else if (obj.args) {
543
- return assignAndDeleteAs({}, obj, { args: obj.args.map(translateToSourceSide) });
544
+ return assignAndDeleteAsAndKey({}, obj, { args: obj.args.map(translateToSourceSide) });
544
545
  }
545
546
 
546
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
  };
@@ -78,12 +78,26 @@ function effectiveCsn( model, options, messageFunctions ) {
78
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;
@@ -10,6 +10,8 @@ const { forEachDefinition,
10
10
  isAspect,
11
11
  getServiceNames,
12
12
  forEachGeneric,
13
+ cardinality2str,
14
+ getUtils
13
15
  } = require('../model/csnUtils');
14
16
  const { checkCSNVersion } = require('../json/csnVersion');
15
17
  const validate = require('../checks/validator');
@@ -18,6 +20,7 @@ const expandToFinalBaseType = require('./odata/toFinalBaseType');
18
20
  const { timetrace } = require('../utils/timetrace');
19
21
  const enrichUniversalCsn = require('./universalCsn/universalCsnEnricher');
20
22
  const flattening = require('./odata/flattening');
23
+ const createForeignKeyElements = require('./odata/createForeignKeys');
21
24
  const associations = require('./db/associations')
22
25
  const expansion = require('./db/expansion');
23
26
  const generateDrafts = require('./draft/odata');
@@ -181,24 +184,31 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
181
184
  // If errors are detected, throwWithAnyError() will return from further processing
182
185
  expandStructsInExpression(csn, { skipArtifact: isExternalServiceMember, drillRef: true });
183
186
 
187
+ // do expansion before Fk creation because of messages reporting
184
188
  if (!structuredOData) {
185
189
  expansion.expandStructureReferences(csn, options, '_',
186
190
  { error, info, throwWithAnyError }, csnUtils,
187
191
  { skipArtifact: isExternalServiceMember });
188
- const resolved = new WeakMap();
192
+ }
193
+
194
+ createForeignKeyElements(csn, options, messageFunctions, csnUtils, { skipArtifact: isExternalServiceMember });
189
195
 
196
+ if (!structuredOData) {
197
+ const resolved = new WeakMap();
190
198
  const { inspectRef, effectiveType } = csnRefs(csn);
199
+ const { getFinalTypeInfo } = getUtils(csn);
191
200
  const { adaptRefs, transformer: refFlattener } =
192
201
  flattening.getStructRefFlatteningTransformer(csn, inspectRef, effectiveType, options, resolved, '_');
193
202
 
194
- flattening.allInOneFlattening(csn, refFlattener, adaptRefs,
195
- inspectRef, isExternalServiceMember, error, csnUtils, options);
203
+ const allMgdAssocDefs = flattening.allInOneFlattening(csn, refFlattener, adaptRefs,
204
+ inspectRef, getFinalTypeInfo, isExternalServiceMember, error, csnUtils, options);
196
205
  flattening.flattenAllStructStepsInRefs(csn, refFlattener, adaptRefs,
197
206
  inspectRef, effectiveType, csnUtils, error, options,
198
207
  { //skip: ['action', 'aspect', 'event', 'function', 'type'],
199
208
  skipArtifact: isExternalServiceMember,
200
209
  });
201
-
210
+ flattening.replaceManagedAssocsAsKeys(allMgdAssocDefs, csnUtils);
211
+
202
212
  // replace structured with flat dictionaries that contain
203
213
  // rewritten path expressions
204
214
  forEachDefinition(csn, (def) => {
@@ -223,16 +233,14 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
223
233
  });
224
234
  }
225
235
 
226
- // TODO: add the generated foreign keys to the columns when we are in a view
227
- // see db/views.js::addForeignKeysToColumns
228
- flattening.handleManagedAssociationsAndCreateForeignKeys(csn, options, messageFunctions, '_',
229
- !structuredOData, csnUtils,{ skipArtifact: isExternalServiceMember });
236
+ bindCsnReferenceOnly();
230
237
 
231
238
  // Allow using managed associations as steps in on-conditions to access their fks
232
239
  // To be done after handleManagedAssociationsAndCreateForeignKeys,
233
240
  // since then the foreign keys of the managed assocs are part of the elements
234
- if(!structuredOData)
241
+ if(!structuredOData) {
235
242
  forEachDefinition(csn, associations.getFKAccessFinalizer(csn, csnUtils, '_'));
243
+ }
236
244
 
237
245
  // structure flattener reports errors, further processing is not safe -> throw exception in case of errors
238
246
  throwWithAnyError();
@@ -277,7 +285,8 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
277
285
  !(propertyName === 'enum' || propertyName === 'returns') &&
278
286
  (!member.virtual || def.query)) {
279
287
  // If we have a 'preserved dotted name' (i.e. we are a result of flattening), use that for the @cds.persistence.name annotation
280
- member['@cds.persistence.name'] = getElementDatabaseNameOf(member.$defPath?.slice(1).join('.') || memberName, options.sqlMapping, 'hana'); // hana to allow "hdbcds"
288
+ member['@cds.persistence.name'] = getElementDatabaseNameOf((!member['@odata.foreignKey4'] && member.$defPath?.slice(1).join('.'))
289
+ || memberName, options.sqlMapping, 'hana'); // hana to allow "hdbcds"
281
290
  }
282
291
 
283
292
  // Mark fields with @odata.on.insert/update as @Core.Computed
@@ -286,6 +295,12 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
286
295
  // Resolve annotation shorthands for elements, actions, action parameters
287
296
  renameShorthandAnnotations(member);
288
297
 
298
+ // If an association was modelled as not null, like so:
299
+ // <associationName>: Association to <target> not null;
300
+ // a cardinality property is set to the association member
301
+ // with the value { "min": 1 };
302
+ setCardinalityToNotNullAssociations(member);
303
+
289
304
  // - If the association target is annotated with @cds.odata.valuelist, annotate the
290
305
  // association with @Common.ValueList.viaAssociation
291
306
  // - Check for @Analytics.Measure and @Aggregation.default
@@ -419,6 +434,27 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
419
434
  }
420
435
  }
421
436
 
437
+ // If an association was modelled as not null, like so:
438
+ // <associationName>: Association to <target> not null;
439
+ // a cardinality property is set to the association member
440
+ // with the value { "min": 1 };
441
+ function setCardinalityToNotNullAssociations(member) {
442
+ if (member.target && member.keys && !member.on) {
443
+ if (member.notNull) {
444
+ if (member.cardinality === undefined)
445
+ member.cardinality = {};
446
+ // min=0 is falsy => check for undefined
447
+ if (member.cardinality.min === undefined) {
448
+ member.cardinality.min = 1;
449
+ }
450
+ else if (member.cardinality.min === 0) {
451
+ warning(null, member.$path, { value: cardinality2str(member, false), code: 'not null' },
452
+ 'Expected target cardinality $(VALUE) and $(CODE) to match');
453
+ }
454
+ }
455
+ }
456
+ }
457
+
422
458
  // Apply default type facets to each type definition and every member
423
459
  // But do not apply default string length (as in DB)
424
460
  function setDefaultTypeFacets(def) {
@@ -466,4 +502,11 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
466
502
  }
467
503
  }
468
504
 
505
+ function bindCsnReferenceOnly() {
506
+ // invalidate caches for CSN ref API
507
+ const csnRefApi = csnRefs(csn);
508
+ Object.assign(csnUtils, csnRefApi);
509
+ }
510
+
511
+
469
512
  } // transform4odataWithCsn
@@ -856,6 +856,13 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
856
856
  checkTypeParamValue(node, 'srid', { max: Number.MAX_SAFE_INTEGER }, path);
857
857
  break;
858
858
  }
859
+ case 'cds.Map': {
860
+ if (options.sqlDialect === 'plain')
861
+ error('ref-unsupported-type', path, { '#': 'dialect', type: node.type, value: 'plain' });
862
+ else if (options.transformation === 'hdbcds')
863
+ error('ref-unsupported-type', path, {'#': 'hdbcds', type: node.type, value: options.sqlDialect });
864
+ break;
865
+ }
859
866
  case 'cds.Vector': {
860
867
  if (options.sqlDialect !== 'hana') {
861
868
  error('ref-unsupported-type', path, {