@sap/cds-compiler 2.13.8 → 2.15.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +128 -4
  2. package/bin/cdsc.js +112 -37
  3. package/lib/api/main.js +63 -22
  4. package/lib/api/options.js +2 -3
  5. package/lib/api/validate.js +6 -6
  6. package/lib/base/message-registry.js +100 -17
  7. package/lib/base/messages.js +85 -64
  8. package/lib/base/optionProcessorHelper.js +19 -0
  9. package/lib/checks/annotationsOData.js +11 -32
  10. package/lib/checks/arrayOfs.js +1 -34
  11. package/lib/checks/validator.js +2 -4
  12. package/lib/compiler/assert-consistency.js +1 -0
  13. package/lib/compiler/base.js +1 -0
  14. package/lib/compiler/builtins.js +11 -0
  15. package/lib/compiler/checks.js +22 -70
  16. package/lib/compiler/define.js +59 -11
  17. package/lib/compiler/extend.js +20 -3
  18. package/lib/compiler/finalize-parse-cdl.js +26 -20
  19. package/lib/compiler/index.js +75 -26
  20. package/lib/compiler/populate.js +36 -17
  21. package/lib/compiler/propagator.js +4 -1
  22. package/lib/compiler/resolve.js +104 -16
  23. package/lib/compiler/shared.js +61 -27
  24. package/lib/compiler/tweak-assocs.js +7 -1
  25. package/lib/edm/annotations/genericTranslation.js +93 -21
  26. package/lib/edm/csn2edm.js +216 -98
  27. package/lib/edm/edm.js +305 -226
  28. package/lib/edm/edmPreprocessor.js +499 -423
  29. package/lib/edm/edmUtils.js +22 -22
  30. package/lib/gen/Dictionary.json +98 -22
  31. package/lib/gen/language.checksum +1 -1
  32. package/lib/gen/language.interp +3 -1
  33. package/lib/gen/languageParser.js +4636 -4368
  34. package/lib/json/csnVersion.js +10 -11
  35. package/lib/json/from-csn.js +3 -2
  36. package/lib/json/to-csn.js +0 -2
  37. package/lib/language/docCommentParser.js +2 -2
  38. package/lib/language/genericAntlrParser.js +47 -2
  39. package/lib/language/language.g4 +59 -27
  40. package/lib/main.d.ts +19 -1
  41. package/lib/main.js +6 -0
  42. package/lib/model/csnRefs.js +33 -6
  43. package/lib/model/csnUtils.js +193 -75
  44. package/lib/model/enrichCsn.js +1 -0
  45. package/lib/model/revealInternalProperties.js +2 -2
  46. package/lib/modelCompare/compare.js +6 -6
  47. package/lib/optionProcessor.js +62 -26
  48. package/lib/render/toCdl.js +844 -679
  49. package/lib/render/toHdbcds.js +189 -243
  50. package/lib/render/toSql.js +180 -198
  51. package/lib/render/utils/common.js +131 -15
  52. package/lib/transform/db/.eslintrc.json +1 -1
  53. package/lib/transform/db/associations.js +2 -2
  54. package/lib/transform/db/constraints.js +3 -1
  55. package/lib/transform/db/expansion.js +15 -10
  56. package/lib/transform/db/flattening.js +94 -64
  57. package/lib/transform/db/transformExists.js +7 -7
  58. package/lib/transform/db/views.js +6 -3
  59. package/lib/transform/forHanaNew.js +43 -26
  60. package/lib/transform/forOdataNew.js +43 -42
  61. package/lib/transform/localized.js +12 -7
  62. package/lib/transform/odata/toFinalBaseType.js +8 -6
  63. package/lib/transform/odata/typesExposure.js +145 -197
  64. package/lib/transform/transformUtilsNew.js +9 -12
  65. package/lib/transform/translateAssocsToJoins.js +5 -1
  66. package/lib/transform/universalCsn/coreComputed.js +5 -3
  67. package/lib/transform/universalCsn/universalCsnEnricher.js +27 -5
  68. package/lib/utils/moduleResolve.js +13 -6
  69. package/package.json +1 -1
  70. package/share/messages/message-explanations.json +2 -1
  71. package/share/messages/syntax-expected-integer.md +37 -0
  72. package/lib/transform/odata/attachPath.js +0 -96
  73. package/lib/transform/odata/expandStructKeysInAssociations.js +0 -59
  74. package/lib/transform/odata/generateForeignKeyElements.js +0 -261
  75. package/lib/transform/odata/referenceFlattener.js +0 -296
  76. package/lib/transform/odata/sortByAssociationDependency.js +0 -105
  77. package/lib/transform/odata/structuralPath.js +0 -72
  78. package/lib/transform/odata/structureFlattener.js +0 -171
@@ -1,14 +1,9 @@
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');
12
7
  const { escapeString, hasUnpairedUnicodeSurrogate } = require('./utils/stringEscapes');
13
8
  const { checkCSNVersion } = require('../json/csnVersion');
14
9
  const { timetrace } = require('../utils/timetrace');
@@ -17,15 +12,15 @@ const { forEachDefinition } = require('../model/csnUtils');
17
12
  const enrichUniversalCsn = require('../transform/universalCsn/universalCsnEnricher');
18
13
  const { isBetaEnabled } = require('../base/model');
19
14
  const { ModelError } = require('../base/error');
15
+ const { typeParameters } = require('../compiler/builtins');
16
+ const { forEach } = require('../utils/objectUtils');
20
17
 
21
18
  /**
22
- * Render the CSN model 'model' to CDS source text. One source is created per
23
- * top-level artifact. Return a dictionary of top-level artifacts
24
- * by their names, like this:
25
- * { "foo" : "using XY; context foo {...};",
26
- * "bar::wiz" : "namespace bar::; entity wiz {...};"
27
- * }
28
- * 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.
29
24
  *
30
25
  * @param {CSN.Model} csn
31
26
  * @param {CSN.Options} [options]
@@ -33,9 +28,7 @@ const { ModelError } = require('../base/error');
33
28
  function toCdsSourceCsn(csn, options) {
34
29
  timetrace.start('CDL rendering');
35
30
  const { artifactRef } = csnRefs(csn);
36
-
37
- // Skip compactModel if already using CSN
38
- // const csn = cloneCsn(model, options);
31
+ let _renderExpr = null;
39
32
 
40
33
  if (options.csnFlavor === 'universal' && isBetaEnabled(options, 'enableUniversalCsn'))
41
34
  enrichUniversalCsn(csn, options);
@@ -43,177 +36,285 @@ function toCdsSourceCsn(csn, options) {
43
36
  checkCSNVersion(csn, options);
44
37
 
45
38
  const cdlResult = Object.create(null);
39
+ cdlResult.model = options.testMode ? '' : `// ${generatedByCompilerVersion()} \n`;
40
+
41
+ const subelementAnnotates = [];
46
42
 
47
- const main = 'model';
43
+ cdlResult.model += renderDefinitions();
44
+ // sub-element annotations that can't be written directly.
45
+ cdlResult.model += renderExtensions(subelementAnnotates, createEnv());
48
46
 
49
- cdlResult[main] = options.testMode ? '' : `// ${generatedByCompilerVersion()} \n`;
47
+ if (csn.vocabularies)
48
+ cdlResult.model += renderVocabularies(csn.vocabularies);
50
49
 
51
- const subelementAnnotates = [];
50
+ if (csn.namespace) {
51
+ cdlResult[csn.namespace] = `namespace ${renderArtifactName(csn.namespace)};\n`;
52
+ cdlResult[csn.namespace] += 'using from \'./model.cds\';';
53
+ }
52
54
 
53
- forEachDefinition(csn, (artifact, artifactName) => {
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());
59
+
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 = '';
54
71
  const env = createEnv();
55
- const sourceStr = renderArtifact(artifactName, artifact, env);
56
- if (sourceStr !== '')
57
- cdlResult[main] += `${sourceStr}\n`;
58
- });
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;
59
91
 
60
- // Apply possible subelement/action return annotations with an "annotate X with"
61
- // Some of them appear in csn.extensions, some not...
62
- if (subelementAnnotates.length > 0) {
63
- for (const [ artName, element, elementName, suffix ] of subelementAnnotates) {
64
- // Suffix is used with action return annotations
65
- let sourceStr = `annotate ${artName} with ${suffix ? `${suffix} ` : ''}{\n`;
66
- if (elementName) // action returns do not have element name - we need less {} there
67
- sourceStr += ` ${elementName} {\n`;
68
- const env = increaseIndent(increaseIndent(createEnv()));
69
- const subelements = renderSubelementAnnotates(element, env);
70
- if (subelements !== '') {
71
- sourceStr += `${subelements}\n`;
72
- if (elementName) // action returns do not have element name - we need less {} there
73
- sourceStr += ' }\n';
74
- sourceStr += '}\n';
75
- 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`;
76
99
  }
77
100
  }
78
101
  }
79
102
 
80
103
  /**
81
- * 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.
82
107
  *
83
- * @param {CSN.Element} element The element to annotate the subelements for
84
- * @param {CdlRenderEnvironment} env Render environment
85
- * @returns {String}
108
+ * @param {CSN.Extension[]} extensions
109
+ * @param {CdlRenderEnvironment} env
110
+ * @return {string}
86
111
  */
87
- function renderSubelementAnnotates(element, env) {
88
- const result = [];
89
- for (const [ name, subelement ] of Object.entries(element.elements)) {
90
- const subresult = [];
91
- const annos = renderAnnotationAssignments(subelement, env);
92
- if (annos !== '')
93
- subresult.push(annos.slice(0, -1));
94
-
95
- const quotedElementName = quoteOrUppercaseId(name);
96
- if (subelement.elements) {
97
- subresult.push(`${env.indent}${quotedElementName} {`);
98
- subresult.push(renderSubelementAnnotates(subelement, increaseIndent(env)));
99
- subresult.push(`${env.indent}};`);
100
- }
101
- else {
102
- subresult.push(`${env.indent}${quotedElementName};`);
103
- }
104
- // Only add result if there really was "something"
105
- if (annos || subelement.elements)
106
- result.push(...subresult);
107
- }
108
- return result.join('\n');
112
+ function renderExtensions(extensions, env) {
113
+ return extensions.map(ext => renderExtension(ext, env)).join('\n');
109
114
  }
110
115
 
111
- if (csn.vocabularies) {
112
- for (const annotationName of Object.keys(csn.vocabularies)) {
113
- // This environment is passed down the call hierarchy, for dealing with
114
- // indentation and name resolution issues
115
- const anno = csn.vocabularies[annotationName];
116
- const env = createEnv();
117
- let sourceStr;
118
- if (!anno._ignore)
119
- 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
+ }
120
128
 
121
- if (sourceStr !== '')
122
- 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;
123
152
  }
124
- }
125
153
 
126
- if (csn.namespace) {
127
- cdlResult[csn.namespace] = `namespace ${renderArtifactName(csn.namespace)};\n`;
128
- 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;
129
193
  }
130
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
+ }
131
210
 
132
- // If there are unapplied 'extend' and 'annotate' statements, render them separately
133
- // FIXME: Clarify if we should also do this for HANA (probably not?)
134
- if (csn.extensions) {
135
- const env = createEnv();
136
- const sourceStr = renderUnappliedExtensions(csn.extensions, env);
137
- cdlResult.unappliedExtensions = 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 '';
138
227
  }
139
228
 
140
- timetrace.stop();
141
- 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
+ }
142
248
 
143
249
  /**
144
- * Render unapplied 'extend' and 'annotate' statements from the 'extensions array'
250
+ * Render an 'annotate' statement.
145
251
  *
146
- * @param {CSN.Extension[]} extensions
252
+ * @param {CSN.Extension} ext
147
253
  * @param {CdlRenderEnvironment} env
148
254
  * @return {string}
149
255
  */
150
- function renderUnappliedExtensions(extensions, env) {
151
- return extensions.map((ext) => {
152
- // Top-level annotations of the artifact
153
- let result = renderAnnotationAssignments(ext, env);
154
- result += `annotate ${ext.annotate}`;
155
- // Element extensions and annotations (possibly nested)
156
- if (ext.elements)
157
- result += renderElementExtensions(ext.elements, env);
158
-
159
- // Returns annotations
160
- if (ext.returns) {
161
- const childEnv = increaseIndent(env);
162
- result += ` with returns${renderElementExtensions(ext.returns.elements, childEnv)}`;
163
- }
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)}`;
164
260
 
165
- // Action annotations
166
- if (ext.actions) {
167
- result += ' actions {\n';
168
- const childEnv = increaseIndent(env);
169
- for (const name in ext.actions) {
170
- const action = ext.actions[name];
171
- result += renderAnnotationAssignments(action, childEnv) + childEnv.indent + quoteIdIfRequired(name);
172
- // Action parameter annotations
173
- if (action.params) {
174
- result += '(\n';
175
- const grandChildEnv = increaseIndent(childEnv);
176
- const paramAnnotations = [];
177
- for (const paramName in action.params)
178
- paramAnnotations.push(renderAnnotationAssignments(action.params[paramName], grandChildEnv) + grandChildEnv.indent + quoteIdIfRequired(paramName));
179
-
180
- result += `${paramAnnotations.join(',\n')}\n${childEnv.indent})`;
181
- }
182
- // Annotations on action returns
183
- if (action.returns && action.returns.elements) {
184
- const grandChildEnv = increaseIndent(childEnv);
185
- result += ` returns${renderElementExtensions(action.returns.elements, grandChildEnv)}`;
186
- }
261
+ if (ext.params)
262
+ result += renderAnnotateParamsInParentheses(ext.params, env);
187
263
 
264
+ // Element extensions and annotations (possibly nested)
265
+ if (ext.elements)
266
+ result += renderAnnotateStatementElements(ext.elements, env);
267
+
268
+ // Returns annotations
269
+ if (ext.returns) {
270
+ const childEnv = increaseIndent(env);
271
+ result += ` returns${renderAnnotateStatementElements(ext.returns.elements, childEnv)}`;
272
+ }
188
273
 
189
- result += ';\n';
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)}`;
190
289
  }
191
- result += `${env.indent}}`;
290
+
291
+ result += ';\n';
192
292
  }
293
+ result += `${env.indent}}`;
294
+ }
193
295
 
194
296
 
195
- result += ';';
196
- return result;
197
- }).join('\n');
297
+ result += ';\n';
298
+ return result;
198
299
  }
199
300
 
200
301
  /**
201
- * 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
202
303
  * 'elements' (assuming that the surrounding parent has just been rendered, without trailing newline).
203
- * Return the resulting source string, ending without a trailing newline, too.
304
+ * Returns the resulting source string, ending without a trailing newline.
204
305
  *
205
306
  * @param {CSN.Elements} elements
206
307
  * @param {CdlRenderEnvironment} env
207
308
  * @return {string}
208
309
  */
209
- function renderElementExtensions(elements, env) {
310
+ function renderAnnotateStatementElements(elements, env) {
210
311
  let result = ' {\n';
211
312
  const childEnv = increaseIndent(env);
212
313
  for (const name in elements) {
213
314
  const elem = elements[name];
214
- result += renderAnnotationAssignments(elem, childEnv) + childEnv.indent + quoteIdIfRequired(name);
315
+ result += renderAnnotationAssignmentsAndDocComment(elem, childEnv) + childEnv.indent + quoteIdIfRequired(name);
215
316
  if (elem.elements)
216
- result += renderElementExtensions(elem.elements, childEnv);
317
+ result += renderAnnotateStatementElements(elem.elements, childEnv);
217
318
 
218
319
  result += ';\n';
219
320
  }
@@ -221,6 +322,24 @@ function toCdsSourceCsn(csn, options) {
221
322
  return result;
222
323
  }
223
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
+
224
343
  /**
225
344
  * Render an artifact. Return the resulting source string.
226
345
  *
@@ -251,7 +370,7 @@ function toCdsSourceCsn(csn, options) {
251
370
  case 'function':
252
371
  return renderActionOrFunction(artifactName, art, env);
253
372
  case 'event':
254
- return renderEventIfCDLMode(artifactName, art, env);
373
+ return renderEvent(artifactName, art, env);
255
374
  default:
256
375
  throw new ModelError(`Unknown artifact kind: ${art.kind}`);
257
376
  }
@@ -262,15 +381,13 @@ function toCdsSourceCsn(csn, options) {
262
381
  * @param {CSN.Artifact} art
263
382
  * @param {CdlRenderEnvironment} env
264
383
  */
265
- function renderEventIfCDLMode(artifactName, art, env) {
266
- let result = renderDocComment(art, env) + renderAnnotationAssignments(art, env);
384
+ function renderEvent(artifactName, art, env) {
385
+ let result = renderAnnotationAssignmentsAndDocComment(art, env);
267
386
  const childEnv = increaseIndent(env);
268
387
  const normalizedArtifactName = renderArtifactName(artifactName);
269
388
  result += `${env.indent}event ${normalizedArtifactName}`;
270
- if (art.includes) {
271
- // Includes are never flattened (don't exist in HANA)
272
- result += ` : ${art.includes.map(name => renderAbsoluteNameWithQuotes(name)).join(', ')}`;
273
- }
389
+ if (art.includes)
390
+ result += renderIncludes(art.includes);
274
391
  if (art.query || art.projection) {
275
392
  env._artifact = art;
276
393
  result += ' : ';
@@ -293,49 +410,6 @@ function toCdsSourceCsn(csn, options) {
293
410
  return result;
294
411
  }
295
412
 
296
- /* FIXME: Not yet required
297
- // Returns the artifact or element that constitutes the final type of
298
- // construct 'node', i.e. the object in which we would find type properties for
299
- // 'node'. Note that this may well be 'node' itself.
300
- function getFinalTypeOf(node) {
301
- if (node && node.type) {
302
- if (isBuiltinType(node.type)) {
303
- return node;
304
- }
305
- return getFinalTypeOf(node.type);
306
- }
307
- return node;
308
- }
309
-
310
- // Resolve path array 'ref' against artifact 'base' (or against 'csn.definitions'
311
- // if no 'base' given).
312
- // Return the resulting artifact or element (or 'undefined' if not found).
313
- function resolveRef(ref, base) {
314
- let result = base;
315
- for (let i = 0; i < ref.length; i++) {
316
- let pathStep = ref[i].id || ref[i];
317
- // Only first path step may be looked up in 'definitions'
318
- if (i === 0 && !base) {
319
- result = csn.definitions[pathStep];
320
- continue;
321
- }
322
- // Structured type
323
- else if (result && result.elements) {
324
- result = getFinalTypeOf(result.elements[pathStep]);
325
- }
326
- // Association
327
- else if (result && result.target) {
328
- result = resolveRef([pathStep], csn.definitions[result.target]);
329
- }
330
- // Not resolvable
331
- else {
332
- return undefined;
333
- }
334
- }
335
- return result;
336
- }
337
- */
338
-
339
413
  /**
340
414
  * Render a context or service. Return the resulting source string.
341
415
  *
@@ -344,12 +418,11 @@ function toCdsSourceCsn(csn, options) {
344
418
  * @param {CdlRenderEnvironment} env
345
419
  */
346
420
  function renderContext(artifactName, art, env) {
347
- let result = renderDocComment(art, env) + renderAnnotationAssignments(art, env);
421
+ let result = renderAnnotationAssignmentsAndDocComment(art, env);
348
422
  result += `${env.indent + (art.abstract ? 'abstract ' : '') + art.kind} ${renderArtifactName(artifactName)}`;
349
- if (art.includes) {
350
- // Includes are never flattened (don't exist in HANA)
351
- result += ` : ${art.includes.map(name => renderAbsoluteNameWithQuotes(name)).join(', ')}`;
352
- }
423
+ if (art.includes)
424
+ result += renderIncludes(art.includes);
425
+
353
426
  return `${result} {};\n`;
354
427
  }
355
428
 
@@ -362,23 +435,29 @@ function toCdsSourceCsn(csn, options) {
362
435
  * @return {string}
363
436
  */
364
437
  function renderEntity(artifactName, art, env) {
365
- let result = renderDocComment(art, env) + renderAnnotationAssignments(art, env);
438
+ let result = renderAnnotationAssignmentsAndDocComment(art, env);
366
439
  const childEnv = increaseIndent(env);
367
440
  const normalizedArtifactName = renderArtifactName(artifactName);
368
441
  result += `${env.indent + (art.abstract ? 'abstract ' : '')}entity ${normalizedArtifactName}`;
369
442
  const parameters = Object.keys(art.params || []).map(name => renderParameter(name, art.params[name], childEnv)).join(',\n');
370
443
  result += (parameters === '') ? '' : ` (\n${parameters}\n${env.indent})`;
371
- if (art.includes) {
372
- // Includes are never flattened (don't exist in HANA)
373
- result += ` : ${art.includes.map(name => renderAbsoluteNameWithQuotes(name)).join(', ')}`;
374
- }
444
+ if (art.includes)
445
+ result += renderIncludes(art.includes);
375
446
  result += ' {\n';
376
447
  for (const name in art.elements) {
377
448
  const element = art.elements[name];
378
449
  // For subelement annotations, this seems to be a pattern to recognize them
379
450
  // plus some other stuff unfortunately...
380
- if (element.type && element.elements)
381
- 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
+ }
382
461
  result += renderElement(name, element, childEnv);
383
462
  }
384
463
 
@@ -398,8 +477,8 @@ function toCdsSourceCsn(csn, options) {
398
477
  */
399
478
  function renderElement(elementName, elm, env, isSubElement) {
400
479
  env.elementName = elementName;
401
- let result = renderDocComment(elm, env) + renderAnnotationAssignments(elm, env);
402
- const quotedElementName = quoteOrUppercaseId(elementName);
480
+ let result = renderAnnotationAssignmentsAndDocComment(elm, env);
481
+ const quotedElementName = quoteIdIfRequired(elementName);
403
482
  result += `${env.indent + (elm.virtual ? 'virtual ' : '') +
404
483
  (elm.key && !isSubElement ? 'key ' : '') +
405
484
  ((elm.masked && !elm._ignoreMasked) ? 'masked ' : '') +
@@ -413,22 +492,6 @@ function toCdsSourceCsn(csn, options) {
413
492
  return `${result};\n`;
414
493
  }
415
494
 
416
- /**
417
- * Return the SELECT of the leading query of query 'query'
418
- *
419
- * @param {CSN.Query} query
420
- */
421
- function leadingQuerySelect(query) {
422
- if (query.SELECT)
423
- return query.SELECT;
424
-
425
- // Sanity checks
426
- if (!query.SET || !query.SET.args || !query.SET.args[0])
427
- throw new ModelError(`Expecting set with args in query: ${JSON.stringify(query)}`);
428
-
429
- return leadingQuerySelect(query.SET.args[0]);
430
- }
431
-
432
495
  /**
433
496
  * Render a query's actions and functions (if any) separately as extend-statements, so that actions
434
497
  * work not only for projections but also for views, which have no syntax (yet) to directly specify
@@ -452,9 +515,13 @@ function toCdsSourceCsn(csn, options) {
452
515
 
453
516
  /**
454
517
  * Render annotations that were extended to a query element of a view or projection (they only
455
- * appear in the view's 'elements', not in their 'columns', because the element itself may not
456
- * even be in 'columns', e.g. if it was expanded from a '*'). Return the resulting 'annotate'
457
- * 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.
458
525
  *
459
526
  * @param {string} artifactName
460
527
  * @param {CSN.Artifact} art
@@ -462,43 +529,61 @@ function toCdsSourceCsn(csn, options) {
462
529
  * @return {string}
463
530
  */
464
531
  function renderQueryElementAnnotations(artifactName, art, env) {
465
- // For preparation, create a map from element names to column objects
466
- const columnMap = Object.create(null);
467
- const select = leadingQuerySelect(getNormalizedQuery(art).query);
468
- for (const col of select.columns || [ '*' ]) {
469
- // Ignore '*'
470
- if (col === '*')
471
- continue;
472
-
473
- // Column must have an alias or be a path - take last part of that as element name
474
- columnMap[col.as || col.func || getLastPartOfRef(col.ref)] = col;
475
- }
476
- // Now iterate elements - render an annotation if it is different from the column's
477
- const childEnv = increaseIndent(env);
478
- let result = '';
479
- for (const elemName in art.elements) {
480
- let elemAnnotations = '';
481
- const elem = art.elements[elemName];
482
- for (const name in elem) {
483
- if (!name.startsWith('@'))
484
- continue;
485
-
486
- const annotationValue = renderAnnotationAssignment(elem[name], name, childEnv);
487
- // Skip annotation if column has the same
488
- if (columnMap[elemName] && columnMap[elemName][name] &&
489
- renderAnnotationAssignment(columnMap[elemName][name], name, childEnv) === annotationValue)
490
- continue;
491
-
492
- // Annotation names are never flattened
493
- elemAnnotations += annotationValue;
494
- }
495
- if (elemAnnotations !== '')
496
- result += `${elemAnnotations}${childEnv.indent}${quoteOrUppercaseId(elemName)};\n`;
497
- }
498
- if (result !== '')
499
- 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
+ }
500
537
 
501
- 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
+ }
502
587
  }
503
588
 
504
589
  /**
@@ -513,25 +598,24 @@ function toCdsSourceCsn(csn, options) {
513
598
  function renderViewSource(source, env) {
514
599
  // Sub-SELECT
515
600
  if (source.SELECT || source.SET) {
516
- let result = `(${renderDocComment(source, env)}${renderQuery(source, false, 'view', increaseIndent(env))})`;
601
+ let result = `(${renderQuery(source, false, 'view', increaseIndent(env))})`;
517
602
  if (source.as)
518
- result += ` as ${quoteOrUppercaseId(source.as)}`;
603
+ result += ` as ${quoteIdIfRequired(source.as)}`;
519
604
 
520
605
  return result;
521
606
  }
522
607
  // JOIN
523
608
  else if (source.join) {
524
609
  // One join operation, possibly with ON-condition
525
- let result = `${renderDocComment(source, env)}${renderViewSource(source.args[0], env)}`;
610
+ let result = `(${renderViewSource(source.args[0], env)}`;
526
611
  for (let i = 1; i < source.args.length; i++) {
527
- result = `(${result} ${source.join} `;
612
+ result += ` ${source.join} `;
528
613
  result += renderJoinCardinality(source.cardinality);
529
614
  result += `join ${renderViewSource(source.args[i], env)}`;
530
615
  if (source.on)
531
616
  result += ` on ${renderExpr(source.on, env, true, true)}`;
532
-
533
- result += ')';
534
617
  }
618
+ result += ')';
535
619
  return result;
536
620
  }
537
621
  // Ordinary path, possibly with an alias
@@ -572,9 +656,8 @@ function toCdsSourceCsn(csn, options) {
572
656
  // Determine the absolute name of the first artifact on the path (before any associations or element traversals)
573
657
  const firstArtifactName = path.ref[0].id || path.ref[0];
574
658
 
575
- let result = renderDocComment(path, env);
576
659
  // Render the first path step (absolute name, with different quoting/naming ..)
577
- result += getResultingName(firstArtifactName);
660
+ let result = renderArtifactName(firstArtifactName);
578
661
 
579
662
  // Even the first step might have parameters and/or a filter
580
663
  if (path.ref[0].args)
@@ -605,11 +688,26 @@ function toCdsSourceCsn(csn, options) {
605
688
  let result = renderAbsolutePath(path, env);
606
689
  if (path.as) {
607
690
  // Source had an alias - render it
608
- result += ` as ${quoteOrUppercaseId(path.as)}`;
691
+ result += ` as ${quoteIdIfRequired(path.as)}`;
609
692
  }
610
693
  return result;
611
694
  }
612
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
+
613
711
  /**
614
712
  * Render a single view or projection column 'col', as it occurs in a select list or
615
713
  * projection list within 'art', possibly with annotations.
@@ -621,7 +719,14 @@ function toCdsSourceCsn(csn, options) {
621
719
  */
622
720
  function renderViewColumn(col, env, element) {
623
721
  // Annotations and column
624
- 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 });
625
730
  result += env.indent;
626
731
 
627
732
  // only if column is virtual, keyword virtual was present in the source text
@@ -632,22 +737,19 @@ function toCdsSourceCsn(csn, options) {
632
737
  // Use special rendering for .expand/.inline - renderExpr cannot easily handle some cases
633
738
  result += key + ((col.expand || col.inline) ? renderInlineExpand(col, env) : renderExpr(col, env, true));
634
739
 
635
- // Alias is already handled by renderInlineExpand
636
- if (!col.inline && !col.expand && col.as)
637
- 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)}`;
638
745
 
639
746
  // Explicit type provided for the view element?
640
747
  if (col.cast) {
641
748
  // Special case: Explicit association type is actually a redirect
642
- if (col.cast.target) {
643
- // Redirections are never flattened (don't exist in HANA)
644
- result += ` : redirected to ${renderAbsoluteNameWithQuotes(col.cast.target)}`;
645
- if (col.cast.on)
646
- result += ` on ${renderExpr(col.cast.on, env, true, true)}`;
647
- }
648
- else {
749
+ if (col.cast.target && !col.cast.type)
750
+ result += ` : ${renderRedirectedTo(col.cast, env)}`;
751
+ else
649
752
  result += ` : ${renderTypeReference(col.cast, env, true)}`;
650
- }
651
753
  }
652
754
  return result;
653
755
  }
@@ -676,16 +778,10 @@ function toCdsSourceCsn(csn, options) {
676
778
 
677
779
  // We found a leaf - no further drilling
678
780
  if (!obj.inline && !obj.expand) {
679
- if (obj.cast && obj.cast.type) {
781
+ if (obj.cast && obj.cast.type)
680
782
  result += ` : ${renderTypeReference(obj.cast, createEnv())}`;
681
- }
682
- else if (obj.cast && obj.cast.target) { // test tbd
683
- result += ` : redirected to ${renderAbsoluteNameWithQuotes(obj.cast.target)}`;
684
- if (obj.cast.on)
685
- result += ` on ${renderExpr(obj.cast.on, env, true, true)}`;
686
- else if (obj.cast.keys)
687
- result += ` { ${Object.keys(obj.cast.keys).map(name => renderForeignKey(obj.cast.keys[name], env)).join(', ')} }`;
688
- }
783
+ else if (obj.cast && obj.cast.target) // test tbd
784
+ result += ` : ${renderRedirectedTo(obj.cast, env)}`;
689
785
  return result;
690
786
  }
691
787
 
@@ -720,7 +816,7 @@ function toCdsSourceCsn(csn, options) {
720
816
  * Render .doc properties as comments in CDL
721
817
  *
722
818
  * @param {object} obj Object to render for
723
- * @param {object} env Env - for indent
819
+ * @param {CdlRenderEnvironment} env
724
820
  * @returns {String}
725
821
  */
726
822
  function renderDocComment(obj, env) {
@@ -729,7 +825,13 @@ function toCdsSourceCsn(csn, options) {
729
825
  else if (obj && obj.doc === null) // empty doc comment needs to be rendered
730
826
  return `\n${env.indent}/** */\n`;
731
827
 
732
- 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`;
733
835
  }
734
836
 
735
837
  /**
@@ -743,7 +845,7 @@ function toCdsSourceCsn(csn, options) {
743
845
  */
744
846
  function renderView(artifactName, art, env) {
745
847
  const syntax = (art.projection) ? 'projection' : 'entity';
746
- let result = renderDocComment(art, env) + renderAnnotationAssignments(art, env);
848
+ let result = renderAnnotationAssignmentsAndDocComment(art, env);
747
849
  result += `${env.indent}${art.abstract ? 'abstract ' : ''}${syntax === 'projection' ? 'entity' : syntax} ${renderArtifactName(artifactName)}`;
748
850
  if (art.params) {
749
851
  const childEnv = increaseIndent(env);
@@ -775,48 +877,28 @@ function toCdsSourceCsn(csn, options) {
775
877
  * @param {object} [elements]
776
878
  */
777
879
  function renderQuery(query, isLeadingQuery, syntax, env, path = [], elements = query.elements || Object.create(null)) {
778
- let result = renderDocComment(query, env);
779
- // Set operator, like UNION, INTERSECT, ...
780
880
  if (query.SET) {
781
- // First arg may be leading query
782
- result += `(${renderQuery(query.SET.args[0], isLeadingQuery, 'view', env, path.concat([ 'SET', 'args', 0 ]), elements)}`;
783
- // FIXME: Clarify if set operators can be n-ary (assuming binary here)
784
- if (query.SET.op) {
785
- // Loop over all other arguments, i.e. for A UNION B UNION C UNION D ...
786
- for (let i = 1; i < query.SET.args.length; i++)
787
- 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)}`;
788
- }
789
- result += ')';
790
- // Set operation may also have an ORDER BY and LIMIT/OFFSET (in contrast to the ones belonging to
791
- // each SELECT)
792
- if (query.SET.orderBy)
793
- result += `${continueIndent(result, env)}order by ${query.SET.orderBy.map(entry => renderOrderByEntry(entry, env)).join(', ')}`;
794
-
795
- if (query.SET.limit)
796
- result += `${continueIndent(result, env)}${renderLimit(query.SET.limit, env)}`;
797
-
798
- return result;
881
+ // Set operator, such as UNION, INTERSECT, or EXCEPT...
882
+ return renderQuerySet();
799
883
  }
800
- // Otherwise must have a SELECT
801
884
  else if (!query.SELECT) {
885
+ // ...otherwise must have a SELECT
802
886
  throw new ModelError(`Unexpected query operation ${JSON.stringify(query)}`);
803
887
  }
888
+
889
+ let result = '';
804
890
  const select = query.SELECT;
805
891
  const childEnv = increaseIndent(env);
806
892
 
807
- if (syntax === 'projection')
808
- result += `projection on ${renderViewSource(select.from, env)}`;
809
- else if (syntax === 'view' || syntax === 'entity')
810
- result += `select from ${renderViewSource(select.from, env)}`;
811
- else
812
- throw new ModelError(`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);
813
896
 
814
897
  if (select.mixin) {
815
898
  let elems = '';
816
- for (const name in select.mixin) {
817
- if (!select.mixin[name]._ignore)
818
- elems += renderElement(name, select.mixin[name], childEnv);
819
- }
899
+ forEach(select.mixin, (name, mixin) => {
900
+ elems += renderElement(name, mixin, childEnv);
901
+ });
820
902
  if (elems) {
821
903
  result += ' mixin {\n';
822
904
  result += elems;
@@ -826,13 +908,12 @@ function toCdsSourceCsn(csn, options) {
826
908
  result += select.distinct ? ' distinct' : '';
827
909
  if (select.columns) {
828
910
  result += ' {\n';
829
- result += `${select.columns.map(col => renderViewColumn(col, childEnv, findElement(elements, col)))
830
- .filter(s => s !== '')
831
- .join(',\n')}\n`;
911
+ result += renderViewColumns(select.columns, increaseIndent(env), elements);
832
912
  result += `${env.indent}}`;
833
913
  }
914
+
834
915
  if (select.excluding) {
835
- 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`;
836
917
  result += `${env.indent}}`;
837
918
  }
838
919
  // FIXME: Currently, only projections can have actions and functions, but we cannot distinguish
@@ -890,6 +971,29 @@ function toCdsSourceCsn(csn, options) {
890
971
 
891
972
  return limitStr;
892
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
+ }
893
997
  }
894
998
 
895
999
  /**
@@ -901,7 +1005,7 @@ function toCdsSourceCsn(csn, options) {
901
1005
  * @return {string}
902
1006
  */
903
1007
  function renderOrderByEntry(entry, env) {
904
- let result = renderDocComment(entry, env) + renderExpr(entry, env, true, false, true);
1008
+ let result = renderAnnotationAssignmentsAndDocComment(entry, env) + renderExpr(entry, env, true, false, true);
905
1009
  if (entry.sort)
906
1010
  result += ` ${entry.sort}`;
907
1011
 
@@ -925,7 +1029,7 @@ function toCdsSourceCsn(csn, options) {
925
1029
  let result = '';
926
1030
  const childEnv = increaseIndent(env);
927
1031
  for (const name in art.actions)
928
- result += renderDocComment(art.actions[name], childEnv) + renderActionOrFunction(name, art.actions[name], childEnv);
1032
+ result += renderActionOrFunction(name, art.actions[name], childEnv);
929
1033
 
930
1034
  // Even if we have seen actions/functions, they might all have been ignored
931
1035
  if (result !== '')
@@ -943,13 +1047,23 @@ function toCdsSourceCsn(csn, options) {
943
1047
  * @return {string}
944
1048
  */
945
1049
  function renderActionOrFunction(actionName, act, env) {
946
- 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)}`;
947
1051
  const childEnv = increaseIndent(env);
948
1052
  const parameters = Object.keys(act.params || []).map(name => renderParameter(name, act.params[name], childEnv)).join(',\n');
949
1053
  result += (parameters === '') ? '()' : `(\n${parameters}\n${env.indent})`;
950
1054
  if (act.returns) {
951
- if (act.returns.type && act.returns.elements) // action returns annotations
952
- 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
+ }
953
1067
  result += ` returns ${renderTypeReference(act.returns, env)}${renderNullability(act.returns)}`;
954
1068
  }
955
1069
 
@@ -966,7 +1080,7 @@ function toCdsSourceCsn(csn, options) {
966
1080
  * @return {string}
967
1081
  */
968
1082
  function renderParameter(parName, par, env) {
969
- 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)}`;
970
1084
  if (par.default)
971
1085
  result += ` default ${renderExpr(par.default, env)}`;
972
1086
 
@@ -985,14 +1099,11 @@ function toCdsSourceCsn(csn, options) {
985
1099
  * @return {string}
986
1100
  */
987
1101
  function renderTypeOrAnnotation(artifactName, art, env, artType) {
988
- if (!options.toCdl && art.kind === 'aspect')
989
- return '';
990
- let result = renderDocComment(art, env) + renderAnnotationAssignments(art, env);
991
- result += `${env.indent + (options.toCdl && (artType || art.$syntax) || art.kind )} ${renderArtifactName(artifactName)}`;
992
- if (art.includes) {
993
- // Includes are never flattened (don't exist in HANA)
994
- result += ` : ${art.includes.map(name => renderAbsoluteNameWithQuotes(name)).join(', ')}`;
995
- }
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
+
996
1107
  const childEnv = increaseIndent(env);
997
1108
  if (art.elements && !art.type) {
998
1109
  // Structured type or annotation with anonymous struct type
@@ -1031,8 +1142,16 @@ function toCdsSourceCsn(csn, options) {
1031
1142
  if (elm.items.notNull != null)
1032
1143
  rc += elm.items.notNull ? ' not null' : ' null';
1033
1144
  // many sub element annotates
1034
- if (elm.items.type && elm.items.elements && env.artifactName)
1035
- 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
+ }
1036
1155
 
1037
1156
  return rc;
1038
1157
  }
@@ -1068,15 +1187,20 @@ function toCdsSourceCsn(csn, options) {
1068
1187
  ((elm.type === comp) ? ' of ' : ' to ');
1069
1188
  }
1070
1189
 
1071
- // normal target or named aspect
1072
- 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') {
1073
1196
  result += renderAbsolutePath({ ref: [ elm.target || elm.targetAspect ] }, env);
1074
1197
  }
1075
- else if (elm.targetAspect && elm.targetAspect.elements) { // anonymous aspect
1198
+ else if (elements) {
1199
+ // anonymous aspect, either parseCdl or client CSN.
1076
1200
  const childEnv = increaseIndent(env);
1077
1201
  result += '{\n';
1078
- for (const name in elm.targetAspect.elements)
1079
- result += renderElement(name, elm.targetAspect.elements[name], childEnv);
1202
+ for (const name in elements)
1203
+ result += renderElement(name, elements[name], childEnv);
1080
1204
 
1081
1205
  result += `${env.indent}}`;
1082
1206
  }
@@ -1084,7 +1208,6 @@ function toCdsSourceCsn(csn, options) {
1084
1208
  throw new ModelError('Association/Composition is missing its target! Throwing exception to trigger recompilation.');
1085
1209
  }
1086
1210
 
1087
-
1088
1211
  // ON-condition (if any)
1089
1212
  if (elm.on)
1090
1213
  result += ` on ${renderExpr(elm.on, env, true, true)}`;
@@ -1116,14 +1239,8 @@ function toCdsSourceCsn(csn, options) {
1116
1239
  }
1117
1240
 
1118
1241
  // If we get here, it must be a named type
1119
- if (isBuiltinType(elm.type)) {
1120
- result += renderBuiltinType(elm);
1121
- }
1122
- else {
1123
- // Simple absolute name
1124
- // Type names are never flattened (derived types are unraveled in HANA)
1125
- result += getResultingName(elm.type);
1126
- }
1242
+ result += renderNamedTypeWithParameters(elm);
1243
+
1127
1244
  if (elm.enum && !noEnum)
1128
1245
  result += renderEnum(elm.enum, env);
1129
1246
 
@@ -1131,18 +1248,48 @@ function toCdsSourceCsn(csn, options) {
1131
1248
  }
1132
1249
 
1133
1250
  /**
1134
- * @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
1135
1255
  * @return {string}
1136
1256
  */
1137
- function renderBuiltinType(elm) {
1138
- // If there is a user-defined type with the same short name (cds.Integer -> Integer),
1139
- // we render the full name, including the leading "cds."
1140
- if (csn.definitions[elm.type.slice(4)])
1141
- 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
+ }
1142
1265
 
1143
- return elm.type.slice(4) + renderTypeParameters(elm);
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 = '';
1273
+
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;
1144
1290
  }
1145
1291
 
1292
+
1146
1293
  /**
1147
1294
  * Render the 'enum { ... } part of a type declaration
1148
1295
  *
@@ -1153,21 +1300,32 @@ function toCdsSourceCsn(csn, options) {
1153
1300
  function renderEnum(enumPart, env) {
1154
1301
  let result = ' enum {\n';
1155
1302
  const childEnv = increaseIndent(env);
1156
- for (const name in enumPart) {
1157
- const enumConst = enumPart[name];
1158
- result += renderDocComment(enumConst, childEnv);
1159
- result += renderAnnotationAssignments(enumConst, childEnv);
1160
- result += childEnv.indent + quoteIdIfRequired(name);
1161
- if (enumConst.val !== undefined)
1162
- result += ` = ${renderExpr(enumConst, childEnv)}`;
1163
- else if (enumConst['#'] !== undefined)
1164
- result += ` = #${enumConst['#']}`;
1165
- result += ';\n';
1166
- }
1303
+ for (const name in enumPart)
1304
+ result += renderEnumElement(name, enumPart[name], childEnv);
1167
1305
  result += `${env.indent}}`;
1168
1306
  return result;
1169
1307
  }
1170
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
+
1171
1329
  /**
1172
1330
  * Render an annotation value (somewhat like a simplified expression, with slightly different
1173
1331
  * representation)
@@ -1177,17 +1335,25 @@ function toCdsSourceCsn(csn, options) {
1177
1335
  */
1178
1336
  function renderAnnotationValue(x, env) {
1179
1337
  if (Array.isArray(x)) {
1180
- // Render array parts as values
1181
- 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(', ')} ]`;
1182
1341
  }
1183
1342
  else if (typeof x === 'object' && x !== null) {
1184
1343
  // Enum symbol
1185
- if (x['#'])
1344
+ if (x['#']) {
1186
1345
  return `#${x['#']}`;
1187
-
1346
+ }
1188
1347
  // Shorthand for absolute path (as string)
1189
- else if (x['='])
1348
+ else if (x['=']) {
1190
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
+ }
1191
1357
 
1192
1358
  // Struct value (can actually only occur within an array)
1193
1359
 
@@ -1206,172 +1372,54 @@ function toCdsSourceCsn(csn, options) {
1206
1372
  }
1207
1373
 
1208
1374
  /**
1209
- * Render an expression (including paths and values) or condition 'x'.
1210
- * (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
1211
1376
  *
1212
- * @param {any} x
1213
- * @param {CdlRenderEnvironment} env
1214
- * @param {boolean} [inline=true]
1215
- * @param {boolean} [inExpr=false] Whether the expression is already inside another expression
1216
- * @param {boolean} [alwaysRenderCast=false] Whether to _always_ render SQL-style casts, even if `inExpr === false`
1217
- * Note: This is a hack for casts() inside groupBy.
1377
+ * @param {string|object} s
1378
+ * @param {number} idx
1379
+ * @returns {string}
1218
1380
  */
1219
- function renderExpr(x, env, inline = true, inExpr = false, alwaysRenderCast = false) {
1220
- // Compound expression
1221
- if (Array.isArray(x))
1222
- return beautifyExprArray(x.map(item => renderExpr(item, env, inline, inExpr)));
1223
-
1224
- if (typeof x === 'object' && x !== null) {
1225
- if ((inExpr || alwaysRenderCast) && x.cast && x.cast.type)
1226
- return renderExplicitTypeCast(renderExprObject());
1227
- return renderExprObject();
1228
- }
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
1229
1386
 
1230
- // Not a literal value but part of an operator, function etc - just leave as it is
1231
- return x;
1232
-
1233
-
1234
- /**
1235
- * Various special cases represented as objects
1236
- *
1237
- * @returns {string}
1238
- */
1239
- function renderExprObject() {
1240
- if (x.list) {
1241
- // set "inExpr" to false: treat list elements as new expressions
1242
- return `(${x.list.map(item => renderExpr(item, env, inline, false)).join(', ')})`;
1243
- }
1244
- else if (x.val !== undefined) {
1245
- // Literal value, possibly with explicit 'literal' property
1246
- switch (x.literal || typeof x.val) {
1247
- case 'number':
1248
- case 'boolean':
1249
- case 'null':
1250
- return x.val;
1251
- case 'x':
1252
- case 'date':
1253
- case 'time':
1254
- case 'timestamp':
1255
- return `${x.literal}'${x.val}'`;
1256
- case 'string':
1257
- return renderString(x.val, env);
1258
- case 'object':
1259
- if (x.val === null)
1260
- return 'null';
1387
+ if (idx === 0 &&
1388
+ s.startsWith('$'))
1389
+ return s;
1261
1390
 
1262
- // otherwise fall through to
1263
- default:
1264
- throw new ModelError(`Unknown literal or type: ${JSON.stringify(x)}`);
1265
- }
1266
- }
1267
- // Enum symbol
1268
- else if (x['#']) {
1269
- return `#${x['#']}`;
1270
- }
1271
- // Reference: Array of path steps, possibly preceded by ':'
1272
- else if (x.ref) {
1273
- // FIXME: no extra magic with x.param or x.global
1274
- return `${(x.param || x.global) ? ':' : ''}${x.ref.map(renderPathStep).join('.')}`;
1275
- }
1276
- else if (x.xpr && x.func) {
1277
- // window function
1278
- const funcDef = renderFunc( x.func, x, null, a => renderArgs(a, '=>', env) );
1279
- const windowFunctionOperator = x.xpr.shift(); // OVER ...
1280
- return `${funcDef} ${windowFunctionOperator} ( ${renderExpr(x.xpr, env, true)} )`;
1281
- }
1282
- // Function call, possibly with args (use '=>' for named args)
1283
- else if (x.func) {
1284
- // test for non-regular HANA identifier that needs to be quoted
1285
- // identifier {letter}({letter_or_digit}|[#$])*
1286
- // letter [A-Za-z_]
1287
- // letter_or_digit [A-Za-z_0-9]
1288
-
1289
- const regex = RegExp(/^[a-zA-Z][\w#$]*$/, 'g');
1290
- const funcName = regex.test(x.func) ? x.func : quoteIdIfRequired(x.func);
1291
- return renderFunc( funcName, x, 'cap', a => renderArgs(a, '=>', env) );
1292
- }
1293
- // Nested expression
1294
- else if (x.xpr) {
1295
- // Ensure `exists` is always enclosed by parentheses
1296
- if ((inExpr && !x.cast ) || x.xpr.some(s => s === 'exists'))
1297
- return `(${renderExpr(x.xpr, env, inline, true)})`;
1298
-
1299
- return renderExpr(x.xpr, env, inline, true);
1300
- }
1301
- // Sub-select
1302
- else if (x.SELECT) {
1303
- // renderQuery for SELECT does not bring its own parentheses (because it is also used in renderView)
1304
- return `(${renderQuery(x, false, 'view', increaseIndent(env))})`;
1305
- }
1306
- else if (x.SET) {
1307
- // renderQuery for SET always brings its own parentheses (because it is also used in renderViewSource)
1308
- return `${renderQuery(x, false, 'view', increaseIndent(env))}`;
1309
- }
1310
- else {
1311
- throw new ModelError(`Unknown expression: ${JSON.stringify(x)}`);
1312
- }
1391
+ return quoteIdIfRequired(s);
1313
1392
  }
1314
-
1315
- /**
1316
- * Renders an explicit `cast()` inside an 'xpr'.
1317
- * @param {string} value
1318
- * @returns {string}
1319
- */
1320
- function renderExplicitTypeCast(value) {
1321
- const typeRef = renderTypeReference(x.cast, env, true);
1322
- return `cast(${value} as ${typeRef})`;
1323
- }
1324
-
1325
- /**
1326
- * Render a single path step 's' at path position 'idx', which can have filters or parameters or be a function
1327
- *
1328
- * @param {string|object} s
1329
- * @param {number} idx
1330
- * @returns {string}
1331
- */
1332
- function renderPathStep(s, idx) {
1333
- // Simple id or absolute name
1334
- if (typeof s === 'string') {
1335
- // In first path position, do not quote $projection and magic $-variables like CURRENT_DATE, $now etc.
1336
- // FIXME: We should rather explicitly recognize quoting somehow
1337
-
1338
- if (idx === 0 &&
1339
- s.startsWith('$'))
1340
- return s;
1341
-
1342
- 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)})`;
1343
1408
  }
1344
- // ID with filters or parameters
1345
- else if (typeof s === 'object') {
1346
- // Sanity check
1347
- if (!s.func && !s.id)
1348
- throw new ModelError(`Unknown path step object: ${JSON.stringify(s)}`);
1349
-
1350
- // Not really a path step but an object-like function call
1351
- if (s.func)
1352
- return `${s.func}(${renderArgs(s, '=>', env)})`;
1353
-
1354
- // Path step, possibly with view parameters and/or filters
1355
- let result = `${quoteOrUppercaseId(s.id)}`;
1356
- if (s.args) {
1357
- // View parameters
1358
- result += `(${renderArgs(s, ':', env)})`;
1359
- }
1360
- if (s.where) {
1361
- // Filter, possibly with cardinality
1362
- result += `[${s.cardinality ? (`${s.cardinality.max}: `) : ''}${renderExpr(s.where, env, inline, true)}]`;
1363
- }
1364
-
1365
- return result;
1409
+ if (s.where) {
1410
+ // Filter, possibly with cardinality
1411
+ result += `[${s.cardinality ? (`${s.cardinality.max}: `) : ''}${renderExpr(s.where, env, inline, true)}]`;
1366
1412
  }
1367
1413
 
1368
- throw new ModelError(`Unknown path step: ${JSON.stringify(s)}`);
1414
+ return result;
1369
1415
  }
1416
+
1417
+ throw new ModelError(`Unknown path step: ${JSON.stringify(s)}`);
1370
1418
  }
1371
1419
 
1372
1420
  /**
1373
1421
  * Render function arguments or view parameters (positional if array, named if object/dict),
1374
- * using 'sep' as separator for positional parameters
1422
+ * using 'sep' as separator for named parameters
1375
1423
  *
1376
1424
  * @param {object} node with `args` to render
1377
1425
  * @param {string} sep
@@ -1379,19 +1427,33 @@ function toCdsSourceCsn(csn, options) {
1379
1427
  * @returns {string}
1380
1428
  */
1381
1429
  function renderArgs(node, sep, env) {
1382
- const args = node.args ? node.args : {};
1430
+ const args = node.args || [];
1431
+
1383
1432
  // Positional arguments
1384
1433
  if (Array.isArray(args))
1385
- return args.map(arg => renderExpr(arg, env, true, false, true)).join(', ');
1434
+ return args.map(arg => renderArgument(arg, env)).join(', ');
1386
1435
 
1387
1436
  // Named arguments (object/dict)
1388
1437
  else if (typeof args === 'object')
1389
- return Object.keys(args).map(key => `${quoteOrUppercaseId(key)} ${sep} ${renderExpr(args[key], env, true, false, true)}`).join(', ');
1390
-
1438
+ return Object.keys(args).map(key => `${quoteIdIfRequired(key)} ${sep} ${renderArgument(args[key], env)}`).join(', ');
1391
1439
 
1392
1440
  throw new ModelError(`Unknown args: ${JSON.stringify(args)}`);
1393
1441
  }
1394
1442
 
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);
1455
+ }
1456
+
1395
1457
  /**
1396
1458
  * Render a cardinality (only those parts that were actually provided)
1397
1459
  *
@@ -1470,28 +1532,35 @@ function toCdsSourceCsn(csn, options) {
1470
1532
  }
1471
1533
 
1472
1534
  /**
1473
- * Render (primitive) type parameters of element 'elm', i.e.
1535
+ * Render (primitive) type parameters of artifact 'artWithType', i.e.
1474
1536
  * length, precision and scale (even if incomplete), plus any other unknown ones.
1475
1537
  *
1476
- * @param {CSN.Element} elm
1538
+ * @param {CSN.Artifact} artWithType
1477
1539
  * @returns {string}
1478
1540
  */
1479
- function renderTypeParameters(elm /* , env */) {
1480
- const params = [];
1481
- // Length, precision and scale (even if incomplete)
1482
- if (elm.length !== undefined)
1483
- 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 '';
1484
1545
 
1485
- if (elm.precision !== undefined)
1486
- 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})`;
1487
1551
 
1488
- if (elm.scale !== undefined)
1489
- 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})`;
1490
1557
 
1491
- if (elm.srid !== undefined)
1492
- params.push(elm.srid);
1558
+ // Render named params
1559
+ const renderedParams = [];
1560
+ for (const param of params)
1561
+ renderedParams.push(`${param}: ${artWithType[param]}`);
1493
1562
 
1494
- return params.length === 0 ? '' : `(${params.join(', ')})`;
1563
+ return `(${renderedParams.join(', ')})`;
1495
1564
  }
1496
1565
 
1497
1566
  /**
@@ -1499,13 +1568,14 @@ function toCdsSourceCsn(csn, options) {
1499
1568
  *
1500
1569
  * @param {object} obj Object that has annotations
1501
1570
  * @param {CdlRenderEnvironment} env
1571
+ * @param {{parens: boolean}} [config] Config for renderAnnotationAssignment()
1502
1572
  * @return {string}
1503
1573
  */
1504
- function renderAnnotationAssignments(obj, env) {
1505
- let result = '';
1574
+ function renderAnnotationAssignmentsAndDocComment(obj, env, config) {
1575
+ let result = renderDocComment(obj, env);
1506
1576
  for (const name in obj) {
1507
1577
  if (name.startsWith('@'))
1508
- result += renderAnnotationAssignment(obj[name], name, env);
1578
+ result += renderAnnotationAssignment(obj[name], name, env, config);
1509
1579
  }
1510
1580
  return result;
1511
1581
  }
@@ -1518,14 +1588,16 @@ function toCdsSourceCsn(csn, options) {
1518
1588
  * @param {any} anno Annotation value
1519
1589
  * @param {string} name Annotation name, e.g. `@A.B.C#foo.C`
1520
1590
  * @param {CdlRenderEnvironment} env
1591
+ * @param {object} [config] parens: Whether the annotation assignment must be surrounded by parentheses.
1521
1592
  * @return {string} Rendered annotation, possibly quoted: `@![A.B.C#foo.C]: value`
1522
1593
  */
1523
- function renderAnnotationAssignment(anno, name, env) {
1594
+ function renderAnnotationAssignment(anno, name, env, config = { parens: false }) {
1524
1595
  name = name.substring(1);
1525
1596
  // Take the annotation assignment apart into <nameBeforeVariant>#<variantAndRest>
1526
1597
  const parts = name.split('#');
1527
1598
  const nameBeforeVariant = parts[0];
1528
1599
  const variant = parts[1];
1600
+ const { parens } = config;
1529
1601
 
1530
1602
  // Identifier according to our grammar: /[$_a-zA-Z][$_a-zA-Z0-9]*/
1531
1603
  // We expand this pattern to also include dots after the first character.
@@ -1536,123 +1608,21 @@ function toCdsSourceCsn(csn, options) {
1536
1608
  // even though that is the result after flattening.
1537
1609
  const variantRequiresQuoting = variant && !/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(variant);
1538
1610
 
1539
- let result;
1611
+ let result = `${env.indent}@`;
1612
+ if (parens)
1613
+ result += '(';
1614
+
1540
1615
  if (annoRequiresQuoting || variantRequiresQuoting)
1541
- result = `${env.indent}@${quote(name)}`;
1616
+ result += quote(name);
1542
1617
  else
1543
- result = `${env.indent}@${name}`;
1618
+ result += name;
1544
1619
 
1545
1620
  result += ` : ${renderAnnotationValue(anno, env)}`;
1621
+ if (parens)
1622
+ result += ')';
1546
1623
  return `${result}\n`;
1547
1624
  }
1548
1625
 
1549
- /**
1550
- * Render an absolute name 'absName', with appropriate quotes. Also record the
1551
- * fact that 'absName' is used in 'env', so that an appropriate USING can be constructed
1552
- * if necessary.
1553
- *
1554
- * @param {string} absName
1555
- * @return {string}
1556
- */
1557
- function renderAbsoluteNameWithQuotes(absName) {
1558
- return absName;
1559
- }
1560
-
1561
- /**
1562
- * Returns a newly created default environment (which keeps track of indentation, required USING
1563
- * declarations and name prefixes.
1564
- *
1565
- * @return {CdlRenderEnvironment}
1566
- */
1567
- function createEnv() {
1568
- return {
1569
- // Current indentation string
1570
- indent: '',
1571
- // Dictionary of aliases for used artifact names, each entry like 'name' : { quotedName, quotedAlias }
1572
- topLevelAliases: Object.create(null),
1573
- // Current name prefix (including trailing dot if not empty)
1574
- namePrefix: '',
1575
- artifactName: null,
1576
- elementName: null,
1577
- };
1578
- }
1579
-
1580
- /**
1581
- * Returns a copy of 'env' with increased indentation (and resetted name prefix)
1582
- *
1583
- * @param {CdlRenderEnvironment} env
1584
- * @returns {CdlRenderEnvironment}
1585
- */
1586
- function increaseIndent(env) {
1587
- return Object.assign({}, env, { namePrefix: '', indent: `${env.indent} ` });
1588
- }
1589
-
1590
- /**
1591
- * Return a path string 'path' with appropriate "-quotes.
1592
- *
1593
- * @param {string} path
1594
- * @returns {string}
1595
- */
1596
- function quotePathString(path) {
1597
- // "foo"."bar"."wiz"."blub"
1598
- return path.split('.').map(quoteIdIfRequired).join('.');
1599
- }
1600
-
1601
- /**
1602
- * Return an id 'id' with appropriate "-quotes
1603
- *
1604
- * @param {string} id
1605
- * @return {string}
1606
- */
1607
- function quoteIdIfRequired(id) {
1608
- // Quote if required for CDL
1609
- if (requiresQuotingForCdl(id))
1610
- return quote(id);
1611
-
1612
- return id;
1613
- }
1614
-
1615
- /**
1616
- * Quotes the identifier using CDL-style ![]-quotes.
1617
- *
1618
- * @param id
1619
- * @returns {string}
1620
- */
1621
- function quote(id) {
1622
- return `![${id.replace(/]/g, ']]')}]`;
1623
- }
1624
-
1625
- /**
1626
- * Returns true if 'id' requires quotes for CDL, i.e. if 'id'
1627
- * 1. starts with a digit
1628
- * 2. contains chars different than:
1629
- * - uppercase letters
1630
- * - lowercase letters
1631
- * - digits
1632
- * - underscore
1633
- * 3. is a CDL keyword or a CDL function without parentheses (CURRENT_*, SYSUUID, ...)
1634
- *
1635
- * @param {string} id
1636
- * @return {boolean}
1637
- */
1638
- function requiresQuotingForCdl(id) {
1639
- return /^\d/.test(id) ||
1640
- /\W/g.test(id.replace(/\./g, '')) ||
1641
- keywords.cdl.includes(id.toUpperCase()) ||
1642
- keywords.cdl_functions.includes(id.toUpperCase());
1643
- }
1644
-
1645
- /**
1646
- * Quote or uppercase an identifier 'id', depending on naming strategy
1647
- *
1648
- * @todo Remove: Now part of toHdbcds.js
1649
- * @param {string} id
1650
- * @return {string}
1651
- */
1652
- function quoteOrUppercaseId(id) {
1653
- return quoteIdIfRequired(id);
1654
- }
1655
-
1656
1626
  /**
1657
1627
  * Render the name of an artifact, using the current name prefix from 'env'
1658
1628
  * and the real name of the artifact. In case of plain names, this
@@ -1667,7 +1637,6 @@ function toCdsSourceCsn(csn, options) {
1667
1637
  * - Replace all dots in this "real name" with underscores
1668
1638
  * - Join with the env prefix
1669
1639
  *
1670
- *
1671
1640
  * @param {string} artifactName Artifact name to render
1672
1641
  * @return {string} Artifact name ready for rendering
1673
1642
  */
@@ -1677,17 +1646,6 @@ function toCdsSourceCsn(csn, options) {
1677
1646
  return prefix ? `${quoteIdIfRequired(prefix)}.${realname.split('.').map(quoteIdIfRequired).join('.')}` : realname.split('.').map(quoteIdIfRequired).join('.');
1678
1647
  }
1679
1648
 
1680
- /**
1681
- * Get the name that the artifact definition has been rendered as.
1682
- * Without quoting/escaping stuff.
1683
- *
1684
- * @param {String} artifactName Artifact name to use
1685
- * @returns {String}
1686
- */
1687
- function getResultingName(artifactName) {
1688
- return renderArtifactName(artifactName);
1689
- }
1690
-
1691
1649
  /**
1692
1650
  * Get the part that is really the name of this artifact and not just prefix caused by a context/service
1693
1651
  *
@@ -1720,6 +1678,213 @@ function toCdsSourceCsn(csn, options) {
1720
1678
  // we seem to have a normal case - just return the last part
1721
1679
  return getLastPartOf(artifactName);
1722
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;
1817
+ }
1818
+
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(', ')}`;
1723
1888
  }
1724
1889
 
1725
1890
  /**
@@ -1801,14 +1966,14 @@ function isSimpleString(str) {
1801
1966
  *
1802
1967
  * @property {string} indent Current indentation as a string, e.g. ' ' for two spaces.
1803
1968
  * @property {CSN.Path} [path] CSN path to the current artifact
1804
- * @property {string} artifactName Name of the artifact - set in renderArtifact
1805
- * @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
1806
1971
  * @property {{[name: string]: {
1807
1972
  quotedName: string,
1808
1973
  quotedAlias: string
1809
1974
  }}} topLevelAliases Dictionary of aliases for used artifact names
1810
1975
  *
1811
- * @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)
1812
1977
  * @property {boolean} [skipKeys]
1813
1978
  * @property {CSN.Artifact} [_artifact]
1814
1979
  */