@sap/cds-compiler 3.9.4 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/CHANGELOG.md +92 -4
  2. package/README.md +0 -1
  3. package/bin/cdsc.js +11 -23
  4. package/bin/cdsse.js +3 -3
  5. package/doc/API.md +5 -0
  6. package/doc/CHANGELOG_ARCHIVE.md +1 -1
  7. package/doc/CHANGELOG_BETA.md +17 -1
  8. package/doc/CHANGELOG_DEPRECATED.md +28 -0
  9. package/lib/api/.eslintrc.json +1 -1
  10. package/lib/api/main.js +26 -8
  11. package/lib/api/options.js +2 -0
  12. package/lib/base/error.js +2 -0
  13. package/lib/base/message-registry.js +143 -64
  14. package/lib/base/messages.js +213 -107
  15. package/lib/base/model.js +11 -11
  16. package/lib/checks/.eslintrc.json +1 -1
  17. package/lib/checks/annotationsOData.js +2 -2
  18. package/lib/checks/elements.js +1 -1
  19. package/lib/checks/enricher.js +26 -3
  20. package/lib/checks/onConditions.js +67 -12
  21. package/lib/checks/queryNoDbArtifacts.js +106 -105
  22. package/lib/checks/sql-snippets.js +2 -0
  23. package/lib/checks/types.js +12 -6
  24. package/lib/checks/validator.js +2 -2
  25. package/lib/compiler/assert-consistency.js +10 -8
  26. package/lib/compiler/builtins.js +8 -2
  27. package/lib/compiler/checks.js +52 -35
  28. package/lib/compiler/define.js +31 -26
  29. package/lib/compiler/extend.js +120 -65
  30. package/lib/compiler/finalize-parse-cdl.js +12 -43
  31. package/lib/compiler/generate.js +16 -5
  32. package/lib/compiler/index.js +8 -5
  33. package/lib/compiler/kick-start.js +4 -3
  34. package/lib/compiler/populate.js +96 -95
  35. package/lib/compiler/propagator.js +7 -8
  36. package/lib/compiler/resolve.js +377 -103
  37. package/lib/compiler/shared.js +794 -517
  38. package/lib/compiler/tweak-assocs.js +8 -6
  39. package/lib/compiler/utils.js +44 -0
  40. package/lib/edm/annotations/genericTranslation.js +12 -4
  41. package/lib/edm/csn2edm.js +34 -32
  42. package/lib/edm/edm.js +34 -31
  43. package/lib/edm/edmAnnoPreprocessor.js +0 -23
  44. package/lib/edm/edmInboundChecks.js +7 -2
  45. package/lib/edm/edmPreprocessor.js +18 -17
  46. package/lib/edm/edmUtils.js +8 -4
  47. package/lib/gen/Dictionary.json +18 -0
  48. package/lib/gen/language.checksum +1 -1
  49. package/lib/gen/language.interp +4 -2
  50. package/lib/gen/languageParser.js +5006 -4582
  51. package/lib/json/from-csn.js +157 -112
  52. package/lib/json/to-csn.js +60 -89
  53. package/lib/language/antlrParser.js +17 -13
  54. package/lib/language/docCommentParser.js +11 -1
  55. package/lib/language/genericAntlrParser.js +13 -10
  56. package/lib/language/language.g4 +168 -97
  57. package/lib/main.d.ts +128 -36
  58. package/lib/main.js +1 -1
  59. package/lib/model/csnRefs.js +24 -5
  60. package/lib/model/csnUtils.js +9 -8
  61. package/lib/model/revealInternalProperties.js +7 -12
  62. package/lib/modelCompare/compare.js +1 -1
  63. package/lib/modelCompare/utils/filter.js +40 -2
  64. package/lib/optionProcessor.js +0 -3
  65. package/lib/render/toCdl.js +247 -214
  66. package/lib/render/toHdbcds.js +197 -181
  67. package/lib/render/toSql.js +325 -289
  68. package/lib/render/utils/common.js +42 -4
  69. package/lib/render/utils/delta.js +1 -1
  70. package/lib/render/utils/sql.js +3 -3
  71. package/lib/transform/braceExpression.js +2 -2
  72. package/lib/transform/db/.eslintrc.json +1 -1
  73. package/lib/transform/db/applyTransformations.js +3 -3
  74. package/lib/transform/db/associations.js +24 -12
  75. package/lib/transform/db/expansion.js +17 -18
  76. package/lib/transform/db/flattening.js +17 -21
  77. package/lib/transform/db/rewriteCalculatedElements.js +171 -64
  78. package/lib/transform/db/views.js +3 -4
  79. package/lib/transform/draft/db.js +21 -12
  80. package/lib/transform/draft/odata.js +4 -0
  81. package/lib/transform/forOdataNew.js +11 -10
  82. package/lib/transform/forRelationalDB.js +12 -7
  83. package/lib/transform/localized.js +4 -2
  84. package/lib/transform/odata/toFinalBaseType.js +5 -5
  85. package/lib/transform/odata/typesExposure.js +3 -3
  86. package/lib/transform/parseExpr.js +3 -0
  87. package/lib/transform/transformUtilsNew.js +43 -23
  88. package/lib/transform/translateAssocsToJoins.js +7 -6
  89. package/lib/transform/universalCsn/.eslintrc.json +1 -1
  90. package/lib/transform/universalCsn/coreComputed.js +7 -5
  91. package/lib/transform/universalCsn/universalCsnEnricher.js +12 -12
  92. package/package.json +2 -2
  93. package/share/messages/{duplicate-autoexposed.md → def-duplicate-autoexposed.md} +5 -1
  94. package/share/messages/message-explanations.json +1 -1
@@ -27,6 +27,27 @@ const { sortCsn } = require('../json/to-csn');
27
27
  const { manageConstraints } = require('./manageConstraints');
28
28
  const { ModelError, CompilerAssertion } = require('../base/error');
29
29
 
30
+ class SqlRenderEnvironment {
31
+ indent = '';
32
+ path = null;
33
+ alterMode = false;
34
+ changeType = null;
35
+
36
+ constructor(values) {
37
+ Object.assign(this, values);
38
+ }
39
+
40
+ withIncreasedIndent() {
41
+ return new SqlRenderEnvironment({ ...this, indent: ` ${this.indent}` });
42
+ }
43
+ withSubPath(path) {
44
+ return new SqlRenderEnvironment({ ...this, path: [ ...this.path, ...path ] });
45
+ }
46
+ cloneWith(values) {
47
+ return Object.assign(new SqlRenderEnvironment(this), values);
48
+ }
49
+ }
50
+
30
51
 
31
52
  /**
32
53
  * Render the CSN model 'model' to SQL DDL statements. One statement is created
@@ -93,14 +114,14 @@ function toSqlDdl( csn, options ) {
93
114
  return `CAST(${this.renderExpr(withoutCast(x))} AS ${typeRef})`;
94
115
  },
95
116
  val: renderExpressionLiteral,
96
- enum(x) {
97
- // TODO: better location; callers should set env.$path
117
+ enum() {
98
118
  // FIXME: We can't do enums yet because they are not resolved (and we don't bother finding their value by hand)
99
- const loc = x.$location || this.env?._artifact?.$location;
100
- error('expr-unexpected-enum', [ loc, null ], 'Enum values are not yet supported for conversion to SQL');
119
+ error('expr-unexpected-enum', this.env.path, 'Enum values are not yet supported for conversion to SQL');
101
120
  return '';
102
121
  },
103
- ref: renderExpressionRef,
122
+ ref(x) {
123
+ return renderExpressionRef(x, this.env);
124
+ },
104
125
  aliasOnly: x => x.as,
105
126
  windowFunction( x) {
106
127
  return renderWindowFunction(smartFuncId(prepareIdentifier(x.func), options.sqlDialect), x, this.env);
@@ -109,17 +130,18 @@ function toSqlDdl( csn, options ) {
109
130
  return renderFunc(smartFuncId(prepareIdentifier(x.func), options.sqlDialect), x, options.sqlDialect, a => renderArgs(a, '=>', this.env, null));
110
131
  },
111
132
  xpr(x) {
133
+ const env = this.env.withSubPath([ 'xpr' ]);
112
134
  if (this.isNestedXpr && !x.cast)
113
- return `(${this.renderSubExpr(x.xpr)})`;
114
- return this.renderSubExpr(x.xpr);
135
+ return `(${this.renderSubExpr(x.xpr, env)})`;
136
+ return this.renderSubExpr(x.xpr, env);
115
137
  },
116
138
  SELECT( x) {
117
- return `(${renderQuery('<subselect>', x, increaseIndent(this.env))})`;
139
+ return `(${renderQuery(x, this.env.withIncreasedIndent())})`;
118
140
  },
119
141
  SET( x) {
120
- return `(${renderQuery('<union>', x, increaseIndent(this.env))})`;
142
+ return `(${renderQuery(x, this.env.withIncreasedIndent())})`;
121
143
  },
122
- });
144
+ }, true);
123
145
 
124
146
  function renderExpr( x, env ) {
125
147
  return exprRenderer.renderExpr(x, env);
@@ -137,7 +159,7 @@ function toSqlDdl( csn, options ) {
137
159
 
138
160
  // FIXME: Currently requires 'options.forHana', because it can only render HANA-ish SQL dialect
139
161
  if (!options.forHana && !isBetaEnabled(options, 'sqlExtensions'))
140
- throw new CompilerAssertion('toSql can currently only be used with HANA preprocessing');
162
+ throw new CompilerAssertion('to.sql can currently only be used with SAP HANA preprocessing');
141
163
 
142
164
  checkCSNVersion(csn, options);
143
165
 
@@ -165,13 +187,7 @@ function toSqlDdl( csn, options ) {
165
187
 
166
188
  // Render each artifact on its own
167
189
  forEachDefinition((options && options.testMode) ? sortCsn(csn, options) : csn, (artifact, artifactName) => {
168
- // This environment is passed down the call hierarchy, for dealing with
169
- // indentation issues
170
- const env = {
171
- // Current indentation string
172
- indent: '',
173
- };
174
- renderArtifactInto(artifactName, artifact, mainResultObj, env);
190
+ renderDefinitionInto(artifactName, artifact, mainResultObj, new SqlRenderEnvironment());
175
191
  });
176
192
 
177
193
  // Render each deleted artifact
@@ -179,27 +195,31 @@ function toSqlDdl( csn, options ) {
179
195
  renderArtifactDeletionInto(artifactName, csn.deletions[artifactName], mainResultObj);
180
196
 
181
197
  // Render each artifact extension
182
- // Only HANA SQL is currently supported.
198
+ // Only SAP HANA SQL is currently supported.
183
199
  // Note that extensions may contain new elements referenced in migrations, thus should be compiled first.
184
200
  if (csn.extensions && (options.sqlDialect === 'hana' || isBetaEnabled(options, 'sqlExtensions'))) {
185
- for (const extension of options && options.testMode ? sortCsn(csn.extensions) : csn.extensions) {
201
+ csn.extensions = options.testMode ? sortCsn(csn.extensions) : csn.extensions;
202
+ for (let i = 0; i < csn.extensions.length; ++i) {
203
+ const extension = csn.extensions[i];
186
204
  if (extension.extend) {
187
205
  const artifactName = extension.extend;
188
- const _artifact = csn.definitions[artifactName];
189
- const env = { indent: '', _artifact };
190
- renderArtifactExtensionInto(artifactName, _artifact, extension, mainResultObj, env);
206
+ const artifact = csn.definitions[artifactName];
207
+ const env = new SqlRenderEnvironment({ path: [ 'extensions', i ] });
208
+ renderArtifactExtensionInto(artifactName, artifact, extension, mainResultObj, env);
191
209
  }
192
210
  }
193
211
  }
194
212
 
195
213
  // Render each artifact change
196
- // Only HANA SQL is currently supported.
214
+ // Only SAP HANA SQL is currently supported.
197
215
  if (csn.migrations && (options.sqlDialect === 'hana' || isBetaEnabled(options, 'sqlExtensions'))) {
198
- for (const migration of options && options.testMode ? sortCsn(csn.migrations) : csn.migrations) {
216
+ csn.migrations = options.testMode ? sortCsn(csn.migrations) : csn.migrations;
217
+ for (const migration of csn.migrations) {
199
218
  if (migration.migrate) {
200
219
  const artifactName = migration.migrate;
201
- const _artifact = csn.definitions[artifactName];
202
- const env = { indent: '', _artifact };
220
+ // There is no "migrations" property in client CSN, so for better locations, use
221
+ // a path to the definition.
222
+ const env = new SqlRenderEnvironment({ path: [ 'definitions', artifactName ] });
203
223
  renderArtifactMigrationInto(artifactName, migration, mainResultObj, env);
204
224
  }
205
225
  }
@@ -260,21 +280,22 @@ function toSqlDdl( csn, options ) {
260
280
  return mainResultObj;
261
281
 
262
282
  /**
263
- * Render an artifact into the appropriate dictionary of 'resultObj'.
283
+ * Render a definition into the appropriate dictionary of 'resultObj'.
264
284
  *
265
285
  * @param {string} artifactName Name of the artifact to render
266
286
  * @param {CSN.Artifact} art Artifact to render
267
287
  * @param {object} resultObj Result collector
268
- * @param {object} env Render environment
288
+ * @param {SqlRenderEnvironment} env Render environment
269
289
  */
270
- function renderArtifactInto( artifactName, art, resultObj, env ) {
290
+ function renderDefinitionInto( artifactName, art, resultObj, env ) {
291
+ env.path = [ 'definitions', artifactName ];
271
292
  // Ignore whole artifacts if forRelationalDB says so
272
293
  if (art.abstract || hasValidSkipOrExists(art))
273
294
  return;
274
295
 
275
296
  switch (art.kind) {
276
297
  case 'entity':
277
- if (getNormalizedQuery(art).query) {
298
+ if (art.query || art.projection) {
278
299
  const result = renderView(artifactName, art, env);
279
300
  if (result)
280
301
  resultObj.hdbview[artifactName] = result;
@@ -301,13 +322,13 @@ function toSqlDdl( csn, options ) {
301
322
 
302
323
  /**
303
324
  * Render an artifact extension into the appropriate dictionary of 'resultObj'.
304
- * Only HANA SQL is currently supported.
325
+ * Only SAP HANA SQL is currently supported.
305
326
  *
306
327
  * @param {string} artifactName Name of the artifact to render
307
328
  * @param {CSN.Artifact} artifact The complete artifact
308
329
  * @param {CSN.Artifact} ext Extension to render
309
330
  * @param {object} resultObj Result collector
310
- * @param {object} env Render environment
331
+ * @param {SqlRenderEnvironment} env Render environment
311
332
  */
312
333
  function renderArtifactExtensionInto( artifactName, artifact, ext, resultObj, env ) {
313
334
  // Property kind is always omitted for elements and can be omitted for
@@ -328,7 +349,7 @@ function toSqlDdl( csn, options ) {
328
349
  }
329
350
 
330
351
  // Render an artifact migration into the appropriate dictionary of 'resultObj'.
331
- // Only HANA SQL is currently supported.
352
+ // Only SAP HANA SQL is currently supported.
332
353
  function renderArtifactMigrationInto( artifactName, migration, resultObj, env ) {
333
354
  function reducesTypeSize( def ) {
334
355
  // HANA does not allow decreasing the value of any of those type parameters.
@@ -338,7 +359,7 @@ function toSqlDdl( csn, options ) {
338
359
  function getEltStr( defVariant, eltName, changeType = 'extension' ) {
339
360
  return defVariant.target
340
361
  ? renderAssociationElement(eltName, defVariant, env)
341
- : renderElement(artifactName, eltName, defVariant, null, null, activateAlterMode(env, changeType));
362
+ : renderElement(eltName, defVariant, null, null, activateAlterMode(env, changeType));
342
363
  }
343
364
  function getEltStrNoProps( defVariant, eltName, ...props ) {
344
365
  const defNoProps = Object.assign({}, defVariant);
@@ -467,16 +488,15 @@ function toSqlDdl( csn, options ) {
467
488
  * @param {string} artifactName Name of the artifact to render
468
489
  * @param {CSN.Artifact} art Artifact to render
469
490
  * @param {object} resultObj Result collector
470
- * @param {object} env Render environment
491
+ * @param {SqlRenderEnvironment} env Render environment
471
492
  */
472
493
  function renderEntityInto( artifactName, art, resultObj, env ) {
473
- env._artifact = art;
474
- const childEnv = increaseIndent(env);
494
+ const childEnv = env.withIncreasedIndent();
475
495
  const hanaTc = art.technicalConfig && art.technicalConfig.hana;
476
496
  // tables can have @sql.prepend and @sql.append
477
497
  const { front, back } = getSqlSnippets(options, art);
478
498
  let result = front;
479
- // Only HANA has row/column tables
499
+ // Only SAP HANA has row/column tables
480
500
  if (options.sqlDialect === 'hana') {
481
501
  if (hanaTc && hanaTc.storeType) {
482
502
  // Explicitly specified
@@ -491,9 +511,10 @@ function toSqlDdl( csn, options ) {
491
511
  definitionsDuplicateChecker.addArtifact(art['@cds.persistence.name'], art.$location, artifactName);
492
512
  result += `TABLE ${tableName}`;
493
513
  result += ' (\n';
494
- const elements = Object.keys(art.elements).map(eltName => renderElement(artifactName, eltName, art.elements[eltName], definitionsDuplicateChecker, getFzIndex(eltName, hanaTc), childEnv)).filter(s => s !== '').join(',\n');
495
- if (elements !== '')
496
- result += elements;
514
+ result += Object.keys(art.elements)
515
+ .map(eltName => renderElement(eltName, art.elements[eltName], definitionsDuplicateChecker, getFzIndex(eltName, hanaTc), childEnv))
516
+ .filter(s => s !== '')
517
+ .join(',\n');
497
518
 
498
519
  const uniqueFields = Object.keys(art.elements).filter(name => art.elements[name].unique && !art.elements[name].virtual)
499
520
  .map(name => quoteSqlId(name))
@@ -507,7 +528,7 @@ function toSqlDdl( csn, options ) {
507
528
 
508
529
  // for `to.sql` w/ dialect `hana` the constraints will be part of the alter statement
509
530
  const constraintsAsAlter = !options.constraintsInCreateTable && options.src === 'sql' && (options.sqlDialect === 'hana' || options.sqlDialect === 'postgres');
510
- if ( !constraintsAsAlter && art.$tableConstraints && art.$tableConstraints.referential) {
531
+ if (!constraintsAsAlter && art.$tableConstraints?.referential) {
511
532
  const renderReferentialConstraintsAsHdbconstraint = options.src === 'hdi';
512
533
  const referentialConstraints = {};
513
534
  forEach(art.$tableConstraints.referential, ( fileName, referentialConstraint ) => {
@@ -546,12 +567,15 @@ function toSqlDdl( csn, options ) {
546
567
  result += renderTechnicalConfiguration(art.technicalConfig, childEnv);
547
568
 
548
569
 
549
- const associations = Object.keys(art.elements).map(name => renderAssociationElement(name, art.elements[name], childEnv))
550
- .filter(s => s !== '')
551
- .join(',\n');
552
- if (associations !== '' && options.sqlDialect === 'hana') {
553
- result += `${env.indent} WITH ASSOCIATIONS (\n${associations}\n`;
554
- result += `${env.indent})`;
570
+ if (options.sqlDialect === 'hana') {
571
+ const associations = Object.keys(art.elements)
572
+ .map(name => renderAssociationElement(name, art.elements[name], childEnv))
573
+ .filter(s => s !== '')
574
+ .join(',\n');
575
+ if (associations !== '') {
576
+ result += `${env.indent} WITH ASSOCIATIONS (\n${associations}\n`;
577
+ result += `${env.indent})`;
578
+ }
555
579
  }
556
580
  // Only HANA has indices
557
581
  // FIXME: Really? We should provide a DB-agnostic way to specify that
@@ -570,13 +594,13 @@ function toSqlDdl( csn, options ) {
570
594
 
571
595
  /**
572
596
  * Render an extended entity into the appropriate dictionaries of 'resultObj'.
573
- * Only HANA SQL is currently supported.
597
+ * Only SAP HANA SQL is currently supported.
574
598
  *
575
599
  * @param {string} artifactName Name of the artifact to render
576
600
  * @param {object} artifactElements Elements comprising the artifact
577
601
  * @param {object} extElements Elements comprising the extension
578
602
  * @param {object} resultObj Result collector
579
- * @param {object} env Render environment
603
+ * @param {SqlRenderEnvironment} env Render environment
580
604
  * @param {DuplicateChecker} duplicateChecker
581
605
  */
582
606
  function renderExtendInto( artifactName, artifactElements, extElements, resultObj, env, duplicateChecker ) {
@@ -638,40 +662,37 @@ function toSqlDdl( csn, options ) {
638
662
  * projection or view), optionally with corresponding fuzzy index 'fzindex' from the
639
663
  * technical configuration.
640
664
  * Ignore association elements (those are rendered later by renderAssociationElement).
641
- * Use 'artifactName' only for error output.
642
665
  * Return the resulting source string (no trailing LF).
643
666
  *
644
- * @param {string} artifactName Name of the artifact containing the element
645
667
  * @param {string} elementName Name of the element to render
646
668
  * @param {CSN.Element} elm CSN element
647
669
  * @param {DuplicateChecker} duplicateChecker Utility for detecting duplicates
648
670
  * @param {object} fzindex Fzindex object for the element
649
- * @param {object} env Render environment
671
+ * @param {SqlRenderEnvironment} env Render environment
650
672
  * @returns {string} Rendered element
651
673
  */
652
- function renderElement( artifactName, elementName, elm, duplicateChecker, fzindex, env ) {
674
+ function renderElement( elementName, elm, duplicateChecker, fzindex, env ) {
653
675
  if (elm.virtual || elm.target)
654
676
  return '';
655
-
677
+ env = env.withSubPath([ 'elements', elementName ]);
656
678
  const isPostgresAlterColumn = env.alterMode && env.changeType === 'migration' && options.sqlDialect === 'postgres';
657
679
  const quotedElementName = quoteSqlId(elementName);
658
680
  if (duplicateChecker)
659
681
  duplicateChecker.addElement(quotedElementName, elm.$location, elementName);
660
682
 
661
- let result = `${env.indent + quotedElementName}${isPostgresAlterColumn ? ' TYPE' : ''} ${renderTypeReference(artifactName, elementName, elm)
683
+ let result = `${env.indent + quotedElementName}${isPostgresAlterColumn ? ' TYPE' : ''} ${renderTypeReference(elm, env)
662
684
  }${renderNullability(elm, true, env.alterMode)}`;
663
- if (elm.default)
664
- result += ` DEFAULT ${renderExpr(elm.default, env)}`;
685
+ // calculated elements (on write) can't have a default; ignore it
686
+ if (elm.default && !elm.value)
687
+ result += ` DEFAULT ${renderExpr(elm.default, env.withSubPath([ 'default' ]))}`;
665
688
 
666
- // Only HANA has fuzzy indices
689
+ // Only SAP HANA has fuzzy indices
667
690
  if (fzindex && options.sqlDialect === 'hana')
668
691
  result += ` ${renderExpr(fzindex, env)}`;
669
692
 
670
693
  // (table) elements can only have a @sql.append
671
694
  const { back } = getSqlSnippets(options, elm);
672
-
673
- if (back !== '') // Needs to be rendered before the COMMENT
674
- result += back;
695
+ result += back; // Needs to be rendered before the COMMENT
675
696
 
676
697
  if (options.sqlDialect === 'hana' && hasHanaComment(elm, options))
677
698
  result += ` COMMENT ${renderStringForSql(getHanaComment(elm), options.sqlDialect)}`;
@@ -690,10 +711,11 @@ function toSqlDdl( csn, options ) {
690
711
  * @todo Duplicate check
691
712
  * @param {string} elementName Name of the element to render
692
713
  * @param {CSN.Element} elm CSN element
693
- * @param {object} env Render environment
714
+ * @param {SqlRenderEnvironment} env Render environment
694
715
  * @returns {string} Rendered association element
695
716
  */
696
717
  function renderAssociationElement( elementName, elm, env ) {
718
+ env = env.withSubPath([ 'elements', elementName ]);
697
719
  let result = '';
698
720
  if (elm.target) {
699
721
  result += env.indent;
@@ -713,7 +735,7 @@ function toSqlDdl( csn, options ) {
713
735
  }
714
736
  result += ' JOIN ';
715
737
  result += `${renderArtifactName(elm.target)} AS ${quoteSqlId(elementName)} ON (`;
716
- result += `${renderExpr(elm.on, env)})`;
738
+ result += `${renderExpr(elm.on, env.withSubPath([ 'on' ]))})`;
717
739
  }
718
740
  return result;
719
741
  }
@@ -726,7 +748,7 @@ function toSqlDdl( csn, options ) {
726
748
  * Return the resulting source string.
727
749
  *
728
750
  * @param {object} tc Technical configuration
729
- * @param {object} env Render environment
751
+ * @param {SqlRenderEnvironment} env Render environment
730
752
  * @returns {string} Rendered technical configuration
731
753
  */
732
754
  function renderTechnicalConfiguration( tc, env ) {
@@ -740,15 +762,15 @@ function toSqlDdl( csn, options ) {
740
762
  // This also affects renderIndexes
741
763
  tc = tc.hana;
742
764
  if (!tc)
743
- throw new ModelError('Expecting a HANA technical configuration');
765
+ throw new ModelError('Expecting a SAP HANA technical configuration');
744
766
 
745
767
  if (tc.tableSuffix) {
746
768
  // Although we could just render the whole bandwurm as one stream of tokens, the
747
769
  // compactor has kindly stored each part (e.g. `migration enabled` `row store`, ...)
748
770
  // in its own `xpr` (for the benefit of the `toCdl` renderer, which needs semicolons
749
- // between parts). We use this here for putting each one one line)
771
+ // between parts). We use this here for putting each one line)
750
772
 
751
- // The ignore array contains technical configurations that are illegal in HANA SQL
773
+ // This array contains technical configurations that are illegal in HANA SQL
752
774
  const ignore = [
753
775
  'PARTITION BY KEEPING EXISTING LAYOUT',
754
776
  'ROW STORE',
@@ -771,7 +793,7 @@ function toSqlDdl( csn, options ) {
771
793
  * @param {object} indexes Indices to render
772
794
  * @param {string} artifactName Artifact to render indices for
773
795
  * @param {object} resultObj Result collector
774
- * @param {object} env Render environment
796
+ * @param {SqlRenderEnvironment} env Render environment
775
797
  */
776
798
  function renderIndexesInto( indexes, artifactName, resultObj, env ) {
777
799
  // Indices and full-text indices
@@ -826,20 +848,19 @@ function toSqlDdl( csn, options ) {
826
848
 
827
849
  /**
828
850
  * Render the source of a query, which may be a path reference, possibly with an alias,
829
- * or a subselect, or a join operation. Use 'artifactName' only for error output.
851
+ * or a sub-select, or a join operation.
830
852
  *
831
853
  * Returns the source as a string.
832
854
  *
833
855
  * @todo Misleading name, should be something like 'renderQueryFrom'. All the query parts should probably also be rearranged.
834
- * @param {string} artifactName Name of the artifact containing the query
835
856
  * @param {object} source Query source
836
- * @param {object} env Render environment
857
+ * @param {SqlRenderEnvironment} env Render environment
837
858
  * @returns {string} Rendered view source
838
859
  */
839
- function renderViewSource( artifactName, source, env ) {
860
+ function renderViewSource( source, env ) {
840
861
  // Sub-SELECT
841
862
  if (source.SELECT || source.SET) {
842
- let result = `(${renderQuery(artifactName, source, increaseIndent(env))})`;
863
+ let result = `(${renderQuery(source, env.withIncreasedIndent())})`;
843
864
  if (source.as)
844
865
  result += ` AS ${quoteSqlId(source.as)}`;
845
866
 
@@ -848,14 +869,14 @@ function toSqlDdl( csn, options ) {
848
869
  // JOIN
849
870
  else if (source.join) {
850
871
  // One join operation, possibly with ON-condition
851
- let result = `${renderViewSource(artifactName, source.args[0], env)}`;
872
+ let result = `${renderViewSource(source.args[0], env.withSubPath([ 'args', 0 ]))}`;
852
873
  for (let i = 1; i < source.args.length; i++) {
853
874
  result = `(${result} ${source.join.toUpperCase()} `;
854
875
  if (options.sqlDialect === 'hana')
855
876
  result += renderJoinCardinality(source.cardinality);
856
- result += `JOIN ${renderViewSource(artifactName, source.args[i], env)}`;
877
+ result += `JOIN ${renderViewSource(source.args[i], env.withSubPath([ 'args', i ]))}`;
857
878
  if (source.on)
858
- result += ` ON ${renderExpr(source.on, env)}`;
879
+ result += ` ON ${renderExpr(source.on, env.withSubPath([ 'on' ]))}`;
859
880
 
860
881
  result += ')';
861
882
  }
@@ -867,7 +888,7 @@ function toSqlDdl( csn, options ) {
867
888
  if (!source.ref)
868
889
  throw new ModelError(`Expecting ref in ${JSON.stringify(source)}`);
869
890
 
870
- return renderAbsolutePathWithAlias(artifactName, source, env);
891
+ return renderAbsolutePathWithAlias(source, env);
871
892
  }
872
893
 
873
894
  /**
@@ -896,19 +917,17 @@ function toSqlDdl( csn, options ) {
896
917
  * Render a path that starts with an absolute name (as used for the source of a query),
897
918
  * possibly with an alias, with plain or quoted names, depending on options. Expects an object 'path' that has a
898
919
  * 'ref' and (in case of an alias) an 'as'. If necessary, an artificial alias
899
- * is created to the original implicit name. Use 'artifactName' only for error output.
920
+ * is created to the original implicit name.
900
921
  * Returns the name and alias as a string.
901
922
  *
902
- * @param {string} artifactName Name of the artifact containing the path - used for error output
903
923
  * @param {object} path Path to render
904
- * @param {object} env Render environment
924
+ * @param {SqlRenderEnvironment} env Render environment
905
925
  * @returns {string} Rendered path
906
926
  */
907
- function renderAbsolutePathWithAlias( artifactName, path, env ) {
927
+ function renderAbsolutePathWithAlias( path, env ) {
908
928
  // This actually can't happen anymore because assoc2joins should have taken care of it
909
929
  if (path.ref[0].where)
910
- throw new ModelError(`"${artifactName}": Filters in FROM are not supported for conversion to SQL`);
911
-
930
+ throw new ModelError(`"At ${JSON.stringify(env.path)}": Filters in FROM are not supported for conversion to SQL (path: ${JSON.stringify(path)})`);
912
931
 
913
932
  // SQL needs a ':' after path.ref[0] to separate associations
914
933
  let result = renderAbsolutePath(path, ':', env);
@@ -939,7 +958,7 @@ function toSqlDdl( csn, options ) {
939
958
  *
940
959
  * @param {object} path Path to render
941
960
  * @param {string} sep Separator between path steps
942
- * @param {object} env Render environment
961
+ * @param {SqlRenderEnvironment} env Render environment
943
962
  * @returns {string} Rendered path
944
963
  */
945
964
  function renderAbsolutePath( path, sep, env ) {
@@ -960,7 +979,9 @@ function toSqlDdl( csn, options ) {
960
979
  // An empty actual parameter list is rendered as `()`.
961
980
  const ref = csn.definitions[path.ref[0].id] || csn.definitions[path.ref[0]];
962
981
  if (ref && ref.params) {
963
- result += `(${renderArgs(path.ref[0] || {}, '=>', env, syntax)})`;
982
+ result += path.ref[0]?.args
983
+ ? `(${renderArgs(path.ref[0], '=>', env.withSubPath([ 'ref', 0 ]), syntax)})`
984
+ : '()';
964
985
  }
965
986
  else if (syntax === 'udf') {
966
987
  // if syntax is user defined function, render empty argument list
@@ -969,7 +990,7 @@ function toSqlDdl( csn, options ) {
969
990
  }
970
991
  if (path.ref[0].where) {
971
992
  const cardinality = path.ref[0].cardinality ? (`${path.ref[0].cardinality.max}: `) : '';
972
- result += `[${cardinality}${renderExpr(path.ref[0].where, env)}]`;
993
+ result += `[${cardinality}${renderExpr(path.ref[0].where, env.withSubPath([ 'ref', 0, 'where' ]))}]`;
973
994
  }
974
995
 
975
996
  // Add any path steps (possibly with parameters and filters) that may follow after that
@@ -986,24 +1007,25 @@ function toSqlDdl( csn, options ) {
986
1007
  *
987
1008
  * @param {object} node with `args` to render
988
1009
  * @param {string} sep Separator between args
989
- * @param {object} env Render environment
1010
+ * @param {SqlRenderEnvironment} env Render environment
990
1011
  * @param {string|null} syntax Some magic A2J parameter - for calcview parameter rendering
991
1012
  * @returns {string} Rendered arguments
992
1013
  * @throws Throws if args is not an array or object.
993
1014
  */
994
1015
  function renderArgs( node, sep, env, syntax ) {
995
- const args = node.args ? node.args : {};
1016
+ if (!node.args)
1017
+ return '';
996
1018
  // Positional arguments
997
- if (Array.isArray(args))
998
- return args.map(arg => renderExpr(arg, env)).join(', ');
1019
+ if (Array.isArray(node.args))
1020
+ return node.args.map((arg, i) => renderExpr(arg, env.withSubPath([ 'args', i ]))).join(', ');
999
1021
 
1000
1022
  // Named arguments (object/dict)
1001
- else if (typeof args === 'object')
1023
+ else if (typeof node.args === 'object')
1002
1024
  // if this is a function param which is not a reference to the model, we must not quote it
1003
- return Object.keys(args).map(key => `${node.func ? key : decorateParameter(key, syntax)} ${sep} ${renderExpr(args[key], env)}`).join(', ');
1025
+ return Object.keys(node.args).map(key => `${node.func ? key : decorateParameter(key, syntax)} ${sep} ${renderExpr(node.args[key], env.withSubPath([ 'args', key ]))}`).join(', ');
1004
1026
 
1005
1027
 
1006
- throw new ModelError(`Unknown args: ${JSON.stringify(args)}`);
1028
+ throw new ModelError(`Unknown args: ${JSON.stringify(node.args)}`);
1007
1029
 
1008
1030
 
1009
1031
  /**
@@ -1027,7 +1049,7 @@ function toSqlDdl( csn, options ) {
1027
1049
  *
1028
1050
  * @param {object} col Column to render
1029
1051
  * @param {CSN.Elements} elements of leading or subquery
1030
- * @param {object} env Render environment
1052
+ * @param {SqlRenderEnvironment} env Render environment
1031
1053
  * @returns {string} Rendered column
1032
1054
  */
1033
1055
  function renderViewColumn( col, elements, env ) {
@@ -1053,11 +1075,10 @@ function toSqlDdl( csn, options ) {
1053
1075
  *
1054
1076
  * @param {string} artifactName Name of the view
1055
1077
  * @param {CSN.Artifact} art CSN view
1056
- * @param {object} env Render environment
1078
+ * @param {SqlRenderEnvironment} env Render environment
1057
1079
  * @returns {string} Rendered view
1058
1080
  */
1059
1081
  function renderView( artifactName, art, env ) {
1060
- env._artifact = art;
1061
1082
  const viewName = renderArtifactName(artifactName);
1062
1083
  definitionsDuplicateChecker.addArtifact(art['@cds.persistence.name'], art && art.$location, artifactName);
1063
1084
  let result = `VIEW ${viewName}`;
@@ -1065,11 +1086,14 @@ function toSqlDdl( csn, options ) {
1065
1086
  if (options.sqlDialect === 'hana' && hasHanaComment(art, options))
1066
1087
  result += ` COMMENT ${renderStringForSql(getHanaComment(art), options.sqlDialect)}`;
1067
1088
 
1068
- result += renderParameterDefinitions(artifactName, art.params);
1069
- result += ` AS ${renderQuery(artifactName, getNormalizedQuery(art).query, env, art.elements)}`;
1089
+ result += renderParameterDefinitions(art.params, env);
1090
+ result += ` AS ${renderQuery(getNormalizedQuery(art).query,
1091
+ env.withSubPath([ art.projection ? 'projection' : 'query' ]),
1092
+ art.elements, !!art.projection)}`;
1070
1093
 
1071
- const childEnv = increaseIndent(env);
1072
- const associations = Object.keys(art.elements).filter(name => !!art.elements[name].target)
1094
+ const childEnv = env.withIncreasedIndent();
1095
+ const associations = Object.keys(art.elements)
1096
+ .filter(name => !!art.elements[name].target)
1073
1097
  .map(name => renderAssociationElement(name, art.elements[name], childEnv))
1074
1098
  .filter(s => s !== '')
1075
1099
  .join(',\n');
@@ -1089,18 +1113,19 @@ function toSqlDdl( csn, options ) {
1089
1113
  /**
1090
1114
  * Render the parameter definition of a view if any. Return the parameters in parentheses, or an empty string
1091
1115
  *
1092
- * @param {string} artifactName Name of the view
1093
1116
  * @param {Object} params Dictionary of parameters
1117
+ * @param {SqlRenderEnvironment} env
1094
1118
  * @returns {string} Rendered parameters
1095
1119
  */
1096
- function renderParameterDefinitions( artifactName, params ) {
1120
+ function renderParameterDefinitions( params, env ) {
1097
1121
  let result = '';
1098
1122
  if (params) {
1099
1123
  const parray = [];
1100
1124
  for (const pn in params) {
1125
+ const paramEnv = env.withSubPath([ 'params', pn ]);
1101
1126
  const p = params[pn];
1102
1127
  if (p.notNull === true || p.notNull === false)
1103
- info('query-ignoring-param-nullability', [ 'definitions', artifactName, 'params', pn ], { '#': 'sql' });
1128
+ info('query-ignoring-param-nullability', paramEnv.path, { '#': 'sql' });
1104
1129
  // do not quote parameter identifiers for naming mode "quoted" / "hdbcds"
1105
1130
  // this would be an incompatible change, as non-uppercased, quoted identifiers
1106
1131
  // are rejected by the HANA compiler.
@@ -1109,9 +1134,9 @@ function toSqlDdl( csn, options ) {
1109
1134
  pIdentifier = prepareIdentifier(pn);
1110
1135
  else
1111
1136
  pIdentifier = quoteSqlId(pn);
1112
- let pstr = `IN ${pIdentifier} ${renderTypeReference(artifactName, pn, p)}`;
1137
+ let pstr = `IN ${pIdentifier} ${renderTypeReference(p, paramEnv)}`;
1113
1138
  if (p.default)
1114
- pstr += ` DEFAULT ${renderExpr(p.default, { indent: '' })}`;
1139
+ pstr += ` DEFAULT ${renderExpr(p.default, new SqlRenderEnvironment({ ...env, indent: '' }))}`;
1115
1140
 
1116
1141
  parray.push(pstr);
1117
1142
  }
@@ -1121,24 +1146,26 @@ function toSqlDdl( csn, options ) {
1121
1146
  }
1122
1147
 
1123
1148
  /**
1124
- * Render a query 'query', i.e. a select statement with where-condition etc. Use 'artifactName' only for error messages.
1149
+ * Render a query 'query', i.e. a select statement with where-condition etc.
1125
1150
  *
1126
- * @param {string} artifactName Artifact containing the query
1127
1151
  * @param {CSN.Query} query CSN query
1128
- * @param {object} env Render environment
1152
+ * @param {SqlRenderEnvironment} env Render environment
1129
1153
  * @param {CSN.Elements} [elements] to override direct query elements - e.g. leading union should win
1154
+ * @param {boolean} [isProjection]
1130
1155
  * @returns {string} Rendered query
1131
1156
  */
1132
- function renderQuery( artifactName, query, env, elements = null ) {
1157
+ function renderQuery( query, env, elements = null, isProjection = false ) {
1133
1158
  let result = '';
1134
1159
  // Set operator, like UNION, INTERSECT, ...
1135
1160
  if (query.SET) {
1161
+ env = env.withSubPath([ 'SET' ]);
1136
1162
  result += query.SET.args
1137
- .map((arg) => {
1163
+ .map((arg, index) => {
1138
1164
  // Wrap each query in the SET in parentheses that
1139
1165
  // - is a SET itself (to preserve precedence between the different SET operations),
1140
1166
  // - has an ORDER BY/LIMIT (because UNION etc. can't stand directly behind an ORDER BY)
1141
- const queryString = renderQuery(artifactName, arg, env, elements || query.SET.elements);
1167
+ const argEnv = env.withSubPath([ 'args', index ]);
1168
+ const queryString = renderQuery( arg, argEnv, elements || query.SET.elements, false);
1142
1169
  return (arg.SET || arg.SELECT && (arg.SELECT.orderBy || arg.SELECT.limit)) ? `(${queryString})` : queryString;
1143
1170
  })
1144
1171
  .join(`\n${env.indent}${query.SET.op && query.SET.op.toUpperCase()}${query.SET.all ? ' ALL ' : ' '}`);
@@ -1150,10 +1177,10 @@ function toSqlDdl( csn, options ) {
1150
1177
  if (query.SET.orderBy || query.SET.limit) {
1151
1178
  result = `(${result})`;
1152
1179
  if (query.SET.orderBy)
1153
- result += `\n${env.indent}ORDER BY ${query.SET.orderBy.map(entry => renderOrderByEntry(entry, env)).join(', ')}`;
1180
+ result += `\n${env.indent}ORDER BY ${query.SET.orderBy.map(entry => renderOrderByEntry(entry, env.withSubPath([ 'orderBy' ]))).join(', ')}`;
1154
1181
 
1155
1182
  if (query.SET.limit)
1156
- result += `\n${env.indent}${renderLimit(query.SET.limit, env)}`;
1183
+ result += `\n${env.indent}${renderLimit(query.SET.limit, env.withSubPath([ 'limit' ]))}`;
1157
1184
  }
1158
1185
  return result;
1159
1186
  }
@@ -1161,30 +1188,37 @@ function toSqlDdl( csn, options ) {
1161
1188
  else if (!query.SELECT) {
1162
1189
  throw new ModelError(`Unexpected query operation ${JSON.stringify(query)}`);
1163
1190
  }
1191
+ if (!isProjection)
1192
+ env = env.withSubPath([ 'SELECT' ]);
1164
1193
  const select = query.SELECT;
1165
- const childEnv = increaseIndent(env);
1194
+ const childEnv = env.withIncreasedIndent();
1166
1195
  result += `SELECT${select.distinct ? ' DISTINCT' : ''}`;
1167
1196
  // FIXME: We probably also need to consider `excluding` here ?
1168
1197
  result += `\n${(select.columns || [ '*' ])
1169
- .filter(col => !select.mixin?.[firstPathStepId(col.ref)]) // No mixin columns
1170
- .map(col => renderViewColumn(col, elements || select.elements, childEnv))
1198
+ .map((col, index) => {
1199
+ if (!select.mixin?.[firstPathStepId(col.ref)]) {
1200
+ const colEnv = select.columns ? childEnv.withSubPath([ 'columns', index ]) : childEnv;
1201
+ return renderViewColumn(col, elements || select.elements, colEnv);
1202
+ }
1203
+ return ''; // No mixin columns
1204
+ })
1171
1205
  .filter(s => s !== '')
1172
1206
  .join(',\n')}\n`;
1173
- result += `${env.indent}FROM ${renderViewSource(artifactName, select.from, env)}`;
1207
+ result += `${env.indent}FROM ${renderViewSource( select.from, env.withSubPath([ 'from' ]))}`;
1174
1208
  if (select.where)
1175
- result += `\n${env.indent}WHERE ${renderExpr(select.where, env)}`;
1209
+ result += `\n${env.indent}WHERE ${renderExpr(select.where, env.withSubPath([ 'where' ]))}`;
1176
1210
 
1177
1211
  if (select.groupBy)
1178
- result += `\n${env.indent}GROUP BY ${select.groupBy.map(expr => renderExpr(expr, env)).join(', ')}`;
1212
+ result += `\n${env.indent}GROUP BY ${select.groupBy.map((expr, i) => renderExpr(expr, env.withSubPath([ 'groupBy', i ]))).join(', ')}`;
1179
1213
 
1180
1214
  if (select.having)
1181
- result += `\n${env.indent}HAVING ${renderExpr(select.having, env)}`;
1215
+ result += `\n${env.indent}HAVING ${renderExpr(select.having, env.withSubPath([ 'having' ]))}`;
1182
1216
 
1183
1217
  if (select.orderBy)
1184
- result += `\n${env.indent}ORDER BY ${select.orderBy.map(entry => renderOrderByEntry(entry, env)).join(', ')}`;
1218
+ result += `\n${env.indent}ORDER BY ${select.orderBy.map((entry, i) => renderOrderByEntry(entry, env.withSubPath([ 'orderBy', i ]))).join(', ')}`;
1185
1219
 
1186
1220
  if (select.limit)
1187
- result += `\n${env.indent}${renderLimit(select.limit, env)}`;
1221
+ result += `\n${env.indent}${renderLimit(select.limit, env.withSubPath([ 'limit' ]))}`;
1188
1222
 
1189
1223
  return result;
1190
1224
  }
@@ -1203,17 +1237,17 @@ function toSqlDdl( csn, options ) {
1203
1237
  * Render a query's LIMIT clause, which may also have OFFSET.
1204
1238
  *
1205
1239
  * @param {CSN.QueryLimit} limit Limit clause
1206
- * @param {object} env Render environment
1240
+ * @param {SqlRenderEnvironment} env Render environment
1207
1241
  * @returns {string} Rendered LIMIT clause
1208
1242
  */
1209
1243
  function renderLimit( limit, env ) {
1210
1244
  let result = '';
1211
1245
  if (limit.rows !== undefined)
1212
- result += `LIMIT ${renderExpr(limit.rows, env)}`;
1246
+ result += `LIMIT ${renderExpr(limit.rows, env.withSubPath([ 'rows' ]))}`;
1213
1247
 
1214
1248
  if (limit.offset !== undefined) {
1215
1249
  const indent = result !== '' ? `\n${env.indent}` : '';
1216
- result += `${indent}OFFSET ${renderExpr(limit.offset, env)}`;
1250
+ result += `${indent}OFFSET ${renderExpr(limit.offset, env.withSubPath([ 'offset' ]))}`;
1217
1251
  }
1218
1252
 
1219
1253
  return result;
@@ -1224,7 +1258,7 @@ function toSqlDdl( csn, options ) {
1224
1258
  * have a 'sort' property for ASC/DESC and a 'nulls' for FIRST/LAST
1225
1259
  *
1226
1260
  * @param {object} entry Part of an ORDER BY
1227
- * @param {object} env Render environment
1261
+ * @param {SqlRenderEnvironment} env Render environment
1228
1262
  * @returns {string} Rendered ORDER BY entry
1229
1263
  */
1230
1264
  function renderOrderByEntry( entry, env ) {
@@ -1239,45 +1273,46 @@ function toSqlDdl( csn, options ) {
1239
1273
  }
1240
1274
 
1241
1275
  /**
1242
- * Render a reference to the type used by 'elm' (with name 'elementName' in 'artifactName', both used only for error messages).
1276
+ * Render a reference to the type used by 'elm'. env.path must point to the element/param.
1243
1277
  *
1244
- * @param {string} artifactName Artifact containing the element
1245
- * @param {string} elementName Element referencing the type
1246
1278
  * @param {CSN.Element} elm CSN element
1279
+ * @param {SqlRenderEnvironment} env
1247
1280
  * @returns {string} Rendered type reference
1248
1281
  */
1249
- function renderTypeReference( artifactName, elementName, elm ) {
1282
+ function renderTypeReference( elm, env ) {
1250
1283
  let result = '';
1251
1284
 
1252
1285
  // Anonymous structured type: Not supported with SQL (but shouldn't happen anyway after forHana flattened them)
1253
1286
  if (!elm.type && !elm.value) {
1254
1287
  if (!elm.elements)
1255
- throw new ModelError(`Missing type of: ${elementName}`);
1288
+ throw new ModelError(`to.sql(): Missing type of: ${JSON.stringify(env.path)}`);
1256
1289
 
1257
- // TODO: Signal is not covered by tests + better location
1258
- error(null, [ 'definitions', artifactName, 'elements', elementName ],
1290
+ // TODO: Signal is not covered by tests
1291
+ error(null, env.path,
1259
1292
  'Anonymous structured types are not supported for conversion to SQL');
1260
1293
  return result;
1261
1294
  }
1262
1295
 
1263
1296
  // Association type
1264
1297
  if (elm.target) {
1265
- // TODO: Signal is not covered by tests + better location
1298
+ // TODO: Signal is not covered by tests
1266
1299
  // We can't do associations yet
1267
- error(null, [ 'definitions', artifactName, 'elements', elementName ],
1300
+ error(null, env.path,
1268
1301
  'Association and composition types are not yet supported for conversion to SQL');
1269
1302
  return result;
1270
1303
  }
1271
1304
 
1272
- // If we get here, it must be a primitive (i.e. builtin) type
1273
- if (isBuiltinType(elm.type)) {
1274
- // cds.Integer => render as INTEGER (no quotes)
1275
- result += renderBuiltinType(elm.type);
1276
- }
1277
- else {
1278
- throw new ModelError(`Unexpected non-primitive type of: ${artifactName}.${elementName}`);
1305
+ if (elm.type) {
1306
+ // If we get here, it must be a primitive (i.e. builtin) type
1307
+ if (isBuiltinType(elm.type)) {
1308
+ // cds.Integer => render as INTEGER (no quotes)
1309
+ result += renderBuiltinType(elm.type);
1310
+ result += renderTypeParameters(elm);
1311
+ }
1312
+ else {
1313
+ throw new ModelError(`Unexpected non-primitive type of: ${JSON.stringify(env.path)}`);
1314
+ }
1279
1315
  }
1280
- result += renderTypeParameters(elm);
1281
1316
 
1282
1317
  if (elm.value) {
1283
1318
  if (!elm.value.stored)
@@ -1286,7 +1321,7 @@ function toSqlDdl( csn, options ) {
1286
1321
  // The SQL standard 2016 describes the syntax in section 11.3 - 11.4
1287
1322
  // of the SQL Foundation spec (for 2003 in 5WD-02-Foundation-2003-09.pdf). Summarized:
1288
1323
  // <generation clause> ::= GENERATED ALWAYS AS '(' <value expression> ')'
1289
- result += ` GENERATED ALWAYS AS (${renderExpr(elm.value)})`;
1324
+ result += ` GENERATED ALWAYS AS (${renderExpr(elm.value, env.withSubPath([ 'value' ]))})`;
1290
1325
  // However, it appears many databases require a trailing "STORED".
1291
1326
  if (options.sqlDialect === 'sqlite' || options.sqlDialect === 'postgres')
1292
1327
  result += ' STORED';
@@ -1327,7 +1362,6 @@ function toSqlDdl( csn, options ) {
1327
1362
  return obj.$notNull ? ' NOT NULL' : ' NULL';
1328
1363
  }
1329
1364
 
1330
-
1331
1365
  if (obj.notNull === undefined && !(obj.key && treatKeyAsNotNull)) {
1332
1366
  // Attribute not set at all
1333
1367
  return '';
@@ -1398,6 +1432,11 @@ function toSqlDdl( csn, options ) {
1398
1432
  }
1399
1433
  }
1400
1434
 
1435
+ /**
1436
+ * @param {object} x
1437
+ * @param {object} env
1438
+ * @return {string}
1439
+ */
1401
1440
  function renderExpressionRef( x, env ) {
1402
1441
  if (!x.param && !x.global) {
1403
1442
  const magicReplacement = getVariableReplacement(x.ref, options);
@@ -1406,13 +1445,13 @@ function toSqlDdl( csn, options ) {
1406
1445
  if (magicReplacement !== null)
1407
1446
  return renderStringForSql(magicReplacement, options.sqlDialect);
1408
1447
 
1409
- const result = render$user();
1448
+ const result = render$user(x);
1410
1449
  // Invalid second path step doesn't cause a return
1411
1450
  if (result)
1412
1451
  return result;
1413
1452
  }
1414
1453
  else if (x.ref[0] === '$at' || x.ref[0] === '$valid') {
1415
- const result = render$at();
1454
+ const result = render$at(x);
1416
1455
  // Invalid second path step doesn't cause a return
1417
1456
  if (result)
1418
1457
  return result;
@@ -1444,174 +1483,171 @@ function toSqlDdl( csn, options ) {
1444
1483
  if (x.param)
1445
1484
  return `:${x.ref[0].toUpperCase()}`;
1446
1485
 
1447
- return x.ref.map(renderPathStep)
1486
+ return x.ref.map((step, i) => renderPathStep(step, i, env.withSubPath([ 'ref', i ])))
1448
1487
  .filter(s => s !== '')
1449
1488
  .join('.');
1489
+ }
1450
1490
 
1451
- /**
1452
- * @returns {string|null} Null in case of an invalid second path step
1453
- */
1454
- function render$user() {
1455
- // FIXME: this is all not enough: we might need an explicit select item alias (?)
1456
- if (x.ref[1] === 'id') {
1457
- if (options.sqlDialect === 'hana')
1458
- return 'SESSION_CONTEXT(\'APPLICATIONUSER\')';
1459
- else if (options.sqlDialect === 'postgres')
1460
- return 'current_setting(\'CAP.APPLICATIONUSER\')';
1461
- else if (options.betterSqliteSessionVariables && options.sqlDialect === 'sqlite')
1462
- return 'session_context( \'$user.id\' )';
1463
- warning(null, null, 'The "$user" variable is not supported. Use option "variableReplacements" to specify a value for "$user.id"');
1464
- return '\'$user.id\'';
1465
- }
1466
- else if (x.ref[1] === 'locale') {
1467
- if (options.sqlDialect === 'hana')
1468
- return 'SESSION_CONTEXT(\'LOCALE\')';
1469
- else if (options.sqlDialect === 'postgres')
1470
- return 'current_setting(\'CAP.LOCALE\')';
1471
- else if (options.betterSqliteSessionVariables && options.sqlDialect === 'sqlite')
1472
- return 'session_context( \'$user.locale\' )';
1473
- return '\'en\''; // default language
1474
- }
1475
- // Basically: Second path step was invalid, do nothing - should not happen.
1476
- return null;
1491
+ /**
1492
+ * @param {object} x
1493
+ * @returns {string|null} Null in case of an invalid second path step
1494
+ */
1495
+ function render$user( x ) {
1496
+ // FIXME: this is all not enough: we might need an explicit select item alias (?)
1497
+ if (x.ref[1] === 'id') {
1498
+ if (options.sqlDialect === 'hana')
1499
+ return 'SESSION_CONTEXT(\'APPLICATIONUSER\')';
1500
+ else if (options.sqlDialect === 'postgres')
1501
+ return 'current_setting(\'CAP.APPLICATIONUSER\')';
1502
+ else if (options.betterSqliteSessionVariables && options.sqlDialect === 'sqlite')
1503
+ return 'session_context( \'$user.id\' )';
1504
+ warning(null, null, 'The "$user" variable is not supported. Use option "variableReplacements" to specify a value for "$user.id"');
1505
+ return '\'$user.id\'';
1477
1506
  }
1478
- /**
1479
- * For a given reference starting with $at, render a 'current_timestamp' literal for plain.
1480
- * For the sql-dialect hana, we render the TO_TIMESTAMP(SESSION_CONTEXT(..)) function.
1481
- *
1482
- *
1483
- * For sqlite, we render the string-format-time (strftime) function.
1484
- * Because the format of `current_timestamp` is like that: '2021-05-14 09:17:19' whereas
1485
- * the format for TimeStamps (at least in Node.js) is like that: '2021-01-01T00:00:00.000Z'
1486
- * --> Therefore the comparison in the temporal where clause doesn't work properly.
1487
- *
1488
- * @returns {string|null} Null in case of an invalid second path step
1489
- */
1490
- function render$at() {
1491
- if (x.ref[1] === 'from') {
1492
- switch (options.sqlDialect) {
1493
- case 'sqlite': {
1494
- if (options.betterSqliteSessionVariables)
1495
- return 'session_context( \'$valid.from\' )';
1496
- const dateFromFormat = '%Y-%m-%dT%H:%M:%S.000Z';
1497
- return `strftime('${dateFromFormat}', 'now')`;
1498
- }
1499
- case 'hana':
1500
- return 'TO_TIMESTAMP(SESSION_CONTEXT(\'VALID-FROM\'))';
1501
- case 'postgres':
1502
- return '(to_timestamp(current_setting(\'CAP.VALID_FROM\'), \'YYYY-MM-DD HH24:MI:SS.FF6\') at time zone \'UTC\')';
1503
- case 'h2':
1504
- case 'plain':
1505
- return 'current_timestamp';
1506
- default:
1507
- break;
1507
+ else if (x.ref[1] === 'locale') {
1508
+ if (options.sqlDialect === 'hana')
1509
+ return 'SESSION_CONTEXT(\'LOCALE\')';
1510
+ else if (options.sqlDialect === 'postgres')
1511
+ return 'current_setting(\'CAP.LOCALE\')';
1512
+ else if (options.betterSqliteSessionVariables && options.sqlDialect === 'sqlite')
1513
+ return 'session_context( \'$user.locale\' )';
1514
+ return '\'en\''; // default language
1515
+ }
1516
+ // Basically: Second path step was invalid, do nothing - should not happen.
1517
+ return null;
1518
+ }
1519
+
1520
+ /**
1521
+ * For a given reference starting with $at, render a 'current_timestamp' literal for plain.
1522
+ * For the sql-dialect hana, we render the TO_TIMESTAMP(SESSION_CONTEXT(..)) function.
1523
+ *
1524
+ *
1525
+ * For sqlite, we render the string-format-time (strftime) function.
1526
+ * Because the format of `current_timestamp` is like that: '2021-05-14 09:17:19' whereas
1527
+ * the format for TimeStamps (at least in Node.js) is like that: '2021-01-01T00:00:00.000Z'
1528
+ * --> Therefore the comparison in the temporal where clause doesn't work properly.
1529
+ *
1530
+ * @param {object} x
1531
+ * @returns {string|null} Null in case of an invalid second path step
1532
+ */
1533
+ function render$at( x ) {
1534
+ if (x.ref[1] === 'from') {
1535
+ switch (options.sqlDialect) {
1536
+ case 'sqlite': {
1537
+ if (options.betterSqliteSessionVariables)
1538
+ return 'session_context( \'$valid.from\' )';
1539
+ const dateFromFormat = '%Y-%m-%dT%H:%M:%S.000Z';
1540
+ return `strftime('${dateFromFormat}', 'now')`;
1508
1541
  }
1542
+ case 'hana':
1543
+ return 'TO_TIMESTAMP(SESSION_CONTEXT(\'VALID-FROM\'))';
1544
+ case 'postgres':
1545
+ return '(to_timestamp(current_setting(\'CAP.VALID_FROM\'), \'YYYY-MM-DD HH24:MI:SS.FF6\') at time zone \'UTC\')';
1546
+ case 'h2':
1547
+ case 'plain':
1548
+ return 'current_timestamp';
1549
+ default:
1550
+ break;
1509
1551
  }
1552
+ }
1510
1553
 
1511
- if (x.ref[1] === 'to') {
1512
- switch (options.sqlDialect) {
1513
- case 'sqlite': {
1514
- if (options.betterSqliteSessionVariables)
1515
- return 'session_context( \'$valid.to\' )';
1516
- // + 1ms compared to $at.from
1517
- const dateToFormat = '%Y-%m-%dT%H:%M:%S.001Z';
1518
- return `strftime('${dateToFormat}', 'now')`;
1519
- }
1520
- case 'hana':
1521
- return 'TO_TIMESTAMP(SESSION_CONTEXT(\'VALID-TO\'))';
1522
- case 'postgres':
1523
- return '(to_timestamp(current_setting(\'CAP.VALID_TO\'), \'YYYY-MM-DD HH24:MI:SS.FF6\') at time zone \'UTC\')';
1524
- case 'h2':
1525
- case 'plain':
1526
- return 'current_timestamp';
1527
- default:
1528
- break;
1554
+ if (x.ref[1] === 'to') {
1555
+ switch (options.sqlDialect) {
1556
+ case 'sqlite': {
1557
+ if (options.betterSqliteSessionVariables)
1558
+ return 'session_context( \'$valid.to\' )';
1559
+ // + 1ms compared to $at.from
1560
+ const dateToFormat = '%Y-%m-%dT%H:%M:%S.001Z';
1561
+ return `strftime('${dateToFormat}', 'now')`;
1529
1562
  }
1563
+ case 'hana':
1564
+ return 'TO_TIMESTAMP(SESSION_CONTEXT(\'VALID-TO\'))';
1565
+ case 'postgres':
1566
+ return '(to_timestamp(current_setting(\'CAP.VALID_TO\'), \'YYYY-MM-DD HH24:MI:SS.FF6\') at time zone \'UTC\')';
1567
+ case 'h2':
1568
+ case 'plain':
1569
+ return 'current_timestamp';
1570
+ default:
1571
+ break;
1530
1572
  }
1531
- return null;
1532
1573
  }
1574
+ return null;
1575
+ }
1533
1576
 
1534
- /**
1535
- * Render a single path step 's' at path position 'idx', which can have filters or parameters or be a function
1536
- *
1537
- * @param {string|object} s Path step to render
1538
- * @param {number} idx index of the path step in the overall path
1539
- * @returns {string} Rendered path step
1540
- */
1541
- function renderPathStep( s, idx ) {
1542
- // Simple id or absolute name
1543
- if (typeof (s) === 'string') {
1544
- // TODO: When is this actually executed and not handled already in renderExpr?
1545
- const magicForHana = {
1546
- '$user.id': 'SESSION_CONTEXT(\'APPLICATIONUSER\')',
1547
- '$user.locale': 'SESSION_CONTEXT(\'LOCALE\')',
1548
- };
1549
- // Some magic for first path steps
1550
- if (idx === 0) {
1551
- // HANA-specific translation of '$now' and '$user'
1552
- // FIXME: this is all not enough: we might need an explicit select item alias
1553
- if (options.sqlDialect === 'hana' && magicForHana[s])
1554
- return magicForHana[s];
1555
-
1556
- // Ignore initial $projection and initial $self
1557
- if (s === '$projection' || s === '$self')
1558
- return '';
1559
- }
1560
- return quoteSqlId(s);
1577
+ /**
1578
+ * Render a single path step 's' at path position 'idx', which can have filters or parameters or be a function
1579
+ *
1580
+ * @param {string|object} s Path step to render
1581
+ * @param {number} idx index of the path step in the overall path
1582
+ * @param {object} env
1583
+ * @returns {string} Rendered path step
1584
+ */
1585
+ function renderPathStep( s, idx, env ) {
1586
+ // Simple id or absolute name
1587
+ if (typeof (s) === 'string') {
1588
+ // TODO: When is this actually executed and not handled already in renderExpr?
1589
+ const magicForHana = {
1590
+ '$user.id': 'SESSION_CONTEXT(\'APPLICATIONUSER\')',
1591
+ '$user.locale': 'SESSION_CONTEXT(\'LOCALE\')',
1592
+ };
1593
+ // Some magic for first path steps
1594
+ if (idx === 0) {
1595
+ // HANA-specific translation of '$now' and '$user'
1596
+ // FIXME: this is all not enough: we might need an explicit select item alias
1597
+ if (options.sqlDialect === 'hana' && magicForHana[s])
1598
+ return magicForHana[s];
1599
+
1600
+ // Ignore initial $projection and initial $self
1601
+ if (s === '$projection' || s === '$self')
1602
+ return '';
1561
1603
  }
1562
- // ID with filters or parameters
1563
- else if (typeof s === 'object') {
1564
- // Sanity check
1565
- if (!s.func && !s.id)
1566
- throw new ModelError(`Unknown path step object: ${JSON.stringify(s)}`);
1567
-
1568
- // Not really a path step but an object-like function call
1569
- if (s.func)
1570
- return `${s.func}(${renderArgs(s, '=>', env, null)})`;
1571
-
1572
- // Path step, possibly with view parameters and/or filters
1573
- let result = `${quoteSqlId(s.id)}`;
1574
- if (s.args) {
1575
- // View parameters
1576
- result += `(${renderArgs(s, '=>', env, null)})`;
1577
- }
1578
- if (s.where) {
1579
- // Filter, possibly with cardinality
1580
- // FIXME: Does SQL understand filter cardinalities?
1581
- const cardinality = s.cardinality ? (`${s.cardinality.max}: `) : '';
1582
- result += `[${cardinality}${renderExpr(s.where, env)}]`;
1583
- }
1584
- return result;
1604
+ return quoteSqlId(s);
1605
+ }
1606
+ // ID with filters or parameters
1607
+ else if (typeof s === 'object') {
1608
+ // Sanity check
1609
+ if (!s.func && !s.id)
1610
+ throw new ModelError(`Unknown path step object: ${JSON.stringify(s)}`);
1611
+
1612
+ // Not really a path step but an object-like function call
1613
+ if (s.func)
1614
+ return `${s.func}(${renderArgs(s, '=>', env, null)})`;
1615
+
1616
+ // Path step, possibly with view parameters and/or filters
1617
+ let result = `${quoteSqlId(s.id)}`;
1618
+ if (s.args) {
1619
+ // View parameters
1620
+ result += `(${renderArgs(s, '=>', env, null)})`;
1585
1621
  }
1586
-
1587
- throw new ModelError(`Unknown path step: ${JSON.stringify(s)}`);
1622
+ if (s.where) {
1623
+ // Filter, possibly with cardinality
1624
+ // FIXME: Does SQL understand filter cardinalities?
1625
+ const cardinality = s.cardinality ? (`${s.cardinality.max}: `) : '';
1626
+ result += `[${cardinality}${renderExpr(s.where, env.withSubPath([ 'where' ]))}]`;
1627
+ }
1628
+ return result;
1588
1629
  }
1630
+
1631
+ throw new ModelError(`Unknown path step: ${JSON.stringify(s)}`);
1589
1632
  }
1590
1633
 
1634
+
1591
1635
  function renderWindowFunction( funcName, node, fctEnv ) {
1592
1636
  let r = `${funcName}(${renderArgs(node, '=>', fctEnv, null)}) `;
1593
- r += renderExpr(node.xpr, fctEnv); // xpr[0] is 'over'
1637
+ r += renderExpr(node.xpr, fctEnv.withSubPath([ 'xpr' ])); // xpr[0] is 'over'
1594
1638
  return r;
1595
1639
  }
1596
1640
 
1597
- /**
1598
- * Returns a copy of 'env' with increased indentation
1599
- *
1600
- * @param {object} env Render environment
1601
- * @returns {object} Render environment with increased indent
1602
- */
1603
- function increaseIndent( env ) {
1604
- return Object.assign({}, env, { indent: `${env.indent} ` });
1605
- }
1641
+
1606
1642
  /**
1607
1643
  * Returns a copy of 'env' with alterMode set to true
1608
1644
  *
1609
- * @param {object} env Render environment
1645
+ * @param {SqlRenderEnvironment} env Render environment
1610
1646
  * @param {string} changeType 'extension' or 'migration'
1611
1647
  * @returns {object} Render environment with alterMode
1612
1648
  */
1613
1649
  function activateAlterMode( env, changeType ) {
1614
- return Object.assign({ alterMode: true, changeType }, env);
1650
+ return env.cloneWith({ alterMode: true, changeType });
1615
1651
  }
1616
1652
  }
1617
1653