@sap/cds-compiler 2.11.2 → 2.13.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 (140) hide show
  1. package/CHANGELOG.md +175 -2
  2. package/bin/.eslintrc.json +1 -2
  3. package/bin/cds_update_identifiers.js +10 -8
  4. package/bin/cdsc.js +23 -17
  5. package/bin/cdsse.js +2 -2
  6. package/bin/cdsv2m.js +3 -2
  7. package/doc/CHANGELOG_ARCHIVE.md +1 -1
  8. package/doc/CHANGELOG_BETA.md +25 -6
  9. package/doc/CHANGELOG_DEPRECATED.md +22 -6
  10. package/doc/NameResolution.md +21 -16
  11. package/lib/api/main.js +32 -79
  12. package/lib/api/options.js +3 -2
  13. package/lib/api/validate.js +2 -1
  14. package/lib/backends.js +16 -26
  15. package/lib/base/dictionaries.js +0 -8
  16. package/lib/base/error.js +26 -0
  17. package/lib/base/keywords.js +10 -19
  18. package/lib/base/location.js +9 -4
  19. package/lib/base/message-registry.js +75 -9
  20. package/lib/base/messages.js +31 -35
  21. package/lib/base/model.js +2 -62
  22. package/lib/base/optionProcessorHelper.js +246 -183
  23. package/lib/checks/.eslintrc.json +2 -0
  24. package/lib/checks/actionsFunctions.js +2 -1
  25. package/lib/checks/annotationsOData.js +1 -1
  26. package/lib/checks/cdsPersistence.js +2 -1
  27. package/lib/checks/emptyOrOnlyVirtual.js +2 -2
  28. package/lib/checks/enricher.js +17 -1
  29. package/lib/checks/foreignKeys.js +4 -4
  30. package/lib/checks/invalidTarget.js +3 -1
  31. package/lib/checks/managedInType.js +4 -4
  32. package/lib/checks/managedWithoutKeys.js +3 -1
  33. package/lib/checks/queryNoDbArtifacts.js +1 -3
  34. package/lib/checks/selectItems.js +4 -4
  35. package/lib/checks/sql-snippets.js +94 -0
  36. package/lib/checks/types.js +1 -1
  37. package/lib/checks/unknownMagic.js +1 -1
  38. package/lib/checks/validator.js +12 -7
  39. package/lib/compiler/assert-consistency.js +12 -8
  40. package/lib/compiler/base.js +0 -1
  41. package/lib/compiler/builtins.js +42 -21
  42. package/lib/compiler/checks.js +46 -12
  43. package/lib/compiler/cycle-detector.js +1 -1
  44. package/lib/compiler/define.js +1103 -0
  45. package/lib/compiler/extend.js +983 -0
  46. package/lib/compiler/finalize-parse-cdl.js +231 -0
  47. package/lib/compiler/index.js +46 -39
  48. package/lib/compiler/kick-start.js +190 -0
  49. package/lib/compiler/moduleLayers.js +4 -4
  50. package/lib/compiler/populate.js +1226 -0
  51. package/lib/compiler/propagator.js +113 -47
  52. package/lib/compiler/resolve.js +1433 -0
  53. package/lib/compiler/shared.js +100 -65
  54. package/lib/compiler/tweak-assocs.js +529 -0
  55. package/lib/compiler/utils.js +215 -33
  56. package/lib/edm/.eslintrc.json +5 -0
  57. package/lib/edm/annotations/genericTranslation.js +38 -25
  58. package/lib/edm/annotations/preprocessAnnotations.js +3 -3
  59. package/lib/edm/csn2edm.js +10 -9
  60. package/lib/edm/edm.js +19 -20
  61. package/lib/edm/edmPreprocessor.js +166 -95
  62. package/lib/edm/edmUtils.js +127 -34
  63. package/lib/gen/Dictionary.json +92 -43
  64. package/lib/gen/language.checksum +1 -1
  65. package/lib/gen/language.interp +11 -1
  66. package/lib/gen/language.tokens +86 -82
  67. package/lib/gen/languageLexer.interp +18 -1
  68. package/lib/gen/languageLexer.js +925 -847
  69. package/lib/gen/languageLexer.tokens +78 -74
  70. package/lib/gen/languageParser.js +5434 -4298
  71. package/lib/json/from-csn.js +59 -17
  72. package/lib/json/to-csn.js +189 -71
  73. package/lib/language/antlrParser.js +3 -3
  74. package/lib/language/docCommentParser.js +3 -3
  75. package/lib/language/errorStrategy.js +26 -8
  76. package/lib/language/genericAntlrParser.js +144 -53
  77. package/lib/language/language.g4 +424 -200
  78. package/lib/language/multiLineStringParser.js +536 -0
  79. package/lib/main.d.ts +550 -61
  80. package/lib/main.js +38 -11
  81. package/lib/model/api.js +3 -1
  82. package/lib/model/csnRefs.js +322 -198
  83. package/lib/model/csnUtils.js +226 -370
  84. package/lib/model/enrichCsn.js +124 -69
  85. package/lib/model/revealInternalProperties.js +29 -7
  86. package/lib/model/sortViews.js +10 -2
  87. package/lib/modelCompare/compare.js +17 -12
  88. package/lib/optionProcessor.js +8 -3
  89. package/lib/render/.eslintrc.json +1 -2
  90. package/lib/render/DuplicateChecker.js +1 -1
  91. package/lib/render/manageConstraints.js +36 -33
  92. package/lib/render/toCdl.js +174 -275
  93. package/lib/render/toHdbcds.js +203 -122
  94. package/lib/render/toRename.js +7 -10
  95. package/lib/render/toSql.js +161 -82
  96. package/lib/render/utils/common.js +22 -8
  97. package/lib/render/utils/sql.js +10 -7
  98. package/lib/render/utils/stringEscapes.js +111 -0
  99. package/lib/sql-identifier.js +1 -1
  100. package/lib/transform/.eslintrc.json +5 -0
  101. package/lib/transform/braceExpression.js +4 -2
  102. package/lib/transform/db/.eslintrc.json +2 -0
  103. package/lib/transform/db/applyTransformations.js +212 -0
  104. package/lib/transform/db/assertUnique.js +1 -1
  105. package/lib/transform/db/associations.js +187 -0
  106. package/lib/transform/db/cdsPersistence.js +150 -0
  107. package/lib/transform/db/constraints.js +61 -56
  108. package/lib/transform/db/expansion.js +50 -29
  109. package/lib/transform/db/flattening.js +556 -106
  110. package/lib/transform/db/groupByOrderBy.js +3 -1
  111. package/lib/transform/db/temporal.js +236 -0
  112. package/lib/transform/db/transformExists.js +103 -28
  113. package/lib/transform/db/views.js +92 -44
  114. package/lib/transform/draft/.eslintrc.json +38 -0
  115. package/lib/transform/{db/draft.js → draft/db.js} +9 -7
  116. package/lib/transform/draft/odata.js +227 -0
  117. package/lib/transform/forHanaNew.js +98 -783
  118. package/lib/transform/forOdataNew.js +22 -175
  119. package/lib/transform/localized.js +36 -32
  120. package/lib/transform/odata/generateForeignKeyElements.js +3 -3
  121. package/lib/transform/odata/referenceFlattener.js +95 -89
  122. package/lib/transform/odata/structureFlattener.js +1 -1
  123. package/lib/transform/odata/toFinalBaseType.js +86 -12
  124. package/lib/transform/odata/typesExposure.js +5 -5
  125. package/lib/transform/odata/utils.js +2 -2
  126. package/lib/transform/transformUtilsNew.js +47 -33
  127. package/lib/transform/translateAssocsToJoins.js +13 -30
  128. package/lib/transform/universalCsn/.eslintrc.json +36 -0
  129. package/lib/transform/universalCsn/coreComputed.js +170 -0
  130. package/lib/transform/universalCsn/universalCsnEnricher.js +715 -0
  131. package/lib/transform/universalCsn/utils.js +63 -0
  132. package/lib/utils/file.js +8 -3
  133. package/lib/utils/objectUtils.js +30 -0
  134. package/lib/utils/timetrace.js +8 -2
  135. package/package.json +1 -1
  136. package/share/messages/README.md +26 -0
  137. package/lib/compiler/definer.js +0 -2349
  138. package/lib/compiler/resolver.js +0 -2922
  139. package/lib/transform/db/helpers.js +0 -58
  140. package/lib/transform/universalCsnEnricher.js +0 -67
@@ -70,22 +70,19 @@ function toRenameDdl(csn, options) {
70
70
 
71
71
  resultStr += Object.keys(art.elements).map((name) => {
72
72
  const e = art.elements[name];
73
- let result = '';
73
+ let str = '';
74
74
 
75
75
  const beforeColumnName = quoteSqlId(name);
76
76
  const afterColumnName = plainSqlId(name);
77
77
 
78
78
  if (!e._ignore) {
79
- if (e.target) {
80
- resultStr += ' ';
81
- result = ` EXEC 'ALTER TABLE ${afterTableName} DROP ASSOCIATION ${beforeColumnName}';\n`;
82
- }
83
- else if (beforeColumnName !== afterColumnName) {
84
- resultStr += ' ';
85
- result = ` EXEC 'RENAME COLUMN ${afterTableName}.${beforeColumnName} TO ${afterColumnName}';\n`;
86
- }
79
+ if (e.target)
80
+ str = ` EXEC 'ALTER TABLE ${afterTableName} DROP ASSOCIATION ${beforeColumnName}';\n`;
81
+
82
+ else if (beforeColumnName !== afterColumnName)
83
+ str = ` EXEC 'RENAME COLUMN ${afterTableName}.${beforeColumnName} TO ${afterColumnName}';\n`;
87
84
  }
88
- return result;
85
+ return str;
89
86
  }).join('');
90
87
  }
91
88
  return resultStr;
@@ -8,6 +8,7 @@ const {
8
8
  } = require('../model/csnUtils');
9
9
  const {
10
10
  renderFunc, beautifyExprArray, cdsToSqlTypes, getHanaComment, hasHanaComment,
11
+ getSqlSnippets,
11
12
  } = require('./utils/common');
12
13
  const {
13
14
  renderReferentialConstraint, getIdentifierUtils,
@@ -19,6 +20,8 @@ const { timetrace } = require('../utils/timetrace');
19
20
  const { isBetaEnabled, isDeprecatedEnabled } = require('../base/model');
20
21
  const { smartFuncId } = require('../sql-identifier');
21
22
  const { sortCsn } = require('../json/to-csn');
23
+ const { manageConstraints } = require('./manageConstraints');
24
+ const { ModelError } = require('../base/error');
22
25
 
23
26
 
24
27
  /**
@@ -170,6 +173,12 @@ function toSqlDdl(csn, options) {
170
173
  comment(comment) {
171
174
  return comment && `'${comment.replace(/'/g, '\'\'')}'` || 'NULL';
172
175
  },
176
+ /*
177
+ Alter SQL snippet for entity.
178
+ */
179
+ alterEntitySqlSnippet(tableName, snippet) {
180
+ return [ `ALTER TABLE ${tableName} ${snippet};` ];
181
+ },
173
182
  /*
174
183
  Concatenate multiple statements which are to be treated as one by the API caller.
175
184
  */
@@ -185,10 +194,10 @@ function toSqlDdl(csn, options) {
185
194
  checkCSNVersion(csn, options);
186
195
 
187
196
  // The final result in hdb-kind-specific form, without leading CREATE, without trailing newlines
188
- // (note that the order here is relevant for transmission into 'resultObj.sql' below and that
197
+ // (note that the order here is relevant for transmission into 'mainResultObj.sql' below and that
189
198
  // the attribute names must be the HDI plugin names for --src hdi)
190
199
  // The result object may have a `sql` dictionary for `toSql`.
191
- const resultObj = {
200
+ const mainResultObj = {
192
201
  hdbtabletype: Object.create(null),
193
202
  hdbtable: Object.create(null),
194
203
  hdbindex: Object.create(null),
@@ -214,13 +223,12 @@ function toSqlDdl(csn, options) {
214
223
  // Current indentation string
215
224
  indent: '',
216
225
  };
217
- renderArtifactInto(artifactName, artifact, resultObj, env);
226
+ renderArtifactInto(artifactName, artifact, mainResultObj, env);
218
227
  });
219
228
 
220
229
  // Render each deleted artifact
221
230
  for (const artifactName in csn.deletions)
222
- renderArtifactDeletionInto(artifactName, csn.deletions[artifactName], resultObj);
223
-
231
+ renderArtifactDeletionInto(artifactName, csn.deletions[artifactName], mainResultObj);
224
232
 
225
233
  // Render each artifact extension
226
234
  // Only HANA SQL is currently supported.
@@ -231,7 +239,7 @@ function toSqlDdl(csn, options) {
231
239
  const artifactName = extension.extend;
232
240
  const _artifact = csn.definitions[artifactName];
233
241
  const env = { indent: '', _artifact };
234
- renderArtifactExtensionInto(artifactName, _artifact, extension, resultObj, env);
242
+ renderArtifactExtensionInto(artifactName, _artifact, extension, mainResultObj, env);
235
243
  }
236
244
  }
237
245
  }
@@ -244,7 +252,7 @@ function toSqlDdl(csn, options) {
244
252
  const artifactName = migration.migrate;
245
253
  const _artifact = csn.definitions[artifactName];
246
254
  const env = { indent: '', _artifact };
247
- renderArtifactMigrationInto(artifactName, migration, resultObj, env);
255
+ renderArtifactMigrationInto(artifactName, migration, mainResultObj, env);
248
256
  }
249
257
  }
250
258
  }
@@ -265,34 +273,42 @@ function toSqlDdl(csn, options) {
265
273
 
266
274
  // Handle hdbKinds separately from alterTable case
267
275
  // eslint-disable-next-line no-unused-vars
268
- const { deletions, migrations, ...hdbKinds } = resultObj;
276
+ const { deletions, migrations: _, ...hdbKinds } = mainResultObj;
269
277
  for (const hdbKind of Object.keys(hdbKinds)) {
270
- for (const name in resultObj[hdbKind]) {
278
+ for (const name in mainResultObj[hdbKind]) {
271
279
  if (options.toSql.src === 'sql') {
272
- let sourceString = resultObj[hdbKind][name];
280
+ let sourceString = mainResultObj[hdbKind][name];
273
281
  // Hack: Other than in 'hdbtable' files, in HANA SQL COLUMN is not mandatory but default.
274
282
  if (options.toSql.dialect === 'hana' && hdbKind === 'hdbtable' && sourceString.startsWith('COLUMN '))
275
283
  sourceString = sourceString.slice('COLUMN '.length);
276
-
277
284
  sql[name] = `${options.testMode ? '' : sqlVersionLine}CREATE ${sourceString};`;
278
285
  }
279
286
  else if (!options.testMode) {
280
- resultObj[hdbKind][name] = sqlVersionLine + resultObj[hdbKind][name];
287
+ mainResultObj[hdbKind][name] = sqlVersionLine + mainResultObj[hdbKind][name];
281
288
  }
282
289
  }
283
290
  if (options.toSql.src === 'sql')
284
- delete resultObj[hdbKind];
291
+ delete mainResultObj[hdbKind];
292
+ }
293
+
294
+ // add `ALTER TABLE ADD CONSTRAINT` statements per default for `to.sql` w/ dialect `hana`
295
+ if (!options.constraintsInCreateTable && options.src === 'sql' && options.sqlDialect === 'hana') {
296
+ const alterStmts = manageConstraints(csn, options);
297
+
298
+ for ( const constraintName of Object.keys(alterStmts))
299
+ sql[constraintName] = `${options.testMode ? '' : sqlVersionLine}${alterStmts[constraintName]}`;
300
+ mainResultObj.sql = sql;
285
301
  }
286
302
 
287
303
  if (options.toSql.src === 'sql')
288
- resultObj.sql = sql;
304
+ mainResultObj.sql = sql;
289
305
 
290
306
  for (const name in deletions)
291
307
  deletions[name] = `${options.testMode ? '' : sqlVersionLine}${deletions[name]}`;
292
308
 
293
309
 
294
310
  timetrace.stop();
295
- return resultObj;
311
+ return mainResultObj;
296
312
 
297
313
  /**
298
314
  * Render an artifact into the appropriate dictionary of 'resultObj'.
@@ -309,7 +325,6 @@ function toSqlDdl(csn, options) {
309
325
 
310
326
  switch (art.kind) {
311
327
  case 'entity':
312
- case 'view':
313
328
  if (getNormalizedQuery(art).query) {
314
329
  const result = renderView(artifactName, art, env);
315
330
  if (result)
@@ -331,7 +346,7 @@ function toSqlDdl(csn, options) {
331
346
  // Ignore: not SQL-relevant
332
347
  return;
333
348
  default:
334
- throw new Error(`Unknown artifact kind: ${art.kind}`);
349
+ throw new ModelError(`Unknown artifact kind: ${art.kind}`);
335
350
  }
336
351
  }
337
352
 
@@ -352,7 +367,7 @@ function toSqlDdl(csn, options) {
352
367
  renderExtendInto(artifactName, artifact.elements, ext.elements, resultObj, env, extensionsDuplicateChecker);
353
368
 
354
369
  if (!artifactName)
355
- throw new Error(`Undefined artifact name: ${artifactName}`);
370
+ throw new ModelError(`Undefined artifact name: ${artifactName}`);
356
371
  }
357
372
 
358
373
  // Render an artifact deletion into the appropriate dictionary of 'resultObj'.
@@ -392,12 +407,26 @@ function toSqlDdl(csn, options) {
392
407
  ? renderAssociationElement(eltName, defVariant, env)
393
408
  : renderElement(artifactName, eltName, defVariant, null, null, env);
394
409
  }
395
- function getEltStrNoProp(defVariant, prop, eltName) {
396
- const defNoProp = Object.assign({}, defVariant);
397
- delete defNoProp[prop];
398
- return getEltStr(defNoProp, eltName);
410
+ function getEltStrNoProps(defVariant, eltName, ...props) {
411
+ const defNoProps = Object.assign({}, defVariant);
412
+ for (const prop of props)
413
+ delete defNoProps[prop];
414
+ return getEltStr(defNoProps, eltName);
415
+ }
416
+ function oldAnnoChangedIncompatibly(defOld, defNew) {
417
+ return typeof defOld === 'string' && defOld.trim().length && !(typeof defNew === 'string' && defNew.trim().startsWith(`${defOld.trim()} `));
418
+ }
419
+ function getUnknownSqlReason(anno, artifactName, defOld, defNew, eltName) {
420
+ const changeKind = defNew === undefined
421
+ ? `removed (previous value: ${JSON.stringify(defOld)})`
422
+ : `changed from ${JSON.stringify(defOld)} to ${JSON.stringify(defNew)}`;
423
+ return eltName
424
+ ? `annotation ${anno} of element ${artifactName}:${eltName} has been ${changeKind}`
425
+ : `annotation ${anno} of artifact ${artifactName} has been ${changeKind}`;
399
426
  }
400
427
 
428
+ const sqlSnippetAnnos = [ '@sql.prepend', '@sql.append' ];
429
+
401
430
  const tableName = renderArtifactName(artifactName);
402
431
 
403
432
  // Change entity properties
@@ -407,6 +436,15 @@ function toSqlDdl(csn, options) {
407
436
  const alterComment = render.alterEntityComment(tableName, def.new);
408
437
  addMigration(resultObj, artifactName, false, alterComment);
409
438
  }
439
+ else if (sqlSnippetAnnos.includes(prop)) { // NOTE: @sql.replace may be supported in the future
440
+ if (oldAnnoChangedIncompatibly(def.old, def.new)) {
441
+ // anno was previously set and current change is not simply an appendix → previous anno would have to be reverted → unknown SQL
442
+ addMigration(resultObj, artifactName, false, null, getUnknownSqlReason(prop, artifactName, def.old, def.new));
443
+ }
444
+ else {
445
+ addMigration(resultObj, artifactName, false, render.alterEntitySqlSnippet(tableName, def.new));
446
+ }
447
+ }
410
448
  }
411
449
  }
412
450
 
@@ -441,9 +479,28 @@ function toSqlDdl(csn, options) {
441
479
  if (eltStrNew === eltStrOld)
442
480
  return; // Prevent spurious migrations, where the column DDL does not change.
443
481
 
482
+ const annosIncompat = [];
483
+ sqlSnippetAnnos
484
+ .filter(anno => def.old[anno] !== def.new[anno])
485
+ .forEach((anno) => { // NOTE: @sql.replace may be supported in the future
486
+ if (oldAnnoChangedIncompatibly(def.old[anno], def.new[anno])) {
487
+ annosIncompat.push(anno);
488
+ // anno was previously set and current change is not simply an appendix → previous anno would have to be reverted → unknown SQL
489
+ addMigration(resultObj, artifactName, false, null, getUnknownSqlReason(anno, artifactName, def.old[anno], def.new[anno], eltName));
490
+ }
491
+ });
492
+
493
+ if (annosIncompat.length) {
494
+ const eltStrOldNoAnnos = getEltStrNoProps(def.old, eltName, ...annosIncompat);
495
+ const eltStrNewNoAnnos = getEltStrNoProps(def.new, eltName, ...annosIncompat);
496
+ if (eltStrOldNoAnnos === eltStrNewNoAnnos) { // only incompatibly-changed annos were modified
497
+ continue;
498
+ }
499
+ }
500
+
444
501
  if (def.old.doc !== def.new.doc) {
445
- const eltStrOldNoDoc = getEltStrNoProp(def.old, 'doc', eltName);
446
- const eltStrNewNoDoc = getEltStrNoProp(def.new, 'doc', eltName);
502
+ const eltStrOldNoDoc = getEltStrNoProps(def.old, eltName, 'doc');
503
+ const eltStrNewNoDoc = getEltStrNoProps(def.new, eltName, 'doc');
447
504
  if (eltStrOldNoDoc === eltStrNewNoDoc) { // only `doc` changed
448
505
  const alterComment = render.alterColumnComment(tableName, sqlId, def.new.doc);
449
506
  addMigration(resultObj, artifactName, false, alterComment);
@@ -483,14 +540,16 @@ function toSqlDdl(csn, options) {
483
540
  env._artifact = art;
484
541
  const childEnv = increaseIndent(env);
485
542
  const hanaTc = art.technicalConfig && art.technicalConfig.hana;
486
- let result = '';
543
+ // tables can have @sql.prepend and @sql.append
544
+ const { front, back } = getSqlSnippets(options, art);
545
+ let result = front;
487
546
  // Only HANA has row/column tables
488
547
  if (options.toSql.dialect === 'hana') {
489
548
  if (hanaTc && hanaTc.storeType) {
490
549
  // Explicitly specified
491
550
  result += `${art.technicalConfig.hana.storeType.toUpperCase()} `;
492
551
  }
493
- else {
552
+ else if (!front) {
494
553
  // in 'hdbtable' files, COLUMN or ROW is mandatory, and COLUMN is the default
495
554
  result += 'COLUMN ';
496
555
  }
@@ -500,13 +559,9 @@ function toSqlDdl(csn, options) {
500
559
  result += `TABLE ${tableName}`;
501
560
  result += ' (\n';
502
561
  const elements = Object.keys(art.elements).map(eltName => renderElement(artifactName, eltName, art.elements[eltName], definitionsDuplicateChecker, getFzIndex(eltName, hanaTc), childEnv)).filter(s => s !== '').join(',\n');
503
- if (elements !== '') {
562
+ if (elements !== '')
504
563
  result += elements;
505
- }
506
- else {
507
- // TODO: Already be handled by 'empty-entity' reclassification; better location
508
- error(null, [ 'definitions', artifactName ], 'Entities must have at least one element that is non-virtual');
509
- }
564
+
510
565
  const uniqueFields = Object.keys(art.elements).filter(name => art.elements[name].unique && !art.elements[name].virtual)
511
566
  .map(name => quoteSqlId(name))
512
567
  .join(', ');
@@ -517,7 +572,9 @@ function toSqlDdl(csn, options) {
517
572
  if (primaryKeys !== '')
518
573
  result += `,\n${childEnv.indent}${primaryKeys}`;
519
574
 
520
- if (art.$tableConstraints && art.$tableConstraints.referential) {
575
+ // for `to.sql` w/ dialect `hana` the constraints will be part of the
576
+ const constraintsAsAlter = !options.constraintsInCreateTable && options.src === 'sql' && options.sqlDialect === 'hana';
577
+ if ( !constraintsAsAlter && art.$tableConstraints && art.$tableConstraints.referential) {
521
578
  const renderReferentialConstraintsAsHdbconstraint = options.toSql.src === 'hdi';
522
579
  const referentialConstraints = {};
523
580
  Object.entries(art.$tableConstraints.referential)
@@ -570,6 +627,9 @@ function toSqlDdl(csn, options) {
570
627
  if (options.toSql.dialect === 'hana' && hasHanaComment(art, options))
571
628
  result += ` COMMENT '${getHanaComment(art)}'`;
572
629
 
630
+ if (back)
631
+ result += back;
632
+
573
633
  resultObj.hdbtable[artifactName] = result;
574
634
  }
575
635
 
@@ -601,16 +661,21 @@ function toSqlDdl(csn, options) {
601
661
  }
602
662
  }
603
663
 
604
- function addMigration(resultObj, artifactName, drop, sqlArray) {
664
+ function addMigration(resultObj, artifactName, drop, sqlArray, description) {
605
665
  if (!(artifactName in resultObj.migrations))
606
666
  resultObj.migrations[artifactName] = [];
607
667
 
608
- const migrations = sqlArray.map(sql => ({ drop, sql }));
668
+ if (!sqlArray) {
669
+ if (description)
670
+ resultObj.migrations[artifactName].push({ description });
671
+ return;
672
+ }
673
+ const migrations = sqlArray.map(migrationSql => ({ drop, sql: migrationSql }));
609
674
  resultObj.migrations[artifactName].push(...migrations);
610
675
  }
611
676
 
612
- function addDeletion(resultObj, artifactName, sql) {
613
- resultObj.deletions[artifactName] = sql;
677
+ function addDeletion(resultObj, artifactName, deletionSql) {
678
+ resultObj.deletions[artifactName] = deletionSql;
614
679
  }
615
680
 
616
681
  /**
@@ -667,7 +732,13 @@ function toSqlDdl(csn, options) {
667
732
  if (fzindex && options.toSql.dialect === 'hana')
668
733
  result += ` ${renderExpr(fzindex, env)}`;
669
734
 
670
- if (options.toSql.dialect === 'hana' && hasHanaComment(elm, options, env._artifact))
735
+ // (table) elements can only have a @sql.append
736
+ const { back } = getSqlSnippets(options, elm);
737
+
738
+ if (back !== '') // Needs to be rendered before the COMMENT
739
+ result += back;
740
+
741
+ if (options.toSql.dialect === 'hana' && hasHanaComment(elm, options))
671
742
  result += ` COMMENT '${getHanaComment(elm)}'`;
672
743
 
673
744
  return result;
@@ -734,7 +805,7 @@ function toSqlDdl(csn, options) {
734
805
  // This also affects renderIndexes
735
806
  tc = tc.hana;
736
807
  if (!tc)
737
- throw new Error('Expecting a HANA technical configuration');
808
+ throw new ModelError('Expecting a HANA technical configuration');
738
809
 
739
810
  if (tc.tableSuffix) {
740
811
  // Although we could just render the whole bandwurm as one stream of tokens, the
@@ -803,7 +874,7 @@ function toSqlDdl(csn, options) {
803
874
  const i = index.indexOf('index');
804
875
  const j = index.indexOf('(');
805
876
  if (i > index.length - 2 || !index[i + 1].ref || j < i || j > index.length - 2)
806
- throw new Error(`Unexpected form of index: "${index}"`);
877
+ throw new ModelError(`Unexpected form of index: "${index}"`);
807
878
 
808
879
  let indexName = renderArtifactName(`${artifactName}.${index[i + 1].ref}`);
809
880
  if (options.toSql.names === 'plain')
@@ -859,7 +930,7 @@ function toSqlDdl(csn, options) {
859
930
 
860
931
  // Sanity check
861
932
  if (!source.ref)
862
- throw new Error(`Expecting ref in ${JSON.stringify(source)}`);
933
+ throw new ModelError(`Expecting ref in ${JSON.stringify(source)}`);
863
934
 
864
935
  return renderAbsolutePathWithAlias(artifactName, source, env);
865
936
  }
@@ -901,7 +972,7 @@ function toSqlDdl(csn, options) {
901
972
  function renderAbsolutePathWithAlias(artifactName, path, env) {
902
973
  // This actually can't happen anymore because assoc2joins should have taken care of it
903
974
  if (path.ref[0].where)
904
- throw new Error(`"${artifactName}": Filters in FROM are not supported for conversion to SQL`);
975
+ throw new ModelError(`"${artifactName}": Filters in FROM are not supported for conversion to SQL`);
905
976
 
906
977
 
907
978
  // SQL needs a ':' after path.ref[0] to separate associations
@@ -939,7 +1010,7 @@ function toSqlDdl(csn, options) {
939
1010
  function renderAbsolutePath(path, sep, env) {
940
1011
  // Sanity checks
941
1012
  if (!path.ref)
942
- throw new Error(`Expecting ref in path: ${JSON.stringify(path)}`);
1013
+ throw new ModelError(`Expecting ref in path: ${JSON.stringify(path)}`);
943
1014
 
944
1015
  // Determine the absolute name of the first artifact on the path (before any associations or element traversals)
945
1016
  const firstArtifactName = path.ref[0].id || path.ref[0];
@@ -956,7 +1027,7 @@ function toSqlDdl(csn, options) {
956
1027
  if (ref && ref.params) {
957
1028
  result += `(${renderArgs(path.ref[0] || {}, '=>', env, syntax)})`;
958
1029
  }
959
- else if ([ 'udf' ].includes(syntax)) {
1030
+ else if (syntax === 'udf') {
960
1031
  // if syntax is user defined function, render empty argument list
961
1032
  // CV without parameters is called as simple view
962
1033
  result += '()';
@@ -979,7 +1050,7 @@ function toSqlDdl(csn, options) {
979
1050
  * @param {object} node with `args` to render
980
1051
  * @param {string} sep Separator between args
981
1052
  * @param {object} env Render environment
982
- * @param {string} syntax Some magic A2J paramter - for calcview parameter rendering
1053
+ * @param {string|null} syntax Some magic A2J parameter - for calcview parameter rendering
983
1054
  * @returns {string} Rendered arguments
984
1055
  * @throws Throws if args is not an array or object.
985
1056
  */
@@ -987,29 +1058,28 @@ function toSqlDdl(csn, options) {
987
1058
  const args = node.args ? node.args : {};
988
1059
  // Positional arguments
989
1060
  if (Array.isArray(args))
990
- return args.map(arg => renderExpr(arg, env)).join(', ');
1061
+ return args.map(arg => renderExpr(arg, env, true, false, true)).join(', ');
991
1062
 
992
1063
  // Named arguments (object/dict)
993
1064
  else if (typeof args === 'object')
994
1065
  // 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(', ');
1066
+ return Object.keys(args).map(key => `${node.func ? key : decorateParameter(key, syntax)} ${sep} ${renderExpr(args[key], env, true, false, true)}`).join(', ');
996
1067
 
997
1068
 
998
- throw new Error(`Unknown args: ${JSON.stringify(args)}`);
1069
+ throw new ModelError(`Unknown args: ${JSON.stringify(args)}`);
999
1070
 
1000
1071
 
1001
1072
  /**
1002
1073
  * Render the given argument/parameter correctly.
1003
1074
  *
1004
1075
  * @param {string} arg Argument to render
1005
- * @param {string} syntax Some magic A2J paramter - for calcview parameter rendering
1076
+ * @param {string|null} parameterSyntax Some magic A2J parameter - for calcview parameter rendering
1006
1077
  * @returns {string} Rendered argument
1007
1078
  */
1008
- function decorateParameter(arg, syntax) {
1009
- if (syntax === 'calcview')
1079
+ function decorateParameter(arg, parameterSyntax) {
1080
+ if (parameterSyntax === 'calcview')
1010
1081
  return `PLACEHOLDER."$$${arg}$$"`;
1011
1082
 
1012
-
1013
1083
  return quoteSqlId(arg);
1014
1084
  }
1015
1085
  }
@@ -1070,6 +1140,11 @@ function toSqlDdl(csn, options) {
1070
1140
  result += `${env.indent})`;
1071
1141
  }
1072
1142
 
1143
+ // views can only have a @sql.append
1144
+ const { back } = getSqlSnippets(options, art);
1145
+ if (back)
1146
+ result += back;
1147
+
1073
1148
  return result;
1074
1149
  }
1075
1150
 
@@ -1077,7 +1152,7 @@ function toSqlDdl(csn, options) {
1077
1152
  * Render the parameter definition of a view if any. Return the parameters in parentheses, or an empty string
1078
1153
  *
1079
1154
  * @param {string} artifactName Name of the view
1080
- * @param {Array} params Array of parameters
1155
+ * @param {Object} params Dictionary of parameters
1081
1156
  * @returns {string} Rendered parameters
1082
1157
  */
1083
1158
  function renderParameterDefinitions(artifactName, params) {
@@ -1087,7 +1162,7 @@ function toSqlDdl(csn, options) {
1087
1162
  for (const pn in params) {
1088
1163
  const p = params[pn];
1089
1164
  if (p.notNull === true || p.notNull === false)
1090
- info(null, [ 'definitions', artifactName, 'params', pn ], 'Not Null constraints on SQL view parameters are not allowed and are ignored');
1165
+ info('query-ignoring-param-nullability', [ 'definitions', artifactName, 'params', pn ], { '#': 'sql' });
1091
1166
  // do not quote parameter identifiers for naming mode "quoted" / "hdbcds"
1092
1167
  // this would be an incompatible change, as non-uppercased, quoted identifiers
1093
1168
  // are rejected by the HANA compiler.
@@ -1145,7 +1220,7 @@ function toSqlDdl(csn, options) {
1145
1220
  }
1146
1221
  // Otherwise must have a SELECT
1147
1222
  else if (!query.SELECT) {
1148
- throw new Error(`Unexpected query operation ${JSON.stringify(query)}`);
1223
+ throw new ModelError(`Unexpected query operation ${JSON.stringify(query)}`);
1149
1224
  }
1150
1225
  const select = query.SELECT;
1151
1226
  const childEnv = increaseIndent(env);
@@ -1161,7 +1236,7 @@ function toSqlDdl(csn, options) {
1161
1236
  result += `\n${env.indent}WHERE ${renderExpr(select.where, env, true, true)}`;
1162
1237
 
1163
1238
  if (select.groupBy)
1164
- result += `\n${env.indent}GROUP BY ${select.groupBy.map(expr => renderExpr(expr, env)).join(', ')}`;
1239
+ result += `\n${env.indent}GROUP BY ${select.groupBy.map(expr => renderExpr(expr, env, true, false, true)).join(', ')}`;
1165
1240
 
1166
1241
  if (select.having)
1167
1242
  result += `\n${env.indent}HAVING ${renderExpr(select.having, env, true, true)}`;
@@ -1212,7 +1287,7 @@ function toSqlDdl(csn, options) {
1212
1287
  * @returns {string} Rendered ORDER BY entry
1213
1288
  */
1214
1289
  function renderOrderByEntry(entry, env) {
1215
- let result = renderExpr(entry, env);
1290
+ let result = renderExpr(entry, env, true, false, true);
1216
1291
  if (entry.sort)
1217
1292
  result += ` ${entry.sort.toUpperCase()}`;
1218
1293
 
@@ -1236,7 +1311,7 @@ function toSqlDdl(csn, options) {
1236
1311
  // Anonymous structured type: Not supported with SQL (but shouldn't happen anyway after forHana flattened them)
1237
1312
  if (!elm.type) {
1238
1313
  if (!elm.elements)
1239
- throw new Error(`Missing type of: ${elementName}`);
1314
+ throw new ModelError(`Missing type of: ${elementName}`);
1240
1315
 
1241
1316
  // TODO: Signal is not covered by tests + better location
1242
1317
  error(null, [ 'definitions', artifactName, 'elements', elementName ],
@@ -1259,7 +1334,7 @@ function toSqlDdl(csn, options) {
1259
1334
  result += renderBuiltinType(elm.type);
1260
1335
  }
1261
1336
  else {
1262
- throw new Error(`Unexpected non-primitive type of: ${artifactName}.${elementName}`);
1337
+ throw new ModelError(`Unexpected non-primitive type of: ${artifactName}.${elementName}`);
1263
1338
  }
1264
1339
  result += renderTypeParameters(elm);
1265
1340
  return result;
@@ -1288,7 +1363,7 @@ function toSqlDdl(csn, options) {
1288
1363
  * Render the nullability of an element or parameter (can be unset, true, or false)
1289
1364
  *
1290
1365
  * @param {object} obj Object to render for
1291
- * @param {boolean} treatKeyAsNotNull Wether to render KEY as not null
1366
+ * @param {boolean} treatKeyAsNotNull Whether to render KEY as not null
1292
1367
  * @returns {string} NULL/NOT NULL or ''
1293
1368
  */
1294
1369
  function renderNullability(obj, treatKeyAsNotNull = false) {
@@ -1333,35 +1408,38 @@ function toSqlDdl(csn, options) {
1333
1408
  * (no trailing LF, don't indent if inline)
1334
1409
  *
1335
1410
  * @todo Reuse this with toCdl
1336
- * @param {Array|object|string} x Expression to render
1411
+ * @param {Array|object|string} expr Expression to render
1337
1412
  * @param {object} env Render environment
1338
- * @param {boolean} inline Wether to render the expression inline
1339
- * @param {boolean} nestedExpr Wether to treat the expression as nested
1413
+ * @param {boolean} [inline=true] Whether to render the expression inline
1414
+ * @param {boolean} [nestedExpr=false] Whether to treat the expression as nested
1415
+ * @param {boolean} [alwaysRenderCast=false] Whether to _always_ render SQL-style casts, even if `nestedExpr === false`.
1416
+ * Note: This is a hack for casts() inside groupBy.
1340
1417
  * @returns {string} Rendered expression
1341
1418
  */
1342
- function renderExpr(x, env, inline = true, nestedExpr = false) {
1419
+ function renderExpr(expr, env, inline = true, nestedExpr = false, alwaysRenderCast = false) {
1343
1420
  // Compound expression
1344
- if (Array.isArray(x)) {
1345
- const tokens = x.map(item => renderExpr(item, env, inline, nestedExpr));
1421
+ if (Array.isArray(expr)) {
1422
+ const tokens = expr.map(item => renderExpr(item, env, inline, nestedExpr));
1346
1423
  return beautifyExprArray(tokens);
1347
1424
  }
1348
- else if (typeof x === 'object' && x !== null) {
1349
- if (nestedExpr && x.cast && x.cast.type)
1350
- return renderExplicitTypeCast(renderExprObject());
1351
- return renderExprObject();
1425
+ else if (typeof expr === 'object' && expr !== null) {
1426
+ if ((nestedExpr || alwaysRenderCast) && expr.cast && expr.cast.type)
1427
+ return renderExplicitTypeCast(expr, renderExprObject(expr));
1428
+ return renderExprObject(expr);
1352
1429
  }
1353
1430
  // Not a literal value but part of an operator, function etc - just leave as it is
1354
1431
  // FIXME: For the sake of simplicity, we should get away from all this uppercasing in toSql
1355
1432
 
1356
- return String(x).toUpperCase();
1433
+ return String(expr).toUpperCase();
1357
1434
 
1358
1435
 
1359
1436
  /**
1360
1437
  * Various special cases represented as objects
1361
1438
  *
1439
+ * @param {object} x Expression
1362
1440
  * @returns {string} String representation of the expression
1363
1441
  */
1364
- function renderExprObject() {
1442
+ function renderExprObject(x) {
1365
1443
  if (x.list) {
1366
1444
  return `(${x.list.map(item => renderExpr(item)).join(', ')})`;
1367
1445
  }
@@ -1404,13 +1482,13 @@ function toSqlDdl(csn, options) {
1404
1482
  return `${renderQuery('<union>', x, increaseIndent(env))}`;
1405
1483
  }
1406
1484
 
1407
- throw new Error(`Unknown expression: ${JSON.stringify(x)}`);
1485
+ throw new ModelError(`Unknown expression: ${JSON.stringify(x)}`);
1408
1486
  }
1409
1487
 
1410
- function renderWindowFunction(funcName, node, env) {
1411
- const suffix = node.xpr.shift(); // OVER
1412
- let r = `${funcName}(${renderArgs(node, '=>', env, null)})`;
1413
- r += ` ${suffix} (${renderExpr(node.xpr, env)})`;
1488
+ function renderWindowFunction(funcName, node, fctEnv) {
1489
+ const suffix = node.xpr[0]; // OVER
1490
+ let r = `${funcName}(${renderArgs(node, '=>', fctEnv, null)})`;
1491
+ r += ` ${suffix} (${renderExpr(node.xpr.slice(1), fctEnv)})`; // do not pass suffix in renderExpr
1414
1492
  return r;
1415
1493
  }
1416
1494
 
@@ -1444,7 +1522,7 @@ function toSqlDdl(csn, options) {
1444
1522
 
1445
1523
  // otherwise fall through to
1446
1524
  default:
1447
- throw new Error(`Unknown literal or type: ${JSON.stringify(x)}`);
1525
+ throw new ModelError(`Unknown literal or type: ${JSON.stringify(x)}`);
1448
1526
  }
1449
1527
  }
1450
1528
 
@@ -1500,7 +1578,7 @@ function toSqlDdl(csn, options) {
1500
1578
  return `'${options.toSql.user.id}'`;
1501
1579
 
1502
1580
  if (options.toSql.dialect === 'sqlite' || options.toSql.dialect === 'plain') {
1503
- warning(null, null, 'The "$user" variable is not supported. Use the "toSql.user" option to set a value for "$user.id"');
1581
+ warning(null, null, 'The "$user" variable is not supported. Use option "variableReplacements" to specify a value for "$user.id"');
1504
1582
  return '\'$user.id\'';
1505
1583
  }
1506
1584
  return 'SESSION_CONTEXT(\'APPLICATIONUSER\')';
@@ -1565,10 +1643,11 @@ function toSqlDdl(csn, options) {
1565
1643
  /**
1566
1644
  * Renders an explicit `cast()` inside an 'xpr'.
1567
1645
  *
1646
+ * @param {object} x Expression with cast
1568
1647
  * @param {string} value Value to cast
1569
1648
  * @returns {string} CAST statement
1570
1649
  */
1571
- function renderExplicitTypeCast(value) {
1650
+ function renderExplicitTypeCast(x, value) {
1572
1651
  const typeRef = renderBuiltinType(x.cast.type) + renderTypeParameters(x.cast);
1573
1652
  return `CAST(${value} AS ${typeRef})`;
1574
1653
  }
@@ -1606,7 +1685,7 @@ function toSqlDdl(csn, options) {
1606
1685
  else if (typeof s === 'object') {
1607
1686
  // Sanity check
1608
1687
  if (!s.func && !s.id)
1609
- throw new Error(`Unknown path step object: ${JSON.stringify(s)}`);
1688
+ throw new ModelError(`Unknown path step object: ${JSON.stringify(s)}`);
1610
1689
 
1611
1690
  // Not really a path step but an object-like function call
1612
1691
  if (s.func)
@@ -1626,7 +1705,7 @@ function toSqlDdl(csn, options) {
1626
1705
  return result;
1627
1706
  }
1628
1707
 
1629
- throw new Error(`Unknown path step: ${JSON.stringify(s)}`);
1708
+ throw new ModelError(`Unknown path step: ${JSON.stringify(s)}`);
1630
1709
  }
1631
1710
  }
1632
1711