@sap/cds-compiler 2.5.0 → 2.10.4

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 (92) hide show
  1. package/CHANGELOG.md +191 -9
  2. package/bin/cdsc.js +2 -2
  3. package/doc/CHANGELOG_BETA.md +33 -3
  4. package/lib/api/main.js +29 -101
  5. package/lib/api/options.js +15 -11
  6. package/lib/api/validate.js +12 -8
  7. package/lib/backends.js +0 -81
  8. package/lib/base/keywords.js +32 -2
  9. package/lib/base/message-registry.js +63 -9
  10. package/lib/base/messages.js +63 -21
  11. package/lib/base/model.js +2 -3
  12. package/lib/checks/defaultValues.js +27 -2
  13. package/lib/checks/elements.js +1 -6
  14. package/lib/checks/foreignKeys.js +0 -6
  15. package/lib/checks/managedWithoutKeys.js +17 -0
  16. package/lib/checks/nonexpandableStructured.js +38 -0
  17. package/lib/checks/onConditions.js +9 -45
  18. package/lib/checks/queryNoDbArtifacts.js +25 -7
  19. package/lib/checks/selectItems.js +25 -2
  20. package/lib/checks/types.js +26 -2
  21. package/lib/checks/unknownMagic.js +38 -0
  22. package/lib/checks/utils.js +61 -0
  23. package/lib/checks/validator.js +60 -7
  24. package/lib/compiler/assert-consistency.js +16 -7
  25. package/lib/compiler/builtins.js +2 -0
  26. package/lib/compiler/checks.js +6 -4
  27. package/lib/compiler/definer.js +99 -42
  28. package/lib/compiler/index.js +73 -27
  29. package/lib/compiler/resolver.js +288 -157
  30. package/lib/compiler/shared.js +31 -11
  31. package/lib/edm/annotations/genericTranslation.js +182 -186
  32. package/lib/edm/csn2edm.js +103 -108
  33. package/lib/edm/edm.js +18 -21
  34. package/lib/edm/edmPreprocessor.js +361 -114
  35. package/lib/edm/edmUtils.js +103 -33
  36. package/lib/gen/Dictionary.json +22 -0
  37. package/lib/gen/language.checksum +1 -1
  38. package/lib/gen/language.interp +12 -1
  39. package/lib/gen/language.tokens +57 -53
  40. package/lib/gen/languageLexer.interp +10 -1
  41. package/lib/gen/languageLexer.js +770 -744
  42. package/lib/gen/languageLexer.tokens +49 -46
  43. package/lib/gen/languageParser.js +4713 -4279
  44. package/lib/json/from-csn.js +103 -45
  45. package/lib/json/to-csn.js +296 -117
  46. package/lib/language/antlrParser.js +4 -3
  47. package/lib/language/errorStrategy.js +1 -0
  48. package/lib/language/genericAntlrParser.js +21 -12
  49. package/lib/language/language.g4 +99 -31
  50. package/lib/main.d.ts +81 -3
  51. package/lib/main.js +30 -7
  52. package/lib/model/api.js +78 -0
  53. package/lib/model/csnRefs.js +329 -142
  54. package/lib/model/csnUtils.js +235 -58
  55. package/lib/model/enrichCsn.js +18 -1
  56. package/lib/model/revealInternalProperties.js +2 -1
  57. package/lib/modelCompare/compare.js +37 -20
  58. package/lib/optionProcessor.js +9 -3
  59. package/lib/render/.eslintrc.json +4 -1
  60. package/lib/render/DuplicateChecker.js +8 -5
  61. package/lib/render/toCdl.js +112 -33
  62. package/lib/render/toHdbcds.js +134 -64
  63. package/lib/render/toSql.js +91 -38
  64. package/lib/render/utils/common.js +8 -13
  65. package/lib/render/utils/sql.js +3 -3
  66. package/lib/sql-identifier.js +6 -1
  67. package/lib/transform/db/assertUnique.js +5 -6
  68. package/lib/transform/db/constraints.js +29 -13
  69. package/lib/transform/db/draft.js +8 -6
  70. package/lib/transform/db/expansion.js +582 -0
  71. package/lib/transform/db/flattening.js +325 -0
  72. package/lib/transform/db/groupByOrderBy.js +2 -2
  73. package/lib/transform/db/transformExists.js +284 -63
  74. package/lib/transform/forHanaNew.js +98 -381
  75. package/lib/transform/forOdataNew.js +21 -22
  76. package/lib/transform/localized.js +37 -10
  77. package/lib/transform/odata/attachPath.js +19 -4
  78. package/lib/transform/odata/generateForeignKeyElements.js +11 -10
  79. package/lib/transform/odata/referenceFlattener.js +60 -39
  80. package/lib/transform/odata/sortByAssociationDependency.js +2 -2
  81. package/lib/transform/odata/structuralPath.js +72 -0
  82. package/lib/transform/odata/structureFlattener.js +19 -18
  83. package/lib/transform/odata/typesExposure.js +22 -12
  84. package/lib/transform/transformUtilsNew.js +134 -78
  85. package/lib/transform/translateAssocsToJoins.js +17 -14
  86. package/lib/transform/universalCsnEnricher.js +67 -0
  87. package/lib/utils/file.js +0 -11
  88. package/lib/utils/moduleResolve.js +6 -8
  89. package/package.json +1 -1
  90. package/lib/json/walker.js +0 -26
  91. package/lib/transform/sqlite +0 -0
  92. package/lib/utils/string.js +0 -17
@@ -7,7 +7,7 @@ const {
7
7
  forEachDefinition, getResultingName,
8
8
  } = require('../model/csnUtils');
9
9
  const {
10
- renderFunc, processExprArray, cdsToSqlTypes, getHanaComment, hasHanaComment,
10
+ renderFunc, beautifyExprArray, cdsToSqlTypes, getHanaComment, hasHanaComment,
11
11
  } = require('./utils/common');
12
12
  const {
13
13
  renderReferentialConstraint, getIdentifierUtils,
@@ -152,6 +152,24 @@ function toSqlDdl(csn, options) {
152
152
  .join(', ');
153
153
  return primaryKeys && `PRIMARY KEY(${primaryKeys})`;
154
154
  },
155
+ /*
156
+ Render entity-comment modifications as HANA SQL.
157
+ */
158
+ alterEntityComment(tableName, comment) {
159
+ return [ `COMMENT ON TABLE ${tableName} IS ${render.comment(comment)};` ];
160
+ },
161
+ /*
162
+ Render column-comment modifications as HANA SQL.
163
+ */
164
+ alterColumnComment(tableName, columnName, comment) {
165
+ return [ `COMMENT ON COLUMN ${tableName}.${columnName} IS ${render.comment(comment)};` ];
166
+ },
167
+ /*
168
+ Render comment string.
169
+ */
170
+ comment(comment) {
171
+ return comment && `'${comment.replace(/'/g, '\'\'')}'` || 'NULL';
172
+ },
155
173
  /*
156
174
  Concatenate multiple statements which are to be treated as one by the API caller.
157
175
  */
@@ -211,8 +229,9 @@ function toSqlDdl(csn, options) {
211
229
  for (const extension of options && options.testMode ? sortCsn(csn.extensions) : csn.extensions) {
212
230
  if (extension.extend) {
213
231
  const artifactName = extension.extend;
214
- const env = { indent: '' };
215
- renderArtifactExtensionInto(artifactName, csn.definitions[artifactName], extension, resultObj, env);
232
+ const _artifact = csn.definitions[artifactName];
233
+ const env = { indent: '', _artifact };
234
+ renderArtifactExtensionInto(artifactName, _artifact, extension, resultObj, env);
216
235
  }
217
236
  }
218
237
  }
@@ -223,7 +242,8 @@ function toSqlDdl(csn, options) {
223
242
  for (const migration of options && options.testMode ? sortCsn(csn.migrations) : csn.migrations) {
224
243
  if (migration.migrate) {
225
244
  const artifactName = migration.migrate;
226
- const env = { indent: '' };
245
+ const _artifact = csn.definitions[artifactName];
246
+ const env = { indent: '', _artifact };
227
247
  renderArtifactMigrationInto(artifactName, migration, resultObj, env);
228
248
  }
229
249
  }
@@ -365,12 +385,32 @@ function toSqlDdl(csn, options) {
365
385
  function reducesTypeSize(def) {
366
386
  // HANA does not allow decreasing the value of any of those type parameters.
367
387
  return def.old.type === def.new.type &&
368
- [ 'length', 'precision', 'scale' ].some(param => def.new[param] < def.old[param]);
388
+ [ 'length', 'precision', 'scale' ].some(param => def.new[param] < def.old[param]);
389
+ }
390
+ function getEltStr(defVariant, eltName) {
391
+ return defVariant.target
392
+ ? renderAssociationElement(eltName, defVariant, env)
393
+ : renderElement(artifactName, eltName, defVariant, null, null, env);
394
+ }
395
+ function getEltStrNoProp(defVariant, prop, eltName) {
396
+ const defNoProp = Object.assign({}, defVariant);
397
+ delete defNoProp[prop];
398
+ return getEltStr(defNoProp, eltName);
369
399
  }
370
400
 
371
401
  const tableName = renderArtifactName(artifactName);
372
402
 
373
- // Drop column (unsupported in sqlite)
403
+ // Change entity properties
404
+ if (migration.properties) {
405
+ for (const [ prop, def ] of Object.entries(migration.properties)) {
406
+ if (prop === 'doc') {
407
+ const alterComment = render.alterEntityComment(tableName, def.new);
408
+ addMigration(resultObj, artifactName, false, alterComment);
409
+ }
410
+ }
411
+ }
412
+
413
+ // Drop columns (unsupported in sqlite)
374
414
  if (migration.remove) {
375
415
  const entries = Object.entries(migration.remove);
376
416
  if (entries.length) {
@@ -389,22 +429,28 @@ function toSqlDdl(csn, options) {
389
429
  }
390
430
  }
391
431
 
392
- // Change column type (unsupported in sqlite)
432
+ // Change column types (unsupported in sqlite)
393
433
  if (migration.change) {
394
434
  changeElementsDuplicateChecker.addArtifact(tableName, undefined, artifactName);
395
435
  for (const [ eltName, def ] of Object.entries(migration.change)) {
396
436
  const sqlId = quoteSqlId(eltName);
397
437
  changeElementsDuplicateChecker.addElement(sqlId, undefined, eltName);
398
438
 
399
- const eltStrOld = def.old.target
400
- ? renderAssociationElement(eltName, def.old, env)
401
- : renderElement(artifactName, eltName, def.old, null, null, env);
402
- const eltStrNew = def.new.target
403
- ? renderAssociationElement(eltName, def.new, env)
404
- : renderElement(artifactName, eltName, def.new, null, null, env);
439
+ const eltStrOld = getEltStr(def.old, eltName);
440
+ const eltStrNew = getEltStr(def.new, eltName);
405
441
  if (eltStrNew === eltStrOld)
406
442
  return; // Prevent spurious migrations, where the column DDL does not change.
407
443
 
444
+ if (def.old.doc !== def.new.doc) {
445
+ const eltStrOldNoDoc = getEltStrNoProp(def.old, 'doc', eltName);
446
+ const eltStrNewNoDoc = getEltStrNoProp(def.new, 'doc', eltName);
447
+ if (eltStrOldNoDoc === eltStrNewNoDoc) { // only `doc` changed
448
+ const alterComment = render.alterColumnComment(tableName, sqlId, def.new.doc);
449
+ addMigration(resultObj, artifactName, false, alterComment);
450
+ continue;
451
+ }
452
+ }
453
+
408
454
  if (options.sqlChangeMode === 'drop' || def.old.target || def.new.target || reducesTypeSize(def)) {
409
455
  // Lossy change because either an association is removed and/or added, or the type size is reduced.
410
456
  // Drop old element and re-add it in its new shape.
@@ -479,12 +525,12 @@ function toSqlDdl(csn, options) {
479
525
  referentialConstraints[fileName] = renderReferentialConstraint(referentialConstraint, childEnv.indent, false, csn, options);
480
526
  });
481
527
  if (renderReferentialConstraintsAsHdbconstraint) {
482
- Object.entries(referentialConstraints).forEach( ([ fileName, constraint ]) => {
528
+ Object.entries(referentialConstraints).forEach(([ fileName, constraint ]) => {
483
529
  resultObj.hdbconstraint[fileName] = constraint;
484
530
  });
485
531
  }
486
532
  else {
487
- Object.values(referentialConstraints).forEach( (constraint) => {
533
+ Object.values(referentialConstraints).forEach((constraint) => {
488
534
  result += `,\n${constraint}`;
489
535
  });
490
536
  }
@@ -500,8 +546,7 @@ function toSqlDdl(csn, options) {
500
546
  = `UNIQUE INVERTED INDEX ${renderArtifactName(`${artifactName}_${cn}`)} ON ${tableName} (${c.map(cpath => quoteSqlId(cpath.ref[0])).join(', ')})`;
501
547
  }
502
548
  else {
503
- result += `,\n${childEnv.indent}CONSTRAINT ${renderArtifactName(`${artifactName}_${cn}`)} UNIQUE (${
504
- c.map(cpath => quoteSqlId(cpath.ref[0])).join(', ')})`;
549
+ result += `,\n${childEnv.indent}CONSTRAINT ${renderArtifactName(`${artifactName}_${cn}`)} UNIQUE (${c.map(cpath => quoteSqlId(cpath.ref[0])).join(', ')})`;
505
550
  }
506
551
  }
507
552
  result += `${env.indent}\n)`;
@@ -613,8 +658,7 @@ function toSqlDdl(csn, options) {
613
658
  if (duplicateChecker)
614
659
  duplicateChecker.addElement(quotedElementName, elm.$location, elementName);
615
660
 
616
- let result = `${env.indent + quotedElementName} ${
617
- renderTypeReference(artifactName, elementName, elm)
661
+ let result = `${env.indent + quotedElementName} ${renderTypeReference(artifactName, elementName, elm)
618
662
  }${renderNullability(elm, true)}`;
619
663
  if (elm.default)
620
664
  result += ` DEFAULT ${renderExpr(elm.default, env)}`;
@@ -910,7 +954,7 @@ function toSqlDdl(csn, options) {
910
954
  // An empty actual parameter list is rendered as `()`.
911
955
  const ref = csn.definitions[path.ref[0].id] || csn.definitions[path.ref[0]];
912
956
  if (ref && ref.params) {
913
- result += `(${renderArgs(path.ref[0].args || {}, '=>', env, syntax)})`;
957
+ result += `(${renderArgs(path.ref[0] || {}, '=>', env, syntax)})`;
914
958
  }
915
959
  else if ([ 'udf' ].includes(syntax)) {
916
960
  // if syntax is user defined function, render empty argument list
@@ -932,21 +976,23 @@ function toSqlDdl(csn, options) {
932
976
  * Render function arguments or view parameters (positional if array, named if object/dict),
933
977
  * using 'sep' as separator for positional parameters
934
978
  *
935
- * @param {Array|object} args Arguments to render
979
+ * @param {object} node with `args` to render
936
980
  * @param {string} sep Separator between args
937
981
  * @param {object} env Render environment
938
982
  * @param {string} syntax Some magic A2J paramter - for calcview parameter rendering
939
983
  * @returns {string} Rendered arguments
940
984
  * @throws Throws if args is not an array or object.
941
985
  */
942
- function renderArgs(args, sep, env, syntax) {
986
+ function renderArgs(node, sep, env, syntax) {
987
+ const args = node.args ? node.args : {};
943
988
  // Positional arguments
944
989
  if (Array.isArray(args))
945
990
  return args.map(arg => renderExpr(arg, env)).join(', ');
946
991
 
947
992
  // Named arguments (object/dict)
948
993
  else if (typeof args === 'object')
949
- return Object.keys(args).map(key => `${decorateParameter(key, syntax)} ${sep} ${renderExpr(args[key], env)}`).join(', ');
994
+ // if this is a function param which is not a reference to the model, we must not quote it
995
+ return Object.keys(args).map(key => `${node.func ? key : decorateParameter(key, syntax)} ${sep} ${renderExpr(args[key], env)}`).join(', ');
950
996
 
951
997
 
952
998
  throw new Error(`Unknown args: ${JSON.stringify(args)}`);
@@ -1042,8 +1088,15 @@ function toSqlDdl(csn, options) {
1042
1088
  const p = params[pn];
1043
1089
  if (p.notNull === true || p.notNull === false)
1044
1090
  info(null, [ 'definitions', artifactName, 'params', pn ], 'Not Null constraints on SQL view parameters are not allowed and are ignored');
1045
-
1046
- let pstr = `IN ${prepareIdentifier(pn)} ${renderTypeReference(artifactName, pn, p)}`;
1091
+ // do not quote parameter identifiers for naming mode "quoted" / "hdbcds"
1092
+ // this would be an incompatible change, as non-uppercased, quoted identifiers
1093
+ // are rejected by the HANA compiler.
1094
+ let pIdentifier;
1095
+ if (options.toSql.names === 'quoted' || options.toSql.names === 'hdbcds')
1096
+ pIdentifier = prepareIdentifier(pn);
1097
+ else
1098
+ pIdentifier = quoteSqlId(pn);
1099
+ let pstr = `IN ${pIdentifier} ${renderTypeReference(artifactName, pn, p)}`;
1047
1100
  if (p.default)
1048
1101
  pstr += ` DEFAULT ${renderExpr(p.default)}`;
1049
1102
 
@@ -1098,12 +1151,11 @@ function toSqlDdl(csn, options) {
1098
1151
  const childEnv = increaseIndent(env);
1099
1152
  result += `SELECT${select.distinct ? ' DISTINCT' : ''}`;
1100
1153
  // FIXME: We probably also need to consider `excluding` here ?
1101
- result += `\n${
1102
- (select.columns || [ '*' ])
1103
- .filter(col => !(select.mixin || Object.create(null))[firstPathStepId(col.ref)]) // No mixin columns
1104
- .map(col => renderViewColumn(col, childEnv))
1105
- .filter(s => s !== '')
1106
- .join(',\n')}\n`;
1154
+ result += `\n${(select.columns || [ '*' ])
1155
+ .filter(col => !(select.mixin || Object.create(null))[firstPathStepId(col.ref)]) // No mixin columns
1156
+ .map(col => renderViewColumn(col, childEnv))
1157
+ .filter(s => s !== '')
1158
+ .join(',\n')}\n`;
1107
1159
  result += `${env.indent}FROM ${renderViewSource(artifactName, select.from, env)}`;
1108
1160
  if (select.where)
1109
1161
  result += `\n${env.indent}WHERE ${renderExpr(select.where, env, true, true)}`;
@@ -1290,7 +1342,8 @@ function toSqlDdl(csn, options) {
1290
1342
  function renderExpr(x, env, inline = true, nestedExpr = false) {
1291
1343
  // Compound expression
1292
1344
  if (Array.isArray(x)) {
1293
- return processExprArray(x, renderExpr, env, inline, nestedExpr);
1345
+ const tokens = x.map(item => renderExpr(item, env, inline, nestedExpr));
1346
+ return beautifyExprArray(tokens);
1294
1347
  }
1295
1348
  else if (typeof x === 'object' && x !== null) {
1296
1349
  if (nestedExpr && x.cast && x.cast.type)
@@ -1358,23 +1411,23 @@ function toSqlDdl(csn, options) {
1358
1411
  case 'number':
1359
1412
  case 'boolean':
1360
1413
  case 'null':
1361
- // 17.42, NULL, TRUE
1414
+ // 17.42, NULL, TRUE
1362
1415
  return String(x.val).toUpperCase();
1363
1416
  case 'x':
1364
- // x'f000'
1417
+ // x'f000'
1365
1418
  return `${x.literal}'${x.val}'`;
1366
1419
  case 'date':
1367
1420
  case 'time':
1368
1421
  case 'timestamp':
1369
1422
  if (options.toSql.dialect === 'sqlite') {
1370
- // simple string literal '2017-11-02'
1423
+ // simple string literal '2017-11-02'
1371
1424
  return `'${x.val}'`;
1372
1425
  }
1373
1426
  // date'2017-11-02'
1374
1427
  return `${x.literal}'${x.val}'`;
1375
1428
 
1376
1429
  case 'string':
1377
- // 'foo', with proper escaping
1430
+ // 'foo', with proper escaping
1378
1431
  return `'${x.val.replace(/'/g, '\'\'')}'`;
1379
1432
  case 'object':
1380
1433
  if (x.val === null)
@@ -1539,13 +1592,13 @@ function toSqlDdl(csn, options) {
1539
1592
 
1540
1593
  // Not really a path step but an object-like function call
1541
1594
  if (s.func)
1542
- return `${s.func}(${renderArgs(s.args, '=>', env, null)})`;
1595
+ return `${s.func}(${renderArgs(s, '=>', env, null)})`;
1543
1596
 
1544
1597
  // Path step, possibly with view parameters and/or filters
1545
1598
  let result = `${quoteSqlId(s.id)}`;
1546
1599
  if (s.args) {
1547
1600
  // View parameters
1548
- result += `(${renderArgs(s.args, '=>', env, null)})`;
1601
+ result += `(${renderArgs(s, '=>', env, null)})`;
1549
1602
  }
1550
1603
  if (s.where) {
1551
1604
  // Filter, possibly with cardinality
@@ -33,7 +33,7 @@ const { implicitAs } = require('../../model/csnRefs');
33
33
  function renderFunc( funcName, node, dialect, renderArgs) {
34
34
  if (funcWithoutParen( node, dialect ))
35
35
  return funcName;
36
- return `${funcName}(${renderArgs( node.args )})`;
36
+ return `${funcName}(${renderArgs( node )})`;
37
37
  }
38
38
 
39
39
  /**
@@ -53,19 +53,14 @@ function funcWithoutParen( node, dialect ) {
53
53
  }
54
54
 
55
55
  /**
56
- * Process an expression array, rendering each subexpression and joining them appropriately
56
+ * Process already rendered expression parts by joining them nicely
57
57
  *
58
- * @param {Array} xpr Expression array
59
- * @param {Function} renderExpr Function to render expressions
60
- * @param {CdlRenderEnvironment} env Environment for rendering
61
- * @param {boolean} inline Wether to render the expression inline
62
- * @param {boolean} inExpr Wether to render as if a subexpression
58
+ * @param {Array} tokens Array of expression tokens
63
59
  *
64
60
  * @returns {string} The rendered xpr
65
61
  */
66
- function processExprArray(xpr, renderExpr, env, inline, inExpr) {
62
+ function beautifyExprArray(tokens) {
67
63
  // Simply concatenate array parts with spaces (with a tiny bit of beautification)
68
- const tokens = xpr.map(item => renderExpr(item, env, inline, inExpr));
69
64
  let result = '';
70
65
  for (let i = 0; i < tokens.length; i++) {
71
66
  result += tokens[i];
@@ -342,11 +337,10 @@ function addIntermediateContexts(csn, killList) {
342
337
  *
343
338
  * @param {CSN.Artifact} obj
344
339
  * @param {CSN.Options} options To check for `disableHanaComments`
345
- * @param {CSN.Artifact} artifact Artifact containing obj (in case of elem or columns)
346
340
  * @returns {boolean}
347
341
  */
348
- function hasHanaComment(obj, options, artifact = {}) {
349
- return !artifact['@cds.persistence.journal'] && !options.disableHanaComments && typeof obj.doc === 'string';
342
+ function hasHanaComment(obj, options) {
343
+ return !options.disableHanaComments && typeof obj.doc === 'string';
350
344
  }
351
345
  /**
352
346
  * Return the comment of the given artifact or element.
@@ -379,7 +373,7 @@ function getHanaComment(obj) {
379
373
 
380
374
  module.exports = {
381
375
  renderFunc,
382
- processExprArray,
376
+ beautifyExprArray,
383
377
  getNamespace,
384
378
  getRealName,
385
379
  addIntermediateContexts,
@@ -388,4 +382,5 @@ module.exports = {
388
382
  hasHanaComment,
389
383
  getHanaComment,
390
384
  findElement,
385
+ funcWithoutParen,
391
386
  };
@@ -20,9 +20,9 @@ const { smartId, delimitedId } = require('../../sql-identifier');
20
20
  function renderReferentialConstraint(constraint, indent, toUpperCase, csn, options, alterConstraint) {
21
21
  let quoteId;
22
22
  // for to.hana we can't utilize the sql identifier utils
23
- if (options.toHana) {
23
+ if (options.transformation === 'hdbcds') {
24
24
  quoteId = (id) => {
25
- if (options.toHana.names === 'plain')
25
+ if (options.sqlMapping === 'plain')
26
26
  return id.replace(/\./g, '_');
27
27
  return `"${id}"`;
28
28
  };
@@ -38,7 +38,7 @@ function renderReferentialConstraint(constraint, indent, toUpperCase, csn, optio
38
38
  constraint.parentTable = constraint.parentTable.toUpperCase();
39
39
  }
40
40
 
41
- const renderAsHdbconstraint = options.toHana ||
41
+ const renderAsHdbconstraint = options.transformation === 'hdbcds' ||
42
42
  (options.toSql && options.toSql.src === 'hdi') ||
43
43
  (options.manageConstraints && options.manageConstraints.src === 'hdi');
44
44
 
@@ -51,7 +51,12 @@ const sqlDialects = {
51
51
  effectiveName: name => name.toUpperCase(),
52
52
  asDelimitedId: name => `"${ name.replace(/"/g, '""')}"`,
53
53
  },
54
- // TODO: hdbcds for HANA CDS
54
+ hdbcds: {
55
+ regularRegex: /^[A-Za-z_][A-Za-z_0-9]*$/,
56
+ reservedWords: keywords.hdbcds,
57
+ effectiveName: name => name,
58
+ asDelimitedId: name => `"${ name.replace(/"/g, '""')}"`,
59
+ },
55
60
  };
56
61
 
57
62
  function smartId( name, dialect ) {
@@ -260,7 +260,7 @@ function processAssertUnique(csn, options, error, info) {
260
260
  * If the output format is SQL, the toSql renderer is responsible
261
261
  * to render the table constraints from the constraint dictionary.
262
262
  *
263
- * If options.toHana, no path flattening is done and association
263
+ * If options.transformation === 'hdbcds', no path flattening is done and association
264
264
  * paths are replaced with the foreign key paths by simply
265
265
  * concatenating the foreign key paths (available in element.keys).
266
266
  *
@@ -279,11 +279,10 @@ function rewriteUniqueConstraints(csn, options, pathDelimiter) {
279
279
  * @param {CSN.Artifact} artifact
280
280
  */
281
281
  function rewrite(artifact) {
282
- const naming = options.forHana.names || options.toSql.names;
283
282
  if (artifact.$tableConstraints && artifact.$tableConstraints.unique) {
284
283
  const uniqueConstraints = artifact.$tableConstraints.unique;
285
284
  // it's safe to add the tc here
286
- if (options.toHana) {
285
+ if (options.transformation === 'hdbcds') {
287
286
  if (!artifact.technicalConfig)
288
287
  artifact.technicalConfig = Object.create(null);
289
288
 
@@ -302,12 +301,12 @@ function rewriteUniqueConstraints(csn, options, pathDelimiter) {
302
301
  c.forEach((cpath) => {
303
302
  // If 'toSql' or 'toHana' and naming !== 'hdbcds'
304
303
  // concatenate path refs with appropriate delimiter
305
- if (!options.toHana || (options.toHana && naming !== 'hdbcds'))
304
+ if (options.transformation !== 'hdbcds' || (options.transformation === 'hdbcds' && options.sqlMapping !== 'hdbcds'))
306
305
  cpath.ref = [ cpath.ref.map(p => p.id).join( pathDelimiter ) ];
307
306
 
308
307
  // Foreign key substitution
309
308
  if (cpath._art.target) {
310
- if (!options.toHana || (options.toHana && naming !== 'hdbcds')) {
309
+ if (options.transformation !== 'hdbcds' || (options.transformation === 'hdbcds' && options.sqlMapping !== 'hdbcds')) {
311
310
  // read out new association and use $generatedFieldName
312
311
  // cpath._art still refers to the assoc definition
313
312
  // before the A2J transformation. This assoc
@@ -332,7 +331,7 @@ function rewriteUniqueConstraints(csn, options, pathDelimiter) {
332
331
  uniqueConstraints[uniqueConstraint] = rewrittenPaths;
333
332
 
334
333
  // now add the index for HANA CDS
335
- if (options.toHana) {
334
+ if (options.transformation === 'hdbcds') {
336
335
  const index = [ 'unique', 'index', { ref: [ uniqueConstraint ] }, 'on', '(' ];
337
336
  let i = 0;
338
337
  for (const constraint of rewrittenPaths) {
@@ -45,7 +45,7 @@ function createReferentialConstraints(csn, options) {
45
45
  if (element.type === ASSOCIATION ||
46
46
  element.type === COMPOSITION && treatCompositionLikeAssociation(element)) {
47
47
  associations.push(() => {
48
- foreignKeyConstraintForAssociation(element, elements, path.concat([ elementName ]));
48
+ foreignKeyConstraintForAssociation(element, elements, path.concat([ elementName ]), elementName);
49
49
  });
50
50
  }
51
51
  }
@@ -80,12 +80,14 @@ function createReferentialConstraints(csn, options) {
80
80
  // mark each dependent key referenced in the on-condition (in target entity)
81
81
  const dependentKeys = Array.from(elementsOfTargetSide(onCondition, csn.definitions[composition.target].elements));
82
82
  const parentKeys = Array.from(elementsOfSourceSide(onCondition, elements));
83
+ const { backlinkName } = composition.$selfOnCondition || {};
83
84
  // sanity check; do not generate constraints for on-conditions like "dependent.idOne = id AND dependent.idTwo = id"
84
- if (dependentKeys.length === parentKeys.length)
85
- attachConstraintsToDependentKeys(dependentKeys, parentKeys, path[path.length - 3], path, 'CASCADE');
85
+ // also: no constraints for compositions of many w/o backlink
86
+ if (dependentKeys.length === parentKeys.length && backlinkName)
87
+ attachConstraintsToDependentKeys(dependentKeys, parentKeys, path[path.length - 3], path, backlinkName, 'CASCADE');
86
88
  }
87
89
  }
88
- else if (!onCondition && composition.keys) {
90
+ else if (!onCondition && composition.keys.length > 0) {
89
91
  throw new Error('Please debug me, an on-condition was expected here, but only found keys');
90
92
  }
91
93
  }
@@ -97,8 +99,9 @@ function createReferentialConstraints(csn, options) {
97
99
  * @param {CSN.Association} association for that a constraint should be generated
98
100
  * @param {CSN.Elements} elements of parent entity.
99
101
  * @param {CSN.Path} path
102
+ * @param {string} assocName passed through as proper constraint suffix
100
103
  */
101
- function foreignKeyConstraintForAssociation(association, elements, path) {
104
+ function foreignKeyConstraintForAssociation(association, elements, path, assocName) {
102
105
  const associationTarget = csn.definitions[association.target];
103
106
  if (skipConstraintGeneration(associationTarget, association))
104
107
  return;
@@ -111,9 +114,9 @@ function createReferentialConstraints(csn, options) {
111
114
  const parentKeys = Array.from(elementsOfTargetSide(onCondition, associationTarget.elements));
112
115
  // sanity check; do not generate constraints for on-conditions like "dependent.idOne = id AND dependent.idTwo = id"
113
116
  if (dependentKeys.length === parentKeys.length)
114
- attachConstraintsToDependentKeys(dependentKeys, parentKeys, association.target, path);
117
+ attachConstraintsToDependentKeys(dependentKeys, parentKeys, association.target, path, assocName);
115
118
  }
116
- else if (!onCondition && association.keys) {
119
+ else if (!onCondition && association.keys.length > 0) {
117
120
  throw new Error('Please debug me, an on-condition was expected here, but only found keys');
118
121
  }
119
122
  }
@@ -127,9 +130,10 @@ function createReferentialConstraints(csn, options) {
127
130
  * @param {Array} parentKeys array holding parent keys in the format [['key1', 'value1'], [...], ...]
128
131
  * @param {CSN.PathSegment} parentTable the sql-table where the foreign key constraints will be pointing to
129
132
  * @param {CSN.Path} path
133
+ * @param {string | null} constraintIdentifierSuffix name of the association / the backlink association
130
134
  * @param {string} onDelete the on delete rule which should be applied. Default for associations is 'RESTRICT'
131
135
  */
132
- function attachConstraintsToDependentKeys(dependentKeys, parentKeys, parentTable, path, onDelete = 'RESTRICT') {
136
+ function attachConstraintsToDependentKeys(dependentKeys, parentKeys, parentTable, path, constraintIdentifierSuffix, onDelete = 'RESTRICT') {
133
137
  while (dependentKeys.length > 0) {
134
138
  const dependentKeyValuePair = dependentKeys.pop();
135
139
  const dependentKey = dependentKeyValuePair[1];
@@ -143,7 +147,7 @@ function createReferentialConstraints(csn, options) {
143
147
  parentKey: parentKeyName,
144
148
  parentTable,
145
149
  sourceAssociation: path[path.length - 1],
146
- nameSuffix: dependentKey['@odata.foreignKey4'] || 'up_',
150
+ nameSuffix: constraintIdentifierSuffix || 'up_',
147
151
  onDelete,
148
152
  validated,
149
153
  enforced,
@@ -229,8 +233,10 @@ function createReferentialConstraints(csn, options) {
229
233
  return true;
230
234
  }
231
235
 
232
- if (element.$skipReferentialConstraintForUp_)
236
+ if (element.$skipReferentialConstraintForUp_) {
237
+ delete element.$skipReferentialConstraintForUp_;
233
238
  return true;
239
+ }
234
240
 
235
241
  if (hasAnnotationValue(parent, '@cds.persistence.skip', true) ||
236
242
  hasAnnotationValue(parent, '@cds.persistence.exists', true) ||
@@ -351,11 +357,21 @@ function createReferentialConstraints(csn, options) {
351
357
  * The constraints will thus be generated in the entity containing the composition and not in the target entity.
352
358
  *
353
359
  * @param {CSN.Composition} composition the composition which might be treated like an association
354
- * @returns {any} true if the composition should be treated as an association in regards to foreign key constraints
360
+ * @returns {boolean} true if the composition should be treated as an association in regards to foreign key constraints
355
361
  */
356
362
  function treatCompositionLikeAssociation(composition) {
357
- const { min, max } = composition.cardinality || {};
358
- return (!min && !max || max === 1) && composition.keys;
363
+ return Boolean((isToOne(composition) && !composition.$selfOnCondition) || composition.keys);
364
+ }
365
+
366
+ /**
367
+ * returns true if the association/composition has a max target cardinality of one
368
+ *
369
+ * @param {CSN.Element} assocOrComposition
370
+ * @returns {boolean}
371
+ */
372
+ function isToOne(assocOrComposition) {
373
+ const { min, max } = assocOrComposition.cardinality || {};
374
+ return !min && !max || max === 1;
359
375
  }
360
376
  }
361
377
 
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  const {
4
- hasAnnotationValue, forEachGeneric, getUtils, getServiceNames, forEachDefinition,
5
- getResultingName,
4
+ hasAnnotationValue, getUtils, getServiceNames, forEachDefinition,
5
+ getResultingName, forEachMemberRecursively,
6
6
  } = require('../../model/csnUtils');
7
7
  const { setProp, isDeprecatedEnabled } = require('../../base/model');
8
8
  const { getTransformers } = require('../transformUtilsNew');
@@ -117,10 +117,12 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
117
117
 
118
118
  // extract keys for UUID inspection
119
119
  const keys = [];
120
- forEachGeneric( artifact, 'elements', (elt) => {
120
+ forEachMemberRecursively(artifact, (elt, name, prop, path) => {
121
+ if (!elt.elements && !elt.type && !elt.virtual) // only check leafs
122
+ error(null, path, 'Expecting element to have a type when used in a draft-enabled artifact');
121
123
  if (elt.key && elt.key === true && !elt.virtual)
122
124
  keys.push(elt);
123
- });
125
+ }, [ 'definitions', artifactName ], true, { elementsOnly: true });
124
126
 
125
127
  // In contrast to EDM, the DB entity may have more than one technical keys but should have idealy exactly one key of type cds.UUID
126
128
  if (keys.length !== 1)
@@ -222,7 +224,7 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
222
224
  // Note that we may need to do the HANA transformation steps for managed associations
223
225
  // (foreign key field generation, generatedFieldName, creating ON-condition) by hand,
224
226
  // because the corresponding transformation steps have already been done on all artifacts
225
- // when we come here). Only for 'keepStructsAssocs' this is not required.
227
+ // when we come here). Only for to.hdbcds with hdbcds names this is not required.
226
228
  /**
227
229
  * The given association has a key named DraftUUID
228
230
  *
@@ -256,7 +258,7 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
256
258
  }
257
259
 
258
260
  const draftUUIDKey = getDraftUUIDKey(draftAdministrativeData.DraftAdministrativeData);
259
- if (!options.forHana.keepStructsAssocs && draftUUIDKey) {
261
+ if (!(options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds') && draftUUIDKey) {
260
262
  const path = [ 'definitions', draftsArtifactName, 'elements', 'DraftAdministrativeData', 'keys', 0 ];
261
263
  createForeignKeyElement(draftAdministrativeData.DraftAdministrativeData, 'DraftAdministrativeData', draftUUIDKey, draftsArtifact, draftsArtifactName, path);
262
264
  draftAdministrativeData.DraftAdministrativeData.on = createAssociationPathComparison('DraftAdministrativeData',