@sap/cds-compiler 4.9.2 → 5.0.6

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 (88) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/bin/cds_remove_invalid_whitespace.js +2 -1
  3. package/bin/cdsc.js +15 -11
  4. package/bin/cdshi.js +1 -0
  5. package/doc/CHANGELOG_BETA.md +7 -0
  6. package/lib/api/main.js +7 -19
  7. package/lib/api/options.js +5 -11
  8. package/lib/api/trace.js +0 -1
  9. package/lib/base/builtins.js +1 -0
  10. package/lib/base/location.js +4 -1
  11. package/lib/base/message-registry.js +29 -29
  12. package/lib/base/messages.js +22 -26
  13. package/lib/base/model.js +0 -2
  14. package/lib/base/node-helpers.js +0 -1
  15. package/lib/checks/enricher.js +1 -5
  16. package/lib/checks/structuredAnnoExpressions.js +30 -0
  17. package/lib/checks/validator.js +8 -0
  18. package/lib/compiler/assert-consistency.js +4 -1
  19. package/lib/compiler/base.js +1 -1
  20. package/lib/compiler/builtins.js +18 -2
  21. package/lib/compiler/checks.js +2 -5
  22. package/lib/compiler/define.js +7 -7
  23. package/lib/compiler/extend.js +68 -33
  24. package/lib/compiler/generate.js +1 -1
  25. package/lib/compiler/index.js +23 -6
  26. package/lib/compiler/lsp-api.js +501 -2
  27. package/lib/compiler/populate.js +2 -2
  28. package/lib/compiler/propagator.js +1 -4
  29. package/lib/compiler/resolve.js +2 -15
  30. package/lib/compiler/shared.js +112 -31
  31. package/lib/compiler/tweak-assocs.js +2 -16
  32. package/lib/compiler/utils.js +2 -1
  33. package/lib/compiler/xsn-model.js +4 -0
  34. package/lib/edm/annotations/genericTranslation.js +95 -42
  35. package/lib/edm/csn2edm.js +16 -4
  36. package/lib/edm/edm.js +2 -3
  37. package/lib/edm/edmAnnoPreprocessor.js +1 -2
  38. package/lib/edm/edmPreprocessor.js +1 -7
  39. package/lib/gen/Dictionary.json +29 -2
  40. package/lib/gen/language.checksum +1 -1
  41. package/lib/gen/language.interp +2 -1
  42. package/lib/gen/languageParser.js +4995 -4817
  43. package/lib/json/csnVersion.js +1 -1
  44. package/lib/json/from-csn.js +4 -7
  45. package/lib/json/to-csn.js +23 -12
  46. package/lib/language/antlrParser.js +2 -2
  47. package/lib/language/errorStrategy.js +0 -1
  48. package/lib/language/genericAntlrParser.js +35 -12
  49. package/lib/language/multiLineStringParser.js +3 -2
  50. package/lib/language/textUtils.js +1 -0
  51. package/lib/main.d.ts +28 -9
  52. package/lib/main.js +7 -4
  53. package/lib/model/csnRefs.js +20 -4
  54. package/lib/model/csnUtils.js +0 -2
  55. package/lib/model/revealInternalProperties.js +1 -1
  56. package/lib/modelCompare/compare.js +1 -1
  57. package/lib/optionProcessor.js +28 -9
  58. package/lib/render/manageConstraints.js +1 -1
  59. package/lib/render/toCdl.js +36 -7
  60. package/lib/render/toSql.js +1 -0
  61. package/lib/render/utils/common.js +12 -9
  62. package/lib/render/utils/stringEscapes.js +1 -0
  63. package/lib/transform/db/applyTransformations.js +13 -8
  64. package/lib/transform/db/associations.js +62 -54
  65. package/lib/transform/db/constraints.js +23 -25
  66. package/lib/transform/db/expansion.js +1 -6
  67. package/lib/transform/db/flattening.js +89 -111
  68. package/lib/transform/db/temporal.js +3 -4
  69. package/lib/transform/db/views.js +0 -1
  70. package/lib/transform/draft/odata.js +51 -3
  71. package/lib/transform/effective/annotations.js +3 -2
  72. package/lib/transform/effective/flattening.js +135 -0
  73. package/lib/transform/effective/main.js +6 -6
  74. package/lib/transform/effective/types.js +13 -9
  75. package/lib/transform/forOdata.js +0 -2
  76. package/lib/transform/forRelationalDB.js +0 -19
  77. package/lib/transform/localized.js +7 -8
  78. package/lib/transform/odata/flattening.js +39 -31
  79. package/lib/transform/odata/typesExposure.js +5 -17
  80. package/lib/transform/transformUtils.js +1 -1
  81. package/lib/transform/translateAssocsToJoins.js +21 -3
  82. package/lib/utils/file.js +13 -7
  83. package/lib/utils/moduleResolve.js +59 -8
  84. package/lib/utils/term.js +3 -2
  85. package/package.json +7 -3
  86. package/share/messages/message-explanations.json +2 -0
  87. package/share/messages/type-unexpected-foreign-keys.md +52 -0
  88. package/share/messages/type-unexpected-on-condition.md +52 -0
@@ -1379,7 +1379,7 @@ function csnToCdl( csn, options, msg ) {
1379
1379
 
1380
1380
  // Foreign keys (if any, unless we also have an ON_condition (which means we have been transformed from managed to unmanaged)
1381
1381
  if (artifact.keys && !artifact.on)
1382
- result += ` { ${Object.keys(artifact.keys).map(name => renderForeignKey(artifact.keys[name], env.withSubPath([ 'keys', name ]))).join(', ')} }`;
1382
+ result += ` ${ renderForeignKeys(artifact, env) }`;
1383
1383
 
1384
1384
  if (artifact.notNull !== undefined && !artifact.on) // unmanaged associations can't be followed by "not null"
1385
1385
  result += renderNullability(artifact);
@@ -1435,7 +1435,7 @@ function csnToCdl( csn, options, msg ) {
1435
1435
  if (art.on)
1436
1436
  result += ` on ${exprRenderer.renderExpr(art.on, env.withSubPath([ 'on' ]))}`;
1437
1437
  else if (art.keys)
1438
- result += ` { ${Object.keys(art.keys).map(name => renderForeignKey(art.keys[name], env.withSubPath([ 'keys', name ]))).join(', ')} }`;
1438
+ result += ` ${ renderForeignKeys(art, env) }`;
1439
1439
  return result;
1440
1440
  }
1441
1441
 
@@ -1813,15 +1813,43 @@ function csnToCdl( csn, options, msg ) {
1813
1813
  }
1814
1814
 
1815
1815
  /**
1816
- * Render a foreign key (no trailing LF)
1816
+ * Render foreign keys.
1817
1817
  *
1818
- * @param {object} fKey
1818
+ * @param {object} art
1819
1819
  * @param {CdlRenderEnvironment} env
1820
1820
  * @return {string}
1821
1821
  */
1822
- function renderForeignKey( fKey, env ) {
1823
- const alias = fKey.as ? renderAlias(fKey.as, env) : '';
1824
- return exprRenderer.renderExpr(fKey, env) + alias;
1822
+ function renderForeignKeys( art, env ) {
1823
+ const renderedKeys = [];
1824
+ let hasAnnotations = false;
1825
+ env = env.withSubPath([ 'keys', -1 ]);
1826
+ env.increaseIndent();
1827
+
1828
+ for (let i = 0; i < art.keys.length; ++i) {
1829
+ env.path[env.path.length - 1] = i;
1830
+ const fKey = art.keys[i];
1831
+
1832
+ const annos = renderAnnotationAssignmentsAndDocComment(fKey, env).trim();
1833
+ if (annos) {
1834
+ hasAnnotations = true;
1835
+ renderedKeys.push(annos);
1836
+ }
1837
+
1838
+ const alias = fKey.as ? renderAlias(fKey.as, env) : '';
1839
+ const key = exprRenderer.renderExpr(fKey, env);
1840
+ renderedKeys.push(`${key}${alias},`);
1841
+ }
1842
+
1843
+ if (hasAnnotations) {
1844
+ const sep = `\n${env.indent}`;
1845
+ env.decreaseIndent();
1846
+ return `{${sep}${renderedKeys.join(sep)}\n${env.indent}}`;
1847
+ }
1848
+
1849
+ let result = renderedKeys.join(' ');
1850
+ if (result[result.length - 1] === ',') // remove trailing comma
1851
+ result = result.slice(0, -1);
1852
+ return `{ ${ result } }`;
1825
1853
  }
1826
1854
 
1827
1855
  /**
@@ -2427,6 +2455,7 @@ function isSimpleString( str ) {
2427
2455
  // <https://www.ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf>
2428
2456
  // On top, because (invalid) surrogate pairs need to be handled, we check for them as well.
2429
2457
  // v3: Not a simple string if ' (\u0027) is in string.
2458
+ // eslint-disable-next-line no-control-regex
2430
2459
  return str === '' || (/^[^\u{0000}-\u{001F}\u2028\u2029]+$/u.test(str) &&
2431
2460
  !hasUnpairedUnicodeSurrogate(str));
2432
2461
  }
@@ -1645,6 +1645,7 @@ function renderStringForSql( str, sqlDialect ) {
1645
1645
  // > Single quotation marks are used to delimit string literals.
1646
1646
  // > A single quotation mark itself can be represented using two single quotation marks.
1647
1647
  str = str.replace(/'/g, '\'\'')
1648
+ // eslint-disable-next-line no-control-regex
1648
1649
  .replace(/\u{0}/ug, '\' || CHAR(0) || \'');
1649
1650
  }
1650
1651
  else {
@@ -372,7 +372,7 @@ const variablesToSql = {
372
372
  '$valid.to': "current_setting('cap.valid_to')::timestamp",
373
373
  $now: 'current_timestamp',
374
374
  },
375
- 'better-sqlite': {
375
+ sqlite: {
376
376
  '$user.id': "session_context( '$user.id' )",
377
377
  '$user.locale': "session_context( '$user.locale' )",
378
378
  $tenant: "session_context( '$tenant' )",
@@ -382,7 +382,7 @@ const variablesToSql = {
382
382
  '$valid.to': "session_context( '$valid.to' )",
383
383
  $now: 'CURRENT_TIMESTAMP',
384
384
  },
385
- sqlite: {
385
+ 'old-sqlite': {
386
386
  // For sqlite, we render the string-format-time (strftime) function.
387
387
  // Because the format of `current_timestamp` is like that: '2021-05-14 09:17:19' whereas
388
388
  // the format for timestamps (at least in Node.js) is like that: '2021-01-01T00:00:00.000Z'
@@ -394,11 +394,14 @@ const variablesToSql = {
394
394
  '$valid.to': "strftime('%Y-%m-%dT%H:%M:%S.001Z', 'now')",
395
395
  $now: 'CURRENT_TIMESTAMP',
396
396
  },
397
- plain: {
398
- '$at.from': 'current_timestamp',
399
- '$at.to': 'current_timestamp',
400
- '$valid.from': 'current_timestamp',
401
- '$valid.to': 'current_timestamp',
397
+ plain: { // better-sqlite defaults
398
+ '$user.id': "session_context( '$user.id' )",
399
+ '$user.locale': "session_context( '$user.locale' )",
400
+ $tenant: "session_context( '$tenant' )",
401
+ '$at.from': "session_context( '$valid.from' )",
402
+ '$at.to': "session_context( '$valid.to' )",
403
+ '$valid.from': "session_context( '$valid.from' )",
404
+ '$valid.to': "session_context( '$valid.to' )",
402
405
  $now: 'CURRENT_TIMESTAMP',
403
406
  },
404
407
  h2: {
@@ -423,8 +426,8 @@ const variablesToSql = {
423
426
  * @return {string|null} `null` if the variable could not be found for the given dialect and in the fallback values.
424
427
  */
425
428
  function variableForDialect( options, variable ) {
426
- const dialect = options.sqlDialect === 'sqlite' && options.betterSqliteSessionVariables
427
- ? 'better-sqlite'
429
+ const dialect = options.sqlDialect === 'sqlite' && options.betterSqliteSessionVariables === false
430
+ ? 'old-sqlite'
428
431
  : options.sqlDialect;
429
432
  return variablesToSql[dialect]?.[variable] || variablesToSql.fallback[variable] || null;
430
433
  }
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ // eslint-disable-next-line no-control-regex
3
4
  const controlCharacters = /[\u{0000}-\u{001F}]/u;
4
5
  const highSurrogate = /[\u{D800}-\u{DBFF}]/u;
5
6
  const lowSurrogate = /[\u{DC00}-\u{DFFF}]/u;
@@ -45,6 +45,7 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
45
45
  };
46
46
 
47
47
  const csnPath = [ ...path ];
48
+ const context = {};
48
49
  if (prop === 'definitions') {
49
50
  definitions( parent, 'definitions', parent.definitions );
50
51
  }
@@ -84,9 +85,9 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
84
85
  for (const name of Object.getOwnPropertyNames( node )) {
85
86
  const trans = transformers[name] || transformers[name.charAt(0)] || standard;
86
87
  if (customTransformers[name])
87
- customTransformers[name](node, name, node[name], csnPath, _parent, _prop);
88
+ customTransformers[name](node, name, node[name], csnPath, _parent, _prop, context);
88
89
  else if (options.processAnnotations && customTransformers['@'] && name.charAt(0) === '@')
89
- customTransformers['@'](node, name, node[name], csnPath, _parent, _prop);
90
+ customTransformers['@'](node, name, node[name], csnPath, _parent, _prop, context);
90
91
  trans( node, name, node[name], csnPath );
91
92
  }
92
93
  }
@@ -110,9 +111,9 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
110
111
  for (const name of Object.getOwnPropertyNames( node )) {
111
112
  const trans = transformers[name] || transformers[name.charAt(0)] || standard;
112
113
  if (customTransformers[name])
113
- customTransformers[name](node, name, node[name], csnPath, dict);
114
+ customTransformers[name](node, name, node[name], csnPath, dict, null, context);
114
115
  else if (options.processAnnotations && customTransformers['@'] && name.charAt(0) === '@')
115
- customTransformers['@'](node, name, node[name], csnPath, dict);
116
+ customTransformers['@'](node, name, node[name], csnPath, dict, null, context);
116
117
  trans( node, name, node[name], csnPath );
117
118
  }
118
119
  csnPath.pop();
@@ -130,12 +131,14 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
130
131
  if (options.skipDict?.[_prop] || dict == null) // with universal CSN, dictionaries might be null
131
132
  return;
132
133
  csnPath.push( _prop );
134
+ context[`$in_${_prop}`] = true;
133
135
  for (const name of Object.getOwnPropertyNames( dict ))
134
136
  dictEntry( dict, name, dict[name] );
135
137
 
136
138
  if (!Object.prototype.propertyIsEnumerable.call( node, _prop ))
137
139
  setProp(node, `$${_prop}`, dict);
138
140
  csnPath.pop();
141
+ context[`$in_${_prop}`] = undefined;
139
142
  }
140
143
 
141
144
  /**
@@ -148,6 +151,7 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
148
151
  */
149
152
  function annotation( _parent, _prop, node ) {
150
153
  if (options.processAnnotations) {
154
+ context.$annotation = { name: _prop, value: node };
151
155
  if (isAnnotationExpression(node)) {
152
156
  standard(_parent, _prop, node);
153
157
  }
@@ -164,6 +168,7 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
164
168
 
165
169
  csnPath.pop();
166
170
  }
171
+ context.$annotation = undefined;
167
172
  }
168
173
  }
169
174
 
@@ -359,18 +364,18 @@ function mergeTransformers( transformers, that ) {
359
364
  remapped[n] = [];
360
365
 
361
366
  if (Array.isArray(fns)) {
362
- remapped[n].push((parent, name, prop, path, parentParent) => fns.forEach(
363
- fn => fn.bind(that)(parent, name, prop, path, parentParent)
367
+ remapped[n].push((parent, name, prop, path, parentParent, opt, context) => fns.forEach(
368
+ fn => fn.bind(that)(parent, name, prop, path, parentParent, opt, context)
364
369
  ));
365
370
  }
366
371
  else {
367
- remapped[n].push((parent, name, prop, path, parentParent) => fns.bind(that)(parent, name, prop, path, parentParent));
372
+ remapped[n].push((parent, name, prop, path, parentParent, opt, context) => fns.bind(that)(parent, name, prop, path, parentParent, opt, context));
368
373
  }
369
374
  }
370
375
  }
371
376
 
372
377
  for (const [ n, fns ] of Object.entries(remapped))
373
- remapped[n] = (parent, name, prop, path, parentParent) => fns.forEach(fn => fn.bind(that)(parent, name, prop, path, parentParent));
378
+ remapped[n] = (parent, name, prop, path, parentParent, opt, context) => fns.forEach(fn => fn.bind(that)(parent, name, prop, path, parentParent, opt, context));
374
379
 
375
380
 
376
381
  return remapped;
@@ -132,44 +132,8 @@ function getFKAccessFinalizer( csn, csnUtils, pathDelimiter, processOnInQueries
132
132
  * @param {string} artifactName Name of the artifact
133
133
  */
134
134
  function handleManagedAssocSteps( artifact, artifactName ) {
135
- const transformer = {
136
- ref: (refOwner, prop, ref, path) => {
137
- // [<assoc base>.]<managed assoc>.<field>
138
- if (ref.length > 1) {
139
- const { links } = inspectRef(path);
140
- if (links) {
141
- let fkAlias = '';
142
- // eslint-disable-next-line for-direction
143
- for (let i = links.length - 1; i >= 0; i--) {
144
- const link = links[i];
145
- // We found the latest managed assoc path step
146
- if (link.art && link.art.target && link.art.keys &&
147
- // Doesn't work when ref-target (filter condition) or similar is used
148
- !ref.slice(i).some(refElement => typeof refElement !== 'string')) {
149
- const fkRef = ref[i + 1];
150
- const fkName = (!fkAlias ? fkRef : `${fkRef}${pathDelimiter}${fkAlias}`);
151
- const fks = link.art.keys.filter(key => key.ref[0] === fkName);
152
-
153
- if (fks.length >= 1) { // after flattening, at most one FK will remain.
154
- // `.as` is set for SQL, but not for OData -> fall back to implicit alias
155
- fkAlias = fks[0].as || fks[0].ref[fks[0].ref.length - 1];
156
- const source = findSource(links, i - 1) || artifact;
157
- const managedAssocStepName = ref[i];
158
- const newFkName = `${managedAssocStepName}${pathDelimiter}${fkAlias}`;
159
- if (source?.elements[newFkName])
160
- refOwner.ref = [ ...ref.slice(0, i), newFkName ];
161
- }
162
- }
163
- else {
164
- fkAlias = '';
165
- // Ignore last path step and unmanaged associations.
166
- // Structures should have been already flattened.
167
- }
168
- }
169
- }
170
- }
171
- },
172
- };
135
+ const transformer = getTransformer();
136
+ const inColumnsTransformer = getTransformer(true);
173
137
  for (const elemName in artifact.elements) {
174
138
  const elem = artifact.elements[elemName];
175
139
  // The association is an unmanaged one
@@ -187,28 +151,72 @@ function getFKAccessFinalizer( csn, csnUtils, pathDelimiter, processOnInQueries
187
151
  where: transform,
188
152
  having: transform,
189
153
  };
190
- if (processOnInQueries)
154
+ if (processOnInQueries) {
155
+ queryTransformers.columns = (parent, prop, columns, path) => {
156
+ for (let i = 0; i < columns.length; i++) {
157
+ const column = columns[i];
158
+ if (column.ref) {
159
+ inColumnsTransformer.ref(column, 'ref', column.ref, path.concat([ 'columns', i ]));
160
+ column.ref.forEach((step, index) => {
161
+ if (step.where)
162
+ transform(step, 'where', step.where, path.concat([ 'columns', i, 'ref', index ]));
163
+ });
164
+ }
165
+ }
166
+ };
191
167
  queryTransformers.on = transform;
192
- applyTransformationsOnNonDictionary(artifact, artifact.query ? 'query' : 'projection', queryTransformers, {}, [ 'definitions', artifactName ]);
168
+ }
169
+ applyTransformationsOnNonDictionary(artifact, artifact.query ? 'query' : 'projection', queryTransformers, { drillRef: true }, [ 'definitions', artifactName ]);
193
170
  }
194
171
 
195
-
196
172
  /**
197
- * Find out where the managed association is
198
173
  *
199
- * @param {Array} links
200
- * @param {number} startIndex
201
- * @returns {object | undefined} CSN definition of the source of the managed association
174
+ * @param {boolean} isColumns Whether the transformation is taking place on a column
175
+ * @returns {object}
202
176
  */
203
- function findSource( links, startIndex ) {
204
- for (let i = startIndex; i >= 0; i--) {
205
- const link = links[i];
206
- // We found the latest assoc step - now check where that points to
207
- if (link.art && link.art.target)
208
- return csn.definitions[link.art.target];
209
- }
210
-
211
- return undefined;
177
+ function getTransformer( isColumns = false ) {
178
+ return {
179
+ ref: (refOwner, prop, ref, path) => {
180
+ // [<assoc base>.]<managed assoc>.<field>
181
+ if (ref.length > 1) {
182
+ const { links } = inspectRef(path);
183
+ if (links) {
184
+ let fkAlias = '';
185
+ for (let i = links.length - 1; i >= 0; i--) {
186
+ const link = links[i];
187
+ // We found the latest managed assoc path step
188
+ if (link.art && link.art.target && link.art.keys &&
189
+ // Doesn't work when ref-target (filter condition) or similar is used
190
+ !ref.slice(i).some(refElement => typeof refElement !== 'string')) {
191
+ const fkRef = ref[i + 1];
192
+ const fkName = (!fkAlias ? fkRef : `${fkRef}${pathDelimiter}${fkAlias}`);
193
+ const fks = link.art.keys.filter(key => key.ref[0] === fkName);
194
+
195
+ if (fks.length >= 1) { // after flattening, at most one FK will remain.
196
+ // `.as` is set for SQL, but not for OData -> fall back to implicit alias
197
+ fkAlias = fks[0].as || fks[0].ref[fks[0].ref.length - 1];
198
+ const managedAssocStepName = ref[i];
199
+ const newFkName = `${managedAssocStepName}${pathDelimiter}${fkAlias}`;
200
+ if (isColumns) {
201
+ refOwner.ref = [ ...ref.slice(0, i), newFkName ];
202
+ if (!refOwner.as)
203
+ refOwner.as = implicitAs(ref);
204
+ }
205
+ else {
206
+ refOwner.ref = [ ...ref.slice(0, i), newFkName ];
207
+ }
208
+ }
209
+ }
210
+ else {
211
+ fkAlias = '';
212
+ // Ignore last path step and unmanaged associations.
213
+ // Structures should have been already flattened.
214
+ }
215
+ }
216
+ }
217
+ }
218
+ },
219
+ };
212
220
  }
213
221
  }
214
222
  }
@@ -203,28 +203,28 @@ function createReferentialConstraints( csn, options ) {
203
203
  // in contrast to foreign keys which stem from managed associations,
204
204
  // a tenant foreign key column may have multiple parent keys as partners
205
205
  const tenantForeignKey = isTenant && dependentKeyValuePair[0] === 'tenant';
206
- if ($foreignKeyConstraint && (!tenantForeignKey || $foreignKeyConstraint.upLinkFor)) {
206
+ if ($foreignKeyConstraint && (!tenantForeignKey || $foreignKeyConstraint.upLinkFor))
207
207
  return;
208
- }
209
- else if ($foreignKeyConstraint && tenantForeignKey) {
210
- parentKeys.pop();
211
- $foreignKeyConstraint.sourceAssociation.push(sourceAssociation);
208
+
209
+ const parentKeyValuePair = parentKeys.pop();
210
+ const parentKeyName = parentKeyValuePair[0];
211
+
212
+ const constraint = {
213
+ parentKey: parentKeyName,
214
+ parentTable,
215
+ upLinkFor,
216
+ sourceAssociation,
217
+ onDelete: upLinkFor ? 'CASCADE' : 'RESTRICT',
218
+ validated,
219
+ enforced,
220
+ };
221
+ if (tenantForeignKey) {
222
+ const dontOverwriteUp = dependentKey.$foreignKeyConstraint && dependentKey.$foreignKeyConstraint.some(c => c.sourceAssociation === sourceAssociation && c.parentTable === parentTable);
223
+ const dontOverwriteTexts = dependentKey.$foreignKeyConstraint && dependentKey.$foreignKeyConstraint.some(c => c.sourceAssociation === 'texts' && c.upLinkFor.texts);
224
+ if (!dontOverwriteUp && !dontOverwriteTexts)
225
+ dependentKey.$foreignKeyConstraint = dependentKey.$foreignKeyConstraint ? [ ...dependentKey.$foreignKeyConstraint, constraint ] : [ constraint ];
212
226
  }
213
227
  else {
214
- const parentKeyValuePair = parentKeys.pop();
215
- const parentKeyName = parentKeyValuePair[0];
216
-
217
- const constraint = {
218
- parentKey: parentKeyName,
219
- parentTable,
220
- upLinkFor,
221
- sourceAssociation: tenantForeignKey
222
- ? [ sourceAssociation ]
223
- : sourceAssociation,
224
- onDelete: upLinkFor ? 'CASCADE' : 'RESTRICT',
225
- validated,
226
- enforced,
227
- };
228
228
  dependentKey.$foreignKeyConstraint = constraint;
229
229
  }
230
230
  }
@@ -509,13 +509,11 @@ function createReferentialConstraints( csn, options ) {
509
509
  if (isTenant && artifact.elements?.tenant) {
510
510
  const element = artifact.elements.tenant;
511
511
  if (element.$foreignKeyConstraint) {
512
- const $foreignKeyConstraint = Object.assign({}, element.$foreignKeyConstraint);
512
+ const tenantConstraints = element.$foreignKeyConstraint;
513
513
  delete element.$foreignKeyConstraint;
514
- // create a foreign key constraint for the tenant column with each association in the dependent entity
515
- $foreignKeyConstraint.sourceAssociation.forEach((sourceAssociation) => {
516
- const copy = Object.assign({}, $foreignKeyConstraint);
517
- copy.sourceAssociation = sourceAssociation;
518
- createReferentialConstraints(copy, 'tenant');
514
+ // create (multiple) foreign key constraint(s) for the tenant column with each association in the dependent entity
515
+ tenantConstraints.forEach((c) => {
516
+ createReferentialConstraints(c, 'tenant');
519
517
  });
520
518
  }
521
519
  }
@@ -47,7 +47,7 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
47
47
  const isComplexQuery = parent.from.join !== undefined;
48
48
  if (!options.toOdata)
49
49
  parent.columns = replaceStar(root, columns, parent.excluding, isComplexQuery);
50
- // FIXME(v5): Remove argument "isComplexOrNestedQuery"; we use path.length > 4 to check
50
+ // FIXME(v6): Remove argument "isComplexOrNestedQuery"; we use path.length > 4 to check
51
51
  // if we're inside the outermost "columns". If so, always prepend a table alias. See #11662
52
52
  parent.columns = expand(parent.columns, path.concat('columns'), true, isComplexQuery || path.length > 4);
53
53
  }
@@ -622,11 +622,6 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
622
622
  return _expandStructCol(art, columnAlias(root), root.ref, ( currentRef, currentAlias) => {
623
623
  const obj = { ...root, ref: currentRef };
624
624
  if (withAlias) {
625
- // TODO: Remove this line in case foreign key annotations should
626
- // be addressed via full path into target instead of using alias
627
- // names. See flattening.js::flattenAllStructStepsInRefs()
628
- // apply transformations on `ref` counterpart comment.
629
- setProp(obj, '$structRef', currentAlias);
630
625
  obj.as = currentAlias.join(pathDelimiter);
631
626
  // alias was implicit - to later distinguish expanded s -> s.a from explicitly written s.a
632
627
  if (root.as === undefined)