@sap/cds-compiler 2.11.4 → 2.13.8

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 (133) hide show
  1. package/CHANGELOG.md +159 -1
  2. package/bin/cds_update_identifiers.js +7 -7
  3. package/bin/cdsc.js +22 -23
  4. package/bin/cdsse.js +2 -2
  5. package/doc/CHANGELOG_ARCHIVE.md +1 -1
  6. package/doc/CHANGELOG_BETA.md +25 -6
  7. package/doc/CHANGELOG_DEPRECATED.md +22 -6
  8. package/doc/NameResolution.md +21 -16
  9. package/lib/api/main.js +30 -63
  10. package/lib/api/options.js +5 -5
  11. package/lib/api/validate.js +0 -5
  12. package/lib/backends.js +15 -23
  13. package/lib/base/dictionaries.js +0 -8
  14. package/lib/base/error.js +26 -0
  15. package/lib/base/keywords.js +7 -17
  16. package/lib/base/location.js +9 -4
  17. package/lib/base/message-registry.js +52 -2
  18. package/lib/base/messages.js +16 -26
  19. package/lib/base/model.js +2 -62
  20. package/lib/base/optionProcessorHelper.js +246 -183
  21. package/lib/checks/.eslintrc.json +2 -0
  22. package/lib/checks/actionsFunctions.js +2 -1
  23. package/lib/checks/annotationsOData.js +1 -1
  24. package/lib/checks/cdsPersistence.js +2 -1
  25. package/lib/checks/enricher.js +17 -1
  26. package/lib/checks/foreignKeys.js +4 -4
  27. package/lib/checks/invalidTarget.js +3 -1
  28. package/lib/checks/managedInType.js +4 -4
  29. package/lib/checks/managedWithoutKeys.js +3 -1
  30. package/lib/checks/queryNoDbArtifacts.js +1 -3
  31. package/lib/checks/selectItems.js +4 -4
  32. package/lib/checks/sql-snippets.js +94 -0
  33. package/lib/checks/types.js +1 -1
  34. package/lib/checks/validator.js +12 -7
  35. package/lib/compiler/assert-consistency.js +10 -6
  36. package/lib/compiler/base.js +0 -1
  37. package/lib/compiler/builtins.js +8 -6
  38. package/lib/compiler/checks.js +46 -12
  39. package/lib/compiler/cycle-detector.js +1 -1
  40. package/lib/compiler/define.js +1103 -0
  41. package/lib/compiler/extend.js +983 -0
  42. package/lib/compiler/finalize-parse-cdl.js +231 -0
  43. package/lib/compiler/index.js +33 -14
  44. package/lib/compiler/kick-start.js +190 -0
  45. package/lib/compiler/moduleLayers.js +4 -4
  46. package/lib/compiler/populate.js +1226 -0
  47. package/lib/compiler/propagator.js +113 -47
  48. package/lib/compiler/resolve.js +1433 -0
  49. package/lib/compiler/shared.js +76 -38
  50. package/lib/compiler/tweak-assocs.js +529 -0
  51. package/lib/compiler/utils.js +204 -33
  52. package/lib/edm/.eslintrc.json +5 -0
  53. package/lib/edm/annotations/genericTranslation.js +38 -25
  54. package/lib/edm/annotations/preprocessAnnotations.js +3 -3
  55. package/lib/edm/csn2edm.js +10 -9
  56. package/lib/edm/edm.js +19 -20
  57. package/lib/edm/edmPreprocessor.js +166 -95
  58. package/lib/edm/edmUtils.js +127 -34
  59. package/lib/gen/Dictionary.json +92 -43
  60. package/lib/gen/language.checksum +1 -1
  61. package/lib/gen/language.interp +11 -1
  62. package/lib/gen/language.tokens +86 -82
  63. package/lib/gen/languageLexer.interp +18 -1
  64. package/lib/gen/languageLexer.js +925 -847
  65. package/lib/gen/languageLexer.tokens +78 -74
  66. package/lib/gen/languageParser.js +5434 -4298
  67. package/lib/json/from-csn.js +59 -17
  68. package/lib/json/to-csn.js +143 -71
  69. package/lib/language/antlrParser.js +3 -3
  70. package/lib/language/docCommentParser.js +3 -3
  71. package/lib/language/genericAntlrParser.js +144 -54
  72. package/lib/language/language.g4 +424 -203
  73. package/lib/language/multiLineStringParser.js +536 -0
  74. package/lib/main.d.ts +472 -61
  75. package/lib/main.js +38 -11
  76. package/lib/model/api.js +3 -1
  77. package/lib/model/csnRefs.js +321 -204
  78. package/lib/model/csnUtils.js +224 -263
  79. package/lib/model/enrichCsn.js +97 -40
  80. package/lib/model/revealInternalProperties.js +27 -6
  81. package/lib/model/sortViews.js +2 -1
  82. package/lib/modelCompare/compare.js +17 -12
  83. package/lib/optionProcessor.js +7 -6
  84. package/lib/render/DuplicateChecker.js +1 -1
  85. package/lib/render/manageConstraints.js +36 -33
  86. package/lib/render/toCdl.js +174 -275
  87. package/lib/render/toHdbcds.js +201 -115
  88. package/lib/render/toRename.js +7 -10
  89. package/lib/render/toSql.js +149 -75
  90. package/lib/render/utils/common.js +22 -8
  91. package/lib/render/utils/sql.js +10 -7
  92. package/lib/render/utils/stringEscapes.js +111 -0
  93. package/lib/sql-identifier.js +1 -1
  94. package/lib/transform/.eslintrc.json +5 -0
  95. package/lib/transform/braceExpression.js +4 -2
  96. package/lib/transform/db/.eslintrc.json +2 -0
  97. package/lib/transform/db/applyTransformations.js +35 -12
  98. package/lib/transform/db/assertUnique.js +1 -1
  99. package/lib/transform/db/associations.js +187 -0
  100. package/lib/transform/db/cdsPersistence.js +150 -0
  101. package/lib/transform/db/constraints.js +61 -56
  102. package/lib/transform/db/expansion.js +50 -29
  103. package/lib/transform/db/flattening.js +552 -105
  104. package/lib/transform/db/groupByOrderBy.js +3 -1
  105. package/lib/transform/db/temporal.js +236 -0
  106. package/lib/transform/db/transformExists.js +94 -28
  107. package/lib/transform/db/views.js +5 -4
  108. package/lib/transform/draft/.eslintrc.json +38 -0
  109. package/lib/transform/{db/draft.js → draft/db.js} +9 -7
  110. package/lib/transform/draft/odata.js +227 -0
  111. package/lib/transform/forHanaNew.js +94 -801
  112. package/lib/transform/forOdataNew.js +22 -175
  113. package/lib/transform/localized.js +36 -32
  114. package/lib/transform/odata/generateForeignKeyElements.js +3 -3
  115. package/lib/transform/odata/referenceFlattener.js +95 -89
  116. package/lib/transform/odata/structureFlattener.js +1 -1
  117. package/lib/transform/odata/toFinalBaseType.js +86 -12
  118. package/lib/transform/odata/typesExposure.js +5 -5
  119. package/lib/transform/odata/utils.js +2 -2
  120. package/lib/transform/transformUtilsNew.js +47 -33
  121. package/lib/transform/translateAssocsToJoins.js +10 -27
  122. package/lib/transform/universalCsn/.eslintrc.json +36 -0
  123. package/lib/transform/universalCsn/coreComputed.js +170 -0
  124. package/lib/transform/universalCsn/universalCsnEnricher.js +715 -0
  125. package/lib/transform/universalCsn/utils.js +63 -0
  126. package/lib/utils/file.js +2 -1
  127. package/lib/utils/objectUtils.js +30 -0
  128. package/lib/utils/timetrace.js +8 -2
  129. package/package.json +1 -1
  130. package/share/messages/README.md +26 -0
  131. package/lib/compiler/definer.js +0 -2340
  132. package/lib/compiler/resolver.js +0 -2988
  133. package/lib/transform/universalCsnEnricher.js +0 -67
@@ -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,
@@ -20,6 +21,7 @@ const { isBetaEnabled, isDeprecatedEnabled } = require('../base/model');
20
21
  const { smartFuncId } = require('../sql-identifier');
21
22
  const { sortCsn } = require('../json/to-csn');
22
23
  const { manageConstraints } = require('./manageConstraints');
24
+ const { ModelError } = require('../base/error');
23
25
 
24
26
 
25
27
  /**
@@ -171,6 +173,12 @@ function toSqlDdl(csn, options) {
171
173
  comment(comment) {
172
174
  return comment && `'${comment.replace(/'/g, '\'\'')}'` || 'NULL';
173
175
  },
176
+ /*
177
+ Alter SQL snippet for entity.
178
+ */
179
+ alterEntitySqlSnippet(tableName, snippet) {
180
+ return [ `ALTER TABLE ${tableName} ${snippet};` ];
181
+ },
174
182
  /*
175
183
  Concatenate multiple statements which are to be treated as one by the API caller.
176
184
  */
@@ -186,10 +194,10 @@ function toSqlDdl(csn, options) {
186
194
  checkCSNVersion(csn, options);
187
195
 
188
196
  // The final result in hdb-kind-specific form, without leading CREATE, without trailing newlines
189
- // (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
190
198
  // the attribute names must be the HDI plugin names for --src hdi)
191
199
  // The result object may have a `sql` dictionary for `toSql`.
192
- const resultObj = {
200
+ const mainResultObj = {
193
201
  hdbtabletype: Object.create(null),
194
202
  hdbtable: Object.create(null),
195
203
  hdbindex: Object.create(null),
@@ -215,12 +223,12 @@ function toSqlDdl(csn, options) {
215
223
  // Current indentation string
216
224
  indent: '',
217
225
  };
218
- renderArtifactInto(artifactName, artifact, resultObj, env);
226
+ renderArtifactInto(artifactName, artifact, mainResultObj, env);
219
227
  });
220
228
 
221
229
  // Render each deleted artifact
222
230
  for (const artifactName in csn.deletions)
223
- renderArtifactDeletionInto(artifactName, csn.deletions[artifactName], resultObj);
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,42 +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
284
  sql[name] = `${options.testMode ? '' : sqlVersionLine}CREATE ${sourceString};`;
277
285
  }
278
286
  else if (!options.testMode) {
279
- resultObj[hdbKind][name] = sqlVersionLine + resultObj[hdbKind][name];
287
+ mainResultObj[hdbKind][name] = sqlVersionLine + mainResultObj[hdbKind][name];
280
288
  }
281
289
  }
282
290
  if (options.toSql.src === 'sql')
283
- delete resultObj[hdbKind];
291
+ delete mainResultObj[hdbKind];
284
292
  }
285
293
 
286
- // add `ALTER TABLE ADD CONSTRAINT` statements if requested
287
- if (options.sqlDialect !== 'sqlite' && options.constraintsAsAlter) {
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') {
288
296
  const alterStmts = manageConstraints(csn, options);
289
297
 
290
298
  for ( const constraintName of Object.keys(alterStmts))
291
299
  sql[constraintName] = `${options.testMode ? '' : sqlVersionLine}${alterStmts[constraintName]}`;
292
- resultObj.sql = sql;
300
+ mainResultObj.sql = sql;
293
301
  }
294
302
 
295
303
  if (options.toSql.src === 'sql')
296
- resultObj.sql = sql;
304
+ mainResultObj.sql = sql;
297
305
 
298
306
  for (const name in deletions)
299
307
  deletions[name] = `${options.testMode ? '' : sqlVersionLine}${deletions[name]}`;
300
308
 
301
309
 
302
310
  timetrace.stop();
303
- return resultObj;
311
+ return mainResultObj;
304
312
 
305
313
  /**
306
314
  * Render an artifact into the appropriate dictionary of 'resultObj'.
@@ -317,7 +325,6 @@ function toSqlDdl(csn, options) {
317
325
 
318
326
  switch (art.kind) {
319
327
  case 'entity':
320
- case 'view':
321
328
  if (getNormalizedQuery(art).query) {
322
329
  const result = renderView(artifactName, art, env);
323
330
  if (result)
@@ -339,7 +346,7 @@ function toSqlDdl(csn, options) {
339
346
  // Ignore: not SQL-relevant
340
347
  return;
341
348
  default:
342
- throw new Error(`Unknown artifact kind: ${art.kind}`);
349
+ throw new ModelError(`Unknown artifact kind: ${art.kind}`);
343
350
  }
344
351
  }
345
352
 
@@ -360,7 +367,7 @@ function toSqlDdl(csn, options) {
360
367
  renderExtendInto(artifactName, artifact.elements, ext.elements, resultObj, env, extensionsDuplicateChecker);
361
368
 
362
369
  if (!artifactName)
363
- throw new Error(`Undefined artifact name: ${artifactName}`);
370
+ throw new ModelError(`Undefined artifact name: ${artifactName}`);
364
371
  }
365
372
 
366
373
  // Render an artifact deletion into the appropriate dictionary of 'resultObj'.
@@ -400,11 +407,25 @@ function toSqlDdl(csn, options) {
400
407
  ? renderAssociationElement(eltName, defVariant, env)
401
408
  : renderElement(artifactName, eltName, defVariant, null, null, env);
402
409
  }
403
- function getEltStrNoProp(defVariant, prop, eltName) {
404
- const defNoProp = Object.assign({}, defVariant);
405
- delete defNoProp[prop];
406
- 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);
407
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}`;
426
+ }
427
+
428
+ const sqlSnippetAnnos = [ '@sql.prepend', '@sql.append' ];
408
429
 
409
430
  const tableName = renderArtifactName(artifactName);
410
431
 
@@ -415,6 +436,15 @@ function toSqlDdl(csn, options) {
415
436
  const alterComment = render.alterEntityComment(tableName, def.new);
416
437
  addMigration(resultObj, artifactName, false, alterComment);
417
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
+ }
418
448
  }
419
449
  }
420
450
 
@@ -449,9 +479,28 @@ function toSqlDdl(csn, options) {
449
479
  if (eltStrNew === eltStrOld)
450
480
  return; // Prevent spurious migrations, where the column DDL does not change.
451
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
+
452
501
  if (def.old.doc !== def.new.doc) {
453
- const eltStrOldNoDoc = getEltStrNoProp(def.old, 'doc', eltName);
454
- const eltStrNewNoDoc = getEltStrNoProp(def.new, 'doc', eltName);
502
+ const eltStrOldNoDoc = getEltStrNoProps(def.old, eltName, 'doc');
503
+ const eltStrNewNoDoc = getEltStrNoProps(def.new, eltName, 'doc');
455
504
  if (eltStrOldNoDoc === eltStrNewNoDoc) { // only `doc` changed
456
505
  const alterComment = render.alterColumnComment(tableName, sqlId, def.new.doc);
457
506
  addMigration(resultObj, artifactName, false, alterComment);
@@ -491,14 +540,16 @@ function toSqlDdl(csn, options) {
491
540
  env._artifact = art;
492
541
  const childEnv = increaseIndent(env);
493
542
  const hanaTc = art.technicalConfig && art.technicalConfig.hana;
494
- let result = '';
543
+ // tables can have @sql.prepend and @sql.append
544
+ const { front, back } = getSqlSnippets(options, art);
545
+ let result = front;
495
546
  // Only HANA has row/column tables
496
547
  if (options.toSql.dialect === 'hana') {
497
548
  if (hanaTc && hanaTc.storeType) {
498
549
  // Explicitly specified
499
550
  result += `${art.technicalConfig.hana.storeType.toUpperCase()} `;
500
551
  }
501
- else {
552
+ else if (!front) {
502
553
  // in 'hdbtable' files, COLUMN or ROW is mandatory, and COLUMN is the default
503
554
  result += 'COLUMN ';
504
555
  }
@@ -521,7 +572,8 @@ function toSqlDdl(csn, options) {
521
572
  if (primaryKeys !== '')
522
573
  result += `,\n${childEnv.indent}${primaryKeys}`;
523
574
 
524
- const constraintsAsAlter = options.constraintsAsAlter && options.sqlDialect !== 'sqlite';
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';
525
577
  if ( !constraintsAsAlter && art.$tableConstraints && art.$tableConstraints.referential) {
526
578
  const renderReferentialConstraintsAsHdbconstraint = options.toSql.src === 'hdi';
527
579
  const referentialConstraints = {};
@@ -575,6 +627,9 @@ function toSqlDdl(csn, options) {
575
627
  if (options.toSql.dialect === 'hana' && hasHanaComment(art, options))
576
628
  result += ` COMMENT '${getHanaComment(art)}'`;
577
629
 
630
+ if (back)
631
+ result += back;
632
+
578
633
  resultObj.hdbtable[artifactName] = result;
579
634
  }
580
635
 
@@ -606,16 +661,21 @@ function toSqlDdl(csn, options) {
606
661
  }
607
662
  }
608
663
 
609
- function addMigration(resultObj, artifactName, drop, sqlArray) {
664
+ function addMigration(resultObj, artifactName, drop, sqlArray, description) {
610
665
  if (!(artifactName in resultObj.migrations))
611
666
  resultObj.migrations[artifactName] = [];
612
667
 
613
- 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 }));
614
674
  resultObj.migrations[artifactName].push(...migrations);
615
675
  }
616
676
 
617
- function addDeletion(resultObj, artifactName, sql) {
618
- resultObj.deletions[artifactName] = sql;
677
+ function addDeletion(resultObj, artifactName, deletionSql) {
678
+ resultObj.deletions[artifactName] = deletionSql;
619
679
  }
620
680
 
621
681
  /**
@@ -672,7 +732,13 @@ function toSqlDdl(csn, options) {
672
732
  if (fzindex && options.toSql.dialect === 'hana')
673
733
  result += ` ${renderExpr(fzindex, env)}`;
674
734
 
675
- 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))
676
742
  result += ` COMMENT '${getHanaComment(elm)}'`;
677
743
 
678
744
  return result;
@@ -739,7 +805,7 @@ function toSqlDdl(csn, options) {
739
805
  // This also affects renderIndexes
740
806
  tc = tc.hana;
741
807
  if (!tc)
742
- throw new Error('Expecting a HANA technical configuration');
808
+ throw new ModelError('Expecting a HANA technical configuration');
743
809
 
744
810
  if (tc.tableSuffix) {
745
811
  // Although we could just render the whole bandwurm as one stream of tokens, the
@@ -808,7 +874,7 @@ function toSqlDdl(csn, options) {
808
874
  const i = index.indexOf('index');
809
875
  const j = index.indexOf('(');
810
876
  if (i > index.length - 2 || !index[i + 1].ref || j < i || j > index.length - 2)
811
- throw new Error(`Unexpected form of index: "${index}"`);
877
+ throw new ModelError(`Unexpected form of index: "${index}"`);
812
878
 
813
879
  let indexName = renderArtifactName(`${artifactName}.${index[i + 1].ref}`);
814
880
  if (options.toSql.names === 'plain')
@@ -864,7 +930,7 @@ function toSqlDdl(csn, options) {
864
930
 
865
931
  // Sanity check
866
932
  if (!source.ref)
867
- throw new Error(`Expecting ref in ${JSON.stringify(source)}`);
933
+ throw new ModelError(`Expecting ref in ${JSON.stringify(source)}`);
868
934
 
869
935
  return renderAbsolutePathWithAlias(artifactName, source, env);
870
936
  }
@@ -906,7 +972,7 @@ function toSqlDdl(csn, options) {
906
972
  function renderAbsolutePathWithAlias(artifactName, path, env) {
907
973
  // This actually can't happen anymore because assoc2joins should have taken care of it
908
974
  if (path.ref[0].where)
909
- 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`);
910
976
 
911
977
 
912
978
  // SQL needs a ':' after path.ref[0] to separate associations
@@ -944,7 +1010,7 @@ function toSqlDdl(csn, options) {
944
1010
  function renderAbsolutePath(path, sep, env) {
945
1011
  // Sanity checks
946
1012
  if (!path.ref)
947
- throw new Error(`Expecting ref in path: ${JSON.stringify(path)}`);
1013
+ throw new ModelError(`Expecting ref in path: ${JSON.stringify(path)}`);
948
1014
 
949
1015
  // Determine the absolute name of the first artifact on the path (before any associations or element traversals)
950
1016
  const firstArtifactName = path.ref[0].id || path.ref[0];
@@ -961,7 +1027,7 @@ function toSqlDdl(csn, options) {
961
1027
  if (ref && ref.params) {
962
1028
  result += `(${renderArgs(path.ref[0] || {}, '=>', env, syntax)})`;
963
1029
  }
964
- else if ([ 'udf' ].includes(syntax)) {
1030
+ else if (syntax === 'udf') {
965
1031
  // if syntax is user defined function, render empty argument list
966
1032
  // CV without parameters is called as simple view
967
1033
  result += '()';
@@ -984,7 +1050,7 @@ function toSqlDdl(csn, options) {
984
1050
  * @param {object} node with `args` to render
985
1051
  * @param {string} sep Separator between args
986
1052
  * @param {object} env Render environment
987
- * @param {string} syntax Some magic A2J paramter - for calcview parameter rendering
1053
+ * @param {string|null} syntax Some magic A2J parameter - for calcview parameter rendering
988
1054
  * @returns {string} Rendered arguments
989
1055
  * @throws Throws if args is not an array or object.
990
1056
  */
@@ -992,29 +1058,28 @@ function toSqlDdl(csn, options) {
992
1058
  const args = node.args ? node.args : {};
993
1059
  // Positional arguments
994
1060
  if (Array.isArray(args))
995
- return args.map(arg => renderExpr(arg, env)).join(', ');
1061
+ return args.map(arg => renderExpr(arg, env, true, false, true)).join(', ');
996
1062
 
997
1063
  // Named arguments (object/dict)
998
1064
  else if (typeof args === 'object')
999
1065
  // if this is a function param which is not a reference to the model, we must not quote it
1000
- 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(', ');
1001
1067
 
1002
1068
 
1003
- throw new Error(`Unknown args: ${JSON.stringify(args)}`);
1069
+ throw new ModelError(`Unknown args: ${JSON.stringify(args)}`);
1004
1070
 
1005
1071
 
1006
1072
  /**
1007
1073
  * Render the given argument/parameter correctly.
1008
1074
  *
1009
1075
  * @param {string} arg Argument to render
1010
- * @param {string} syntax Some magic A2J paramter - for calcview parameter rendering
1076
+ * @param {string|null} parameterSyntax Some magic A2J parameter - for calcview parameter rendering
1011
1077
  * @returns {string} Rendered argument
1012
1078
  */
1013
- function decorateParameter(arg, syntax) {
1014
- if (syntax === 'calcview')
1079
+ function decorateParameter(arg, parameterSyntax) {
1080
+ if (parameterSyntax === 'calcview')
1015
1081
  return `PLACEHOLDER."$$${arg}$$"`;
1016
1082
 
1017
-
1018
1083
  return quoteSqlId(arg);
1019
1084
  }
1020
1085
  }
@@ -1075,6 +1140,11 @@ function toSqlDdl(csn, options) {
1075
1140
  result += `${env.indent})`;
1076
1141
  }
1077
1142
 
1143
+ // views can only have a @sql.append
1144
+ const { back } = getSqlSnippets(options, art);
1145
+ if (back)
1146
+ result += back;
1147
+
1078
1148
  return result;
1079
1149
  }
1080
1150
 
@@ -1082,7 +1152,7 @@ function toSqlDdl(csn, options) {
1082
1152
  * Render the parameter definition of a view if any. Return the parameters in parentheses, or an empty string
1083
1153
  *
1084
1154
  * @param {string} artifactName Name of the view
1085
- * @param {Array} params Array of parameters
1155
+ * @param {Object} params Dictionary of parameters
1086
1156
  * @returns {string} Rendered parameters
1087
1157
  */
1088
1158
  function renderParameterDefinitions(artifactName, params) {
@@ -1150,7 +1220,7 @@ function toSqlDdl(csn, options) {
1150
1220
  }
1151
1221
  // Otherwise must have a SELECT
1152
1222
  else if (!query.SELECT) {
1153
- throw new Error(`Unexpected query operation ${JSON.stringify(query)}`);
1223
+ throw new ModelError(`Unexpected query operation ${JSON.stringify(query)}`);
1154
1224
  }
1155
1225
  const select = query.SELECT;
1156
1226
  const childEnv = increaseIndent(env);
@@ -1166,7 +1236,7 @@ function toSqlDdl(csn, options) {
1166
1236
  result += `\n${env.indent}WHERE ${renderExpr(select.where, env, true, true)}`;
1167
1237
 
1168
1238
  if (select.groupBy)
1169
- 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(', ')}`;
1170
1240
 
1171
1241
  if (select.having)
1172
1242
  result += `\n${env.indent}HAVING ${renderExpr(select.having, env, true, true)}`;
@@ -1217,7 +1287,7 @@ function toSqlDdl(csn, options) {
1217
1287
  * @returns {string} Rendered ORDER BY entry
1218
1288
  */
1219
1289
  function renderOrderByEntry(entry, env) {
1220
- let result = renderExpr(entry, env);
1290
+ let result = renderExpr(entry, env, true, false, true);
1221
1291
  if (entry.sort)
1222
1292
  result += ` ${entry.sort.toUpperCase()}`;
1223
1293
 
@@ -1241,7 +1311,7 @@ function toSqlDdl(csn, options) {
1241
1311
  // Anonymous structured type: Not supported with SQL (but shouldn't happen anyway after forHana flattened them)
1242
1312
  if (!elm.type) {
1243
1313
  if (!elm.elements)
1244
- throw new Error(`Missing type of: ${elementName}`);
1314
+ throw new ModelError(`Missing type of: ${elementName}`);
1245
1315
 
1246
1316
  // TODO: Signal is not covered by tests + better location
1247
1317
  error(null, [ 'definitions', artifactName, 'elements', elementName ],
@@ -1264,7 +1334,7 @@ function toSqlDdl(csn, options) {
1264
1334
  result += renderBuiltinType(elm.type);
1265
1335
  }
1266
1336
  else {
1267
- throw new Error(`Unexpected non-primitive type of: ${artifactName}.${elementName}`);
1337
+ throw new ModelError(`Unexpected non-primitive type of: ${artifactName}.${elementName}`);
1268
1338
  }
1269
1339
  result += renderTypeParameters(elm);
1270
1340
  return result;
@@ -1293,7 +1363,7 @@ function toSqlDdl(csn, options) {
1293
1363
  * Render the nullability of an element or parameter (can be unset, true, or false)
1294
1364
  *
1295
1365
  * @param {object} obj Object to render for
1296
- * @param {boolean} treatKeyAsNotNull Wether to render KEY as not null
1366
+ * @param {boolean} treatKeyAsNotNull Whether to render KEY as not null
1297
1367
  * @returns {string} NULL/NOT NULL or ''
1298
1368
  */
1299
1369
  function renderNullability(obj, treatKeyAsNotNull = false) {
@@ -1338,35 +1408,38 @@ function toSqlDdl(csn, options) {
1338
1408
  * (no trailing LF, don't indent if inline)
1339
1409
  *
1340
1410
  * @todo Reuse this with toCdl
1341
- * @param {Array|object|string} x Expression to render
1411
+ * @param {Array|object|string} expr Expression to render
1342
1412
  * @param {object} env Render environment
1343
- * @param {boolean} inline Wether to render the expression inline
1344
- * @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.
1345
1417
  * @returns {string} Rendered expression
1346
1418
  */
1347
- function renderExpr(x, env, inline = true, nestedExpr = false) {
1419
+ function renderExpr(expr, env, inline = true, nestedExpr = false, alwaysRenderCast = false) {
1348
1420
  // Compound expression
1349
- if (Array.isArray(x)) {
1350
- 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));
1351
1423
  return beautifyExprArray(tokens);
1352
1424
  }
1353
- else if (typeof x === 'object' && x !== null) {
1354
- if (nestedExpr && x.cast && x.cast.type)
1355
- return renderExplicitTypeCast(renderExprObject());
1356
- 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);
1357
1429
  }
1358
1430
  // Not a literal value but part of an operator, function etc - just leave as it is
1359
1431
  // FIXME: For the sake of simplicity, we should get away from all this uppercasing in toSql
1360
1432
 
1361
- return String(x).toUpperCase();
1433
+ return String(expr).toUpperCase();
1362
1434
 
1363
1435
 
1364
1436
  /**
1365
1437
  * Various special cases represented as objects
1366
1438
  *
1439
+ * @param {object} x Expression
1367
1440
  * @returns {string} String representation of the expression
1368
1441
  */
1369
- function renderExprObject() {
1442
+ function renderExprObject(x) {
1370
1443
  if (x.list) {
1371
1444
  return `(${x.list.map(item => renderExpr(item)).join(', ')})`;
1372
1445
  }
@@ -1409,13 +1482,13 @@ function toSqlDdl(csn, options) {
1409
1482
  return `${renderQuery('<union>', x, increaseIndent(env))}`;
1410
1483
  }
1411
1484
 
1412
- throw new Error(`Unknown expression: ${JSON.stringify(x)}`);
1485
+ throw new ModelError(`Unknown expression: ${JSON.stringify(x)}`);
1413
1486
  }
1414
1487
 
1415
- function renderWindowFunction(funcName, node, env) {
1416
- const suffix = node.xpr.shift(); // OVER
1417
- let r = `${funcName}(${renderArgs(node, '=>', env, null)})`;
1418
- 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
1419
1492
  return r;
1420
1493
  }
1421
1494
 
@@ -1449,7 +1522,7 @@ function toSqlDdl(csn, options) {
1449
1522
 
1450
1523
  // otherwise fall through to
1451
1524
  default:
1452
- throw new Error(`Unknown literal or type: ${JSON.stringify(x)}`);
1525
+ throw new ModelError(`Unknown literal or type: ${JSON.stringify(x)}`);
1453
1526
  }
1454
1527
  }
1455
1528
 
@@ -1570,10 +1643,11 @@ function toSqlDdl(csn, options) {
1570
1643
  /**
1571
1644
  * Renders an explicit `cast()` inside an 'xpr'.
1572
1645
  *
1646
+ * @param {object} x Expression with cast
1573
1647
  * @param {string} value Value to cast
1574
1648
  * @returns {string} CAST statement
1575
1649
  */
1576
- function renderExplicitTypeCast(value) {
1650
+ function renderExplicitTypeCast(x, value) {
1577
1651
  const typeRef = renderBuiltinType(x.cast.type) + renderTypeParameters(x.cast);
1578
1652
  return `CAST(${value} AS ${typeRef})`;
1579
1653
  }
@@ -1611,7 +1685,7 @@ function toSqlDdl(csn, options) {
1611
1685
  else if (typeof s === 'object') {
1612
1686
  // Sanity check
1613
1687
  if (!s.func && !s.id)
1614
- throw new Error(`Unknown path step object: ${JSON.stringify(s)}`);
1688
+ throw new ModelError(`Unknown path step object: ${JSON.stringify(s)}`);
1615
1689
 
1616
1690
  // Not really a path step but an object-like function call
1617
1691
  if (s.func)
@@ -1631,7 +1705,7 @@ function toSqlDdl(csn, options) {
1631
1705
  return result;
1632
1706
  }
1633
1707
 
1634
- throw new Error(`Unknown path step: ${JSON.stringify(s)}`);
1708
+ throw new ModelError(`Unknown path step: ${JSON.stringify(s)}`);
1635
1709
  }
1636
1710
  }
1637
1711
 
@@ -20,7 +20,6 @@ const {
20
20
 
21
21
  const { implicitAs } = require('../../model/csnRefs');
22
22
 
23
-
24
23
  /**
25
24
  * Render the given function
26
25
  *
@@ -37,7 +36,7 @@ function renderFunc( funcName, node, dialect, renderArgs) {
37
36
  }
38
37
 
39
38
  /**
40
- * Checks wether the given function is to be rendered without parentheses
39
+ * Checks whether the given function is to be rendered without parentheses
41
40
  *
42
41
  * @param {object} node Content of the function
43
42
  * @param {string} dialect One of 'hana', 'cap' or 'sqlite' - only 'hana' is relevant atm
@@ -205,12 +204,11 @@ function addMissingChildContexts(csn, artifactName, killList) {
205
204
  addPossibleGaps(name.slice(artifactName.length + 1).split('.'), artifactName);
206
205
  }
207
206
 
208
- function addPossibleGaps(possibleGaps, artifactName) {
209
- let possibleGap = artifactName;
207
+ function addPossibleGaps(possibleGaps, gapArtifactName) {
210
208
  for (const gap of possibleGaps) {
211
- possibleGap += `.${gap}`;
212
- if (!csn.definitions[possibleGap]) {
213
- const contextName = possibleGap;
209
+ gapArtifactName += `.${gap}`;
210
+ if (!csn.definitions[gapArtifactName]) {
211
+ const contextName = gapArtifactName;
214
212
  csn.definitions[contextName] = {
215
213
  kind: 'context',
216
214
  };
@@ -332,7 +330,7 @@ function addIntermediateContexts(csn, killList) {
332
330
  }
333
331
 
334
332
  /**
335
- * Check wether the given artifact or element has a comment that needs to be rendered.
333
+ * Check whether the given artifact or element has a comment that needs to be rendered.
336
334
  * Things annotated with @cds.persistence.journal (for HANA SQL), should not get a comment.
337
335
  *
338
336
  * @param {CSN.Artifact} obj
@@ -355,6 +353,21 @@ function getHanaComment(obj) {
355
353
  return obj.doc.split('\n\n')[0].trim().replace(/'/g, "''");
356
354
  }
357
355
 
356
+ /**
357
+ * Get the @sql.prepend/append if set - already add a space after/before.
358
+ * If no value is set, use '';
359
+ *
360
+ * @param {CSN.Options} options
361
+ * @param {object} obj
362
+ * @returns {object} object with .front and .back
363
+ */
364
+ function getSqlSnippets(options, obj) {
365
+ const front = obj['@sql.prepend'] ? `${obj['@sql.prepend']} ` : '';
366
+ const back = obj['@sql.append'] ? ` ${obj['@sql.append']}` : '';
367
+
368
+ return { front, back };
369
+ }
370
+
358
371
  /**
359
372
  * @typedef CdlRenderEnvironment Rendering environment used throughout the render process.
360
373
  *
@@ -383,4 +396,5 @@ module.exports = {
383
396
  getHanaComment,
384
397
  findElement,
385
398
  funcWithoutParen,
399
+ getSqlSnippets,
386
400
  };