@sap/cds-compiler 2.12.0 → 2.15.2

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 (128) hide show
  1. package/CHANGELOG.md +221 -15
  2. package/bin/cdsc.js +125 -50
  3. package/bin/cdsse.js +2 -2
  4. package/doc/CHANGELOG_BETA.md +13 -6
  5. package/doc/CHANGELOG_DEPRECATED.md +22 -6
  6. package/doc/NameResolution.md +21 -16
  7. package/lib/api/main.js +47 -84
  8. package/lib/api/options.js +5 -6
  9. package/lib/api/validate.js +6 -11
  10. package/lib/backends.js +15 -23
  11. package/lib/base/dictionaries.js +0 -8
  12. package/lib/base/error.js +26 -0
  13. package/lib/base/keywords.js +7 -17
  14. package/lib/base/location.js +9 -4
  15. package/lib/base/message-registry.js +114 -18
  16. package/lib/base/messages.js +101 -90
  17. package/lib/base/model.js +2 -63
  18. package/lib/base/optionProcessorHelper.js +177 -123
  19. package/lib/checks/annotationsOData.js +12 -33
  20. package/lib/checks/arrayOfs.js +1 -34
  21. package/lib/checks/cdsPersistence.js +2 -1
  22. package/lib/checks/enricher.js +17 -1
  23. package/lib/checks/invalidTarget.js +3 -1
  24. package/lib/checks/managedWithoutKeys.js +3 -1
  25. package/lib/checks/selectItems.js +4 -4
  26. package/lib/checks/sql-snippets.js +27 -26
  27. package/lib/checks/types.js +1 -1
  28. package/lib/checks/validator.js +6 -11
  29. package/lib/compiler/assert-consistency.js +6 -3
  30. package/lib/compiler/base.js +1 -0
  31. package/lib/compiler/builtins.js +19 -6
  32. package/lib/compiler/checks.js +23 -60
  33. package/lib/compiler/cycle-detector.js +1 -1
  34. package/lib/compiler/define.js +1151 -0
  35. package/lib/compiler/extend.js +1000 -0
  36. package/lib/compiler/finalize-parse-cdl.js +237 -0
  37. package/lib/compiler/index.js +107 -39
  38. package/lib/compiler/kick-start.js +190 -0
  39. package/lib/compiler/moduleLayers.js +4 -4
  40. package/lib/compiler/populate.js +1227 -0
  41. package/lib/compiler/propagator.js +114 -46
  42. package/lib/compiler/resolve.js +1521 -0
  43. package/lib/compiler/shared.js +126 -65
  44. package/lib/compiler/tweak-assocs.js +535 -0
  45. package/lib/compiler/utils.js +197 -33
  46. package/lib/edm/.eslintrc.json +5 -0
  47. package/lib/edm/annotations/genericTranslation.js +38 -24
  48. package/lib/edm/annotations/preprocessAnnotations.js +2 -2
  49. package/lib/edm/csn2edm.js +219 -100
  50. package/lib/edm/edm.js +302 -230
  51. package/lib/edm/edmPreprocessor.js +554 -419
  52. package/lib/edm/edmUtils.js +138 -44
  53. package/lib/gen/Dictionary.json +100 -19
  54. package/lib/gen/language.checksum +1 -1
  55. package/lib/gen/language.interp +11 -1
  56. package/lib/gen/language.tokens +86 -83
  57. package/lib/gen/languageLexer.interp +10 -1
  58. package/lib/gen/languageLexer.js +860 -833
  59. package/lib/gen/languageLexer.tokens +78 -75
  60. package/lib/gen/languageParser.js +5765 -4480
  61. package/lib/json/csnVersion.js +10 -11
  62. package/lib/json/from-csn.js +15 -3
  63. package/lib/json/to-csn.js +126 -68
  64. package/lib/language/docCommentParser.js +4 -4
  65. package/lib/language/genericAntlrParser.js +123 -5
  66. package/lib/language/language.g4 +355 -156
  67. package/lib/language/multiLineStringParser.js +5 -5
  68. package/lib/main.d.ts +486 -59
  69. package/lib/main.js +41 -9
  70. package/lib/model/api.js +3 -1
  71. package/lib/model/csnRefs.js +252 -156
  72. package/lib/model/csnUtils.js +384 -297
  73. package/lib/model/enrichCsn.js +71 -29
  74. package/lib/model/revealInternalProperties.js +29 -8
  75. package/lib/model/sortViews.js +2 -1
  76. package/lib/modelCompare/compare.js +23 -18
  77. package/lib/optionProcessor.js +63 -26
  78. package/lib/render/manageConstraints.js +35 -32
  79. package/lib/render/toCdl.js +897 -947
  80. package/lib/render/toHdbcds.js +205 -257
  81. package/lib/render/toSql.js +264 -225
  82. package/lib/render/utils/common.js +136 -25
  83. package/lib/render/utils/sql.js +4 -3
  84. package/lib/render/utils/stringEscapes.js +111 -0
  85. package/lib/sql-identifier.js +1 -1
  86. package/lib/transform/.eslintrc.json +5 -0
  87. package/lib/transform/db/.eslintrc.json +3 -1
  88. package/lib/transform/db/applyTransformations.js +35 -12
  89. package/lib/transform/db/assertUnique.js +1 -1
  90. package/lib/transform/db/associations.js +104 -306
  91. package/lib/transform/db/cdsPersistence.js +2 -2
  92. package/lib/transform/db/constraints.js +58 -53
  93. package/lib/transform/db/expansion.js +60 -33
  94. package/lib/transform/db/flattening.js +582 -104
  95. package/lib/transform/db/groupByOrderBy.js +3 -1
  96. package/lib/transform/db/transformExists.js +66 -13
  97. package/lib/transform/db/views.js +11 -7
  98. package/lib/transform/draft/.eslintrc.json +38 -0
  99. package/lib/transform/{db/draft.js → draft/db.js} +6 -5
  100. package/lib/transform/draft/odata.js +227 -0
  101. package/lib/transform/forHanaNew.js +109 -208
  102. package/lib/transform/forOdataNew.js +59 -212
  103. package/lib/transform/localized.js +46 -26
  104. package/lib/transform/odata/toFinalBaseType.js +85 -11
  105. package/lib/transform/odata/typesExposure.js +147 -199
  106. package/lib/transform/odata/utils.js +2 -2
  107. package/lib/transform/transformUtilsNew.js +44 -33
  108. package/lib/transform/translateAssocsToJoins.js +3 -20
  109. package/lib/transform/universalCsn/.eslintrc.json +36 -0
  110. package/lib/transform/universalCsn/coreComputed.js +172 -0
  111. package/lib/transform/universalCsn/universalCsnEnricher.js +737 -0
  112. package/lib/transform/universalCsn/utils.js +63 -0
  113. package/lib/utils/moduleResolve.js +13 -6
  114. package/lib/utils/objectUtils.js +30 -0
  115. package/package.json +1 -1
  116. package/share/messages/README.md +26 -0
  117. package/share/messages/message-explanations.json +2 -1
  118. package/share/messages/syntax-expected-integer.md +37 -0
  119. package/lib/compiler/definer.js +0 -2361
  120. package/lib/compiler/resolver.js +0 -3079
  121. package/lib/transform/odata/attachPath.js +0 -96
  122. package/lib/transform/odata/expandStructKeysInAssociations.js +0 -59
  123. package/lib/transform/odata/generateForeignKeyElements.js +0 -261
  124. package/lib/transform/odata/referenceFlattener.js +0 -290
  125. package/lib/transform/odata/sortByAssociationDependency.js +0 -105
  126. package/lib/transform/odata/structuralPath.js +0 -72
  127. package/lib/transform/odata/structureFlattener.js +0 -171
  128. package/lib/transform/universalCsnEnricher.js +0 -237
@@ -1,29 +1,26 @@
1
1
  'use strict';
2
2
 
3
- const {
4
- getLastPartOf,
5
- getLastPartOfRef,
6
- } = require('../model/csnUtils');
7
- const {
8
- isBuiltinType, generatedByCompilerVersion, getNormalizedQuery,
9
- } = require('../model/csnUtils');
10
3
  const keywords = require('../base/keywords');
11
- const { renderFunc, beautifyExprArray, findElement } = require('./utils/common');
4
+ const { getLastPartOf } = require('../model/csnUtils');
5
+ const { isBuiltinType, generatedByCompilerVersion, getNormalizedQuery } = require('../model/csnUtils');
6
+ const { renderFunc, findElement, getExpressionRenderer } = require('./utils/common');
7
+ const { escapeString, hasUnpairedUnicodeSurrogate } = require('./utils/stringEscapes');
12
8
  const { checkCSNVersion } = require('../json/csnVersion');
13
9
  const { timetrace } = require('../utils/timetrace');
14
10
  const { csnRefs } = require('../model/csnRefs');
15
11
  const { forEachDefinition } = require('../model/csnUtils');
16
- const enrichUniversalCsn = require('../transform/universalCsnEnricher');
12
+ const enrichUniversalCsn = require('../transform/universalCsn/universalCsnEnricher');
17
13
  const { isBetaEnabled } = require('../base/model');
14
+ const { ModelError } = require('../base/error');
15
+ const { typeParameters } = require('../compiler/builtins');
16
+ const { forEach } = require('../utils/objectUtils');
18
17
 
19
18
  /**
20
- * Render the CSN model 'model' to CDS source text. One source is created per
21
- * top-level artifact. Return a dictionary of top-level artifacts
22
- * by their names, like this:
23
- * { "foo" : "using XY; context foo {...};",
24
- * "bar::wiz" : "namespace bar::; entity wiz {...};"
25
- * }
26
- * FIXME: This comment no longer tells the whole truth
19
+ * Render the CSN model 'model' to CDS source text.
20
+ * Returned object has the following properties:
21
+ * - `model`: CSN model rendered as CDL (string).
22
+ * - [csn.namespace]: Namespace statement + `using from './model.cds'.
23
+ * - `unappliedExtensions`: Annotations / Extensions from the `csn.extensions` array.
27
24
  *
28
25
  * @param {CSN.Model} csn
29
26
  * @param {CSN.Options} [options]
@@ -31,9 +28,7 @@ const { isBetaEnabled } = require('../base/model');
31
28
  function toCdsSourceCsn(csn, options) {
32
29
  timetrace.start('CDL rendering');
33
30
  const { artifactRef } = csnRefs(csn);
34
-
35
- // Skip compactModel if already using CSN
36
- // const csn = cloneCsn(model, options);
31
+ let _renderExpr = null;
37
32
 
38
33
  if (options.csnFlavor === 'universal' && isBetaEnabled(options, 'enableUniversalCsn'))
39
34
  enrichUniversalCsn(csn, options);
@@ -41,177 +36,285 @@ function toCdsSourceCsn(csn, options) {
41
36
  checkCSNVersion(csn, options);
42
37
 
43
38
  const cdlResult = Object.create(null);
39
+ cdlResult.model = options.testMode ? '' : `// ${generatedByCompilerVersion()} \n`;
40
+
41
+ const subelementAnnotates = [];
44
42
 
45
- const main = 'model';
43
+ cdlResult.model += renderDefinitions();
44
+ // sub-element annotations that can't be written directly.
45
+ cdlResult.model += renderExtensions(subelementAnnotates, createEnv());
46
46
 
47
- cdlResult[main] = options.testMode ? '' : `// ${generatedByCompilerVersion()} \n`;
47
+ if (csn.vocabularies)
48
+ cdlResult.model += renderVocabularies(csn.vocabularies);
48
49
 
49
- const subelementAnnotates = [];
50
+ if (csn.namespace) {
51
+ cdlResult[csn.namespace] = `namespace ${renderArtifactName(csn.namespace)};\n`;
52
+ cdlResult[csn.namespace] += 'using from \'./model.cds\';';
53
+ }
54
+
55
+ // If there are extensions, such as 'extend' and 'annotate' statements, render them separately.
56
+ // Used for e.g. parseCdl-style CSN or Universal CSN.
57
+ if (csn.extensions)
58
+ cdlResult.unappliedExtensions = renderExtensions(csn.extensions, createEnv());
50
59
 
51
- forEachDefinition(csn, (artifact, artifactName) => {
60
+ timetrace.stop();
61
+ return cdlResult;
62
+
63
+ /**
64
+ * Render entries from the `csn.definitions` dictionary.
65
+ * Returns an empty string if nothing is rendered.
66
+ *
67
+ * @return {string}
68
+ */
69
+ function renderDefinitions() {
70
+ let result = '';
52
71
  const env = createEnv();
53
- const sourceStr = renderArtifact(artifactName, artifact, env); // Must come first because it populates 'env.topLevelAliases'
54
- if (sourceStr !== '')
55
- cdlResult[main] += `${sourceStr}\n`;
56
- });
72
+ forEachDefinition(csn, (artifact, artifactName) => {
73
+ const sourceStr = renderArtifact(artifactName, artifact, env);
74
+ if (sourceStr !== '')
75
+ result += `${sourceStr}\n`;
76
+ });
77
+ return result;
78
+ }
79
+
80
+ /**
81
+ * Render annotation definitions, i.e. entries from csn.vocabularies.
82
+ * Returns an empty string if there isn't anything to render.
83
+ *
84
+ * @param {object} vocabularies
85
+ * @return {string}
86
+ */
87
+ function renderVocabularies(vocabularies) {
88
+ let result = '';
89
+ forEach(vocabularies, renderVocabulariesEntry);
90
+ return result;
57
91
 
58
- // Apply possible subelement/action return annotations with an "annotate X with"
59
- // Some of them appear in csn.extensions, some not...
60
- if (subelementAnnotates.length > 0) {
61
- for (const [ artName, element, elementName, suffix ] of subelementAnnotates) {
62
- // Suffix is used with action return annotations
63
- let sourceStr = `annotate ${artName} with ${suffix ? `${suffix} ` : ''}{\n`;
64
- if (elementName) // action returns do not have element name - we need less {} there
65
- sourceStr += ` ${elementName} {\n`;
66
- const env = increaseIndent(increaseIndent(createEnv()));
67
- const subelements = renderSubelementAnnotates(element, env);
68
- if (subelements !== '') {
69
- sourceStr += `${subelements}\n`;
70
- if (elementName) // action returns do not have element name - we need less {} there
71
- sourceStr += ' }\n';
72
- sourceStr += '}\n';
73
- cdlResult[main] += `${sourceStr}\n`;
92
+ function renderVocabulariesEntry(name, anno) {
93
+ if (!anno._ignore) {
94
+ // This environment is passed down the call hierarchy, for dealing with
95
+ // indentation and name resolution issues
96
+ const env = createEnv();
97
+ const sourceStr = renderTypeOrAnnotation(name, anno, env, 'annotation');
98
+ result += `${sourceStr}\n`;
74
99
  }
75
100
  }
76
101
  }
77
102
 
78
103
  /**
79
- * Render annotations for subelements as part of an "annotate X with" statement
104
+ * Render 'extend' and 'annotate' statements from the `extensions` array.
105
+ * Could be annotate-statements for sub-elements annotations or from parseCdl's
106
+ * extensions array or just unapplied extensions.
80
107
  *
81
- * @param {CSN.Element} element The element to annotate the subelements for
82
- * @param {CdlRenderEnvironment} env Render environment
83
- * @returns {String}
108
+ * @param {CSN.Extension[]} extensions
109
+ * @param {CdlRenderEnvironment} env
110
+ * @return {string}
84
111
  */
85
- function renderSubelementAnnotates(element, env) {
86
- const result = [];
87
- for (const [ name, subelement ] of Object.entries(element.elements)) {
88
- const subresult = [];
89
- const annos = renderAnnotationAssignments(subelement, env);
90
- if (annos !== '')
91
- subresult.push(annos.slice(0, -1));
92
-
93
- const quotedElementName = quoteOrUppercaseId(name);
94
- if (subelement.elements) {
95
- subresult.push(`${env.indent}${quotedElementName} {`);
96
- subresult.push(renderSubelementAnnotates(subelement, increaseIndent(env)));
97
- subresult.push(`${env.indent}};`);
98
- }
99
- else {
100
- subresult.push(`${env.indent}${quotedElementName};`);
101
- }
102
- // Only add result if there really was "something"
103
- if (annos || subelement.elements)
104
- result.push(...subresult);
105
- }
106
- return result.join('\n');
112
+ function renderExtensions(extensions, env) {
113
+ return extensions.map(ext => renderExtension(ext, env)).join('\n');
107
114
  }
108
115
 
109
- if (csn.vocabularies) {
110
- for (const annotationName of Object.keys(csn.vocabularies)) {
111
- // This environment is passed down the call hierarchy, for dealing with
112
- // indentation and name resolution issues
113
- const anno = csn.vocabularies[annotationName];
114
- const env = createEnv();
115
- let sourceStr;
116
- if (!anno._ignore)
117
- sourceStr = renderTypeOrAnnotation(annotationName, anno, env, 'annotation');
116
+ /**
117
+ * Render an 'extend' and 'annotate' statement.
118
+ *
119
+ * @param {CSN.Extension} ext
120
+ * @param {CdlRenderEnvironment} env
121
+ * @return {string}
122
+ */
123
+ function renderExtension(ext, env) {
124
+ if (ext.extend)
125
+ return renderExtendStatement(ext.extend, ext, env);
126
+ return renderAnnotateStatement(ext, env);
127
+ }
118
128
 
119
- if (sourceStr !== '')
120
- cdlResult[main] += `${sourceStr}\n`;
129
+ /**
130
+ * Render an 'extend' statement.
131
+ * `extName` is the extension's artifact's name, most likely `ext.extend`.
132
+ * This function is recursive, which is why you need to pass it explicitly.
133
+ *
134
+ * @param {string} extName
135
+ * @param {object} ext
136
+ * @param {CdlRenderEnvironment} env
137
+ * @return {string}
138
+ */
139
+ function renderExtendStatement(extName, ext, env) {
140
+ extName = renderArtifactName(extName);
141
+ // Element extensions have `kind` set.
142
+ const isElementExtend = (ext.kind === 'extend');
143
+ let result = renderAnnotationAssignmentsAndDocComment(ext, env);
144
+
145
+ if (ext.includes && ext.includes.length > 0) {
146
+ // Includes can't be combined with anything in braces {}. Multiple includes
147
+ // are possible through CSN, but in CDL, only one include at once is possible.
148
+ const affix = isElementExtend ? 'element ' : '';
149
+ for (const id of ext.includes)
150
+ result += `${env.indent}extend ${affix}${extName} with ${quoteIdIfRequired(id)};\n`;
151
+ return result;
121
152
  }
122
- }
123
153
 
124
- if (csn.namespace) {
125
- cdlResult[csn.namespace] = `namespace ${renderArtifactName(csn.namespace)};\n`;
126
- cdlResult[csn.namespace] += `using from './${main}.cds';`;
154
+ // We have the "old-style" prefix syntax and the "new-style" postfix "with <type>" syntax.
155
+ // The former one can not only extend (sub-)elements but also actions in the same statement whereas
156
+ // the latter cannot.
157
+ // If there are actions, check if there are also elements/columns, and if so, use the prefix notation.
158
+ const usePrefixNotation = isElementExtend || ext.actions && (ext.columns || ext.elements);
159
+ if (usePrefixNotation)
160
+ result += `${env.indent}extend ${getExtendPrefixVariant(ext)} ${extName} with {\n`;
161
+ else
162
+ result += `${env.indent}extend ${extName} with ${getExtendPostfixVariant(ext)} {\n`;
163
+
164
+ if (ext.columns) {
165
+ result += renderViewColumns(ext.columns, increaseIndent(env));
166
+ }
167
+ else if (ext.elements) {
168
+ result += renderExtendStatementElements(ext, env);
169
+ }
170
+ else if (ext.enum) {
171
+ const childEnv = increaseIndent(env);
172
+ forEach(ext.enum, (name, value) => {
173
+ result += renderEnumElement(name, value, childEnv);
174
+ });
175
+ }
176
+
177
+ // Not part of if/else cascade, because it may be in postfix notation.
178
+ if (ext.actions) {
179
+ // TODO: Merge with renderActionsAndFunctions() -> requires removal of static `with actions`
180
+ const childEnv = increaseIndent(env);
181
+ let actions = '';
182
+ forEach(ext.actions, (actionName, action) => {
183
+ actions += renderActionOrFunction(actionName, action, childEnv);
184
+ });
185
+ if (!usePrefixNotation)
186
+ result += actions;
187
+ else if (actions !== '')
188
+ result += `${env.indent}} actions {\n${actions}`;
189
+ }
190
+
191
+ result += `${env.indent}};\n`;
192
+ return result;
127
193
  }
128
194
 
195
+ /**
196
+ * What <extend> prefix type to use. Used to render `extend <type> <ref>` statements.
197
+ *
198
+ * @param {object} ext
199
+ * @return {string}
200
+ */
201
+ function getExtendPrefixVariant(ext) {
202
+ if (ext.kind === 'extend')
203
+ return 'element'; // element extensions inside an `extend`
204
+ if (ext.columns)
205
+ return 'projection';
206
+ if (ext.elements)
207
+ return 'entity';
208
+ return '';
209
+ }
129
210
 
130
- // If there are unapplied 'extend' and 'annotate' statements, render them separately
131
- // FIXME: Clarify if we should also do this for HANA (probably not?)
132
- if (csn.extensions) {
133
- const env = createEnv();
134
- const sourceStr = renderUnappliedExtensions(csn.extensions, env);
135
- cdlResult.unappliedExtensions = renderUsings('', env) + sourceStr;
211
+ /**
212
+ * What <extend> postfix type to use. Used to render `extend <ref> with <type>` statements.
213
+ *
214
+ * @param {CSN.Extension} ext
215
+ * @return {string}
216
+ */
217
+ function getExtendPostfixVariant(ext) {
218
+ if (ext.columns)
219
+ return 'columns';
220
+ if (ext.actions)
221
+ return 'actions';
222
+ if (ext.elements)
223
+ return 'elements';
224
+ if (ext.enum)
225
+ return 'enum';
226
+ return '';
136
227
  }
137
228
 
138
- timetrace.stop();
139
- return cdlResult;
229
+ /**
230
+ * Render the elements inside an `extend` statement. They may themselves be `extend` statements.
231
+ *
232
+ * @param {CSN.Extension} ext
233
+ * @param {CdlRenderEnvironment} env
234
+ * @return {string}
235
+ */
236
+ function renderExtendStatementElements(ext, env) {
237
+ let result = '';
238
+ forEach(ext.elements || {}, (elemName, element) => {
239
+ if (element.kind === 'extend')
240
+ result += renderExtendStatement(elemName, element, increaseIndent(env));
241
+ else
242
+ // As soon as we are inside an element, nested `extend` are not possible,
243
+ // since we can't extend an existing element of a new one.
244
+ result += renderElement(elemName, element, increaseIndent(env), true);
245
+ });
246
+ return result;
247
+ }
140
248
 
141
249
  /**
142
- * Render unapplied 'extend' and 'annotate' statements from the 'extensions array'
250
+ * Render an 'annotate' statement.
143
251
  *
144
- * @param {CSN.Extension[]} extensions
252
+ * @param {CSN.Extension} ext
145
253
  * @param {CdlRenderEnvironment} env
146
254
  * @return {string}
147
255
  */
148
- function renderUnappliedExtensions(extensions, env) {
149
- return extensions.map((ext) => {
150
- // Top-level annotations of the artifact
151
- let result = renderAnnotationAssignments(ext, env);
152
- result += `annotate ${ext.annotate}`;
153
- // Element extensions and annotations (possibly nested)
154
- if (ext.elements)
155
- result += renderElementExtensions(ext.elements, env);
156
-
157
- // Returns annotations
158
- if (ext.returns) {
159
- const childEnv = increaseIndent(env);
160
- result += ` with returns${renderElementExtensions(ext.returns.elements, childEnv)}`;
161
- }
256
+ function renderAnnotateStatement(ext, env) {
257
+ // Top-level annotations of the artifact
258
+ let result = renderAnnotationAssignmentsAndDocComment(ext, env);
259
+ result += `${env.indent}annotate ${renderArtifactName(ext.annotate)}`;
162
260
 
163
- // Action annotations
164
- if (ext.actions) {
165
- result += ' actions {\n';
166
- const childEnv = increaseIndent(env);
167
- for (const name in ext.actions) {
168
- const action = ext.actions[name];
169
- result += renderAnnotationAssignments(action, childEnv) + childEnv.indent + quoteIdIfRequired(name);
170
- // Action parameter annotations
171
- if (action.params) {
172
- result += '(\n';
173
- const grandChildEnv = increaseIndent(childEnv);
174
- const paramAnnotations = [];
175
- for (const paramName in action.params)
176
- paramAnnotations.push(renderAnnotationAssignments(action.params[paramName], grandChildEnv) + grandChildEnv.indent + quoteIdIfRequired(paramName));
177
-
178
- result += `${paramAnnotations.join(',\n')}\n${childEnv.indent})`;
179
- }
180
- // Annotations on action returns
181
- if (action.returns && action.returns.elements) {
182
- const grandChildEnv = increaseIndent(childEnv);
183
- result += ` returns${renderElementExtensions(action.returns.elements, grandChildEnv)}`;
184
- }
261
+ if (ext.params)
262
+ result += renderAnnotateParamsInParentheses(ext.params, env);
185
263
 
264
+ // Element extensions and annotations (possibly nested)
265
+ if (ext.elements)
266
+ result += renderAnnotateStatementElements(ext.elements, env);
186
267
 
187
- result += ';\n';
268
+ // Returns annotations
269
+ if (ext.returns) {
270
+ const childEnv = increaseIndent(env);
271
+ result += ` returns${renderAnnotateStatementElements(ext.returns.elements, childEnv)}`;
272
+ }
273
+
274
+ // Action annotations
275
+ if (ext.actions) {
276
+ result += ' actions {\n';
277
+ const childEnv = increaseIndent(env);
278
+ for (const name in ext.actions) {
279
+ const action = ext.actions[name];
280
+ result += renderAnnotationAssignmentsAndDocComment(action, childEnv) + childEnv.indent + quoteIdIfRequired(name);
281
+ // Action parameter annotations
282
+ if (action.params)
283
+ result += renderAnnotateParamsInParentheses(action.params, childEnv);
284
+
285
+ // Annotations on action returns
286
+ if (action.returns && action.returns.elements) {
287
+ const grandChildEnv = increaseIndent(childEnv);
288
+ result += ` returns${renderAnnotateStatementElements(action.returns.elements, grandChildEnv)}`;
188
289
  }
189
- result += `${env.indent}}`;
290
+
291
+ result += ';\n';
190
292
  }
293
+ result += `${env.indent}}`;
294
+ }
191
295
 
192
296
 
193
- result += ';';
194
- return result;
195
- }).join('\n');
297
+ result += ';\n';
298
+ return result;
196
299
  }
197
300
 
198
301
  /**
199
- * Render the elements-specific part of an 'extend' or 'annotate' statement for an element dictionary
302
+ * Render the elements-specific part of an 'annotate' statement for an element dictionary
200
303
  * 'elements' (assuming that the surrounding parent has just been rendered, without trailing newline).
201
- * Return the resulting source string, ending without a trailing newline, too.
304
+ * Returns the resulting source string, ending without a trailing newline.
202
305
  *
203
306
  * @param {CSN.Elements} elements
204
307
  * @param {CdlRenderEnvironment} env
205
308
  * @return {string}
206
309
  */
207
- function renderElementExtensions(elements, env) {
310
+ function renderAnnotateStatementElements(elements, env) {
208
311
  let result = ' {\n';
209
312
  const childEnv = increaseIndent(env);
210
313
  for (const name in elements) {
211
314
  const elem = elements[name];
212
- result += renderAnnotationAssignments(elem, childEnv) + childEnv.indent + quoteIdIfRequired(name);
315
+ result += renderAnnotationAssignmentsAndDocComment(elem, childEnv) + childEnv.indent + quoteIdIfRequired(name);
213
316
  if (elem.elements)
214
- result += renderElementExtensions(elem.elements, childEnv);
317
+ result += renderAnnotateStatementElements(elem.elements, childEnv);
215
318
 
216
319
  result += ';\n';
217
320
  }
@@ -219,6 +322,24 @@ function toCdsSourceCsn(csn, options) {
219
322
  return result;
220
323
  }
221
324
 
325
+ /**
326
+ * Render a parameter list for `annotate` statements, in parentheses `()`.
327
+ *
328
+ * @param {object} params
329
+ * @param {CdlRenderEnvironment} env
330
+ * @return {string}
331
+ */
332
+ function renderAnnotateParamsInParentheses(params, env) {
333
+ const childEnv = increaseIndent(env);
334
+ let result = '(\n';
335
+ const paramAnnotations = [];
336
+ forEach(params, (paramName, param) => {
337
+ paramAnnotations.push( renderAnnotationAssignmentsAndDocComment(param, childEnv) + childEnv.indent + quoteIdIfRequired(paramName) );
338
+ });
339
+ result += `${paramAnnotations.join(',\n')}\n${env.indent})`;
340
+ return result;
341
+ }
342
+
222
343
  /**
223
344
  * Render an artifact. Return the resulting source string.
224
345
  *
@@ -233,7 +354,6 @@ function toCdsSourceCsn(csn, options) {
233
354
 
234
355
  switch (art.kind) {
235
356
  case 'entity':
236
- case 'view':
237
357
  if (art.query || art.projection)
238
358
  return renderView(artifactName, art, env);
239
359
 
@@ -242,8 +362,6 @@ function toCdsSourceCsn(csn, options) {
242
362
  case 'context':
243
363
  case 'service':
244
364
  return renderContext(artifactName, art, env);
245
- case 'namespace':
246
- return renderNamespace(artifactName, art, env);
247
365
  case 'type':
248
366
  case 'aspect':
249
367
  case 'annotation':
@@ -252,9 +370,9 @@ function toCdsSourceCsn(csn, options) {
252
370
  case 'function':
253
371
  return renderActionOrFunction(artifactName, art, env);
254
372
  case 'event':
255
- return renderEventIfCDLMode(artifactName, art, env);
373
+ return renderEvent(artifactName, art, env);
256
374
  default:
257
- throw new Error(`Unknown artifact kind: ${art.kind}`);
375
+ throw new ModelError(`Unknown artifact kind: ${art.kind}`);
258
376
  }
259
377
  }
260
378
 
@@ -263,15 +381,13 @@ function toCdsSourceCsn(csn, options) {
263
381
  * @param {CSN.Artifact} art
264
382
  * @param {CdlRenderEnvironment} env
265
383
  */
266
- function renderEventIfCDLMode(artifactName, art, env) {
267
- let result = renderDocComment(art, env) + renderAnnotationAssignments(art, env);
384
+ function renderEvent(artifactName, art, env) {
385
+ let result = renderAnnotationAssignmentsAndDocComment(art, env);
268
386
  const childEnv = increaseIndent(env);
269
387
  const normalizedArtifactName = renderArtifactName(artifactName);
270
388
  result += `${env.indent}event ${normalizedArtifactName}`;
271
- if (art.includes) {
272
- // Includes are never flattened (don't exist in HANA)
273
- result += ` : ${art.includes.map(name => renderAbsoluteNameWithQuotes(name)).join(', ')}`;
274
- }
389
+ if (art.includes)
390
+ result += renderIncludes(art.includes);
275
391
  if (art.query || art.projection) {
276
392
  env._artifact = art;
277
393
  result += ' : ';
@@ -294,80 +410,6 @@ function toCdsSourceCsn(csn, options) {
294
410
  return result;
295
411
  }
296
412
 
297
- /**
298
- * Return a dictionary with the direct sub-artifacts of the artifact with name 'artifactName' in the csn
299
- *
300
- * @param {string} artifactName
301
- * @return {object}
302
- */
303
- function getSubArtifacts(artifactName) {
304
- const prefix = `${artifactName}.`;
305
- const result = Object.create(null);
306
- for (const name in csn.definitions) {
307
- // We have a direct child if its name starts with prefix and contains no more dots
308
- if (name.startsWith(prefix) && !name.substring(prefix.length).includes('.')) {
309
- result[getLastPartOf(name)] = csn.definitions[name];
310
- }
311
- else if (name.startsWith(prefix)) {
312
- const prefixPlusNextPart = name.substring(0, name.substring(prefix.length).indexOf('.') + prefix.length);
313
- if (csn.definitions[prefixPlusNextPart]) {
314
- const art = csn.definitions[prefixPlusNextPart];
315
- if (![ 'service', 'context', 'namespace' ].includes(art.kind)) {
316
- const nameWithoutPrefix = name.substring(prefix.length);
317
- result[nameWithoutPrefix] = csn.definitions[name];
318
- }
319
- }
320
- else {
321
- result[name.substring(prefix.length)] = csn.definitions[name];
322
- }
323
- }
324
- }
325
- return result;
326
- }
327
-
328
- /* FIXME: Not yet required
329
- // Returns the artifact or element that constitutes the final type of
330
- // construct 'node', i.e. the object in which we would find type properties for
331
- // 'node'. Note that this may well be 'node' itself.
332
- function getFinalTypeOf(node) {
333
- if (node && node.type) {
334
- if (isBuiltinType(node.type)) {
335
- return node;
336
- }
337
- return getFinalTypeOf(node.type);
338
- }
339
- return node;
340
- }
341
-
342
- // Resolve path array 'ref' against artifact 'base' (or against 'csn.definitions'
343
- // if no 'base' given).
344
- // Return the resulting artifact or element (or 'undefined' if not found).
345
- function resolveRef(ref, base) {
346
- let result = base;
347
- for (let i = 0; i < ref.length; i++) {
348
- let pathStep = ref[i].id || ref[i];
349
- // Only first path step may be looked up in 'definitions'
350
- if (i === 0 && !base) {
351
- result = csn.definitions[pathStep];
352
- continue;
353
- }
354
- // Structured type
355
- else if (result && result.elements) {
356
- result = getFinalTypeOf(result.elements[pathStep]);
357
- }
358
- // Association
359
- else if (result && result.target) {
360
- result = resolveRef([pathStep], csn.definitions[result.target]);
361
- }
362
- // Not resolvable
363
- else {
364
- return undefined;
365
- }
366
- }
367
- return result;
368
- }
369
- */
370
-
371
413
  /**
372
414
  * Render a context or service. Return the resulting source string.
373
415
  *
@@ -376,44 +418,12 @@ function toCdsSourceCsn(csn, options) {
376
418
  * @param {CdlRenderEnvironment} env
377
419
  */
378
420
  function renderContext(artifactName, art, env) {
379
- let result = renderDocComment(art, env) + renderAnnotationAssignments(art, env);
421
+ let result = renderAnnotationAssignmentsAndDocComment(art, env);
380
422
  result += `${env.indent + (art.abstract ? 'abstract ' : '') + art.kind} ${renderArtifactName(artifactName)}`;
381
- if (art.includes) {
382
- // Includes are never flattened (don't exist in HANA)
383
- result += ` : ${art.includes.map(name => renderAbsoluteNameWithQuotes(name)).join(', ')}`;
384
- }
385
- return `${result} {};\n`;
386
- }
423
+ if (art.includes)
424
+ result += renderIncludes(art.includes);
387
425
 
388
- function updatePrefixForDottedName(env, name) {
389
- let innerEnv = env;
390
- if (name.indexOf('.') !== -1) {
391
- const parts = name.split('.');
392
- for (let i = 0; i < parts.length - 1; i++)
393
- innerEnv = addNamePrefix(innerEnv, parts[i]);
394
- }
395
-
396
- return innerEnv;
397
- }
398
-
399
- /**
400
- * Render a namespace. Return the resulting source string.
401
- *
402
- * @param {string} artifactName
403
- * @param {CSN.Artifact} art
404
- * @param {CdlRenderEnvironment} env
405
- * @return {string}
406
- */
407
- function renderNamespace(artifactName, art, env) {
408
- // We currently do not render anything for a namespace, we just append its id to
409
- // the environment's current name prefix and descend into its children
410
- let result = renderDocComment(art, env);
411
- const childEnv = addNamePrefix(env, getLastPartOf(artifactName));
412
- const subArtifacts = getSubArtifacts(artifactName);
413
- for (const name in subArtifacts)
414
- result += renderArtifact(`${artifactName}.${name}`, subArtifacts[name], updatePrefixForDottedName(childEnv, name));
415
-
416
- return result;
426
+ return `${result} {};\n`;
417
427
  }
418
428
 
419
429
  /**
@@ -425,105 +435,37 @@ function toCdsSourceCsn(csn, options) {
425
435
  * @return {string}
426
436
  */
427
437
  function renderEntity(artifactName, art, env) {
428
- let result = renderDocComment(art, env) + renderAnnotationAssignments(art, env);
438
+ let result = renderAnnotationAssignmentsAndDocComment(art, env);
429
439
  const childEnv = increaseIndent(env);
430
440
  const normalizedArtifactName = renderArtifactName(artifactName);
431
441
  result += `${env.indent + (art.abstract ? 'abstract ' : '')}entity ${normalizedArtifactName}`;
432
442
  const parameters = Object.keys(art.params || []).map(name => renderParameter(name, art.params[name], childEnv)).join(',\n');
433
443
  result += (parameters === '') ? '' : ` (\n${parameters}\n${env.indent})`;
434
- if (art.includes) {
435
- // Includes are never flattened (don't exist in HANA)
436
- result += ` : ${art.includes.map(name => renderAbsoluteNameWithQuotes(name)).join(', ')}`;
437
- }
444
+ if (art.includes)
445
+ result += renderIncludes(art.includes);
438
446
  result += ' {\n';
439
447
  for (const name in art.elements) {
440
448
  const element = art.elements[name];
441
449
  // For subelement annotations, this seems to be a pattern to recognize them
442
450
  // plus some other stuff unfortunately...
443
- if (element.type && element.elements)
444
- subelementAnnotates.push([ artifactName, element, name ]);
451
+ if (element.type && element.elements) {
452
+ subelementAnnotates.push({
453
+ annotate: artifactName,
454
+ elements: {
455
+ [name]: {
456
+ elements: element.elements,
457
+ },
458
+ },
459
+ });
460
+ }
445
461
  result += renderElement(name, element, childEnv);
446
462
  }
447
463
 
448
464
  result += `${env.indent}}`;
449
- result += `${renderActionsAndFunctions(art, env) + renderTechnicalConfiguration(art.technicalConfig, env)};\n`;
465
+ result += `${renderActionsAndFunctions(art, env)};\n`;
450
466
  return result;
451
467
  }
452
468
 
453
- // Render the 'technical configuration { ... }' section 'tc' of an entity.
454
- // Return the resulting source string.
455
- function renderTechnicalConfiguration(tc, env) {
456
- let result = renderDocComment(tc, env);
457
- const childEnv = increaseIndent(env);
458
-
459
- if (!tc)
460
- return result;
461
-
462
-
463
- // FIXME: How to deal with non-HANA technical configurations? We should probably just iterate all entries
464
- // in 'tc' that we find and render them all (is it syntactically allowed yet to have more than one?)
465
- tc = tc.hana;
466
- if (!tc)
467
- throw new Error('Expecting a HANA technical configuration');
468
-
469
- result += `\n${env.indent}technical ${tc.calculated ? '' : 'hana '}configuration {\n`;
470
-
471
- // Store type (must be separate because SQL wants it between 'CREATE' and 'TABLE')
472
- if (tc.storeType)
473
- result += `${tc.storeType} store;\n`;
474
-
475
- // Fixed parts belonging to the table (includes migration, unload prio, extended storage,
476
- // auto merge, partitioning, ...)
477
- if (tc.tableSuffix) {
478
- // Unlike SQL, CDL and HANA CDS require a semicolon after each table-suffix part
479
- // (e.g. `migration enabled; row store; ...`). In order to keep both
480
- // the simplicity of "the whole bandwurm is just one expression that can be
481
- // rendered to SQL without further knowledge" and at the same time telling
482
- // CDS about the boundaries, the compactor has put each part into its own `xpr`
483
- // object. Semantically equivalent because a "trivial" SQL renderer would just
484
- // concatenate them.
485
- for (const xpr of tc.tableSuffix)
486
- result += `${childEnv.indent + renderExpr(xpr, childEnv)};\n`;
487
- }
488
-
489
- // Indices and full-text indices
490
- for (const idxName in tc.indexes || {}) {
491
- if (Array.isArray(tc.indexes[idxName][0])) {
492
- // FIXME: Should we allow multiple indices with the same name at all?
493
- for (const index of tc.indexes[idxName])
494
- result += `${childEnv.indent + renderExpr(index, childEnv)};\n`;
495
- }
496
- else {
497
- result += `${childEnv.indent + renderExpr(tc.indexes[idxName], childEnv)};\n`;
498
- }
499
- }
500
- // Fuzzy search indices
501
- for (const columnName in tc.fzindexes || {}) {
502
- if (Array.isArray(tc.fzindexes[columnName][0])) {
503
- // FIXME: Should we allow multiple fuzzy search indices on the same column at all?
504
- // And if not, why do we wrap this into an array?
505
- for (const index of tc.fzindexes[columnName])
506
- result += `${childEnv.indent + renderExpr(fixFuzzyIndex(index, columnName), childEnv)};\n`;
507
- }
508
- else {
509
- result += `${childEnv.indent + renderExpr(fixFuzzyIndex(tc.fzindexes[columnName], columnName), childEnv)};\n`;
510
- }
511
- }
512
- result += `${env.indent}}`;
513
- return result;
514
-
515
- // Fuzzy indices are stored in compact CSN as they would appear in SQL after the column name,
516
- // i.e. the whole line in SQL looks somewhat like this:
517
- // s nvarchar(10) FUZZY SEARCH INDEX ON FUZZY SEARCH MODE 'ALPHANUM'
518
- // But in CDL, we don't write fuzzy search indices together with the table column, so we need
519
- // to insert the name of the column after 'ON' in CDS syntax, making it look like this:
520
- // fuzzy search mode on (s) search mode 'ALPHANUM'
521
- // This function expects an array with the original expression and returns an array with the modified expression
522
- function fixFuzzyIndex(fuzzyIndex, columnName) {
523
- return fuzzyIndex.map(token => (token === 'on' ? { xpr: [ 'on', '(', { ref: columnName.split('.') }, ')' ] } : token));
524
- }
525
- }
526
-
527
469
  /**
528
470
  * Render an element (of an entity, type or annotation, not a projection or view).
529
471
  * Return the resulting source string.
@@ -535,8 +477,8 @@ function toCdsSourceCsn(csn, options) {
535
477
  */
536
478
  function renderElement(elementName, elm, env, isSubElement) {
537
479
  env.elementName = elementName;
538
- let result = renderDocComment(elm, env) + renderAnnotationAssignments(elm, env);
539
- const quotedElementName = quoteOrUppercaseId(elementName);
480
+ let result = renderAnnotationAssignmentsAndDocComment(elm, env);
481
+ const quotedElementName = quoteIdIfRequired(elementName);
540
482
  result += `${env.indent + (elm.virtual ? 'virtual ' : '') +
541
483
  (elm.key && !isSubElement ? 'key ' : '') +
542
484
  ((elm.masked && !elm._ignoreMasked) ? 'masked ' : '') +
@@ -550,22 +492,6 @@ function toCdsSourceCsn(csn, options) {
550
492
  return `${result};\n`;
551
493
  }
552
494
 
553
- /**
554
- * Return the SELECT of the leading query of query 'query'
555
- *
556
- * @param {CSN.Query} query
557
- */
558
- function leadingQuerySelect(query) {
559
- if (query.SELECT)
560
- return query.SELECT;
561
-
562
- // Sanity checks
563
- if (!query.SET || !query.SET.args || !query.SET.args[0])
564
- throw new Error(`Expecting set with args in query: ${JSON.stringify(query)}`);
565
-
566
- return leadingQuerySelect(query.SET.args[0]);
567
- }
568
-
569
495
  /**
570
496
  * Render a query's actions and functions (if any) separately as extend-statements, so that actions
571
497
  * work not only for projections but also for views, which have no syntax (yet) to directly specify
@@ -589,9 +515,13 @@ function toCdsSourceCsn(csn, options) {
589
515
 
590
516
  /**
591
517
  * Render annotations that were extended to a query element of a view or projection (they only
592
- * appear in the view's 'elements', not in their 'columns', because the element itself may not
593
- * even be in 'columns', e.g. if it was expanded from a '*'). Return the resulting 'annotate'
594
- * statement or an empty string if none required.
518
+ * appear in the view's 'elements', not in their 'columns' for client CSN, because the element
519
+ * itself may not even be in 'columns', e.g. if it was expanded from a '*'). Return the
520
+ * resulting rendered 'annotate' statement or an empty string if none required.
521
+ *
522
+ * Note: In the past, we checked if the annotation also exists in the respective column,
523
+ * however, in client CSN, annotations are not part of the column and in parseCdl CSN,
524
+ * no `elements` exist.
595
525
  *
596
526
  * @param {string} artifactName
597
527
  * @param {CSN.Artifact} art
@@ -599,43 +529,61 @@ function toCdsSourceCsn(csn, options) {
599
529
  * @return {string}
600
530
  */
601
531
  function renderQueryElementAnnotations(artifactName, art, env) {
602
- // For preparation, create a map from element names to column objects
603
- const columnMap = Object.create(null);
604
- const select = leadingQuerySelect(getNormalizedQuery(art).query);
605
- for (const col of select.columns || [ '*' ]) {
606
- // Ignore '*'
607
- if (col === '*')
608
- continue;
609
-
610
- // Column must have an alias or be a path - take last part of that as element name
611
- columnMap[col.as || col.func || getLastPartOfRef(col.ref)] = col;
612
- }
613
- // Now iterate elements - render an annotation if it is different from the column's
614
- const childEnv = increaseIndent(env);
615
- let result = '';
616
- for (const elemName in art.elements) {
617
- let elemAnnotations = '';
618
- const elem = art.elements[elemName];
619
- for (const name in elem) {
620
- if (!name.startsWith('@'))
621
- continue;
622
-
623
- const annotationValue = renderAnnotationValue(elem[name], childEnv);
624
- // Skip annotation if column has the same
625
- if (columnMap[elemName] && columnMap[elemName][name] &&
626
- renderAnnotationValue(columnMap[elemName][name], childEnv) === annotationValue)
627
- continue;
628
-
629
- // Annotation names are never flattened
630
- elemAnnotations += `${childEnv.indent}${`@${renderAbsoluteNameWithQuotes(name.substring(1))}`} : ${annotationValue}\n`;
631
- }
632
- if (elemAnnotations !== '')
633
- result += `${elemAnnotations}${childEnv.indent}${quoteOrUppercaseId(elemName)};\n`;
634
- }
635
- if (result !== '')
636
- result = `${env.indent}annotate ${renderArtifactName(artifactName)} with {\n${result}${env.indent}};\n`;
532
+ const annotate = collectAnnotationsOfElements(artifactName, art);
533
+ if (annotate)
534
+ return renderExtensions([ annotate ], env);
535
+ return '';
536
+ }
637
537
 
638
- return result;
538
+ /**
539
+ * Create an "annotate" statement as a CSN extension for all annotations of (sub-)elements.
540
+ * If no annotation was found, we return `null`.
541
+ *
542
+ * @param {string} artifactName
543
+ * @param {CSN.Artifact} artWithElements
544
+ * @return {CSN.Extension|null}
545
+ */
546
+ function collectAnnotationsOfElements(artifactName, artWithElements) {
547
+ const annotate = { annotate: artifactName };
548
+ return collectAnnos(annotate, artWithElements) ? annotate : null;
549
+
550
+ /**
551
+ * Recursive function to collect annotations. `annotateObj` will get an `elements`
552
+ * object with annotations only if there are annotations on `art`'s (sub-)elements.
553
+ *
554
+ * @return {boolean} True, if there were annotations, false otherwise.
555
+ */
556
+ function collectAnnos(annotateObj, art) {
557
+ if (!art.elements)
558
+ return false;
559
+
560
+ const collected = { elements: Object.create(null) };
561
+ let hasAnnotation = false;
562
+
563
+ forEach(art.elements, (elemName, element) => {
564
+ if (!collected.elements[elemName])
565
+ collected.elements[elemName] = { };
566
+
567
+ let hasElementAnnotations = false;
568
+ for (const name in element) {
569
+ if (name.startsWith('@')) {
570
+ collected.elements[elemName][name] = element[name];
571
+ hasElementAnnotations = true;
572
+ hasAnnotation = true;
573
+ }
574
+ }
575
+
576
+ const hasSubAnnotations = collectAnnos(collected.elements[elemName], element);
577
+ if (!hasElementAnnotations && !hasSubAnnotations)
578
+ delete collected.elements[elemName]; // delete if no annotations exist
579
+ hasAnnotation = hasAnnotation || hasSubAnnotations;
580
+ });
581
+
582
+ if (hasAnnotation)
583
+ annotateObj.elements = collected.elements;
584
+
585
+ return hasAnnotation;
586
+ }
639
587
  }
640
588
 
641
589
  /**
@@ -650,25 +598,24 @@ function toCdsSourceCsn(csn, options) {
650
598
  function renderViewSource(source, env) {
651
599
  // Sub-SELECT
652
600
  if (source.SELECT || source.SET) {
653
- let result = `(${renderDocComment(source, env)}${renderQuery(source, false, 'view', increaseIndent(env))})`;
601
+ let result = `(${renderQuery(source, false, 'view', increaseIndent(env))})`;
654
602
  if (source.as)
655
- result += ` as ${quoteOrUppercaseId(source.as)}`;
603
+ result += ` as ${quoteIdIfRequired(source.as)}`;
656
604
 
657
605
  return result;
658
606
  }
659
607
  // JOIN
660
608
  else if (source.join) {
661
609
  // One join operation, possibly with ON-condition
662
- let result = `${renderDocComment(source, env)}${renderViewSource(source.args[0], env)}`;
610
+ let result = `(${renderViewSource(source.args[0], env)}`;
663
611
  for (let i = 1; i < source.args.length; i++) {
664
- result = `(${result} ${source.join} `;
612
+ result += ` ${source.join} `;
665
613
  result += renderJoinCardinality(source.cardinality);
666
614
  result += `join ${renderViewSource(source.args[i], env)}`;
667
615
  if (source.on)
668
616
  result += ` on ${renderExpr(source.on, env, true, true)}`;
669
-
670
- result += ')';
671
617
  }
618
+ result += ')';
672
619
  return result;
673
620
  }
674
621
  // Ordinary path, possibly with an alias
@@ -703,15 +650,14 @@ function toCdsSourceCsn(csn, options) {
703
650
  function renderAbsolutePath(path, env) {
704
651
  // Sanity checks
705
652
  if (!path.ref)
706
- throw new Error(`Expecting ref in path: ${JSON.stringify(path)}`);
653
+ throw new ModelError(`Expecting ref in path: ${JSON.stringify(path)}`);
707
654
 
708
655
 
709
656
  // Determine the absolute name of the first artifact on the path (before any associations or element traversals)
710
657
  const firstArtifactName = path.ref[0].id || path.ref[0];
711
658
 
712
- let result = renderDocComment(path, env);
713
659
  // Render the first path step (absolute name, with different quoting/naming ..)
714
- result += getResultingName(firstArtifactName);
660
+ let result = renderArtifactName(firstArtifactName);
715
661
 
716
662
  // Even the first step might have parameters and/or a filter
717
663
  if (path.ref[0].args)
@@ -742,11 +688,26 @@ function toCdsSourceCsn(csn, options) {
742
688
  let result = renderAbsolutePath(path, env);
743
689
  if (path.as) {
744
690
  // Source had an alias - render it
745
- result += ` as ${quoteOrUppercaseId(path.as)}`;
691
+ result += ` as ${quoteIdIfRequired(path.as)}`;
746
692
  }
747
693
  return result;
748
694
  }
749
695
 
696
+ /**
697
+ * Render the given columns.
698
+ *
699
+ * @param {any[]} columns
700
+ * @param {object} elements
701
+ * @param {CdlRenderEnvironment} env
702
+ * @return {string}
703
+ */
704
+ function renderViewColumns(columns, env, elements = Object.create(null)) {
705
+ const result = columns.map(col => renderViewColumn(col, env, findElement(elements, col)))
706
+ .filter(s => s !== '')
707
+ .join(',\n');
708
+ return `${result}\n`;
709
+ }
710
+
750
711
  /**
751
712
  * Render a single view or projection column 'col', as it occurs in a select list or
752
713
  * projection list within 'art', possibly with annotations.
@@ -758,7 +719,14 @@ function toCdsSourceCsn(csn, options) {
758
719
  */
759
720
  function renderViewColumn(col, env, element) {
760
721
  // Annotations and column
761
- let result = renderDocComment(col.doc ? col : element, env) + renderAnnotationAssignments(col, env);
722
+ let result = '';
723
+ if (!col.doc) {
724
+ // TODO: In contrast to annotations, we do not render the doc comment as part
725
+ // of an `annotate` statement. That may change in the future.
726
+ result += renderDocComment(element, env);
727
+ }
728
+ // Note: parentheses are a workaround for #9015
729
+ result += renderAnnotationAssignmentsAndDocComment(col, env, { parens: true });
762
730
  result += env.indent;
763
731
 
764
732
  // only if column is virtual, keyword virtual was present in the source text
@@ -769,22 +737,19 @@ function toCdsSourceCsn(csn, options) {
769
737
  // Use special rendering for .expand/.inline - renderExpr cannot easily handle some cases
770
738
  result += key + ((col.expand || col.inline) ? renderInlineExpand(col, env) : renderExpr(col, env, true));
771
739
 
772
- // Alias is already handled by renderInlineExpand
773
- if (!col.inline && !col.expand && col.as)
774
- result += ` as ${quoteOrUppercaseId(col.as)}`;
740
+ // Alias for inline/expand is already handled by renderInlineExpand
741
+ // A new association (cast with `type` and `target`) uses `as` as its primary name, not alias.
742
+ const isNewAssociation = col.cast && col.cast.type && col.cast.target;
743
+ if (col.as && !col.inline && !col.expand && !isNewAssociation)
744
+ result += ` as ${quoteIdIfRequired(col.as)}`;
775
745
 
776
746
  // Explicit type provided for the view element?
777
747
  if (col.cast) {
778
748
  // Special case: Explicit association type is actually a redirect
779
- if (col.cast.target) {
780
- // Redirections are never flattened (don't exist in HANA)
781
- result += ` : redirected to ${renderAbsoluteNameWithQuotes(col.cast.target)}`;
782
- if (col.cast.on)
783
- result += ` on ${renderExpr(col.cast.on, env, true, true)}`;
784
- }
785
- else {
749
+ if (col.cast.target && !col.cast.type)
750
+ result += ` : ${renderRedirectedTo(col.cast, env)}`;
751
+ else
786
752
  result += ` : ${renderTypeReference(col.cast, env, true)}`;
787
- }
788
753
  }
789
754
  return result;
790
755
  }
@@ -813,16 +778,10 @@ function toCdsSourceCsn(csn, options) {
813
778
 
814
779
  // We found a leaf - no further drilling
815
780
  if (!obj.inline && !obj.expand) {
816
- if (obj.cast && obj.cast.type) {
781
+ if (obj.cast && obj.cast.type)
817
782
  result += ` : ${renderTypeReference(obj.cast, createEnv())}`;
818
- }
819
- else if (obj.cast && obj.cast.target) { // test tbd
820
- result += ` : redirected to ${renderAbsoluteNameWithQuotes(obj.cast.target)}`;
821
- if (obj.cast.on)
822
- result += ` on ${renderExpr(obj.cast.on, env, true, true)}`;
823
- else if (obj.cast.keys)
824
- result += ` { ${Object.keys(obj.cast.keys).map(name => renderForeignKey(obj.cast.keys[name], env)).join(', ')} }`;
825
- }
783
+ else if (obj.cast && obj.cast.target) // test tbd
784
+ result += ` : ${renderRedirectedTo(obj.cast, env)}`;
826
785
  return result;
827
786
  }
828
787
 
@@ -857,7 +816,7 @@ function toCdsSourceCsn(csn, options) {
857
816
  * Render .doc properties as comments in CDL
858
817
  *
859
818
  * @param {object} obj Object to render for
860
- * @param {object} env Env - for indent
819
+ * @param {CdlRenderEnvironment} env
861
820
  * @returns {String}
862
821
  */
863
822
  function renderDocComment(obj, env) {
@@ -866,7 +825,13 @@ function toCdsSourceCsn(csn, options) {
866
825
  else if (obj && obj.doc === null) // empty doc comment needs to be rendered
867
826
  return `\n${env.indent}/** */\n`;
868
827
 
869
- return `\n${env.indent}/**\n${obj.doc.split('\n').map(line => `${env.indent} * ${line}`).join('\n')}\n${env.indent} */\n`;
828
+ // Smaller comment for single-line comments. If the comments starts or ends with whitespace
829
+ // we must use a block comment, or it will be lost when compiling the source again.
830
+ if (!obj.doc.includes('\n') && !/^\s|\s$/.test(obj.doc))
831
+ return `${env.indent}/** ${obj.doc} */\n`;
832
+
833
+ const comment = obj.doc.split('\n').map(line => `${env.indent} * ${line}`).join('\n');
834
+ return `${env.indent}/**\n${comment}\n${env.indent} */\n`;
870
835
  }
871
836
 
872
837
  /**
@@ -880,7 +845,7 @@ function toCdsSourceCsn(csn, options) {
880
845
  */
881
846
  function renderView(artifactName, art, env) {
882
847
  const syntax = (art.projection) ? 'projection' : 'entity';
883
- let result = renderDocComment(art, env) + renderAnnotationAssignments(art, env);
848
+ let result = renderAnnotationAssignmentsAndDocComment(art, env);
884
849
  result += `${env.indent}${art.abstract ? 'abstract ' : ''}${syntax === 'projection' ? 'entity' : syntax} ${renderArtifactName(artifactName)}`;
885
850
  if (art.params) {
886
851
  const childEnv = increaseIndent(env);
@@ -909,50 +874,31 @@ function toCdsSourceCsn(csn, options) {
909
874
  * @param {string} syntax The query syntax, either "projection", "entity" or "view"
910
875
  * @param {CdlRenderEnvironment} env
911
876
  * @param {CSN.Path} [path=[]]
877
+ * @param {object} [elements]
912
878
  */
913
879
  function renderQuery(query, isLeadingQuery, syntax, env, path = [], elements = query.elements || Object.create(null)) {
914
- let result = renderDocComment(query, env);
915
- // Set operator, like UNION, INTERSECT, ...
916
880
  if (query.SET) {
917
- // First arg may be leading query
918
- result += `(${renderQuery(query.SET.args[0], isLeadingQuery, 'view', env, path.concat([ 'SET', 'args', 0 ]), elements)}`;
919
- // FIXME: Clarify if set operators can be n-ary (assuming binary here)
920
- if (query.SET.op) {
921
- // Loop over all other arguments, i.e. for A UNION B UNION C UNION D ...
922
- for (let i = 1; i < query.SET.args.length; i++)
923
- result += `\n${env.indent}${query.SET.op}${query.SET.all ? ' all' : ''} ${renderQuery(query.SET.args[i], false, 'view', env, path.concat([ 'SET', 'args', i ]), elements)}`;
924
- }
925
- result += ')';
926
- // Set operation may also have an ORDER BY and LIMIT/OFFSET (in contrast to the ones belonging to
927
- // each SELECT)
928
- if (query.SET.orderBy)
929
- result += `${continueIndent(result, env)}order by ${query.SET.orderBy.map(entry => renderOrderByEntry(entry, env)).join(', ')}`;
930
-
931
- if (query.SET.limit)
932
- result += `${continueIndent(result, env)}${renderLimit(query.SET.limit, env)}`;
933
-
934
- return result;
881
+ // Set operator, such as UNION, INTERSECT, or EXCEPT...
882
+ return renderQuerySet();
935
883
  }
936
- // Otherwise must have a SELECT
937
884
  else if (!query.SELECT) {
938
- throw new Error(`Unexpected query operation ${JSON.stringify(query)}`);
885
+ // ...otherwise must have a SELECT
886
+ throw new ModelError(`Unexpected query operation ${JSON.stringify(query)}`);
939
887
  }
888
+
889
+ let result = '';
940
890
  const select = query.SELECT;
941
891
  const childEnv = increaseIndent(env);
942
892
 
943
- if (syntax === 'projection')
944
- result += `projection on ${renderViewSource(select.from, env)}`;
945
- else if (syntax === 'view' || syntax === 'entity')
946
- result += `select from ${renderViewSource(select.from, env)}`;
947
- else
948
- throw new Error(`Unknown query syntax: ${syntax}`);
893
+ // If not a projection, must be view/entity.
894
+ result += (syntax === 'projection') ? 'projection on ' : 'select from ';
895
+ result += renderViewSource(select.from, env);
949
896
 
950
897
  if (select.mixin) {
951
898
  let elems = '';
952
- for (const name in select.mixin) {
953
- if (!select.mixin[name]._ignore)
954
- elems += renderElement(name, select.mixin[name], childEnv);
955
- }
899
+ forEach(select.mixin, (name, mixin) => {
900
+ elems += renderElement(name, mixin, childEnv);
901
+ });
956
902
  if (elems) {
957
903
  result += ' mixin {\n';
958
904
  result += elems;
@@ -962,13 +908,12 @@ function toCdsSourceCsn(csn, options) {
962
908
  result += select.distinct ? ' distinct' : '';
963
909
  if (select.columns) {
964
910
  result += ' {\n';
965
- result += `${select.columns.map(col => renderViewColumn(col, childEnv, findElement(elements, col)))
966
- .filter(s => s !== '')
967
- .join(',\n')}\n`;
911
+ result += renderViewColumns(select.columns, increaseIndent(env), elements);
968
912
  result += `${env.indent}}`;
969
913
  }
914
+
970
915
  if (select.excluding) {
971
- result += ` excluding {\n${select.excluding.map(id => `${childEnv.indent}${quoteOrUppercaseId(id)}`).join(',\n')}\n`;
916
+ result += ` excluding {\n${select.excluding.map(id => `${childEnv.indent}${quoteIdIfRequired(id)}`).join(',\n')}\n`;
972
917
  result += `${env.indent}}`;
973
918
  }
974
919
  // FIXME: Currently, only projections can have actions and functions, but we cannot distinguish
@@ -980,7 +925,7 @@ function toCdsSourceCsn(csn, options) {
980
925
  result += `${continueIndent(result, env)}where ${renderExpr(select.where, env, true, true)}`;
981
926
 
982
927
  if (select.groupBy)
983
- result += `${continueIndent(result, env)}group by ${select.groupBy.map(expr => renderExpr(expr, env)).join(', ')}`;
928
+ result += `${continueIndent(result, env)}group by ${select.groupBy.map(expr => renderExpr(expr, env, true, false, true)).join(', ')}`;
984
929
 
985
930
  if (select.having)
986
931
  result += `${continueIndent(result, env)}having ${renderExpr(select.having, env, true, true)}`;
@@ -1026,6 +971,29 @@ function toCdsSourceCsn(csn, options) {
1026
971
 
1027
972
  return limitStr;
1028
973
  }
974
+
975
+ /**
976
+ * Render UNION, INTERSECT, and EXCEPT, i.e. sets.
977
+ *
978
+ * @return {string}
979
+ */
980
+ function renderQuerySet() {
981
+ const subQueries = query.SET.args.map((arg, i) => {
982
+ // First arg may be leading query
983
+ const subQuery = renderQuery(arg, isLeadingQuery && (i === 0), 'view', env, path.concat([ 'SET', 'args', i ]), elements);
984
+ return `(${subQuery})`;
985
+ });
986
+
987
+ let setResult = subQueries.join(`\n${env.indent}${query.SET.op}${query.SET.all ? ' all' : ''} `);
988
+ // Set operation may also have an ORDER BY and LIMIT/OFFSET (in contrast to the ones belonging to
989
+ // each SELECT)
990
+ if (query.SET.orderBy)
991
+ setResult += `${continueIndent(setResult, env)}order by ${query.SET.orderBy.map(entry => renderOrderByEntry(entry, env)).join(', ')}`;
992
+
993
+ if (query.SET.limit)
994
+ setResult += `${continueIndent(setResult, env)}${renderLimit(query.SET.limit, env)}`;
995
+ return setResult;
996
+ }
1029
997
  }
1030
998
 
1031
999
  /**
@@ -1037,7 +1005,7 @@ function toCdsSourceCsn(csn, options) {
1037
1005
  * @return {string}
1038
1006
  */
1039
1007
  function renderOrderByEntry(entry, env) {
1040
- let result = renderDocComment(entry, env) + renderExpr(entry, env);
1008
+ let result = renderAnnotationAssignmentsAndDocComment(entry, env) + renderExpr(entry, env, true, false, true);
1041
1009
  if (entry.sort)
1042
1010
  result += ` ${entry.sort}`;
1043
1011
 
@@ -1061,7 +1029,7 @@ function toCdsSourceCsn(csn, options) {
1061
1029
  let result = '';
1062
1030
  const childEnv = increaseIndent(env);
1063
1031
  for (const name in art.actions)
1064
- result += renderDocComment(art.actions[name], childEnv) + renderActionOrFunction(name, art.actions[name], childEnv);
1032
+ result += renderActionOrFunction(name, art.actions[name], childEnv);
1065
1033
 
1066
1034
  // Even if we have seen actions/functions, they might all have been ignored
1067
1035
  if (result !== '')
@@ -1079,13 +1047,23 @@ function toCdsSourceCsn(csn, options) {
1079
1047
  * @return {string}
1080
1048
  */
1081
1049
  function renderActionOrFunction(actionName, act, env) {
1082
- let result = `${renderDocComment(act, env) + renderAnnotationAssignments(act, env) + env.indent + act.kind} ${renderArtifactName(actionName)}`;
1050
+ let result = `${renderAnnotationAssignmentsAndDocComment(act, env) + env.indent + act.kind} ${renderArtifactName(actionName)}`;
1083
1051
  const childEnv = increaseIndent(env);
1084
1052
  const parameters = Object.keys(act.params || []).map(name => renderParameter(name, act.params[name], childEnv)).join(',\n');
1085
1053
  result += (parameters === '') ? '()' : `(\n${parameters}\n${env.indent})`;
1086
1054
  if (act.returns) {
1087
- if (act.returns.type && act.returns.elements) // action returns annotations
1088
- subelementAnnotates.push([ actionName, act.returns, '', 'returns' ]);
1055
+ if (act.returns.type && act.returns.elements) {
1056
+ // Annotation in action returns' structure. Render it as an annotate statement.
1057
+ const isBoundAction = (env.artifactName !== actionName);
1058
+ const anno = {
1059
+ annotate: env.artifactName || actionName,
1060
+ };
1061
+ if (isBoundAction)
1062
+ anno.actions = { [actionName]: { returns: { elements: act.returns.elements } } };
1063
+ else
1064
+ anno.returns = { elements: act.returns.elements };
1065
+ subelementAnnotates.push(anno);
1066
+ }
1089
1067
  result += ` returns ${renderTypeReference(act.returns, env)}${renderNullability(act.returns)}`;
1090
1068
  }
1091
1069
 
@@ -1102,7 +1080,7 @@ function toCdsSourceCsn(csn, options) {
1102
1080
  * @return {string}
1103
1081
  */
1104
1082
  function renderParameter(parName, par, env) {
1105
- let result = `${renderDocComment(par, env) + renderAnnotationAssignments(par, env) + env.indent + quoteOrUppercaseId(parName)} : ${renderTypeReference(par, env)}`;
1083
+ let result = `${renderAnnotationAssignmentsAndDocComment(par, env) + env.indent + quoteIdIfRequired(parName)} : ${renderTypeReference(par, env)}`;
1106
1084
  if (par.default)
1107
1085
  result += ` default ${renderExpr(par.default, env)}`;
1108
1086
 
@@ -1121,14 +1099,11 @@ function toCdsSourceCsn(csn, options) {
1121
1099
  * @return {string}
1122
1100
  */
1123
1101
  function renderTypeOrAnnotation(artifactName, art, env, artType) {
1124
- if (!options.toCdl && art.kind === 'aspect')
1125
- return '';
1126
- let result = renderDocComment(art, env) + renderAnnotationAssignments(art, env);
1127
- result += `${env.indent + (options.toCdl && (artType || art.$syntax) || art.kind )} ${renderArtifactName(artifactName)}`;
1128
- if (art.includes) {
1129
- // Includes are never flattened (don't exist in HANA)
1130
- result += ` : ${art.includes.map(name => renderAbsoluteNameWithQuotes(name)).join(', ')}`;
1131
- }
1102
+ let result = renderAnnotationAssignmentsAndDocComment(art, env);
1103
+ result += `${env.indent + (artType || art.$syntax || art.kind )} ${renderArtifactName(artifactName)}`;
1104
+ if (art.includes)
1105
+ result += renderIncludes(art.includes);
1106
+
1132
1107
  const childEnv = increaseIndent(env);
1133
1108
  if (art.elements && !art.type) {
1134
1109
  // Structured type or annotation with anonymous struct type
@@ -1140,7 +1115,10 @@ function toCdsSourceCsn(csn, options) {
1140
1115
  }
1141
1116
  else {
1142
1117
  // Derived type or annotation with non-anonymous type
1143
- result += ` : ${renderTypeReference(art, env, false)};\n`;
1118
+ result += ` : ${renderTypeReference(art, env, false)}`;
1119
+ if (art.default)
1120
+ result += ` default ${renderExpr(art.default, env)}`;
1121
+ result += ';\n';
1144
1122
  }
1145
1123
  return result;
1146
1124
  }
@@ -1164,8 +1142,16 @@ function toCdsSourceCsn(csn, options) {
1164
1142
  if (elm.items.notNull != null)
1165
1143
  rc += elm.items.notNull ? ' not null' : ' null';
1166
1144
  // many sub element annotates
1167
- if (elm.items.type && elm.items.elements && env.artifactName)
1168
- subelementAnnotates.push([ env.artifactName, elm.items, env.elementName ]);
1145
+ // TODO(andre): This does handle deeply nested elements because we take the full `elements`.
1146
+ // But when is env.elementName set and when not?
1147
+ if (elm.items.type && elm.items.elements && env.artifactName) {
1148
+ const annotate = { annotate: env.artifactName };
1149
+ if (env.elementName)
1150
+ annotate.elements = { [env.elementName]: elm.items };
1151
+ else
1152
+ annotate.elements = elm.items.elements;
1153
+ subelementAnnotates.push(annotate);
1154
+ }
1169
1155
 
1170
1156
  return rc;
1171
1157
  }
@@ -1176,7 +1162,7 @@ function toCdsSourceCsn(csn, options) {
1176
1162
  // Anonymous structured type
1177
1163
  if (!elm.type) {
1178
1164
  if (!elm.elements)
1179
- throw new Error(`Missing type of: ${JSON.stringify(elm)}`);
1165
+ throw new ModelError(`Missing type of: ${JSON.stringify(elm)}`);
1180
1166
 
1181
1167
  result += '{\n';
1182
1168
  const childEnv = increaseIndent(env);
@@ -1201,23 +1187,27 @@ function toCdsSourceCsn(csn, options) {
1201
1187
  ((elm.type === comp) ? ' of ' : ' to ');
1202
1188
  }
1203
1189
 
1204
- // normal target or named aspect
1205
- if (elm.target || elm.targetAspect && typeof elm.targetAspect === 'string') {
1190
+ // `targetAspect` may be set by the core compiler and refers to the original named or unnamed aspect.
1191
+ // In parseCdl, `target` may still be an object containing elements. This would be replaced
1192
+ // by targetAspect in client CSN, but we can't rely on that.
1193
+ // If a name exists (either in target or targetAspect), prefer it over rendering elements.
1194
+ const elements = elm.target && elm.target.elements || elm.targetAspect && elm.targetAspect.elements;
1195
+ if (typeof elm.target === 'string' || typeof elm.targetAspect === 'string') {
1206
1196
  result += renderAbsolutePath({ ref: [ elm.target || elm.targetAspect ] }, env);
1207
1197
  }
1208
- else if (elm.targetAspect && elm.targetAspect.elements) { // anonymous aspect
1198
+ else if (elements) {
1199
+ // anonymous aspect, either parseCdl or client CSN.
1209
1200
  const childEnv = increaseIndent(env);
1210
1201
  result += '{\n';
1211
- for (const name in elm.targetAspect.elements)
1212
- result += renderElement(name, elm.targetAspect.elements[name], childEnv);
1202
+ for (const name in elements)
1203
+ result += renderElement(name, elements[name], childEnv);
1213
1204
 
1214
1205
  result += `${env.indent}}`;
1215
1206
  }
1216
1207
  else {
1217
- throw new Error('Association/Composition is missing its target! Throwing exception to trigger recompilation.');
1208
+ throw new ModelError('Association/Composition is missing its target! Throwing exception to trigger recompilation.');
1218
1209
  }
1219
1210
 
1220
-
1221
1211
  // ON-condition (if any)
1222
1212
  if (elm.on)
1223
1213
  result += ` on ${renderExpr(elm.on, env, true, true)}`;
@@ -1249,14 +1239,8 @@ function toCdsSourceCsn(csn, options) {
1249
1239
  }
1250
1240
 
1251
1241
  // If we get here, it must be a named type
1252
- if (isBuiltinType(elm.type)) {
1253
- result += renderBuiltinType(elm);
1254
- }
1255
- else {
1256
- // Simple absolute name
1257
- // Type names are never flattened (derived types are unraveled in HANA)
1258
- result += getResultingName(elm.type);
1259
- }
1242
+ result += renderNamedTypeWithParameters(elm);
1243
+
1260
1244
  if (elm.enum && !noEnum)
1261
1245
  result += renderEnum(elm.enum, env);
1262
1246
 
@@ -1264,18 +1248,48 @@ function toCdsSourceCsn(csn, options) {
1264
1248
  }
1265
1249
 
1266
1250
  /**
1267
- * @param {CSN.Element} elm
1251
+ * Render REDIRECTED TO with its keys/on condition for the given artifact.
1252
+ *
1253
+ * @param {object} art
1254
+ * @param {CdlRenderEnvironment} env
1268
1255
  * @return {string}
1269
1256
  */
1270
- function renderBuiltinType(elm) {
1271
- // If there is a user-defined type with the same short name (cds.Integer -> Integer),
1272
- // we render the full name, including the leading "cds."
1273
- if (csn.definitions[elm.type.slice(4)])
1274
- return elm.type + renderTypeParameters(elm);
1257
+ function renderRedirectedTo(art, env) {
1258
+ let result = `redirected to ${quoteIdIfRequired(art.target)}`;
1259
+ if (art.on)
1260
+ result += ` on ${renderExpr(art.on, env, true, true)}`;
1261
+ else if (art.keys)
1262
+ result += ` { ${Object.keys(art.keys).map(name => renderForeignKey(art.keys[name], env)).join(', ')} }`;
1263
+ return result;
1264
+ }
1265
+
1266
+ /**
1267
+ * Render the named type with optional parameters, e.g. `MyString(length: 10)`.
1268
+ * @param {CSN.Artifact} artWithType
1269
+ * @return {string}
1270
+ */
1271
+ function renderNamedTypeWithParameters(artWithType) {
1272
+ let result = '';
1275
1273
 
1276
- return elm.type.slice(4) + renderTypeParameters(elm);
1274
+ if (isBuiltinType(artWithType.type)) {
1275
+ // If there is a user-defined type with the same short name (cds.Integer -> Integer),
1276
+ // we render the full name, including the leading "cds."
1277
+ if (csn.definitions[artWithType.type.slice(4)])
1278
+ result += artWithType.type;
1279
+ else
1280
+ result += artWithType.type.slice(4);
1281
+ }
1282
+ else {
1283
+ // Simple absolute name
1284
+ // Type names are never flattened (derived types are unraveled in HANA)
1285
+ result += renderArtifactName(artWithType.type);
1286
+ }
1287
+
1288
+ result += renderTypeParameters(artWithType);
1289
+ return result;
1277
1290
  }
1278
1291
 
1292
+
1279
1293
  /**
1280
1294
  * Render the 'enum { ... } part of a type declaration
1281
1295
  *
@@ -1286,21 +1300,32 @@ function toCdsSourceCsn(csn, options) {
1286
1300
  function renderEnum(enumPart, env) {
1287
1301
  let result = ' enum {\n';
1288
1302
  const childEnv = increaseIndent(env);
1289
- for (const name in enumPart) {
1290
- const enumConst = enumPart[name];
1291
- result += renderDocComment(enumConst, childEnv);
1292
- result += renderAnnotationAssignments(enumConst, childEnv);
1293
- result += childEnv.indent + quoteIdIfRequired(name);
1294
- if (enumConst.val !== undefined)
1295
- result += ` = ${renderExpr(enumConst, childEnv)}`;
1296
- else if (enumConst['#'] !== undefined)
1297
- result += ` = #${enumConst['#']}`;
1298
- result += ';\n';
1299
- }
1303
+ for (const name in enumPart)
1304
+ result += renderEnumElement(name, enumPart[name], childEnv);
1300
1305
  result += `${env.indent}}`;
1301
1306
  return result;
1302
1307
  }
1303
1308
 
1309
+ /**
1310
+ * Render the element of a `<type> enum {}` structure.
1311
+ *
1312
+ * @param {string} name
1313
+ * @param {CSN.EnumValue} enumValue
1314
+ * @param {CdlRenderEnvironment} env
1315
+ * @return {string}
1316
+ */
1317
+ function renderEnumElement(name, enumValue, env) {
1318
+ let result = '';
1319
+ result += renderAnnotationAssignmentsAndDocComment(enumValue, env);
1320
+ result += env.indent + quoteIdIfRequired(name);
1321
+ if (enumValue.val !== undefined)
1322
+ result += ` = ${renderExpr(enumValue, env)}`;
1323
+ else if (enumValue['#'] !== undefined)
1324
+ result += ` = #${enumValue['#']}`;
1325
+ result += ';\n';
1326
+ return result;
1327
+ }
1328
+
1304
1329
  /**
1305
1330
  * Render an annotation value (somewhat like a simplified expression, with slightly different
1306
1331
  * representation)
@@ -1310,17 +1335,25 @@ function toCdsSourceCsn(csn, options) {
1310
1335
  */
1311
1336
  function renderAnnotationValue(x, env) {
1312
1337
  if (Array.isArray(x)) {
1313
- // Render array parts as values
1314
- return `[${x.map(item => renderAnnotationValue(item, env)).join(', ')}]`;
1338
+ // Render array parts as values. Spaces required if last array value is
1339
+ // a delimited identifier.
1340
+ return `[ ${x.map(item => renderAnnotationValue(item, env)).join(', ')} ]`;
1315
1341
  }
1316
1342
  else if (typeof x === 'object' && x !== null) {
1317
1343
  // Enum symbol
1318
- if (x['#'])
1344
+ if (x['#']) {
1319
1345
  return `#${x['#']}`;
1320
-
1346
+ }
1321
1347
  // Shorthand for absolute path (as string)
1322
- else if (x['='])
1348
+ else if (x['=']) {
1323
1349
  return quotePathString(x['=']);
1350
+ }
1351
+ // Shorthand for ellipsis: `... up to <val>`
1352
+ else if (x['...']) {
1353
+ if (x['...'] === true)
1354
+ return '...';
1355
+ return `... up to ${renderAnnotationValue(x['...'], env)}`;
1356
+ }
1324
1357
 
1325
1358
  // Struct value (can actually only occur within an array)
1326
1359
 
@@ -1339,170 +1372,54 @@ function toCdsSourceCsn(csn, options) {
1339
1372
  }
1340
1373
 
1341
1374
  /**
1342
- * Render an expression (including paths and values) or condition 'x'.
1343
- * (no trailing LF, don't indent if inline)
1375
+ * Render a single path step 's' at path position 'idx', which can have filters or parameters or be a function
1344
1376
  *
1345
- * @param {any} x
1346
- * @param {CdlRenderEnvironment} env
1347
- * @param {boolean} [inline=true]
1348
- * @param {boolean} [inExpr=false] Whether the expression is already inside another expression
1377
+ * @param {string|object} s
1378
+ * @param {number} idx
1379
+ * @returns {string}
1349
1380
  */
1350
- function renderExpr(x, env, inline = true, inExpr = false) {
1351
- // Compound expression
1352
- if (Array.isArray(x))
1353
- return beautifyExprArray(x.map(item => renderExpr(item, env, inline, inExpr)));
1354
-
1355
- if (typeof x === 'object' && x !== null) {
1356
- if (inExpr && x.cast && x.cast.type)
1357
- return renderExplicitTypeCast(renderExprObject());
1358
- return renderExprObject();
1359
- }
1360
-
1361
- // Not a literal value but part of an operator, function etc - just leave as it is
1362
- return x;
1363
-
1364
-
1365
- /**
1366
- * Various special cases represented as objects
1367
- *
1368
- * @returns {string}
1369
- */
1370
- function renderExprObject() {
1371
- if (x.list) {
1372
- // set "inExpr" to false: treat list elements as new expressions
1373
- return `(${x.list.map(item => renderExpr(item, env, inline, false)).join(', ')})`;
1374
- }
1375
- else if (x.val !== undefined) {
1376
- // Literal value, possibly with explicit 'literal' property
1377
- switch (x.literal || typeof x.val) {
1378
- case 'number':
1379
- case 'boolean':
1380
- case 'null':
1381
- return x.val;
1382
- case 'x':
1383
- case 'date':
1384
- case 'time':
1385
- case 'timestamp':
1386
- return `${x.literal}'${x.val}'`;
1387
- case 'string':
1388
- return renderString(x.val, env);
1389
- case 'object':
1390
- if (x.val === null)
1391
- return 'null';
1392
-
1393
- // otherwise fall through to
1394
- default:
1395
- throw new Error(`Unknown literal or type: ${JSON.stringify(x)}`);
1396
- }
1397
- }
1398
- // Enum symbol
1399
- else if (x['#']) {
1400
- return `#${x['#']}`;
1401
- }
1402
- // Reference: Array of path steps, possibly preceded by ':'
1403
- else if (x.ref) {
1404
- // FIXME: no extra magic with x.param or x.global
1405
- return `${(x.param || x.global) ? ':' : ''}${x.ref.map(renderPathStep).join('.')}`;
1406
- }
1407
- else if (x.xpr && x.func) {
1408
- // window function
1409
- const funcDef = renderFunc( x.func, x, null, a => renderArgs(a, '=>', env) );
1410
- const windowFunctionOperator = x.xpr.shift(); // OVER ...
1411
- return `${funcDef} ${windowFunctionOperator} ( ${renderExpr(x.xpr, env, true)} )`;
1412
- }
1413
- // Function call, possibly with args (use '=>' for named args)
1414
- else if (x.func) {
1415
- // test for non-regular HANA identifier that needs to be quoted
1416
- // identifier {letter}({letter_or_digit}|[#$])*
1417
- // letter [A-Za-z_]
1418
- // letter_or_digit [A-Za-z_0-9]
1419
-
1420
- const regex = RegExp(/^[a-zA-Z][\w#$]*$/, 'g');
1421
- const funcName = regex.test(x.func) ? x.func : quoteIdIfRequired(x.func);
1422
- return renderFunc( funcName, x, 'cap', a => renderArgs(a, '=>', env) );
1423
- }
1424
- // Nested expression
1425
- else if (x.xpr) {
1426
- // Ensure `exists` is always enclosed by parentheses
1427
- if ((inExpr && !x.cast ) || x.xpr.some(s => s === 'exists'))
1428
- return `(${renderExpr(x.xpr, env, inline, true)})`;
1381
+ function renderPathStep(s, idx, inline, env) {
1382
+ // Simple id or absolute name
1383
+ if (typeof s === 'string') {
1384
+ // In first path position, do not quote $projection and magic $-variables like CURRENT_DATE, $now etc.
1385
+ // FIXME: We should rather explicitly recognize quoting somehow
1429
1386
 
1430
- return renderExpr(x.xpr, env, inline, true);
1431
- }
1432
- // Sub-select
1433
- else if (x.SELECT) {
1434
- // renderQuery for SELECT does not bring its own parentheses (because it is also used in renderView)
1435
- return `(${renderQuery(x, false, 'view', increaseIndent(env))})`;
1436
- }
1437
- else if (x.SET) {
1438
- // renderQuery for SET always brings its own parentheses (because it is also used in renderViewSource)
1439
- return `${renderQuery(x, false, 'view', increaseIndent(env))}`;
1440
- }
1441
- else {
1442
- throw new Error(`Unknown expression: ${JSON.stringify(x)}`);
1443
- }
1444
- }
1387
+ if (idx === 0 &&
1388
+ s.startsWith('$'))
1389
+ return s;
1445
1390
 
1446
- /**
1447
- * Renders an explicit `cast()` inside an 'xpr'.
1448
- * @param {string} value
1449
- * @returns {string}
1450
- */
1451
- function renderExplicitTypeCast(value) {
1452
- const typeRef = renderTypeReference(x.cast, env, true);
1453
- return `cast(${value} as ${typeRef})`;
1391
+ return quoteIdIfRequired(s);
1454
1392
  }
1455
-
1456
- /**
1457
- * Render a single path step 's' at path position 'idx', which can have filters or parameters or be a function
1458
- *
1459
- * @param {string|object} s
1460
- * @param {number} idx
1461
- * @returns {string}
1462
- */
1463
- function renderPathStep(s, idx) {
1464
- // Simple id or absolute name
1465
- if (typeof s === 'string') {
1466
- // In first path position, do not quote $projection and magic $-variables like CURRENT_DATE, $now etc.
1467
- // FIXME: We should rather explicitly recognize quoting somehow
1468
-
1469
- if (idx === 0 &&
1470
- s.startsWith('$'))
1471
- return s;
1472
-
1473
- return quoteOrUppercaseId(s);
1393
+ // ID with filters or parameters
1394
+ else if (typeof s === 'object') {
1395
+ // Sanity check
1396
+ if (!s.func && !s.id)
1397
+ throw new ModelError(`Unknown path step object: ${JSON.stringify(s)}`);
1398
+
1399
+ // Not really a path step but an object-like function call
1400
+ if (s.func)
1401
+ return `${s.func}(${renderArgs(s, '=>', env)})`;
1402
+
1403
+ // Path step, possibly with view parameters and/or filters
1404
+ let result = `${quoteIdIfRequired(s.id)}`;
1405
+ if (s.args) {
1406
+ // View parameters
1407
+ result += `(${renderArgs(s, ':', env)})`;
1474
1408
  }
1475
- // ID with filters or parameters
1476
- else if (typeof s === 'object') {
1477
- // Sanity check
1478
- if (!s.func && !s.id)
1479
- throw new Error(`Unknown path step object: ${JSON.stringify(s)}`);
1480
-
1481
- // Not really a path step but an object-like function call
1482
- if (s.func)
1483
- return `${s.func}(${renderArgs(s, '=>', env)})`;
1484
-
1485
- // Path step, possibly with view parameters and/or filters
1486
- let result = `${quoteOrUppercaseId(s.id)}`;
1487
- if (s.args) {
1488
- // View parameters
1489
- result += `(${renderArgs(s, ':', env)})`;
1490
- }
1491
- if (s.where) {
1492
- // Filter, possibly with cardinality
1493
- result += `[${s.cardinality ? (`${s.cardinality.max}: `) : ''}${renderExpr(s.where, env, inline, true)}]`;
1494
- }
1495
-
1496
- return result;
1409
+ if (s.where) {
1410
+ // Filter, possibly with cardinality
1411
+ result += `[${s.cardinality ? (`${s.cardinality.max}: `) : ''}${renderExpr(s.where, env, inline, true)}]`;
1497
1412
  }
1498
1413
 
1499
- throw new Error(`Unknown path step: ${JSON.stringify(s)}`);
1414
+ return result;
1500
1415
  }
1416
+
1417
+ throw new ModelError(`Unknown path step: ${JSON.stringify(s)}`);
1501
1418
  }
1502
1419
 
1503
1420
  /**
1504
1421
  * Render function arguments or view parameters (positional if array, named if object/dict),
1505
- * using 'sep' as separator for positional parameters
1422
+ * using 'sep' as separator for named parameters
1506
1423
  *
1507
1424
  * @param {object} node with `args` to render
1508
1425
  * @param {string} sep
@@ -1510,17 +1427,31 @@ function toCdsSourceCsn(csn, options) {
1510
1427
  * @returns {string}
1511
1428
  */
1512
1429
  function renderArgs(node, sep, env) {
1513
- const args = node.args ? node.args : {};
1430
+ const args = node.args || [];
1431
+
1514
1432
  // Positional arguments
1515
1433
  if (Array.isArray(args))
1516
- return args.map(arg => renderExpr(arg, env)).join(', ');
1434
+ return args.map(arg => renderArgument(arg, env)).join(', ');
1517
1435
 
1518
1436
  // Named arguments (object/dict)
1519
1437
  else if (typeof args === 'object')
1520
- return Object.keys(args).map(key => `${quoteOrUppercaseId(key)} ${sep} ${renderExpr(args[key], env)}`).join(', ');
1438
+ return Object.keys(args).map(key => `${quoteIdIfRequired(key)} ${sep} ${renderArgument(args[key], env)}`).join(', ');
1521
1439
 
1440
+ throw new ModelError(`Unknown args: ${JSON.stringify(args)}`);
1441
+ }
1522
1442
 
1523
- throw new Error(`Unknown args: ${JSON.stringify(args)}`);
1443
+ /**
1444
+ * Render a function argument, e.g. for generic functions or CAST().
1445
+ * Ensures that parentheses are used if necessary, e.g. for `someFct( (1=1), (1=1) )`.
1446
+ *
1447
+ * @param {any} arg
1448
+ * @param {CdlRenderEnvironment} env
1449
+ * @return {string}
1450
+ */
1451
+ function renderArgument(arg, env) {
1452
+ // If the argument is a xpr with e.g. `=`, it may require parentheses.
1453
+ // For nested xpr, `renderExpr()` will already add parentheses.
1454
+ return renderExpr(arg, env, true, !isSimpleFunctionExpression(arg && arg.xpr), true);
1524
1455
  }
1525
1456
 
1526
1457
  /**
@@ -1601,28 +1532,35 @@ function toCdsSourceCsn(csn, options) {
1601
1532
  }
1602
1533
 
1603
1534
  /**
1604
- * Render (primitive) type parameters of element 'elm', i.e.
1535
+ * Render (primitive) type parameters of artifact 'artWithType', i.e.
1605
1536
  * length, precision and scale (even if incomplete), plus any other unknown ones.
1606
1537
  *
1607
- * @param {CSN.Element} elm
1538
+ * @param {CSN.Artifact} artWithType
1608
1539
  * @returns {string}
1609
1540
  */
1610
- function renderTypeParameters(elm /* , env */) {
1611
- const params = [];
1612
- // Length, precision and scale (even if incomplete)
1613
- if (elm.length !== undefined)
1614
- params.push(elm.length);
1541
+ function renderTypeParameters(artWithType) {
1542
+ const params = typeParameters.list.filter(param => artWithType[param] !== undefined);
1543
+ if (params.length === 0)
1544
+ return '';
1615
1545
 
1616
- if (elm.precision !== undefined)
1617
- params.push(elm.precision);
1546
+ // TODO(v3): Remove special handling of srid
1547
+ // For backwards compatibility, do not render srid as a named argument for POINT/GEOMETRY builtins.
1548
+ if (artWithType.srid !== undefined &&
1549
+ (artWithType.type === 'cds.hana.ST_POINT' || artWithType.type === 'cds.hana.ST_GEOMETRY'))
1550
+ return `(${artWithType.srid})`;
1618
1551
 
1619
- if (elm.scale !== undefined)
1620
- params.push(elm.scale);
1552
+ // Special cases for 1 or 2 arguments.
1553
+ if (params.length === 1 && artWithType.length !== undefined)
1554
+ return `(${artWithType.length})`;
1555
+ if (params.length === 2 && artWithType.precision !== undefined && artWithType.scale !== undefined)
1556
+ return `(${artWithType.precision}, ${artWithType.scale})`;
1621
1557
 
1622
- if (elm.srid !== undefined)
1623
- params.push(elm.srid);
1558
+ // Render named params
1559
+ const renderedParams = [];
1560
+ for (const param of params)
1561
+ renderedParams.push(`${param}: ${artWithType[param]}`);
1624
1562
 
1625
- return params.length === 0 ? '' : `(${params.join(', ')})`;
1563
+ return `(${renderedParams.join(', ')})`;
1626
1564
  }
1627
1565
 
1628
1566
  /**
@@ -1630,13 +1568,14 @@ function toCdsSourceCsn(csn, options) {
1630
1568
  *
1631
1569
  * @param {object} obj Object that has annotations
1632
1570
  * @param {CdlRenderEnvironment} env
1571
+ * @param {{parens: boolean}} [config] Config for renderAnnotationAssignment()
1633
1572
  * @return {string}
1634
1573
  */
1635
- function renderAnnotationAssignments(obj, env) {
1636
- let result = '';
1574
+ function renderAnnotationAssignmentsAndDocComment(obj, env, config) {
1575
+ let result = renderDocComment(obj, env);
1637
1576
  for (const name in obj) {
1638
1577
  if (name.startsWith('@'))
1639
- result += renderAnnotationAssignment(obj[name], name, env);
1578
+ result += renderAnnotationAssignment(obj[name], name, env, config);
1640
1579
  }
1641
1580
  return result;
1642
1581
  }
@@ -1649,14 +1588,16 @@ function toCdsSourceCsn(csn, options) {
1649
1588
  * @param {any} anno Annotation value
1650
1589
  * @param {string} name Annotation name, e.g. `@A.B.C#foo.C`
1651
1590
  * @param {CdlRenderEnvironment} env
1591
+ * @param {object} [config] parens: Whether the annotation assignment must be surrounded by parentheses.
1652
1592
  * @return {string} Rendered annotation, possibly quoted: `@![A.B.C#foo.C]: value`
1653
1593
  */
1654
- function renderAnnotationAssignment(anno, name, env) {
1594
+ function renderAnnotationAssignment(anno, name, env, config = { parens: false }) {
1655
1595
  name = name.substring(1);
1656
1596
  // Take the annotation assignment apart into <nameBeforeVariant>#<variantAndRest>
1657
1597
  const parts = name.split('#');
1658
1598
  const nameBeforeVariant = parts[0];
1659
1599
  const variant = parts[1];
1600
+ const { parens } = config;
1660
1601
 
1661
1602
  // Identifier according to our grammar: /[$_a-zA-Z][$_a-zA-Z0-9]*/
1662
1603
  // We expand this pattern to also include dots after the first character.
@@ -1667,151 +1608,21 @@ function toCdsSourceCsn(csn, options) {
1667
1608
  // even though that is the result after flattening.
1668
1609
  const variantRequiresQuoting = variant && !/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(variant);
1669
1610
 
1670
- let result;
1611
+ let result = `${env.indent}@`;
1612
+ if (parens)
1613
+ result += '(';
1614
+
1671
1615
  if (annoRequiresQuoting || variantRequiresQuoting)
1672
- result = `${env.indent}@${quote(name)}`;
1616
+ result += quote(name);
1673
1617
  else
1674
- result = `${env.indent}@${name}`;
1618
+ result += name;
1675
1619
 
1676
1620
  result += ` : ${renderAnnotationValue(anno, env)}`;
1621
+ if (parens)
1622
+ result += ')';
1677
1623
  return `${result}\n`;
1678
1624
  }
1679
1625
 
1680
- /**
1681
- * Render an absolute name 'absName', with appropriate quotes. Also record the
1682
- * fact that 'absName' is used in 'env', so that an appropriate USING can be constructed
1683
- * if necessary.
1684
- *
1685
- * @param {string} absName
1686
- * @return {string}
1687
- */
1688
- function renderAbsoluteNameWithQuotes(absName) {
1689
- return absName;
1690
- }
1691
-
1692
- /**
1693
- * Render appropriate USING directives for all artifacts used by artifact 'artifactName' in 'env'.
1694
- *
1695
- * @param {string} artifactName
1696
- * @param {CdlRenderEnvironment} env
1697
- * @return {string}
1698
- */
1699
- function renderUsings(artifactName, env) {
1700
- const distinct = {};
1701
- Object.keys(env.topLevelAliases)
1702
- .forEach((name) => {
1703
- distinct[`using ${env.topLevelAliases[name].quotedName} as ${env.topLevelAliases[name].quotedAlias};\n`] = '';
1704
- });
1705
-
1706
- return Object.keys(distinct).join('');
1707
- }
1708
-
1709
- /**
1710
- * Returns a newly created default environment (which keeps track of indentation, required USING
1711
- * declarations and name prefixes.
1712
- *
1713
- * @return {CdlRenderEnvironment}
1714
- */
1715
- function createEnv() {
1716
- return {
1717
- // Current indentation string
1718
- indent: '',
1719
- // Dictionary of aliases for used artifact names, each entry like 'name' : { quotedName, quotedAlias }
1720
- topLevelAliases: Object.create(null),
1721
- // Current name prefix (including trailing dot if not empty)
1722
- namePrefix: '',
1723
- artifactName: null,
1724
- elementName: null,
1725
- };
1726
- }
1727
-
1728
- /**
1729
- * Returns a copy of 'env' with (quoted) name prefix 'id' and a dot appended to the current name prefix
1730
- *
1731
- * @param {CdlRenderEnvironment} env
1732
- * @param {string} id
1733
- * @returns {CdlRenderEnvironment}
1734
- */
1735
- function addNamePrefix(env, id) {
1736
- return Object.assign({}, env, { namePrefix: `${env.namePrefix + quoteIdIfRequired(id)}.` });
1737
- }
1738
-
1739
- /**
1740
- * Returns a copy of 'env' with increased indentation (and resetted name prefix)
1741
- *
1742
- * @param {CdlRenderEnvironment} env
1743
- * @returns {CdlRenderEnvironment}
1744
- */
1745
- function increaseIndent(env) {
1746
- return Object.assign({}, env, { namePrefix: '', indent: `${env.indent} ` });
1747
- }
1748
-
1749
- /**
1750
- * Return a path string 'path' with appropriate "-quotes.
1751
- *
1752
- * @param {string} path
1753
- * @returns {string}
1754
- */
1755
- function quotePathString(path) {
1756
- // "foo"."bar"."wiz"."blub"
1757
- return path.split('.').map(quoteIdIfRequired).join('.');
1758
- }
1759
-
1760
- /**
1761
- * Return an id 'id' with appropriate "-quotes
1762
- *
1763
- * @param {string} id
1764
- * @return {string}
1765
- */
1766
- function quoteIdIfRequired(id) {
1767
- // Quote if required for CDL
1768
- if (requiresQuotingForCdl(id))
1769
- return quote(id);
1770
-
1771
- return id;
1772
- }
1773
-
1774
- /**
1775
- * Quotes the identifier using CDL-style ![]-quotes.
1776
- *
1777
- * @param id
1778
- * @returns {string}
1779
- */
1780
- function quote(id) {
1781
- return `![${id.replace(/]/g, ']]')}]`;
1782
- }
1783
-
1784
- /**
1785
- * Returns true if 'id' requires quotes for CDL, i.e. if 'id'
1786
- * 1. starts with a digit
1787
- * 2. contains chars different than:
1788
- * - uppercase letters
1789
- * - lowercase letters
1790
- * - digits
1791
- * - underscore
1792
- * 3. is a CDL keyword or a CDL function without parentheses (CURRENT_*, SYSUUID, ...)
1793
- *
1794
- * @param {string} id
1795
- * @return {boolean}
1796
- */
1797
- function requiresQuotingForCdl(id) {
1798
- return /^\d/.test(id) ||
1799
- /\W/g.test(id.replace(/\./g, '')) ||
1800
- keywords.cdl.includes(id.toUpperCase()) ||
1801
- keywords.cdl_functions.includes(id.toUpperCase());
1802
- }
1803
-
1804
- /**
1805
- * Quote or uppercase an identifier 'id', depending on naming strategy
1806
- *
1807
- * @todo Remove: Now part of toHdbcds.js
1808
- * @param {string} id
1809
- * @return {string}
1810
- */
1811
- function quoteOrUppercaseId(id) {
1812
- return quoteIdIfRequired(id);
1813
- }
1814
-
1815
1626
  /**
1816
1627
  * Render the name of an artifact, using the current name prefix from 'env'
1817
1628
  * and the real name of the artifact. In case of plain names, this
@@ -1821,12 +1632,11 @@ function toCdsSourceCsn(csn, options) {
1821
1632
  * of another entity -> Service.E and Service.E.Sub
1822
1633
  *
1823
1634
  * To handle such cases for hdbcds in quoted/hdbcds, we:
1824
- * - Find the part of the name that is no longer prefix (context/service/namespace)
1635
+ * - Find the part of the name that is no longer prefix (context/service)
1825
1636
  * - For Service.E -> E, for Service.E.Sub -> E.Sub
1826
1637
  * - Replace all dots in this "real name" with underscores
1827
1638
  * - Join with the env prefix
1828
1639
  *
1829
- *
1830
1640
  * @param {string} artifactName Artifact name to render
1831
1641
  * @return {string} Artifact name ready for rendering
1832
1642
  */
@@ -1837,25 +1647,14 @@ function toCdsSourceCsn(csn, options) {
1837
1647
  }
1838
1648
 
1839
1649
  /**
1840
- * Get the name that the artifact definition has been rendered as.
1841
- * Without quoting/escaping stuff.
1842
- *
1843
- * @param {String} artifactName Artifact name to use
1844
- * @returns {String}
1845
- */
1846
- function getResultingName(artifactName) {
1847
- return renderArtifactName(artifactName);
1848
- }
1849
-
1850
- /**
1851
- * Get the part that is really the name of this artifact and not just prefix caused by a context/service/namespace
1650
+ * Get the part that is really the name of this artifact and not just prefix caused by a context/service
1852
1651
  *
1853
1652
  * @param {String} artifactName Artifact name to use
1854
1653
  * @returns {String} non-prefix part of the artifact name
1855
1654
  */
1856
1655
  function getRealName(artifactName) {
1857
1656
  const parts = artifactName.split('.');
1858
- // Lenght of 1 -> There can be no prefix
1657
+ // Length of 1 -> There can be no prefix
1859
1658
  if (parts.length === 1)
1860
1659
  return artifactName;
1861
1660
 
@@ -1869,7 +1668,7 @@ function toCdsSourceCsn(csn, options) {
1869
1668
 
1870
1669
 
1871
1670
  const art = csn.definitions[seen];
1872
- if (!art || ![ 'service', 'context', 'namespace' ].includes(art.kind)) {
1671
+ if (!art || (art.kind !== 'service' && art.kind !== 'context')) {
1873
1672
  // We found a case where the prefix ended
1874
1673
  // Return everything following
1875
1674
  return parts.slice(i).join('.');
@@ -1879,15 +1678,218 @@ function toCdsSourceCsn(csn, options) {
1879
1678
  // we seem to have a normal case - just return the last part
1880
1679
  return getLastPartOf(artifactName);
1881
1680
  }
1681
+
1682
+ /**
1683
+ * Render an expression.
1684
+ *
1685
+ * @param {any} expr
1686
+ * @param {CdlRenderEnvironment} env
1687
+ * @param {boolean} [inline]
1688
+ * @param {boolean} [nestedExpr]
1689
+ * @param {boolean} [alwaysRenderCast]
1690
+ * @returns {string}
1691
+ */
1692
+ function renderExpr(expr, env, inline, nestedExpr, alwaysRenderCast) {
1693
+ if (!_renderExpr) {
1694
+ _renderExpr = getExpressionRenderer({
1695
+ finalize: x => x,
1696
+ explicitTypeCast: (x, env) => {
1697
+ const typeRef = renderTypeReference(x.cast, env, true);
1698
+ const arg = { ...x, cast: null }; // "arg" without cast to avoid recursion.
1699
+ return `cast(${renderArgument(arg, env)} as ${typeRef})`;
1700
+ },
1701
+ val(x, env) {
1702
+ // Literal value, possibly with explicit 'literal' property
1703
+ switch (x.literal || typeof x.val) {
1704
+ case 'number':
1705
+ case 'boolean':
1706
+ case 'null':
1707
+ return x.val;
1708
+ case 'x':
1709
+ case 'date':
1710
+ case 'time':
1711
+ case 'timestamp':
1712
+ return `${x.literal}'${x.val}'`;
1713
+ case 'string':
1714
+ return renderString(x.val, env);
1715
+ case 'object':
1716
+ if (x.val === null)
1717
+ return 'null';
1718
+ // otherwise fall through to
1719
+ default:
1720
+ throw new ModelError(`Unknown literal or type: ${JSON.stringify(x)}`);
1721
+ }
1722
+ },
1723
+ aliasOnly(x, _env) {
1724
+ return x.as;
1725
+ },
1726
+ enum: x => `#${x['#']}`,
1727
+ ref(x, env) {
1728
+ const { inline } = this;
1729
+ return `${(x.param || x.global) ? ':' : ''}${x.ref.map((step, index) => renderPathStep(step, index, inline, env)).join('.')}`;
1730
+ },
1731
+ windowFunction: (x, env) => {
1732
+ const funcDef = renderFunc(x.func, x, null, a => renderArgs(a, '=>', env));
1733
+ const windowFunctionOperator = x.xpr.shift(); // OVER ...
1734
+ return `${funcDef} ${windowFunctionOperator} ( ${renderExpr(x.xpr, env, true)} )`;
1735
+ },
1736
+ func: (x, env) => {
1737
+ // FIXME: Why care about HANA identifier?
1738
+ // test for non-regular HANA identifier that needs to be quoted
1739
+ // identifier {letter}({letter_or_digit}|[#$])*
1740
+ // letter [A-Za-z_]
1741
+ // letter_or_digit [A-Za-z_0-9]
1742
+
1743
+ const regex = /^[a-zA-Z][\w#$]*$/g;
1744
+ const funcName = regex.test(x.func) ? x.func : quoteIdIfRequired(x.func);
1745
+ return renderFunc(funcName, x, 'cap', a => renderArgs(a, '=>', env));
1746
+ },
1747
+ xpr(x, env) {
1748
+ if (this.nestedExpr && !x.cast || x.xpr.some(s => s === 'exists'))
1749
+ return `(${renderExpr(x.xpr, env, this.inline, true)})`;
1750
+
1751
+ return renderExpr(x.xpr, env, this.inline, true);
1752
+ },
1753
+ // Sub-queries in expressions need to be in parentheses, otherwise
1754
+ // left-associativity of UNIONS may result in different results.
1755
+ // For example: `select from E where id in (select from E union select from E);`:
1756
+ // Without parentheses, it would be different query.
1757
+ SET: (x, env) => `(${renderQuery(x, false, 'view', increaseIndent(env))})`,
1758
+ SELECT: (x, env) => `(${renderQuery(x, false, 'view', increaseIndent(env))})`,
1759
+ });
1760
+ }
1761
+ return _renderExpr(expr, env, inline, nestedExpr, alwaysRenderCast);
1762
+ }
1763
+ }
1764
+
1765
+ /**
1766
+ * Returns a newly created default environment (which keeps track of indentation, required USING
1767
+ * declarations and name prefixes.
1768
+ *
1769
+ * @return {CdlRenderEnvironment}
1770
+ */
1771
+ function createEnv() {
1772
+ return {
1773
+ // Current indentation string
1774
+ indent: '',
1775
+ // Dictionary of aliases for used artifact names, each entry like 'name' : { quotedName, quotedAlias }
1776
+ topLevelAliases: Object.create(null),
1777
+ // Current name prefix (including trailing dot if not empty)
1778
+ namePrefix: '',
1779
+ artifactName: '',
1780
+ elementName: '',
1781
+ };
1782
+ }
1783
+
1784
+ /**
1785
+ * Returns a copy of 'env' with increased indentation (and reset name prefix)
1786
+ *
1787
+ * @param {CdlRenderEnvironment} env
1788
+ * @returns {CdlRenderEnvironment}
1789
+ */
1790
+ function increaseIndent(env) {
1791
+ return Object.assign({}, env, { namePrefix: '', indent: `${env.indent} ` });
1792
+ }
1793
+
1794
+ /**
1795
+ * Return a path string 'path' with appropriate "-quotes.
1796
+ *
1797
+ * @param {string} path
1798
+ * @returns {string}
1799
+ */
1800
+ function quotePathString(path) {
1801
+ // "foo"."bar"."wiz"."blub"
1802
+ return path.split('.').map(quoteIdIfRequired).join('.');
1803
+ }
1804
+
1805
+ /**
1806
+ * Return an id 'id' with appropriate "-quotes
1807
+ *
1808
+ * @param {string} id
1809
+ * @return {string}
1810
+ */
1811
+ function quoteIdIfRequired(id) {
1812
+ // Quote if required for CDL
1813
+ if (requiresQuotingForCdl(id))
1814
+ return quote(id);
1815
+
1816
+ return id;
1882
1817
  }
1883
1818
 
1884
- const controlCharacters = /[\u{0000}-\u{0009}\u{000B}\u{001F}]/u;
1885
- const highSurrogate = /[\u{D800}-\u{DBFF}]/u;
1886
- const lowSurrogate = /[\u{DC00}-\u{DFFF}]/u;
1819
+ /**
1820
+ * Quotes the identifier using CDL-style ![]-quotes.
1821
+ *
1822
+ * @param id
1823
+ * @returns {string}
1824
+ */
1825
+ function quote(id) {
1826
+ return `![${id.replace(/]/g, ']]')}]`;
1827
+ }
1828
+
1829
+ /**
1830
+ * Returns true if 'id' requires quotes for CDL, i.e. if 'id'
1831
+ * 1. starts with a digit
1832
+ * 2. contains chars different than:
1833
+ * - uppercase letters
1834
+ * - lowercase letters
1835
+ * - digits
1836
+ * - underscore
1837
+ * 3. is a CDL keyword or a CDL function without parentheses (CURRENT_*, SYSUUID, ...)
1838
+ *
1839
+ * @param {string} id
1840
+ * @return {boolean}
1841
+ */
1842
+ function requiresQuotingForCdl(id) {
1843
+ return /^\d/.test(id) ||
1844
+ /\W/g.test(id.replace(/\./g, '')) ||
1845
+ keywords.cdl.includes(id.toUpperCase()) ||
1846
+ keywords.cdl_functions.includes(id.toUpperCase());
1847
+ }
1848
+
1849
+ const functionExpressionOperatorsRequireParentheses = [
1850
+ // Antlr rule 'condition', 'conditionAnd'
1851
+ 'and', 'or',
1852
+
1853
+ // Antlr rule 'conditionTerm'
1854
+ '=', '<>', '>', '>=', '<', '<=', '!=',
1855
+ // These are not forbidden, since they must be preceded by one of the comparators above.
1856
+ // 'any', 'some', 'all',
1857
+
1858
+ 'is', 'in', 'not', 'null', 'exists',
1859
+ // Antlr rule 'predicate'
1860
+ 'between', 'like', 'escape',
1861
+ ];
1862
+
1863
+ /**
1864
+ * Returns true if the given xpr-array can be rendered without parentheses
1865
+ * in a `fct(<xpr>)` expression such as `cast(<xpr> as Type)`. We only need to
1866
+ * look at the first nesting level. Otherwise, `renderExpr()` will already add parentheses.
1867
+ *
1868
+ * The list was created by looking at the `expression` Antlr rule.
1869
+ *
1870
+ * This is more of a heuristic for "nicer" CDL output. For example the
1871
+ * following snippet is parsable without parentheses:
1872
+ * `cast( case when int > 1 then int else 0 end as Integer ),`
1873
+ * However, because it is a flat xpr-array, we see `>` and assume that it is not a simple expression.
1874
+ *
1875
+ * @param {any[]} xpr
1876
+ * @return {boolean}
1877
+ */
1878
+ function isSimpleFunctionExpression(xpr) {
1879
+ return !xpr || xpr.every(val => typeof val !== 'string' || !functionExpressionOperatorsRequireParentheses.includes(val.toLowerCase()));
1880
+ }
1881
+
1882
+ /**
1883
+ * @param {string[]} includes
1884
+ * @return {string}
1885
+ */
1886
+ function renderIncludes(includes) {
1887
+ return ` : ${includes.map(name => quoteIdIfRequired(name)).join(', ')}`;
1888
+ }
1887
1889
 
1888
1890
  /**
1889
1891
  * Render the given string. Uses back-tick strings.
1890
- * env is used for indentation for three-back-tick strings.
1892
+ * env is used for indentation of three-back-tick strings.
1891
1893
  *
1892
1894
  * @param str
1893
1895
  * @param env
@@ -1897,90 +1899,33 @@ function renderString(str, env) {
1897
1899
  if (isSimpleString(str))
1898
1900
  return `'${str.replace(/'/g, '\'\'')}'`;
1899
1901
 
1900
- const output = [];
1901
-
1902
1902
  // We try to work similar to how JavaScript implements JSON.stringify.
1903
- // JSON.stringify, however, checks for unpaired unicode surrogates (see §25.5.2.2,
1904
- // <https://tc39.es/ecma262/#sec-quotejsonstring>), which we do not do here.
1905
-
1906
- for (let i = 0; i < str.length; ++i) {
1907
- const char = str[i];
1908
-
1909
- switch (char) {
1910
- case '$':
1911
- output.push('\\$');
1912
- break;
1913
- case '`':
1914
- output.push('\\`');
1915
- break;
1916
- case '\\':
1917
- output.push('\\\\');
1918
- break;
1919
- // Replace commonly known escape sequences for control characters
1920
- // See lib/language/multiLineStringParser.js
1921
- case '\f':
1922
- output.push('\\f');
1923
- break;
1924
- case '\v':
1925
- output.push('\\v');
1926
- break;
1927
- case '\t':
1928
- output.push('\\t');
1929
- break;
1930
- case '\b':
1931
- output.push('\\b');
1932
- break;
1933
- // If CR, LS, or PS appear, they have been encoded explicitly. If we don't escape
1934
- // them, a recompilation may yield different results because of newline normalization.
1935
- case '\r':
1936
- output.push('\\r');
1937
- break;
1938
- case '\u{2028}':
1939
- output.push('\\u{2028}');
1940
- break;
1941
- case '\u{2029}':
1942
- output.push('\\u{2029}');
1943
- break;
1944
- default: {
1945
- // Control Characters: C0
1946
- // See <https://en.wikipedia.org/wiki/C0_and_C1_control_codes#Basic_ASCII_control_codes>
1947
- //
1948
- // JSON.stringify is not required to escape all control characters, but if used, files may
1949
- // be interpreted as binary. Therefore, we replace them.
1950
- //
1951
- // We exclude LF from this list (\u000A). Characters with "nice" escapes have been replaced above.
1952
- if (controlCharacters.test(char)) {
1953
- const hex = char.codePointAt(0).toString(16);
1954
- output.push(`\\u{${hex}}`);
1955
- break;
1956
- }
1903
+ // JSON.stringify() also checks for unpaired unicode surrogates (see §25.5.2.2,
1904
+ // <https://tc39.es/ecma262/#sec-quotejsonstring>).
1905
+ str = escapeString(str, {
1906
+ $: '\\$',
1907
+ '`': '\\`',
1908
+ '\\': '\\\\',
1909
+ // Replace commonly known escape sequences for control characters
1910
+ // See lib/language/multiLineStringParser.js
1911
+ '\f': '\\f',
1912
+ '\v': '\\v',
1913
+ '\t': '\\t',
1914
+ '\b': '\\b',
1915
+ // If CR, LS, or PS appear, they have been encoded explicitly. If we don't escape
1916
+ // them, a recompilation may yield different results because of newline normalization.
1917
+ '\r': '\\r',
1918
+ '\u{2028}': '\\u{2028}',
1919
+ '\u{2029}': '\\u{2029}',
1920
+ // Don't encode LF
1921
+ '\n': '\n',
1922
+ // JSON.stringify() is not required to escape all control characters, but if used, files may
1923
+ // be interpreted as binary. Therefore, we replace them.
1924
+ // We exclude LF from this list (\n). Characters with "nice" escapes have been replaced above.
1925
+ control: hexEscape,
1926
+ unpairedSurrogate: hexEscape,
1927
+ });
1957
1928
 
1958
- // Unicode Surrogates
1959
- // These characters appear in _pairs_. A high surrogate must be followed by a low surrogate.
1960
- // If this is not the case, either needs to be encoded. This is also done by JSON.
1961
- // See also <https://docs.microsoft.com/en-us/globalization/encoding/surrogate-pairs>
1962
- if (highSurrogate.test(char)) {
1963
- if (i + 1 >= str.length || !lowSurrogate.test(str[i + 1])) {
1964
- const hex = char.codePointAt(0).toString(16);
1965
- output.push(`\\u{${hex}}`);
1966
- }
1967
- else {
1968
- output.push(char);
1969
- ++i;
1970
- output.push(str[i]);
1971
- }
1972
- }
1973
- else if (lowSurrogate.test(char)) {
1974
- const hex = char.codePointAt(0).toString(16);
1975
- output.push(`\\u{${hex}}`);
1976
- }
1977
- else {
1978
- output.push(char);
1979
- }
1980
- }
1981
- }
1982
- }
1983
- str = output.join('');
1984
1929
  // Note: String is normalized, only \n is the line separator.
1985
1930
  const lines = str.split('\n');
1986
1931
  // We don't know whether a text block was used or not. But if there
@@ -1994,6 +1939,12 @@ function renderString(str, env) {
1994
1939
  return `\`${str}\``;
1995
1940
  }
1996
1941
 
1942
+ /** @param {number} codePoint */
1943
+ function hexEscape(codePoint) {
1944
+ const hex = codePoint.toString(16);
1945
+ return `\\u{${hex}}`;
1946
+ }
1947
+
1997
1948
  /**
1998
1949
  * Returns true if the given string can be represented by using single quotes.
1999
1950
  * @param {string} str
@@ -2001,14 +1952,13 @@ function renderString(str, env) {
2001
1952
  function isSimpleString(str) {
2002
1953
  // A single-line string allows everything except certain line separators/breaks.
2003
1954
  // See ANTLR grammar for specifics.
2004
- // Furthermore, if control characters \u{0000}-\u{001F} are used, we escape them,
1955
+ // Furthermore, if control characters are used, we escape them,
2005
1956
  // as these are explicitly mentioned in the JSON spec (§9):
2006
1957
  // <https://www.ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf>
2007
- // On top, as (invalid) surrogate pairs need to be handled, we check for them as well.
1958
+ // On top, because (invalid) surrogate pairs need to be handled, we check for them as well.
2008
1959
  // v3: Not a simple string if ' (\u0027) is in string.
2009
1960
  return str === '' || (/^[^\u{0000}-\u{001F}\u2028\u2029]+$/u.test(str) &&
2010
- !highSurrogate.test(str) &&
2011
- !lowSurrogate.test(str));
1961
+ !hasUnpairedUnicodeSurrogate(str));
2012
1962
  }
2013
1963
 
2014
1964
  /**
@@ -2016,14 +1966,14 @@ function isSimpleString(str) {
2016
1966
  *
2017
1967
  * @property {string} indent Current indentation as a string, e.g. ' ' for two spaces.
2018
1968
  * @property {CSN.Path} [path] CSN path to the current artifact
2019
- * @property {string} artifactName Name of the artifact - set in renderArtifact
2020
- * @property {string} elementName Name of the element being rendered - set in renderElement
1969
+ * @property {string} [artifactName] Name of the artifact - set in renderArtifact
1970
+ * @property {string} [elementName] Name of the element being rendered - set in renderElement
2021
1971
  * @property {{[name: string]: {
2022
1972
  quotedName: string,
2023
1973
  quotedAlias: string
2024
1974
  }}} topLevelAliases Dictionary of aliases for used artifact names
2025
1975
  *
2026
- * @property {string} namePrefix Current name prefix (including trailing dot if not empty)
1976
+ * @property {string} [namePrefix] Current name prefix (including trailing dot if not empty)
2027
1977
  * @property {boolean} [skipKeys]
2028
1978
  * @property {CSN.Artifact} [_artifact]
2029
1979
  */