@sap/cds-compiler 2.13.8 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +155 -1594
  2. package/bin/cdsc.js +144 -66
  3. package/doc/CHANGELOG_ARCHIVE.md +1592 -0
  4. package/doc/CHANGELOG_BETA.md +3 -4
  5. package/doc/CHANGELOG_DEPRECATED.md +35 -1
  6. package/doc/{DeprecatedOptions.md → DeprecatedOptions_v2.md} +3 -1
  7. package/doc/Versioning.md +20 -1
  8. package/lib/api/.eslintrc.json +2 -2
  9. package/lib/api/main.js +237 -122
  10. package/lib/api/options.js +17 -88
  11. package/lib/api/validate.js +12 -16
  12. package/lib/base/keywords.js +216 -109
  13. package/lib/base/message-registry.js +152 -37
  14. package/lib/base/messages.js +145 -83
  15. package/lib/base/model.js +44 -2
  16. package/lib/base/optionProcessorHelper.js +19 -0
  17. package/lib/checks/actionsFunctions.js +7 -5
  18. package/lib/checks/annotationsOData.js +11 -32
  19. package/lib/checks/arrayOfs.js +1 -34
  20. package/lib/checks/cdsPersistence.js +1 -0
  21. package/lib/checks/elements.js +6 -6
  22. package/lib/checks/invalidTarget.js +1 -1
  23. package/lib/checks/nonexpandableStructured.js +1 -1
  24. package/lib/checks/queryNoDbArtifacts.js +2 -1
  25. package/lib/checks/selectItems.js +5 -1
  26. package/lib/checks/types.js +4 -2
  27. package/lib/checks/utils.js +2 -2
  28. package/lib/checks/validator.js +4 -5
  29. package/lib/compiler/assert-consistency.js +16 -10
  30. package/lib/compiler/base.js +1 -0
  31. package/lib/compiler/builtins.js +98 -9
  32. package/lib/compiler/checks.js +22 -70
  33. package/lib/compiler/define.js +61 -13
  34. package/lib/compiler/extend.js +79 -14
  35. package/lib/compiler/finalize-parse-cdl.js +46 -29
  36. package/lib/compiler/index.js +100 -37
  37. package/lib/compiler/moduleLayers.js +7 -0
  38. package/lib/compiler/populate.js +19 -18
  39. package/lib/compiler/propagator.js +7 -4
  40. package/lib/compiler/resolve.js +297 -234
  41. package/lib/compiler/shared.js +107 -102
  42. package/lib/compiler/tweak-assocs.js +16 -11
  43. package/lib/compiler/utils.js +5 -0
  44. package/lib/edm/annotations/genericTranslation.js +93 -21
  45. package/lib/edm/csn2edm.js +230 -115
  46. package/lib/edm/edm.js +305 -226
  47. package/lib/edm/edmPreprocessor.js +509 -438
  48. package/lib/edm/edmUtils.js +31 -45
  49. package/lib/gen/Dictionary.json +98 -22
  50. package/lib/gen/language.checksum +1 -1
  51. package/lib/gen/language.interp +10 -30
  52. package/lib/gen/language.tokens +105 -114
  53. package/lib/gen/languageLexer.interp +1 -34
  54. package/lib/gen/languageLexer.js +889 -1007
  55. package/lib/gen/languageLexer.tokens +95 -106
  56. package/lib/gen/languageParser.js +20786 -22199
  57. package/lib/json/csnVersion.js +10 -11
  58. package/lib/json/from-csn.js +59 -51
  59. package/lib/json/to-csn.js +10 -10
  60. package/lib/language/antlrParser.js +2 -2
  61. package/lib/language/docCommentParser.js +62 -39
  62. package/lib/language/errorStrategy.js +52 -40
  63. package/lib/language/genericAntlrParser.js +348 -229
  64. package/lib/language/language.g4 +629 -653
  65. package/lib/language/multiLineStringParser.js +14 -42
  66. package/lib/language/textUtils.js +44 -0
  67. package/lib/main.d.ts +46 -43
  68. package/lib/main.js +108 -79
  69. package/lib/model/csnRefs.js +34 -7
  70. package/lib/model/csnUtils.js +337 -332
  71. package/lib/model/enrichCsn.js +1 -0
  72. package/lib/model/revealInternalProperties.js +30 -10
  73. package/lib/model/sortViews.js +32 -31
  74. package/lib/modelCompare/compare.js +6 -6
  75. package/lib/optionProcessor.js +73 -46
  76. package/lib/render/.eslintrc.json +1 -1
  77. package/lib/render/DuplicateChecker.js +4 -7
  78. package/lib/render/manageConstraints.js +70 -2
  79. package/lib/render/toCdl.js +1042 -882
  80. package/lib/render/toHdbcds.js +195 -245
  81. package/lib/render/toRename.js +44 -22
  82. package/lib/render/toSql.js +225 -241
  83. package/lib/render/utils/common.js +145 -15
  84. package/lib/render/utils/sql.js +20 -19
  85. package/lib/sql-identifier.js +6 -0
  86. package/lib/transform/db/.eslintrc.json +4 -3
  87. package/lib/transform/db/associations.js +2 -2
  88. package/lib/transform/db/cdsPersistence.js +5 -15
  89. package/lib/transform/db/constraints.js +4 -2
  90. package/lib/transform/db/expansion.js +22 -16
  91. package/lib/transform/db/flattening.js +109 -80
  92. package/lib/transform/db/transformExists.js +7 -7
  93. package/lib/transform/db/views.js +9 -6
  94. package/lib/transform/draft/.eslintrc.json +2 -2
  95. package/lib/transform/draft/db.js +6 -6
  96. package/lib/transform/draft/odata.js +6 -7
  97. package/lib/transform/forHanaNew.js +62 -48
  98. package/lib/transform/forOdataNew.js +49 -50
  99. package/lib/transform/localized.js +31 -20
  100. package/lib/transform/odata/toFinalBaseType.js +16 -14
  101. package/lib/transform/odata/typesExposure.js +146 -198
  102. package/lib/transform/odata/utils.js +1 -38
  103. package/lib/transform/transformUtilsNew.js +67 -84
  104. package/lib/transform/translateAssocsToJoins.js +7 -3
  105. package/lib/transform/universalCsn/.eslintrc.json +2 -2
  106. package/lib/transform/universalCsn/coreComputed.js +16 -9
  107. package/lib/transform/universalCsn/universalCsnEnricher.js +60 -10
  108. package/lib/utils/file.js +3 -3
  109. package/lib/utils/moduleResolve.js +13 -6
  110. package/lib/utils/timetrace.js +20 -21
  111. package/package.json +35 -4
  112. package/share/messages/message-explanations.json +2 -1
  113. package/share/messages/syntax-expected-integer.md +37 -0
  114. package/doc/ApiMigration.md +0 -237
  115. package/doc/CommandLineMigration.md +0 -58
  116. package/doc/ErrorMessages.md +0 -175
  117. package/doc/FioriAnnotations.md +0 -94
  118. package/doc/ODataTransformation.md +0 -273
  119. package/lib/backends.js +0 -529
  120. package/lib/fix_antlr4-8_warning.js +0 -56
  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 -296
  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
@@ -1,41 +1,33 @@
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 { isBuiltinType, generatedByCompilerVersion, getNormalizedQuery } = require('../model/csnUtils');
5
+ const { findElement, getExpressionRenderer } = require('./utils/common');
12
6
  const { escapeString, hasUnpairedUnicodeSurrogate } = require('./utils/stringEscapes');
13
7
  const { checkCSNVersion } = require('../json/csnVersion');
14
8
  const { timetrace } = require('../utils/timetrace');
15
- const { csnRefs } = require('../model/csnRefs');
16
9
  const { forEachDefinition } = require('../model/csnUtils');
17
10
  const enrichUniversalCsn = require('../transform/universalCsn/universalCsnEnricher');
18
11
  const { isBetaEnabled } = require('../base/model');
19
12
  const { ModelError } = require('../base/error');
13
+ const { typeParameters, specialFunctions } = require('../compiler/builtins');
14
+ const { forEach } = require('../utils/objectUtils');
15
+
16
+ const identifierRegex = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/;
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
+ * - `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]
32
27
  */
33
- function toCdsSourceCsn(csn, options) {
28
+ function csnToCdl(csn, options) {
34
29
  timetrace.start('CDL rendering');
35
- const { artifactRef } = csnRefs(csn);
36
-
37
- // Skip compactModel if already using CSN
38
- // const csn = cloneCsn(model, options);
30
+ let _renderExpr = null;
39
31
 
40
32
  if (options.csnFlavor === 'universal' && isBetaEnabled(options, 'enableUniversalCsn'))
41
33
  enrichUniversalCsn(csn, options);
@@ -43,177 +35,284 @@ function toCdsSourceCsn(csn, options) {
43
35
  checkCSNVersion(csn, options);
44
36
 
45
37
  const cdlResult = Object.create(null);
38
+ cdlResult.model = options.testMode ? '' : `// ${generatedByCompilerVersion()} \n`;
46
39
 
47
- const main = 'model';
40
+ const subelementAnnotates = [];
48
41
 
49
- cdlResult[main] = options.testMode ? '' : `// ${generatedByCompilerVersion()} \n`;
42
+ cdlResult.model += renderDefinitions();
43
+ // sub-element annotations that can't be written directly.
44
+ cdlResult.model += renderExtensions(subelementAnnotates, createEnv());
50
45
 
51
- const subelementAnnotates = [];
46
+ if (csn.vocabularies)
47
+ cdlResult.model += renderVocabularies(csn.vocabularies);
48
+
49
+ if (csn.namespace) {
50
+ cdlResult.namespace = `namespace ${renderArtifactName(csn.namespace)};\n`;
51
+ cdlResult.namespace += 'using from \'./model.cds\';';
52
+ }
53
+
54
+ // If there are extensions, such as 'extend' and 'annotate' statements, render them separately.
55
+ // Used for e.g. parseCdl-style CSN or Universal CSN.
56
+ if (csn.extensions)
57
+ cdlResult.unappliedExtensions = renderExtensions(csn.extensions, createEnv());
52
58
 
53
- forEachDefinition(csn, (artifact, artifactName) => {
59
+ timetrace.stop();
60
+ return cdlResult;
61
+
62
+ /**
63
+ * Render entries from the `csn.definitions` dictionary.
64
+ * Returns an empty string if nothing is rendered.
65
+ *
66
+ * @return {string}
67
+ */
68
+ function renderDefinitions() {
69
+ let result = '';
54
70
  const env = createEnv();
55
- const sourceStr = renderArtifact(artifactName, artifact, env);
56
- if (sourceStr !== '')
57
- cdlResult[main] += `${sourceStr}\n`;
58
- });
71
+ forEachDefinition(csn, (artifact, artifactName) => {
72
+ const sourceStr = renderArtifact(artifactName, artifact, env);
73
+ if (sourceStr !== '')
74
+ result += `${sourceStr}\n`;
75
+ });
76
+ return result;
77
+ }
59
78
 
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`;
79
+ /**
80
+ * Render annotation definitions, i.e. entries from csn.vocabularies.
81
+ * Returns an empty string if there isn't anything to render.
82
+ *
83
+ * @param {object} vocabularies
84
+ * @return {string}
85
+ */
86
+ function renderVocabularies(vocabularies) {
87
+ let result = '';
88
+ forEach(vocabularies, renderVocabulariesEntry);
89
+ return result;
90
+
91
+ function renderVocabulariesEntry(name, anno) {
92
+ if (!anno._ignore) {
93
+ // This environment is passed down the call hierarchy, for dealing with
94
+ // indentation and name resolution issues
95
+ const env = envNewPath(createEnv(), [ 'vocabularies', name ]);
96
+ const sourceStr = renderTypeOrAnnotation(name, anno, env, 'annotation');
97
+ result += `${sourceStr}\n`;
76
98
  }
77
99
  }
78
100
  }
79
101
 
80
102
  /**
81
- * Render annotations for subelements as part of an "annotate X with" statement
103
+ * Render 'extend' and 'annotate' statements from the `extensions` array.
104
+ * Could be annotate-statements for sub-elements annotations or from parseCdl's
105
+ * extensions array or just unapplied extensions.
82
106
  *
83
- * @param {CSN.Element} element The element to annotate the subelements for
84
- * @param {CdlRenderEnvironment} env Render environment
85
- * @returns {String}
107
+ * @param {CSN.Extension[]} extensions
108
+ * @param {CdlRenderEnvironment} env
109
+ * @return {string}
86
110
  */
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');
111
+ function renderExtensions(extensions, env) {
112
+ return extensions.map(ext => renderExtension(ext, env)).join('\n');
109
113
  }
110
114
 
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');
115
+ /**
116
+ * Render an 'extend' and 'annotate' statement.
117
+ *
118
+ * @param {CSN.Extension} ext
119
+ * @param {CdlRenderEnvironment} env
120
+ * @return {string}
121
+ */
122
+ function renderExtension(ext, env) {
123
+ if (ext.extend)
124
+ return renderExtendStatement(ext.extend, ext, env);
125
+ return renderAnnotateStatement(ext, env);
126
+ }
120
127
 
121
- if (sourceStr !== '')
122
- cdlResult[main] += `${sourceStr}\n`;
128
+ /**
129
+ * Render an 'extend' statement.
130
+ * `extName` is the extension's artifact's name, most likely `ext.extend`.
131
+ * This function is recursive, which is why you need to pass it explicitly.
132
+ *
133
+ * @param {string} extName
134
+ * @param {object} ext
135
+ * @param {CdlRenderEnvironment} env
136
+ * @return {string}
137
+ */
138
+ function renderExtendStatement(extName, ext, env) {
139
+ extName = renderArtifactName(extName);
140
+ // Element extensions have `kind` set.
141
+ const isElementExtend = (ext.kind === 'extend');
142
+ let result = renderAnnotationAssignmentsAndDocComment(ext, env);
143
+
144
+ if (ext.includes && ext.includes.length > 0) {
145
+ // Includes can't be combined with anything in braces {}. Multiple includes
146
+ // are possible through CSN, but in CDL, only one include at once is possible.
147
+ const affix = isElementExtend ? 'element ' : '';
148
+ for (const id of ext.includes)
149
+ result += `${env.indent}extend ${affix}${extName} with ${quotePathIfRequired(id)};\n`;
150
+ return result;
123
151
  }
124
- }
125
152
 
126
- if (csn.namespace) {
127
- cdlResult[csn.namespace] = `namespace ${renderArtifactName(csn.namespace)};\n`;
128
- cdlResult[csn.namespace] += `using from './${main}.cds';`;
153
+ // We have the "old-style" prefix syntax and the "new-style" postfix "with <type>" syntax.
154
+ // The former one can not only extend (sub-)elements but also actions in the same statement whereas
155
+ // the latter cannot.
156
+ // If there are actions, check if there are also elements/columns, and if so, use the prefix notation.
157
+ const usePrefixNotation = isElementExtend || ext.actions && (ext.columns || ext.elements);
158
+ if (usePrefixNotation)
159
+ result += `${env.indent}extend ${getExtendPrefixVariant(ext)} ${extName} with {\n`;
160
+ else
161
+ result += `${env.indent}extend ${extName} with ${getExtendPostfixVariant(ext)} {\n`;
162
+
163
+ if (ext.columns) {
164
+ result += renderViewColumns(ext.columns, increaseIndent(env));
165
+ }
166
+ else if (ext.elements) {
167
+ result += renderExtendStatementElements(ext, env);
168
+ }
169
+ else if (ext.enum) {
170
+ const childEnv = increaseIndent(env);
171
+ forEach(ext.enum, (name, value) => {
172
+ result += renderEnumElement(name, value, childEnv);
173
+ });
174
+ }
175
+
176
+ // Not part of if/else cascade, because it may be in postfix notation.
177
+ if (ext.actions) {
178
+ // TODO: Merge with renderActionsAndFunctions() -> requires removal of static `with actions`
179
+ const childEnv = increaseIndent(env);
180
+ let actions = '';
181
+ forEach(ext.actions, (actionName, action) => {
182
+ actions += renderActionOrFunction(actionName, action, childEnv);
183
+ });
184
+ if (!usePrefixNotation)
185
+ result += actions;
186
+ else if (actions !== '')
187
+ result += `${env.indent}} actions {\n${actions}`;
188
+ }
189
+
190
+ result += `${env.indent}};\n`;
191
+ return result;
129
192
  }
130
193
 
194
+ /**
195
+ * What <extend> prefix type to use. Used to render `extend <type> <ref>` statements.
196
+ *
197
+ * @param {object} ext
198
+ * @return {string}
199
+ */
200
+ function getExtendPrefixVariant(ext) {
201
+ if (ext.kind === 'extend')
202
+ return 'element'; // element extensions inside an `extend`
203
+ if (ext.columns)
204
+ return 'projection';
205
+ if (ext.elements)
206
+ return 'entity';
207
+ return '';
208
+ }
131
209
 
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;
210
+ /**
211
+ * What <extend> postfix type to use. Used to render `extend <ref> with <type>` statements.
212
+ *
213
+ * @param {CSN.Extension} ext
214
+ * @return {string}
215
+ */
216
+ function getExtendPostfixVariant(ext) {
217
+ if (ext.columns)
218
+ return 'columns';
219
+ if (ext.actions)
220
+ return 'actions';
221
+ if (ext.elements)
222
+ return 'elements';
223
+ if (ext.enum)
224
+ return 'enum';
225
+ return '';
138
226
  }
139
227
 
140
- timetrace.stop();
141
- return cdlResult;
228
+ /**
229
+ * Render the elements inside an `extend` statement. They may themselves be `extend` statements.
230
+ *
231
+ * @param {CSN.Extension} ext
232
+ * @param {CdlRenderEnvironment} env
233
+ * @return {string}
234
+ */
235
+ function renderExtendStatementElements(ext, env) {
236
+ let result = '';
237
+ forEach(ext.elements || {}, (elemName, element) => {
238
+ if (element.kind === 'extend')
239
+ result += renderExtendStatement(elemName, element, increaseIndent(env));
240
+ else
241
+ // As soon as we are inside an element, nested `extend` are not possible,
242
+ // since we can't extend an existing element of a new one.
243
+ result += renderElement(elemName, element, increaseIndent(env), true);
244
+ });
245
+ return result;
246
+ }
142
247
 
143
248
  /**
144
- * Render unapplied 'extend' and 'annotate' statements from the 'extensions array'
249
+ * Render an 'annotate' statement.
145
250
  *
146
- * @param {CSN.Extension[]} extensions
251
+ * @param {CSN.Extension} ext
147
252
  * @param {CdlRenderEnvironment} env
148
253
  * @return {string}
149
254
  */
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
- }
255
+ function renderAnnotateStatement(ext, env) {
256
+ // Top-level annotations of the artifact
257
+ let result = renderAnnotationAssignmentsAndDocComment(ext, env);
258
+ result += `${env.indent}annotate ${renderArtifactName(ext.annotate)}`;
164
259
 
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
- }
260
+ if (ext.params)
261
+ result += renderAnnotateParamsInParentheses(ext.params, env);
187
262
 
263
+ // Element extensions and annotations (possibly nested)
264
+ if (ext.elements)
265
+ result += renderAnnotateStatementElements(ext.elements, env);
188
266
 
189
- result += ';\n';
267
+ // Returns annotations
268
+ if (ext.returns) {
269
+ const childEnv = increaseIndent(env);
270
+ result += ` returns${renderAnnotateStatementElements(ext.returns.elements, childEnv)}`;
271
+ }
272
+
273
+ // Action annotations
274
+ if (ext.actions) {
275
+ result += ' actions {\n';
276
+ const childEnv = increaseIndent(env);
277
+ for (const name in ext.actions) {
278
+ const action = ext.actions[name];
279
+ result += renderAnnotationAssignmentsAndDocComment(action, childEnv) + childEnv.indent + quoteIdIfRequired(name);
280
+ // Action parameter annotations
281
+ if (action.params)
282
+ result += renderAnnotateParamsInParentheses(action.params, childEnv);
283
+
284
+ // Annotations on action returns
285
+ if (action.returns && action.returns.elements) {
286
+ const grandChildEnv = increaseIndent(childEnv);
287
+ result += ` returns${renderAnnotateStatementElements(action.returns.elements, grandChildEnv)}`;
190
288
  }
191
- result += `${env.indent}}`;
192
- }
193
289
 
290
+ result += ';\n';
291
+ }
292
+ result += `${env.indent}}`;
293
+ }
194
294
 
195
- result += ';';
196
- return result;
197
- }).join('\n');
295
+ result += ';\n';
296
+ return result;
198
297
  }
199
298
 
200
299
  /**
201
- * Render the elements-specific part of an 'extend' or 'annotate' statement for an element dictionary
300
+ * Render the elements-specific part of an 'annotate' statement for an element dictionary
202
301
  * '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.
302
+ * Returns the resulting source string, ending without a trailing newline.
204
303
  *
205
304
  * @param {CSN.Elements} elements
206
305
  * @param {CdlRenderEnvironment} env
207
306
  * @return {string}
208
307
  */
209
- function renderElementExtensions(elements, env) {
308
+ function renderAnnotateStatementElements(elements, env) {
210
309
  let result = ' {\n';
211
310
  const childEnv = increaseIndent(env);
212
311
  for (const name in elements) {
213
312
  const elem = elements[name];
214
- result += renderAnnotationAssignments(elem, childEnv) + childEnv.indent + quoteIdIfRequired(name);
313
+ result += renderAnnotationAssignmentsAndDocComment(elem, childEnv) + childEnv.indent + quoteIdIfRequired(name);
215
314
  if (elem.elements)
216
- result += renderElementExtensions(elem.elements, childEnv);
315
+ result += renderAnnotateStatementElements(elem.elements, childEnv);
217
316
 
218
317
  result += ';\n';
219
318
  }
@@ -221,6 +320,24 @@ function toCdsSourceCsn(csn, options) {
221
320
  return result;
222
321
  }
223
322
 
323
+ /**
324
+ * Render a parameter list for `annotate` statements, in parentheses `()`.
325
+ *
326
+ * @param {object} params
327
+ * @param {CdlRenderEnvironment} env
328
+ * @return {string}
329
+ */
330
+ function renderAnnotateParamsInParentheses(params, env) {
331
+ const childEnv = increaseIndent(env);
332
+ let result = '(\n';
333
+ const paramAnnotations = [];
334
+ forEach(params, (paramName, param) => {
335
+ paramAnnotations.push( renderAnnotationAssignmentsAndDocComment(param, childEnv) + childEnv.indent + quoteIdIfRequired(paramName) );
336
+ });
337
+ result += `${paramAnnotations.join(',\n')}\n${env.indent})`;
338
+ return result;
339
+ }
340
+
224
341
  /**
225
342
  * Render an artifact. Return the resulting source string.
226
343
  *
@@ -229,29 +346,27 @@ function toCdsSourceCsn(csn, options) {
229
346
  * @param {CdlRenderEnvironment} env
230
347
  */
231
348
  function renderArtifact(artifactName, art, env) {
232
- // FIXME: Correctly build the paths during runtime to give better locations
233
- env.path = [ 'definitions', artifactName ];
349
+ env = envNewPath(env, [ 'definitions', artifactName ]);
234
350
  env.artifactName = artifactName;
235
351
 
236
352
  switch (art.kind) {
237
353
  case 'entity':
238
354
  if (art.query || art.projection)
239
355
  return renderView(artifactName, art, env);
240
-
241
356
  return renderEntity(artifactName, art, env);
242
357
 
243
358
  case 'context':
244
359
  case 'service':
245
- return renderContext(artifactName, art, env);
360
+ return renderContextOrService(artifactName, art, env);
246
361
  case 'type':
247
362
  case 'aspect':
248
- case 'annotation':
249
- return renderTypeOrAnnotation(artifactName, art, env, art.$syntax);
363
+ case 'annotation': // annotation in 'csn.definitions' for compiler v1 compatibility
364
+ return renderTypeOrAnnotation(artifactName, art, env);
250
365
  case 'action':
251
366
  case 'function':
252
367
  return renderActionOrFunction(artifactName, art, env);
253
368
  case 'event':
254
- return renderEventIfCDLMode(artifactName, art, env);
369
+ return renderEvent(artifactName, art, env);
255
370
  default:
256
371
  throw new ModelError(`Unknown artifact kind: ${art.kind}`);
257
372
  }
@@ -262,26 +377,22 @@ function toCdsSourceCsn(csn, options) {
262
377
  * @param {CSN.Artifact} art
263
378
  * @param {CdlRenderEnvironment} env
264
379
  */
265
- function renderEventIfCDLMode(artifactName, art, env) {
266
- let result = renderDocComment(art, env) + renderAnnotationAssignments(art, env);
380
+ function renderEvent(artifactName, art, env) {
381
+ let result = renderAnnotationAssignmentsAndDocComment(art, env);
267
382
  const childEnv = increaseIndent(env);
268
383
  const normalizedArtifactName = renderArtifactName(artifactName);
269
384
  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
- }
385
+ if (art.includes)
386
+ result += renderIncludes(art.includes);
274
387
  if (art.query || art.projection) {
275
- env._artifact = art;
276
388
  result += ' : ';
277
389
  result += renderQuery(getNormalizedQuery(art).query, true, 'projection', env,
278
390
  [ 'definitions', artifactName, 'query' ]);
279
391
  result += ';\n';
280
- delete env._artifact;
281
392
  }
282
393
  else if (art.type) {
283
394
  // Derived type or annotation with non-anonymous type
284
- result += ` : ${renderTypeReference(art, env)};\n`;
395
+ result += ` : ${renderTypeReferenceAndProps(art, env)};\n`;
285
396
  }
286
397
  else if (art.elements) {
287
398
  result += ' {\n';
@@ -293,63 +404,15 @@ function toCdsSourceCsn(csn, options) {
293
404
  return result;
294
405
  }
295
406
 
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
407
  /**
340
- * Render a context or service. Return the resulting source string.
341
- *
342
408
  * @param {string} artifactName
343
409
  * @param {CSN.Artifact} art
344
410
  * @param {CdlRenderEnvironment} env
411
+ * @returns {string}
345
412
  */
346
- function renderContext(artifactName, art, env) {
347
- let result = renderDocComment(art, env) + renderAnnotationAssignments(art, env);
348
- 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
- }
413
+ function renderContextOrService(artifactName, art, env) {
414
+ let result = renderAnnotationAssignmentsAndDocComment(art, env);
415
+ result += `${env.indent}${art.kind} ${renderArtifactName(artifactName)}`;
353
416
  return `${result} {};\n`;
354
417
  }
355
418
 
@@ -362,23 +425,17 @@ function toCdsSourceCsn(csn, options) {
362
425
  * @return {string}
363
426
  */
364
427
  function renderEntity(artifactName, art, env) {
365
- let result = renderDocComment(art, env) + renderAnnotationAssignments(art, env);
428
+ let result = renderAnnotationAssignmentsAndDocComment(art, env);
366
429
  const childEnv = increaseIndent(env);
367
430
  const normalizedArtifactName = renderArtifactName(artifactName);
368
431
  result += `${env.indent + (art.abstract ? 'abstract ' : '')}entity ${normalizedArtifactName}`;
369
432
  const parameters = Object.keys(art.params || []).map(name => renderParameter(name, art.params[name], childEnv)).join(',\n');
370
433
  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
- }
434
+ if (art.includes)
435
+ result += renderIncludes(art.includes);
375
436
  result += ' {\n';
376
437
  for (const name in art.elements) {
377
438
  const element = art.elements[name];
378
- // For subelement annotations, this seems to be a pattern to recognize them
379
- // plus some other stuff unfortunately...
380
- if (element.type && element.elements)
381
- subelementAnnotates.push([ artifactName, element, name ]);
382
439
  result += renderElement(name, element, childEnv);
383
440
  }
384
441
 
@@ -397,36 +454,16 @@ function toCdsSourceCsn(csn, options) {
397
454
  * @param {Boolean} [isSubElement]
398
455
  */
399
456
  function renderElement(elementName, elm, env, isSubElement) {
400
- env.elementName = elementName;
401
- let result = renderDocComment(elm, env) + renderAnnotationAssignments(elm, env);
402
- const quotedElementName = quoteOrUppercaseId(elementName);
403
- result += `${env.indent + (elm.virtual ? 'virtual ' : '') +
404
- (elm.key && !isSubElement ? 'key ' : '') +
405
- ((elm.masked && !elm._ignoreMasked) ? 'masked ' : '') +
406
- quotedElementName} : ${
407
- renderTypeReference(elm, env, undefined)
408
- }${elm.on ? '' : renderNullability(elm)}`;
409
- if (elm.default)
410
- result += ` default ${renderExpr(elm.default, env)}`;
411
-
412
- delete env.elementName;
413
- return `${result};\n`;
414
- }
415
-
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)}`);
457
+ env = envAddPath(env, [ 'elements', elementName ]);
458
+ let result = renderAnnotationAssignmentsAndDocComment(elm, env);
459
+ result += env.indent;
460
+ result += elm.virtual ? 'virtual ' : '';
461
+ result += elm.key && !isSubElement ? 'key ' : '';
462
+ // TODO(v4): Remove once deprecated flag for `masked` is removed.
463
+ result += elm.masked ? 'masked ' : '';
464
+ result += `${quoteIdIfRequired(elementName)} : ${renderTypeReferenceAndProps(elm, env)}`;
428
465
 
429
- return leadingQuerySelect(query.SET.args[0]);
466
+ return `${result};\n`;
430
467
  }
431
468
 
432
469
  /**
@@ -452,9 +489,13 @@ function toCdsSourceCsn(csn, options) {
452
489
 
453
490
  /**
454
491
  * 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.
492
+ * appear in the view's 'elements', not in their 'columns' for client CSN, because the element
493
+ * itself may not even be in 'columns', e.g. if it was expanded from a '*'). Return the
494
+ * resulting rendered 'annotate' statement or an empty string if none required.
495
+ *
496
+ * Note: In the past, we checked if the annotation also exists in the respective column,
497
+ * however, in client CSN, annotations are not part of the column and in parseCdl CSN,
498
+ * no `elements` exist.
458
499
  *
459
500
  * @param {string} artifactName
460
501
  * @param {CSN.Artifact} art
@@ -462,43 +503,88 @@ function toCdsSourceCsn(csn, options) {
462
503
  * @return {string}
463
504
  */
464
505
  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;
506
+ const annotate = collectAnnotationsOfElements(art, { artifactName, path: env.path });
507
+ if (annotate)
508
+ return renderExtensions([ annotate ], env);
509
+ return '';
510
+ }
511
+
512
+ /**
513
+ * Create an "annotate" statement as a CSN extension for all annotations of (sub-)elements.
514
+ * If no annotation was found, we return `null`.
515
+ *
516
+ * @param {CSN.Artifact} artWithElements
517
+ * @param {CdlRenderEnvironment} env
518
+ * @return {CSN.Extension|null}
519
+ */
520
+ function collectAnnotationsOfElements(artWithElements, env) {
521
+ // Array of structures, which may be annotated as well.
522
+ if (!artWithElements.elements && artWithElements.items) {
523
+ env = envAddPath(env, [ 'items' ]);
524
+ artWithElements = artWithElements.items;
475
525
  }
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;
526
+
527
+ const annotate = { annotate: env.path[1] };
528
+
529
+ // Based on the current path, create a correctly nested structure
530
+ // of elements for which we collect annotations.
531
+ // TODO: More properties?
532
+ let obj = annotate;
533
+ for (let i = 2; i < env.path.length; ++i) {
534
+ const key = env.path[i];
535
+ if (key === 'elements' || key === 'actions') {
536
+ obj[key] = Object.create(null);
537
+ const elem = env.path[i + 1];
538
+ obj[key][elem] = {};
539
+ obj = obj[key][elem];
540
+ }
541
+ else if (key === 'returns') {
542
+ obj.returns = {};
543
+ obj = obj.returns;
544
+ }
545
+ else {
546
+ // ignore others, e.g. 'items'
494
547
  }
495
- if (elemAnnotations !== '')
496
- result += `${elemAnnotations}${childEnv.indent}${quoteOrUppercaseId(elemName)};\n`;
497
548
  }
498
- if (result !== '')
499
- result = `${env.indent}annotate ${renderArtifactName(artifactName)} with {\n${result}${env.indent}};\n`;
549
+ return collectAnnos(obj, artWithElements) ? annotate : null;
500
550
 
501
- return result;
551
+ /**
552
+ * Recursive function to collect annotations. `annotateObj` will get an `elements`
553
+ * object with annotations only if there are annotations on `art`'s (sub-)elements.
554
+ *
555
+ * @return {boolean} True, if there were annotations, false otherwise.
556
+ */
557
+ function collectAnnos(annotateObj, art) {
558
+ if (!art.elements)
559
+ return false;
560
+
561
+ const collected = { elements: Object.create(null) };
562
+ let hasAnnotation = false;
563
+
564
+ forEach(art.elements, (elemName, element) => {
565
+ if (!collected.elements[elemName])
566
+ collected.elements[elemName] = { };
567
+
568
+ let hasElementAnnotations = false;
569
+ for (const name in element) {
570
+ if (name.startsWith('@')) {
571
+ collected.elements[elemName][name] = element[name];
572
+ hasElementAnnotations = true;
573
+ hasAnnotation = true;
574
+ }
575
+ }
576
+
577
+ const hasSubAnnotations = collectAnnos(collected.elements[elemName], element);
578
+ if (!hasElementAnnotations && !hasSubAnnotations)
579
+ delete collected.elements[elemName]; // delete if no annotations exist
580
+ hasAnnotation = hasAnnotation || hasSubAnnotations;
581
+ });
582
+
583
+ if (hasAnnotation)
584
+ annotateObj.elements = collected.elements;
585
+
586
+ return hasAnnotation;
587
+ }
502
588
  }
503
589
 
504
590
  /**
@@ -513,25 +599,24 @@ function toCdsSourceCsn(csn, options) {
513
599
  function renderViewSource(source, env) {
514
600
  // Sub-SELECT
515
601
  if (source.SELECT || source.SET) {
516
- let result = `(${renderDocComment(source, env)}${renderQuery(source, false, 'view', increaseIndent(env))})`;
602
+ let result = `(${renderQuery(source, false, 'view', increaseIndent(env))})`;
517
603
  if (source.as)
518
- result += ` as ${quoteOrUppercaseId(source.as)}`;
604
+ result += ` as ${quoteIdIfRequired(source.as)}`;
519
605
 
520
606
  return result;
521
607
  }
522
608
  // JOIN
523
609
  else if (source.join) {
524
610
  // One join operation, possibly with ON-condition
525
- let result = `${renderDocComment(source, env)}${renderViewSource(source.args[0], env)}`;
611
+ let result = `(${renderViewSource(source.args[0], env)}`;
526
612
  for (let i = 1; i < source.args.length; i++) {
527
- result = `(${result} ${source.join} `;
613
+ result += ` ${source.join} `;
528
614
  result += renderJoinCardinality(source.cardinality);
529
615
  result += `join ${renderViewSource(source.args[i], env)}`;
530
616
  if (source.on)
531
617
  result += ` on ${renderExpr(source.on, env, true, true)}`;
532
-
533
- result += ')';
534
618
  }
619
+ result += ')';
535
620
  return result;
536
621
  }
537
622
  // Ordinary path, possibly with an alias
@@ -572,9 +657,8 @@ function toCdsSourceCsn(csn, options) {
572
657
  // Determine the absolute name of the first artifact on the path (before any associations or element traversals)
573
658
  const firstArtifactName = path.ref[0].id || path.ref[0];
574
659
 
575
- let result = renderDocComment(path, env);
576
660
  // Render the first path step (absolute name, with different quoting/naming ..)
577
- result += getResultingName(firstArtifactName);
661
+ let result = renderArtifactName(firstArtifactName);
578
662
 
579
663
  // Even the first step might have parameters and/or a filter
580
664
  if (path.ref[0].args)
@@ -605,11 +689,26 @@ function toCdsSourceCsn(csn, options) {
605
689
  let result = renderAbsolutePath(path, env);
606
690
  if (path.as) {
607
691
  // Source had an alias - render it
608
- result += ` as ${quoteOrUppercaseId(path.as)}`;
692
+ result += ` as ${quoteIdIfRequired(path.as)}`;
609
693
  }
610
694
  return result;
611
695
  }
612
696
 
697
+ /**
698
+ * Render the given columns.
699
+ *
700
+ * @param {any[]} columns
701
+ * @param {object} elements
702
+ * @param {CdlRenderEnvironment} env
703
+ * @return {string}
704
+ */
705
+ function renderViewColumns(columns, env, elements = Object.create(null)) {
706
+ const result = columns.map(col => renderViewColumn(col, env, findElement(elements, col)))
707
+ .filter(s => s !== '')
708
+ .join(',\n');
709
+ return `${result}\n`;
710
+ }
711
+
613
712
  /**
614
713
  * Render a single view or projection column 'col', as it occurs in a select list or
615
714
  * projection list within 'art', possibly with annotations.
@@ -621,7 +720,14 @@ function toCdsSourceCsn(csn, options) {
621
720
  */
622
721
  function renderViewColumn(col, env, element) {
623
722
  // Annotations and column
624
- let result = renderDocComment(col.doc ? col : element, env) + renderAnnotationAssignments(col, env);
723
+ let result = '';
724
+ if (!col.doc) {
725
+ // TODO: In contrast to annotations, we do not render the doc comment as part
726
+ // of an `annotate` statement. That may change in the future.
727
+ result += renderDocComment(element, env);
728
+ }
729
+ // Note: parentheses are a workaround for #9015
730
+ result += renderAnnotationAssignmentsAndDocComment(col, env, { parens: true });
625
731
  result += env.indent;
626
732
 
627
733
  // only if column is virtual, keyword virtual was present in the source text
@@ -632,22 +738,19 @@ function toCdsSourceCsn(csn, options) {
632
738
  // Use special rendering for .expand/.inline - renderExpr cannot easily handle some cases
633
739
  result += key + ((col.expand || col.inline) ? renderInlineExpand(col, env) : renderExpr(col, env, true));
634
740
 
635
- // Alias is already handled by renderInlineExpand
636
- if (!col.inline && !col.expand && col.as)
637
- result += ` as ${quoteOrUppercaseId(col.as)}`;
741
+ // Alias for inline/expand is already handled by renderInlineExpand
742
+ // A new association (cast with `type` and `target`) uses `as` as its primary name, not alias.
743
+ const isNewAssociation = col.cast && col.cast.type && col.cast.target;
744
+ if (col.as && !col.inline && !col.expand && !isNewAssociation)
745
+ result += ` as ${quoteIdIfRequired(col.as)}`;
638
746
 
639
747
  // Explicit type provided for the view element?
640
748
  if (col.cast) {
641
749
  // 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 {
649
- result += ` : ${renderTypeReference(col.cast, env, true)}`;
650
- }
750
+ if (col.cast.target && !col.cast.type)
751
+ result += ` : ${renderRedirectedTo(col.cast, env)}`;
752
+ else
753
+ result += ` : ${renderTypeReferenceAndProps(col.cast, env, { typeRefOnly: true, noAnnoCollect: true })}`;
651
754
  }
652
755
  return result;
653
756
  }
@@ -656,71 +759,58 @@ function toCdsSourceCsn(csn, options) {
656
759
  * For the current column, render a (nested) inline/expand. If the current column
657
760
  * does not have an .expand/.inline, '' is returned
658
761
  *
659
- * @param {object} col Thing with .expand or .inline
660
- * @param {CdlRenderEnvironment} parentEnv
762
+ * @param {object} obj Thing with .expand or .inline
763
+ * @param {CdlRenderEnvironment} env
661
764
  * @returns {string}
662
765
  */
663
- function renderInlineExpand(col, parentEnv) {
664
- if (!col.inline && !col.expand)
665
- return '';
666
-
667
- return renderIX(col, parentEnv);
668
-
669
- function renderIX(obj, env) {
670
- // No expression to render for { * } as alias
671
- let result = (obj.as && obj.expand && !obj.ref) ? '' : renderExpr(obj, env);
672
-
673
- // s as alias { * }
674
- if (obj.as && (obj.ref || obj.xpr || obj.val !== undefined || obj.func !== undefined))
675
- result += ` as ${obj.as}`;
676
-
677
- // We found a leaf - no further drilling
678
- if (!obj.inline && !obj.expand) {
679
- if (obj.cast && obj.cast.type) {
680
- 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
- }
689
- return result;
690
- }
691
-
692
- if (obj.inline)
693
- result += '.{\n';
694
- else
695
- result += result !== '' ? ' {\n' : '{\n';
696
-
697
- // Drill down and render children of the expand/inline
698
- const childEnv = increaseIndent(env);
699
- const expandInline = obj.expand || obj.inline;
700
- expandInline.forEach((elm, i) => {
701
- result += `${childEnv.indent}${renderIX(elm, childEnv)}`;
702
- if (i < expandInline.length - 1)
703
- result += ',\n';
704
- });
705
- result += `\n${env.indent}}`;
766
+ function renderInlineExpand(obj, env) {
767
+ // No expression to render for { * } as alias
768
+ let result = (obj.as && obj.expand && !obj.ref) ? '' : renderExpr(obj, env);
769
+
770
+ // s as alias { * }
771
+ if (obj.as && (obj.ref || obj.xpr || obj.val !== undefined || obj.func !== undefined))
772
+ result += ` as ${obj.as}`;
773
+
774
+ // We found a leaf - no further drilling
775
+ if (!obj.inline && !obj.expand) {
776
+ if (obj.cast && obj.cast.type)
777
+ result += ` : ${renderTypeReferenceAndProps(obj.cast, createEnv(), { noAnnoCollect: true })}`;
778
+ else if (obj.cast && obj.cast.target) // test tbd
779
+ result += ` : ${renderRedirectedTo(obj.cast, env)}`;
780
+ return result;
781
+ }
706
782
 
707
- // Don't forget about the .excluding
708
- if (obj.excluding)
709
- result += ` excluding { ${obj.excluding.join(',')} }`;
783
+ if (obj.inline)
784
+ result += '.{\n';
785
+ else
786
+ result += result !== '' ? ' {\n' : '{\n';
710
787
 
711
- // { * } as expand
712
- if (!obj.ref && obj.as)
713
- result += ` as ${obj.as}`;
788
+ // Drill down and render children of the expand/inline
789
+ const childEnv = increaseIndent(env);
790
+ const expandInline = obj.expand || obj.inline;
791
+ expandInline.forEach((elm, i) => {
792
+ result += `${childEnv.indent}${renderInlineExpand(elm, childEnv)}`;
793
+ if (i < expandInline.length - 1)
794
+ result += ',\n';
795
+ });
796
+ result += `\n${env.indent}}`;
797
+
798
+ // Don't forget about the .excluding
799
+ if (obj.excluding)
800
+ result += ` excluding { ${obj.excluding.join(',')} }`;
801
+
802
+ // { * } as expand
803
+ if (!obj.ref && obj.as)
804
+ result += ` as ${obj.as}`;
714
805
 
715
- return result;
716
- }
806
+ return result;
717
807
  }
718
808
 
719
809
  /**
720
810
  * Render .doc properties as comments in CDL
721
811
  *
722
812
  * @param {object} obj Object to render for
723
- * @param {object} env Env - for indent
813
+ * @param {CdlRenderEnvironment} env
724
814
  * @returns {String}
725
815
  */
726
816
  function renderDocComment(obj, env) {
@@ -729,7 +819,13 @@ function toCdsSourceCsn(csn, options) {
729
819
  else if (obj && obj.doc === null) // empty doc comment needs to be rendered
730
820
  return `\n${env.indent}/** */\n`;
731
821
 
732
- return `\n${env.indent}/**\n${obj.doc.split('\n').map(line => `${env.indent} * ${line}`).join('\n')}\n${env.indent} */\n`;
822
+ // Smaller comment for single-line comments. If the comments starts or ends with whitespace
823
+ // we must use a block comment, or it will be lost when compiling the source again.
824
+ if (!obj.doc.includes('\n') && !/^\s|\s$/.test(obj.doc))
825
+ return `${env.indent}/** ${obj.doc} */\n`;
826
+
827
+ const comment = obj.doc.split('\n').map(line => `${env.indent} * ${line}`).join('\n');
828
+ return `${env.indent}/**\n${comment}\n${env.indent} */\n`;
733
829
  }
734
830
 
735
831
  /**
@@ -743,7 +839,7 @@ function toCdsSourceCsn(csn, options) {
743
839
  */
744
840
  function renderView(artifactName, art, env) {
745
841
  const syntax = (art.projection) ? 'projection' : 'entity';
746
- let result = renderDocComment(art, env) + renderAnnotationAssignments(art, env);
842
+ let result = renderAnnotationAssignmentsAndDocComment(art, env);
747
843
  result += `${env.indent}${art.abstract ? 'abstract ' : ''}${syntax === 'projection' ? 'entity' : syntax} ${renderArtifactName(artifactName)}`;
748
844
  if (art.params) {
749
845
  const childEnv = increaseIndent(env);
@@ -753,7 +849,6 @@ function toCdsSourceCsn(csn, options) {
753
849
  else {
754
850
  result += ' as ';
755
851
  }
756
- env._artifact = art;
757
852
  result += renderQuery(getNormalizedQuery(art).query, true, syntax, env, [ 'definitions', artifactName, 'query' ], art.elements);
758
853
  result += ';\n';
759
854
  result += renderQueryElementAnnotations(artifactName, art, env);
@@ -775,48 +870,28 @@ function toCdsSourceCsn(csn, options) {
775
870
  * @param {object} [elements]
776
871
  */
777
872
  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
873
  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;
874
+ // Set operator, such as UNION, INTERSECT, or EXCEPT...
875
+ return renderQuerySet();
799
876
  }
800
- // Otherwise must have a SELECT
801
877
  else if (!query.SELECT) {
878
+ // ...otherwise must have a SELECT
802
879
  throw new ModelError(`Unexpected query operation ${JSON.stringify(query)}`);
803
880
  }
881
+
882
+ let result = '';
804
883
  const select = query.SELECT;
805
884
  const childEnv = increaseIndent(env);
806
885
 
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}`);
886
+ // If not a projection, must be view/entity.
887
+ result += (syntax === 'projection') ? 'projection on ' : 'select from ';
888
+ result += renderViewSource(select.from, env);
813
889
 
814
890
  if (select.mixin) {
815
891
  let elems = '';
816
- for (const name in select.mixin) {
817
- if (!select.mixin[name]._ignore)
818
- elems += renderElement(name, select.mixin[name], childEnv);
819
- }
892
+ forEach(select.mixin, (name, mixin) => {
893
+ elems += renderElement(name, mixin, childEnv);
894
+ });
820
895
  if (elems) {
821
896
  result += ' mixin {\n';
822
897
  result += elems;
@@ -826,13 +901,12 @@ function toCdsSourceCsn(csn, options) {
826
901
  result += select.distinct ? ' distinct' : '';
827
902
  if (select.columns) {
828
903
  result += ' {\n';
829
- result += `${select.columns.map(col => renderViewColumn(col, childEnv, findElement(elements, col)))
830
- .filter(s => s !== '')
831
- .join(',\n')}\n`;
904
+ result += renderViewColumns(select.columns, increaseIndent(env), elements);
832
905
  result += `${env.indent}}`;
833
906
  }
907
+
834
908
  if (select.excluding) {
835
- result += ` excluding {\n${select.excluding.map(id => `${childEnv.indent}${quoteOrUppercaseId(id)}`).join(',\n')}\n`;
909
+ result += ` excluding {\n${select.excluding.map(id => `${childEnv.indent}${quoteIdIfRequired(id)}`).join(',\n')}\n`;
836
910
  result += `${env.indent}}`;
837
911
  }
838
912
  // FIXME: Currently, only projections can have actions and functions, but we cannot distinguish
@@ -874,7 +948,7 @@ function toCdsSourceCsn(csn, options) {
874
948
  }
875
949
 
876
950
  /**
877
- * Render a query's LIMIT clause, which may have also have OFFSET.
951
+ * Render a query's LIMIT clause, which may also have OFFSET.
878
952
  *
879
953
  * @param {CSN.QueryLimit} limit
880
954
  * @param {CdlRenderEnvironment} limitEnv
@@ -890,6 +964,29 @@ function toCdsSourceCsn(csn, options) {
890
964
 
891
965
  return limitStr;
892
966
  }
967
+
968
+ /**
969
+ * Render UNION, INTERSECT, and EXCEPT, i.e. sets.
970
+ *
971
+ * @return {string}
972
+ */
973
+ function renderQuerySet() {
974
+ const subQueries = query.SET.args.map((arg, i) => {
975
+ // First arg may be leading query
976
+ const subQuery = renderQuery(arg, isLeadingQuery && (i === 0), 'view', env, path.concat([ 'SET', 'args', i ]), elements);
977
+ return `(${subQuery})`;
978
+ });
979
+
980
+ let setResult = subQueries.join(`\n${env.indent}${query.SET.op}${query.SET.all ? ' all' : ''} `);
981
+ // Set operation may also have an ORDER BY and LIMIT/OFFSET (in contrast to the ones belonging to
982
+ // each SELECT)
983
+ if (query.SET.orderBy)
984
+ setResult += `${continueIndent(setResult, env)}order by ${query.SET.orderBy.map(entry => renderOrderByEntry(entry, env)).join(', ')}`;
985
+
986
+ if (query.SET.limit)
987
+ setResult += `${continueIndent(setResult, env)}${renderLimit(query.SET.limit, env)}`;
988
+ return setResult;
989
+ }
893
990
  }
894
991
 
895
992
  /**
@@ -901,7 +998,7 @@ function toCdsSourceCsn(csn, options) {
901
998
  * @return {string}
902
999
  */
903
1000
  function renderOrderByEntry(entry, env) {
904
- let result = renderDocComment(entry, env) + renderExpr(entry, env, true, false, true);
1001
+ let result = renderAnnotationAssignmentsAndDocComment(entry, env) + renderExpr(entry, env, true, false, true);
905
1002
  if (entry.sort)
906
1003
  result += ` ${entry.sort}`;
907
1004
 
@@ -925,7 +1022,7 @@ function toCdsSourceCsn(csn, options) {
925
1022
  let result = '';
926
1023
  const childEnv = increaseIndent(env);
927
1024
  for (const name in art.actions)
928
- result += renderDocComment(art.actions[name], childEnv) + renderActionOrFunction(name, art.actions[name], childEnv);
1025
+ result += renderActionOrFunction(name, art.actions[name], envAddPath(childEnv, [ 'actions', name ]));
929
1026
 
930
1027
  // Even if we have seen actions/functions, they might all have been ignored
931
1028
  if (result !== '')
@@ -943,14 +1040,13 @@ function toCdsSourceCsn(csn, options) {
943
1040
  * @return {string}
944
1041
  */
945
1042
  function renderActionOrFunction(actionName, act, env) {
946
- let result = `${renderDocComment(act, env) + renderAnnotationAssignments(act, env) + env.indent + act.kind} ${renderArtifactName(actionName)}`;
1043
+ let result = `${renderAnnotationAssignmentsAndDocComment(act, env) + env.indent + act.kind} ${renderArtifactName(actionName)}`;
947
1044
  const childEnv = increaseIndent(env);
948
1045
  const parameters = Object.keys(act.params || []).map(name => renderParameter(name, act.params[name], childEnv)).join(',\n');
949
1046
  result += (parameters === '') ? '()' : `(\n${parameters}\n${env.indent})`;
950
1047
  if (act.returns) {
951
- if (act.returns.type && act.returns.elements) // action returns annotations
952
- subelementAnnotates.push([ actionName, act.returns, '', 'returns' ]);
953
- result += ` returns ${renderTypeReference(act.returns, env)}${renderNullability(act.returns)}`;
1048
+ const actEnv = envAddPath(env, [ 'returns' ]);
1049
+ result += ` returns ${renderTypeReferenceAndProps(act.returns, actEnv)}`;
954
1050
  }
955
1051
 
956
1052
  result += ';\n';
@@ -966,11 +1062,9 @@ function toCdsSourceCsn(csn, options) {
966
1062
  * @return {string}
967
1063
  */
968
1064
  function renderParameter(parName, par, env) {
969
- let result = `${renderDocComment(par, env) + renderAnnotationAssignments(par, env) + env.indent + quoteOrUppercaseId(parName)} : ${renderTypeReference(par, env)}`;
970
- if (par.default)
971
- result += ` default ${renderExpr(par.default, env)}`;
972
-
973
- result += renderNullability(par);
1065
+ env = envAddPath(env, [ 'params', parName ]);
1066
+ let result = `${renderAnnotationAssignmentsAndDocComment(par, env)}${env.indent}`;
1067
+ result += `${quoteIdIfRequired(parName)} : ${renderTypeReferenceAndProps(par, env)}`;
974
1068
  return result;
975
1069
  }
976
1070
 
@@ -981,102 +1075,87 @@ function toCdsSourceCsn(csn, options) {
981
1075
  * @param {string} artifactName
982
1076
  * @param {CSN.Artifact} art
983
1077
  * @param {CdlRenderEnvironment} env
984
- * @param {String} artType - used for rendering csn.vocabularies, as the annotations there do not have a kind. Only in toCdl mode
1078
+ * @param {String} [artType] - used for rendering csn.vocabularies, as the annotations there do not have a kind. Only in toCdl mode
985
1079
  * @return {string}
986
1080
  */
987
1081
  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
- }
996
- const childEnv = increaseIndent(env);
997
- if (art.elements && !art.type) {
998
- // Structured type or annotation with anonymous struct type
999
- result += ' {\n';
1000
- for (const name in art.elements)
1001
- result += renderElement(name, art.elements[name], childEnv);
1082
+ let result = renderAnnotationAssignmentsAndDocComment(art, env);
1083
+ result += `${env.indent + (artType || art.$syntax || art.kind )} ${renderArtifactName(artifactName)}`;
1084
+ if (art.includes)
1085
+ result += renderIncludes(art.includes);
1002
1086
 
1003
- result += `${env.indent}};\n`;
1004
- }
1005
- else {
1006
- // Derived type or annotation with non-anonymous type
1007
- result += ` : ${renderTypeReference(art, env, false)}`;
1008
- if (art.default)
1009
- result += ` default ${renderExpr(art.default, env)}`;
1010
- result += ';\n';
1011
- }
1087
+ if (!art.type && art.elements) // For nicer output, no colon if unnamed structure is used.
1088
+ result += ` ${renderTypeReferenceAndProps(art, env)};\n`;
1089
+ else
1090
+ result += ` : ${renderTypeReferenceAndProps(art, env)};\n`;
1012
1091
  return result;
1013
1092
  }
1014
1093
 
1015
1094
  /**
1016
- * Render a reference to a type used by 'elm' (named or inline)
1017
- * Allow suppressing enum-rendering - used in columns for example
1095
+ * Render a reference to a type used by 'artifact' (named or inline) and (element) properties
1096
+ * such as `not null` and `default <xpr>`.
1097
+ * Allow suppressing rendering of structs such as enums - used in columns for example.
1018
1098
  *
1019
- * @param {CSN.Element} elm
1099
+ * @param {object} artifact
1020
1100
  * @param {CdlRenderEnvironment} env
1021
- * @param {boolean} [noEnum=false]
1101
+ * @param {object} [config={}] - `typeRefOnly` Whether to only render type defs, no arrayed/structured/enum.
1102
+ * - `noAnnoCollect` Do not collect annotations of sub-elements.
1022
1103
  * @return {string}
1023
1104
  */
1024
- function renderTypeReference(elm, env, noEnum = false) {
1105
+ function renderTypeReferenceAndProps(artifact, env, config = {}) {
1025
1106
  let result = '';
1026
-
1027
- // Array type: Render items instead
1028
- if (elm.items && !elm.type) {
1029
- // HANA CDS does not support keyword many
1030
- let rc = `many ${renderTypeReference(elm.items, env)}`;
1031
- if (elm.items.notNull != null)
1032
- rc += elm.items.notNull ? ' not null' : ' null';
1033
- // many sub element annotates
1034
- if (elm.items.type && elm.items.elements && env.artifactName)
1035
- subelementAnnotates.push([ env.artifactName, elm.items, env.elementName ]);
1036
-
1037
- return rc;
1107
+ const { typeRefOnly, noAnnoCollect } = config;
1108
+ let isTypeDef = env.path?.length === 2; // e.g [ 'definitions', typeDef ];
1109
+
1110
+ if (typeRefOnly && !artifact.type)
1111
+ throw new ModelError(`Expected artifact to have a type; in: ${env.artifactName}`);
1112
+
1113
+ if (artifact.localized) // works even for type definitions
1114
+ result += 'localized ';
1115
+
1116
+ if (!artifact.type && artifact.items) {
1117
+ result += 'many '; // alternative: 'array of'; but not used
1118
+ artifact = artifact.items;
1119
+ env = envAddPath(env, 'items');
1120
+ // element keywords allowed in MANY case; was an oversight when arrays were introduced.
1121
+ isTypeDef = false;
1122
+ // "many many" does not work in CDL, so we don't check for it.
1038
1123
  }
1039
1124
 
1040
- // FIXME: Is this a type attribute?
1041
- result += (elm.localized ? 'localized ' : '');
1042
-
1043
- // Anonymous structured type
1044
- if (!elm.type) {
1045
- if (!elm.elements)
1046
- throw new ModelError(`Missing type of: ${JSON.stringify(elm)}`);
1047
-
1125
+ if (!artifact.type && artifact.elements) {
1048
1126
  result += '{\n';
1049
- const childEnv = increaseIndent(env);
1050
- for (const name in elm.elements)
1051
- result += renderElement(name, elm.elements[name], childEnv, null);
1127
+ const childEnv = envAddPath(increaseIndent(env), 'items');
1128
+ for (const name in artifact.elements)
1129
+ result += renderElement(name, artifact.elements[name], childEnv, null);
1052
1130
 
1053
1131
  result += `${env.indent}}`;
1132
+ if (!isTypeDef)
1133
+ result += renderNullability(artifact);
1134
+ // structured default not possible at the moment
1054
1135
  return result;
1055
1136
  }
1056
1137
 
1057
- const comp = 'cds.Composition';
1058
1138
  // Association type
1059
- if ([ 'cds.Association', comp ].includes(elm.type)) {
1139
+ if (artifact.type === 'cds.Association' || artifact.type === 'cds.Composition') {
1140
+ const isComp = artifact.type === 'cds.Composition';
1060
1141
  // Type, cardinality and target; CAPire uses CamelCase
1061
- result += (elm.type === comp) ? 'Composition' : 'Association';
1062
-
1063
- if (isSimpleCardinality(elm.cardinality)) {
1064
- result += renderSimpleCardinality(elm);
1065
- }
1066
- else {
1067
- result += renderCardinality(elm.cardinality) +
1068
- ((elm.type === comp) ? ' of ' : ' to ');
1069
- }
1070
-
1071
- // normal target or named aspect
1072
- if (elm.target || elm.targetAspect && typeof elm.targetAspect === 'string') {
1073
- result += renderAbsolutePath({ ref: [ elm.target || elm.targetAspect ] }, env);
1142
+ result += isComp ? 'Composition' : 'Association';
1143
+ result += renderCardinality(artifact);
1144
+
1145
+ // `targetAspect` may be set by the core compiler and refers to the original named or unnamed aspect.
1146
+ // In parseCdl, `target` may still be an object containing elements. This would be replaced
1147
+ // by targetAspect in client CSN, but we can't rely on that.
1148
+ // If a name exists (either in target or targetAspect), prefer it over rendering elements.
1149
+ const elements = artifact.target && artifact.target.elements || artifact.targetAspect && artifact.targetAspect.elements;
1150
+ if (typeof artifact.target === 'string' || typeof artifact.targetAspect === 'string') {
1151
+ result += renderAbsolutePath({ ref: [ artifact.target || artifact.targetAspect ] }, env);
1074
1152
  }
1075
- else if (elm.targetAspect && elm.targetAspect.elements) { // anonymous aspect
1153
+ else if (elements) {
1154
+ // anonymous aspect, either parseCdl or client CSN.
1076
1155
  const childEnv = increaseIndent(env);
1077
1156
  result += '{\n';
1078
- for (const name in elm.targetAspect.elements)
1079
- result += renderElement(name, elm.targetAspect.elements[name], childEnv);
1157
+ for (const name in elements)
1158
+ result += renderElement(name, elements[name], childEnv);
1080
1159
 
1081
1160
  result += `${env.indent}}`;
1082
1161
  }
@@ -1084,65 +1163,94 @@ function toCdsSourceCsn(csn, options) {
1084
1163
  throw new ModelError('Association/Composition is missing its target! Throwing exception to trigger recompilation.');
1085
1164
  }
1086
1165
 
1087
-
1088
1166
  // ON-condition (if any)
1089
- if (elm.on)
1090
- result += ` on ${renderExpr(elm.on, env, true, true)}`;
1167
+ if (artifact.on)
1168
+ result += ` on ${renderExpr(artifact.on, env, true, true)}`;
1091
1169
 
1092
1170
 
1093
1171
  // Foreign keys (if any, unless we also have an ON_condition (which means we have been transformed from managed to unmanaged)
1094
- if (elm.keys && !elm.on)
1095
- result += ` { ${Object.keys(elm.keys).map(name => renderForeignKey(elm.keys[name], env)).join(', ')} }`;
1172
+ if (artifact.keys && !artifact.on)
1173
+ result += ` { ${Object.keys(artifact.keys).map(name => renderForeignKey(artifact.keys[name], env)).join(', ')} }`;
1174
+
1175
+ if (!isTypeDef && !artifact.on) // unmanaged associations can't be followed by "not null"
1176
+ result += renderNullability(artifact);
1177
+ // DEFAULT not possible here.
1096
1178
 
1097
1179
  return result;
1098
1180
  }
1099
1181
 
1100
- // Reference to another element
1101
- if (elm.type.ref) {
1102
- if (elm.enum) {
1103
- const source = artifactRef(elm.type);
1104
- if (!source.enum) {
1105
- // enum was defined at this element and not at the referenced one
1106
- result += renderAbsolutePath(elm.type, env) + renderEnum(elm.enum, env);
1107
- }
1108
- else {
1109
- result += renderAbsolutePath(elm.type, env);
1110
- }
1111
- }
1112
- else {
1113
- result += renderAbsolutePath(elm.type, env);
1114
- }
1115
- return result;
1182
+ // At this point, we will render a named type.
1183
+
1184
+ // If we have a type and elements, we may have sub-structure annotates that would
1185
+ // get lost if we only render the type name.
1186
+ // TODO: Can we annotate elements of targetAspect?
1187
+ // If so, move this block before the composition rendering.
1188
+ if (!noAnnoCollect && (artifact.elements || artifact.items?.elements)) {
1189
+ const annotate = collectAnnotationsOfElements(artifact, env);
1190
+ if (annotate)
1191
+ subelementAnnotates.push(annotate);
1116
1192
  }
1117
1193
 
1118
- // If we get here, it must be a named type
1119
- if (isBuiltinType(elm.type)) {
1120
- result += renderBuiltinType(elm);
1194
+ // Reference to another artifact
1195
+ if (typeof artifact.type === 'string') {
1196
+ // If we get here, it must be a named type
1197
+ result += renderNamedTypeWithParameters(artifact);
1121
1198
  }
1122
- else {
1123
- // Simple absolute name
1124
- // Type names are never flattened (derived types are unraveled in HANA)
1125
- result += getResultingName(elm.type);
1199
+ else if (artifact.type?.ref) {
1200
+ result += renderAbsolutePath(artifact.type, env);
1126
1201
  }
1127
- if (elm.enum && !noEnum)
1128
- result += renderEnum(elm.enum, env);
1202
+
1203
+ if (artifact.enum && !typeRefOnly)
1204
+ result += renderEnum(artifact.enum, env);
1205
+ if (!isTypeDef) // NOT NULL not possible for not-arrayed type definitions
1206
+ result += renderNullability(artifact);
1207
+ if (artifact.default)
1208
+ result += ` default ${renderExpr(artifact.default, env)}`;
1129
1209
 
1130
1210
  return result;
1131
1211
  }
1132
1212
 
1133
1213
  /**
1134
- * @param {CSN.Element} elm
1214
+ * Render REDIRECTED TO with its keys/on condition for the given artifact.
1215
+ *
1216
+ * @param {object} art
1217
+ * @param {CdlRenderEnvironment} env
1135
1218
  * @return {string}
1136
1219
  */
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);
1220
+ function renderRedirectedTo(art, env) {
1221
+ let result = `redirected to ${quotePathIfRequired(art.target)}`;
1222
+ if (art.on)
1223
+ result += ` on ${renderExpr(art.on, env, true, true)}`;
1224
+ else if (art.keys)
1225
+ result += ` { ${Object.keys(art.keys).map(name => renderForeignKey(art.keys[name], env)).join(', ')} }`;
1226
+ return result;
1227
+ }
1142
1228
 
1143
- return elm.type.slice(4) + renderTypeParameters(elm);
1229
+ /**
1230
+ * Render the named type with optional parameters, e.g. `MyString(length: 10)`.
1231
+ * @param {CSN.Artifact} artWithType
1232
+ * @return {string}
1233
+ */
1234
+ function renderNamedTypeWithParameters(artWithType) {
1235
+ let result = '';
1236
+
1237
+ if (isBuiltinType(artWithType.type)) {
1238
+ // If there is a user-defined type with the same short name (cds.Integer -> Integer),
1239
+ // we render the full name, including the leading "cds."
1240
+ if (csn.definitions[artWithType.type.slice(4)])
1241
+ result += artWithType.type;
1242
+ else
1243
+ result += artWithType.type.slice(4);
1244
+ }
1245
+ else {
1246
+ result += renderArtifactName(artWithType.type);
1247
+ }
1248
+
1249
+ result += renderTypeParameters(artWithType);
1250
+ return result;
1144
1251
  }
1145
1252
 
1253
+
1146
1254
  /**
1147
1255
  * Render the 'enum { ... } part of a type declaration
1148
1256
  *
@@ -1153,21 +1261,32 @@ function toCdsSourceCsn(csn, options) {
1153
1261
  function renderEnum(enumPart, env) {
1154
1262
  let result = ' enum {\n';
1155
1263
  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
- }
1264
+ for (const name in enumPart)
1265
+ result += renderEnumElement(name, enumPart[name], childEnv);
1167
1266
  result += `${env.indent}}`;
1168
1267
  return result;
1169
1268
  }
1170
1269
 
1270
+ /**
1271
+ * Render the element of a `<type> enum {}` structure.
1272
+ *
1273
+ * @param {string} name
1274
+ * @param {CSN.EnumValue} enumValue
1275
+ * @param {CdlRenderEnvironment} env
1276
+ * @return {string}
1277
+ */
1278
+ function renderEnumElement(name, enumValue, env) {
1279
+ let result = '';
1280
+ result += renderAnnotationAssignmentsAndDocComment(enumValue, env);
1281
+ result += env.indent + quoteIdIfRequired(name);
1282
+ if (enumValue.val !== undefined)
1283
+ result += ` = ${renderExpr(enumValue, env)}`;
1284
+ else if (enumValue['#'] !== undefined)
1285
+ result += ` = #${enumValue['#']}`;
1286
+ result += ';\n';
1287
+ return result;
1288
+ }
1289
+
1171
1290
  /**
1172
1291
  * Render an annotation value (somewhat like a simplified expression, with slightly different
1173
1292
  * representation)
@@ -1177,23 +1296,35 @@ function toCdsSourceCsn(csn, options) {
1177
1296
  */
1178
1297
  function renderAnnotationValue(x, env) {
1179
1298
  if (Array.isArray(x)) {
1180
- // Render array parts as values
1181
- return `[${x.map(item => renderAnnotationValue(item, env)).join(', ')}]`;
1299
+ // Render array parts as values. Spaces required if last array value is
1300
+ // a delimited identifier.
1301
+ return `[ ${x.map(item => renderAnnotationValue(item, env)).join(', ')} ]`;
1182
1302
  }
1183
1303
  else if (typeof x === 'object' && x !== null) {
1184
1304
  // Enum symbol
1185
- if (x['#'])
1305
+ if (x['#']) {
1186
1306
  return `#${x['#']}`;
1187
-
1307
+ }
1188
1308
  // Shorthand for absolute path (as string)
1189
- else if (x['='])
1190
- return quotePathString(x['=']);
1191
-
1192
- // Struct value (can actually only occur within an array)
1309
+ else if (x['=']) {
1310
+ return quotePathIfRequired(x['=']);
1311
+ }
1312
+ // Shorthand for ellipsis: `... up to <val>`
1313
+ else if (x['...']) {
1314
+ if (x['...'] === true)
1315
+ return '...';
1316
+ return `... up to ${renderAnnotationValue(x['...'], env)}`;
1317
+ }
1193
1318
 
1194
- // Note that we have to quote the struct keys here manually and not use quoteIdIfRequired, because they may even contain dots (yuc!)
1195
- // FIXME: Should that really be allowed?
1196
- return `{${Object.keys(x).map(key => `![${key}]: ${renderAnnotationValue(x[key], env)}`).join(', ')}}`;
1319
+ // Struct value (can currently only occur within an array)
1320
+ // Render as one-liner if there is at most one key. Render as multi-line
1321
+ // struct if there are more and use nicer indentation.
1322
+ const keys = Object.keys(x);
1323
+ const childEnv = keys.length <= 1 ? env : increaseIndent(env);
1324
+ const values = keys.map(key => `${quoteAnnotationPathIfRequired(key)}: ${renderAnnotationValue(x[key], childEnv)}`);
1325
+ if (values.length <= 1)
1326
+ return `{ ${values.join(', ')} }`;
1327
+ return `{\n${childEnv.indent}${values.join(`,\n${childEnv.indent}`)}\n${env.indent}}`;
1197
1328
  }
1198
1329
  // Null
1199
1330
  else if (x === null) {
@@ -1206,172 +1337,54 @@ function toCdsSourceCsn(csn, options) {
1206
1337
  }
1207
1338
 
1208
1339
  /**
1209
- * Render an expression (including paths and values) or condition 'x'.
1210
- * (no trailing LF, don't indent if inline)
1340
+ * Render a single path step 's' at path position 'idx', which can have filters or parameters or be a function
1211
1341
  *
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.
1342
+ * @param {string|object} s
1343
+ * @param {number} idx
1344
+ * @returns {string}
1218
1345
  */
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
- }
1229
-
1230
- // Not a literal value but part of an operator, function etc - just leave as it is
1231
- return x;
1346
+ function renderPathStep(s, idx, inline, env) {
1347
+ // Simple id or absolute name
1348
+ if (typeof s === 'string') {
1349
+ // In first path position, do not quote $projection and magic $-variables like CURRENT_DATE, $now etc.
1350
+ // FIXME: We should rather explicitly recognize quoting somehow
1232
1351
 
1352
+ if (idx === 0 &&
1353
+ s.startsWith('$'))
1354
+ return s;
1233
1355
 
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';
1261
-
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
- }
1356
+ return quoteIdIfRequired(s);
1313
1357
  }
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);
1358
+ // ID with filters or parameters
1359
+ else if (typeof s === 'object') {
1360
+ // Sanity check
1361
+ if (!s.func && !s.id)
1362
+ throw new ModelError(`Unknown path step object: ${JSON.stringify(s)}`);
1363
+
1364
+ // Not really a path step but an object-like function call
1365
+ if (s.func)
1366
+ return `${s.func}(${renderArgs(s, '=>', env)})`;
1367
+
1368
+ // Path step, possibly with view parameters and/or filters
1369
+ let result = `${quoteIdIfRequired(s.id)}`;
1370
+ if (s.args) {
1371
+ // View parameters
1372
+ result += `(${renderArgs(s, ':', env)})`;
1343
1373
  }
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;
1374
+ if (s.where) {
1375
+ // Filter, possibly with cardinality
1376
+ result += `[${s.cardinality ? (`${s.cardinality.max}: `) : ''}${renderExpr(s.where, env, inline, true)}]`;
1366
1377
  }
1367
1378
 
1368
- throw new ModelError(`Unknown path step: ${JSON.stringify(s)}`);
1379
+ return result;
1369
1380
  }
1381
+
1382
+ throw new ModelError(`Unknown path step: ${JSON.stringify(s)}`);
1370
1383
  }
1371
1384
 
1372
1385
  /**
1373
1386
  * Render function arguments or view parameters (positional if array, named if object/dict),
1374
- * using 'sep' as separator for positional parameters
1387
+ * using 'sep' as separator for named parameters
1375
1388
  *
1376
1389
  * @param {object} node with `args` to render
1377
1390
  * @param {string} sep
@@ -1379,28 +1392,65 @@ function toCdsSourceCsn(csn, options) {
1379
1392
  * @returns {string}
1380
1393
  */
1381
1394
  function renderArgs(node, sep, env) {
1382
- const args = node.args ? node.args : {};
1395
+ const args = node.args || [];
1396
+
1383
1397
  // Positional arguments
1384
- if (Array.isArray(args))
1385
- return args.map(arg => renderExpr(arg, env, true, false, true)).join(', ');
1398
+ if (Array.isArray(args)) {
1399
+ const func = node.func?.toUpperCase();
1400
+ if (func)
1401
+ return args.map((arg, i) => renderArgument(arg, env, getKeywordsForSpecialFunctionArgument(func, i))).join(', ');
1386
1402
 
1387
- // Named arguments (object/dict)
1388
- else if (typeof args === 'object')
1389
- return Object.keys(args).map(key => `${quoteOrUppercaseId(key)} ${sep} ${renderExpr(args[key], env, true, false, true)}`).join(', ');
1403
+ return args.map(arg => renderArgument(arg, env)).join(', ');
1404
+ }
1390
1405
 
1406
+ // Named arguments (object/dict)
1407
+ else if (typeof args === 'object') {
1408
+ return Object.keys(args).map(key => `${quoteIdIfRequired(key)} ${sep} ${renderArgument(args[key], env)}`).join(', ');
1409
+ }
1391
1410
 
1392
1411
  throw new ModelError(`Unknown args: ${JSON.stringify(args)}`);
1393
1412
  }
1394
1413
 
1414
+ /**
1415
+ * Render a function argument, e.g. for generic functions or CAST().
1416
+ * Ensures that parentheses are used if necessary, e.g. for `someFct( (1=1), (1=1) )`.
1417
+ *
1418
+ * @param {any} arg
1419
+ * @param {CdlRenderEnvironment} env
1420
+ * @param {string[]} additionalAllowedKeywords
1421
+ * @return {string}
1422
+ */
1423
+ function renderArgument(arg, env, additionalAllowedKeywords = []) {
1424
+ // If the argument is a xpr with e.g. `=`, it may require parentheses.
1425
+ // For nested xpr, `renderExpr()` will already add parentheses.
1426
+ return renderExpr(arg, env, true, !isSimpleFunctionExpression(arg && arg.xpr, additionalAllowedKeywords), true);
1427
+ }
1428
+
1429
+ /**
1430
+ * Render an artifact's cardinality.
1431
+ *
1432
+ * @param artifact
1433
+ * @returns {string}
1434
+ */
1435
+ function renderCardinality(artifact) {
1436
+ if (isSimpleCardinality(artifact.cardinality))
1437
+ return renderSimpleCardinality(artifact);
1438
+ return renderBracketCardinality(artifact);
1439
+ }
1440
+
1395
1441
  /**
1396
1442
  * Render a cardinality (only those parts that were actually provided)
1397
1443
  *
1398
- * @param {CSN.Cardinality} card
1444
+ * @param {CSN.Artifact} art
1399
1445
  * @return {string}
1400
1446
  */
1401
- function renderCardinality(card) {
1447
+ function renderBracketCardinality(art) {
1448
+ const isComp = art.type === 'cds.Composition';
1449
+ const suffix = (isComp ? ' of ' : ' to ');
1450
+ const card = art.cardinality;
1451
+
1402
1452
  if (!card)
1403
- return '';
1453
+ return suffix;
1404
1454
 
1405
1455
  let result = '[';
1406
1456
  if (card.src !== undefined)
@@ -1412,7 +1462,7 @@ function toCdsSourceCsn(csn, options) {
1412
1462
  if (card.max !== undefined)
1413
1463
  result += card.max;
1414
1464
 
1415
- return `${result}]`;
1465
+ return `${result}]${suffix}`;
1416
1466
  }
1417
1467
 
1418
1468
  /**
@@ -1470,28 +1520,29 @@ function toCdsSourceCsn(csn, options) {
1470
1520
  }
1471
1521
 
1472
1522
  /**
1473
- * Render (primitive) type parameters of element 'elm', i.e.
1523
+ * Render (primitive) type parameters of artifact 'artWithType', i.e.
1474
1524
  * length, precision and scale (even if incomplete), plus any other unknown ones.
1475
1525
  *
1476
- * @param {CSN.Element} elm
1526
+ * @param {CSN.Artifact} artWithType
1477
1527
  * @returns {string}
1478
1528
  */
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);
1484
-
1485
- if (elm.precision !== undefined)
1486
- params.push(elm.precision);
1529
+ function renderTypeParameters(artWithType) {
1530
+ const params = typeParameters.list.filter(param => artWithType[param] !== undefined);
1531
+ if (params.length === 0)
1532
+ return '';
1487
1533
 
1488
- if (elm.scale !== undefined)
1489
- params.push(elm.scale);
1534
+ // Special cases for 1 or 2 arguments.
1535
+ if (params.length === 1 && artWithType.length !== undefined)
1536
+ return `(${artWithType.length})`;
1537
+ if (params.length === 2 && artWithType.precision !== undefined && artWithType.scale !== undefined)
1538
+ return `(${artWithType.precision}, ${artWithType.scale})`;
1490
1539
 
1491
- if (elm.srid !== undefined)
1492
- params.push(elm.srid);
1540
+ // Render named params
1541
+ const renderedParams = [];
1542
+ for (const param of params)
1543
+ renderedParams.push(`${param}: ${artWithType[param]}`);
1493
1544
 
1494
- return params.length === 0 ? '' : `(${params.join(', ')})`;
1545
+ return `(${renderedParams.join(', ')})`;
1495
1546
  }
1496
1547
 
1497
1548
  /**
@@ -1499,13 +1550,14 @@ function toCdsSourceCsn(csn, options) {
1499
1550
  *
1500
1551
  * @param {object} obj Object that has annotations
1501
1552
  * @param {CdlRenderEnvironment} env
1553
+ * @param {{parens: boolean}} [config] Config for renderAnnotationAssignment()
1502
1554
  * @return {string}
1503
1555
  */
1504
- function renderAnnotationAssignments(obj, env) {
1505
- let result = '';
1556
+ function renderAnnotationAssignmentsAndDocComment(obj, env, config) {
1557
+ let result = renderDocComment(obj, env);
1506
1558
  for (const name in obj) {
1507
1559
  if (name.startsWith('@'))
1508
- result += renderAnnotationAssignment(obj[name], name, env);
1560
+ result += renderAnnotationAssignment(obj[name], name, env, config);
1509
1561
  }
1510
1562
  return result;
1511
1563
  }
@@ -1518,208 +1570,324 @@ function toCdsSourceCsn(csn, options) {
1518
1570
  * @param {any} anno Annotation value
1519
1571
  * @param {string} name Annotation name, e.g. `@A.B.C#foo.C`
1520
1572
  * @param {CdlRenderEnvironment} env
1573
+ * @param {object} [config] parens: Whether the annotation assignment must be surrounded by parentheses.
1521
1574
  * @return {string} Rendered annotation, possibly quoted: `@![A.B.C#foo.C]: value`
1522
1575
  */
1523
- function renderAnnotationAssignment(anno, name, env) {
1576
+ function renderAnnotationAssignment(anno, name, env, config = { parens: false }) {
1524
1577
  name = name.substring(1);
1525
1578
  // Take the annotation assignment apart into <nameBeforeVariant>#<variantAndRest>
1526
1579
  const parts = name.split('#');
1527
1580
  const nameBeforeVariant = parts[0];
1528
1581
  const variant = parts[1];
1582
+ const { parens } = config;
1529
1583
 
1530
1584
  // Identifier according to our grammar: /[$_a-zA-Z][$_a-zA-Z0-9]*/
1531
1585
  // We expand this pattern to also include dots after the first character.
1532
1586
  // If the annotation does not follow this pattern `ident(.@ident)*`, it must be quoted:
1533
1587
  // `@identifier@identifier` must be quoted but `@identifier.@identifier` should not.
1588
+ // TODO: Use quoteAnnotationPathIfRequired()
1534
1589
  const annoRequiresQuoting = !/^[$_a-zA-Z][$_a-zA-Z0-9.]*(?:\.@[$_a-zA-Z][$_a-zA-Z0-9.]*)*$/.test(nameBeforeVariant);
1535
1590
  // Unfortunately, the compiler does not allow `.` after the first variant identifier,
1536
1591
  // even though that is the result after flattening.
1537
1592
  const variantRequiresQuoting = variant && !/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(variant);
1538
1593
 
1539
- let result;
1594
+ let result = `${env.indent}@`;
1595
+ if (parens)
1596
+ result += '(';
1597
+
1540
1598
  if (annoRequiresQuoting || variantRequiresQuoting)
1541
- result = `${env.indent}@${quote(name)}`;
1599
+ result += quote(name);
1542
1600
  else
1543
- result = `${env.indent}@${name}`;
1601
+ result += name;
1544
1602
 
1545
1603
  result += ` : ${renderAnnotationValue(anno, env)}`;
1604
+ if (parens)
1605
+ result += ')';
1546
1606
  return `${result}\n`;
1547
1607
  }
1548
1608
 
1549
1609
  /**
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.
1610
+ * Render the name of an artifact, quote path steps if necessary.
1564
1611
  *
1565
- * @return {CdlRenderEnvironment}
1612
+ * @param {string} artifactName Artifact name to render
1613
+ * @return {string} Artifact name ready for rendering
1566
1614
  */
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
- };
1615
+ function renderArtifactName(artifactName) {
1616
+ return quotePathIfRequired(artifactName);
1578
1617
  }
1579
1618
 
1580
1619
  /**
1581
- * Returns a copy of 'env' with increased indentation (and resetted name prefix)
1620
+ * Render a function expression.
1582
1621
  *
1622
+ * @param {object} obj Object with .func and optionally .args
1583
1623
  * @param {CdlRenderEnvironment} env
1584
- * @returns {CdlRenderEnvironment}
1624
+ * @returns {string}
1585
1625
  */
1586
- function increaseIndent(env) {
1587
- return Object.assign({}, env, { namePrefix: '', indent: `${env.indent} ` });
1626
+ function renderFuncExpr( obj, env ) {
1627
+ if (keywords.cdl_functions.includes(obj.func.toUpperCase()))
1628
+ return obj.func;
1629
+ const name = identifierRegex.test(obj.func) ? obj.func : quote(obj.func);
1630
+ return `${name}(${renderArgs( obj, '=>', env )})`;
1588
1631
  }
1589
1632
 
1590
1633
  /**
1591
- * Return a path string 'path' with appropriate "-quotes.
1634
+ * Render an expression.
1592
1635
  *
1593
- * @param {string} path
1636
+ * @param {any} expr
1637
+ * @param {CdlRenderEnvironment} exprEnv
1638
+ * @param {boolean} [isInline]
1639
+ * @param {boolean} [isNestedExpr]
1640
+ * @param {boolean} [alwaysRenderCast]
1594
1641
  * @returns {string}
1595
1642
  */
1596
- function quotePathString(path) {
1597
- // "foo"."bar"."wiz"."blub"
1598
- return path.split('.').map(quoteIdIfRequired).join('.');
1643
+ function renderExpr(expr, exprEnv, isInline, isNestedExpr, alwaysRenderCast) {
1644
+ if (!_renderExpr) {
1645
+ _renderExpr = getExpressionRenderer({
1646
+ finalize(x) {
1647
+ return x;
1648
+ },
1649
+ explicitTypeCast(x, env) {
1650
+ const typeRef = renderTypeReferenceAndProps(x.cast, env, { typeRefOnly: true, noAnnoCollect: true });
1651
+ const arg = { ...x, cast: null }; // "arg" without cast to avoid recursion.
1652
+ return `cast(${renderArgument(arg, env)} as ${typeRef})`;
1653
+ },
1654
+ val(x, env) {
1655
+ // Literal value, possibly with explicit 'literal' property
1656
+ switch (x.literal || typeof x.val) {
1657
+ case 'number':
1658
+ case 'boolean':
1659
+ case 'null':
1660
+ return x.val;
1661
+ case 'x':
1662
+ case 'date':
1663
+ case 'time':
1664
+ case 'timestamp':
1665
+ return `${x.literal}'${x.val}'`;
1666
+ case 'string':
1667
+ return renderString(x.val, env);
1668
+ case 'object':
1669
+ if (x.val === null)
1670
+ return 'null';
1671
+ // otherwise fall through to
1672
+ default:
1673
+ throw new ModelError(`Unknown literal or type: ${JSON.stringify(x)}`);
1674
+ }
1675
+ },
1676
+ aliasOnly(x, _env) {
1677
+ return x.as;
1678
+ },
1679
+ enum(x) {
1680
+ return `#${x['#']}`;
1681
+ },
1682
+ ref(x, env) {
1683
+ const { inline } = this;
1684
+ return `${(x.param || x.global) ? ':' : ''}${x.ref.map((step, index) => renderPathStep(step, index, inline, env)).join('.')}`;
1685
+ },
1686
+ windowFunction(x, env) {
1687
+ const funcDef = renderFuncExpr(x, env);
1688
+ const windowFunctionOperator = x.xpr.shift(); // OVER ...
1689
+ return `${funcDef} ${windowFunctionOperator} ( ${renderExpr(x.xpr, env, true)} )`;
1690
+ },
1691
+ func(x, env) {
1692
+ return renderFuncExpr(x, env);
1693
+ },
1694
+ xpr(x, env) {
1695
+ if (this.nestedExpr && !x.cast || x.xpr.some(s => s === 'exists'))
1696
+ return `(${renderExpr(x.xpr, env, this.inline, true)})`;
1697
+ return renderExpr(x.xpr, env, this.inline, true);
1698
+ },
1699
+ // Sub-queries in expressions need to be in parentheses, otherwise
1700
+ // left-associativity of UNIONS may result in different results.
1701
+ // For example: `select from E where id in (select from E union select from E);`:
1702
+ // Without parentheses, it would be different query.
1703
+ SET(x, env) {
1704
+ return `(${renderQuery(x, false, 'view', increaseIndent(env))})`;
1705
+ },
1706
+ SELECT(x, env) {
1707
+ return `(${renderQuery(x, false, 'view', increaseIndent(env))})`;
1708
+ },
1709
+ });
1710
+ }
1711
+
1712
+ return _renderExpr(expr, exprEnv, isInline, isNestedExpr, alwaysRenderCast);
1599
1713
  }
1714
+ }
1600
1715
 
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);
1716
+ /**
1717
+ * Returns a newly created default environment (which keeps track of indentation, required USING
1718
+ * declarations and name prefixes.
1719
+ *
1720
+ * @return {CdlRenderEnvironment}
1721
+ */
1722
+ function createEnv() {
1723
+ return {
1724
+ // Current indentation string
1725
+ indent: '',
1726
+ path: null,
1727
+ artifactName: '',
1728
+ elementName: '',
1729
+ };
1730
+ }
1611
1731
 
1612
- return id;
1613
- }
1732
+ function envAddPath(env, path) {
1733
+ return Object.assign({}, env, { path: [ ...env.path, ...path ] } );
1734
+ }
1735
+ function envNewPath(env, path) {
1736
+ return Object.assign({}, env, { path: [ ...path ] } );
1737
+ }
1614
1738
 
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
- }
1739
+ /**
1740
+ * Returns a copy of 'env' with increased indentation (and reset name prefix)
1741
+ *
1742
+ * @param {CdlRenderEnvironment} env
1743
+ * @returns {CdlRenderEnvironment}
1744
+ */
1745
+ function increaseIndent(env) {
1746
+ return Object.assign({}, env, { indent: `${env.indent} ` });
1747
+ }
1624
1748
 
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
- }
1749
+ /**
1750
+ * Quote the path steps with `![]` if necessary. For simple ids such as
1751
+ * `elem` use `quoteIdIfRequired` instead.
1752
+ *
1753
+ * @param {string} path
1754
+ * @returns {string}
1755
+ *
1756
+ * @todo For paths such as `E.key`, `key` does not have to be in quotes.
1757
+ */
1758
+ function quotePathIfRequired(path) {
1759
+ return path.split('.').map(quoteIdIfRequired).join('.');
1760
+ }
1644
1761
 
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
- }
1762
+ /**
1763
+ * Quote the id with `![]` if necessary. For paths such as `E.key` use
1764
+ * `quotePathIfRequired` instead.
1765
+ *
1766
+ * @param {string} id
1767
+ * @return {string}
1768
+ */
1769
+ function quoteIdIfRequired(id) {
1770
+ // Quote if required for CDL
1771
+ if (requiresQuotingForCdl(id))
1772
+ return quote(id);
1655
1773
 
1656
- /**
1657
- * Render the name of an artifact, using the current name prefix from 'env'
1658
- * and the real name of the artifact. In case of plain names, this
1659
- * is equivalent to simply flattening and uppercasing the whole name.
1660
- *
1661
- * In cdlMode, the prefix is extended to handle cases like an entity shadowing the prefix
1662
- * of another entity -> Service.E and Service.E.Sub
1663
- *
1664
- * To handle such cases for hdbcds in quoted/hdbcds, we:
1665
- * - Find the part of the name that is no longer prefix (context/service)
1666
- * - For Service.E -> E, for Service.E.Sub -> E.Sub
1667
- * - Replace all dots in this "real name" with underscores
1668
- * - Join with the env prefix
1669
- *
1670
- *
1671
- * @param {string} artifactName Artifact name to render
1672
- * @return {string} Artifact name ready for rendering
1673
- */
1674
- function renderArtifactName(artifactName) {
1675
- const realname = getRealName(artifactName);
1676
- const prefix = (realname !== artifactName) ? artifactName.slice(0, artifactName.length - realname.length - 1) : '';
1677
- return prefix ? `${quoteIdIfRequired(prefix)}.${realname.split('.').map(quoteIdIfRequired).join('.')}` : realname.split('.').map(quoteIdIfRequired).join('.');
1678
- }
1774
+ return id;
1775
+ }
1679
1776
 
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
- }
1777
+ /**
1778
+ * Quote an annotation path, e.g. `@My.@Anno.Description` if necessary.
1779
+ * `anno` can start with `@` but is not required to be.
1780
+ * Example of an annotation path that needs to be quoted:
1781
+ * `@![ spaces in path ].@!["double quotes"]`.
1782
+ *
1783
+ * @param {string} anno
1784
+ * @returns {string}
1785
+ */
1786
+ function quoteAnnotationPathIfRequired(anno) {
1787
+ return anno.split('.').map((segment) => {
1788
+ if (segment.startsWith('@'))
1789
+ return `@${quoteIdIfRequired(segment.slice(1))}`;
1790
+ return quoteIdIfRequired(segment);
1791
+ }).join('.');
1792
+ }
1690
1793
 
1691
- /**
1692
- * Get the part that is really the name of this artifact and not just prefix caused by a context/service
1693
- *
1694
- * @param {String} artifactName Artifact name to use
1695
- * @returns {String} non-prefix part of the artifact name
1696
- */
1697
- function getRealName(artifactName) {
1698
- const parts = artifactName.split('.');
1699
- // Length of 1 -> There can be no prefix
1700
- if (parts.length === 1)
1701
- return artifactName;
1794
+ /**
1795
+ * Quotes the identifier using CDL-style ![]-quotes.
1796
+ *
1797
+ * @param id
1798
+ * @returns {string}
1799
+ */
1800
+ function quote(id) {
1801
+ return `![${id.replace(/]/g, ']]')}]`;
1802
+ }
1702
1803
 
1804
+ /**
1805
+ * Returns true if 'id' requires quotes for CDL, i.e. if 'id'
1806
+ * does not match the first part of the `Identifier` rule of `language.g4`
1807
+ * or if 'id' is a reserved keyword.
1808
+ *
1809
+ * @param {string} id
1810
+ * @return {boolean}
1811
+ */
1812
+ function requiresQuotingForCdl(id) {
1813
+ return !identifierRegex.test(id) ||
1814
+ keywords.cdl.includes(id.toUpperCase()) ||
1815
+ keywords.cdl_functions.includes(id.toUpperCase());
1816
+ }
1703
1817
 
1704
- let seen = '';
1705
- for (let i = 0; i < parts.length; i++) {
1706
- if (seen !== '')
1707
- seen = `${seen}.${parts[i]}`;
1708
- else
1709
- seen = parts[i];
1818
+ const functionExpressionOperatorsRequireParentheses = [
1819
+ // Antlr rule 'condition', 'conditionAnd'
1820
+ 'and', 'or',
1710
1821
 
1822
+ // Antlr rule 'conditionTerm'
1823
+ '=', '<>', '>', '>=', '<', '<=', '!=',
1824
+ // These are not forbidden, since they must be preceded by one of the comparators above.
1825
+ // 'any', 'some', 'all',
1711
1826
 
1712
- const art = csn.definitions[seen];
1713
- if (!art || (art.kind !== 'service' && art.kind !== 'context')) {
1714
- // We found a case where the prefix ended
1715
- // Return everything following
1716
- return parts.slice(i).join('.');
1717
- }
1718
- }
1827
+ 'is', 'in', 'not', 'null', 'exists',
1828
+ // Antlr rule 'predicate'
1829
+ 'between', 'like', 'escape',
1830
+ ];
1719
1831
 
1720
- // we seem to have a normal case - just return the last part
1721
- return getLastPartOf(artifactName);
1722
- }
1832
+ /**
1833
+ * Returns true if the given xpr-array can be rendered without parentheses
1834
+ * in a `fct(<xpr>)` expression such as `cast(<xpr> as Type)`. We only need to
1835
+ * look at the first nesting level. Otherwise, `renderExpr()` will already add parentheses.
1836
+ *
1837
+ * The list of `functionExpressionOperatorsRequireParentheses` was created by looking at
1838
+ * the `expression` Antlr rule.
1839
+ * Because of token-rewrites, there are functions that allow operators/tokens that would
1840
+ * require parentheses in other functions. For example *regex functions allow `IN` but
1841
+ * if `IN` is used in other functions, it requires parentheses. To allow for that case,
1842
+ * you can set `additionalAllowedKeywords` to list of tokens that are allowed.
1843
+ *
1844
+ * Note that this is more of a heuristic for "nicer" CDL output. For example the
1845
+ * following snippet is parsable without parentheses:
1846
+ * `cast( case when int > 1 then int else 0 end as Integer ),`
1847
+ * However, because it is a flat xpr-array, we see `>` and assume that it is not
1848
+ * a simple expression.
1849
+ *
1850
+ * @param {any[]} xpr
1851
+ * @param {string[]} additionalAllowedKeywords
1852
+ * @return {boolean}
1853
+ */
1854
+ function isSimpleFunctionExpression(xpr, additionalAllowedKeywords = []) {
1855
+ return !xpr || xpr.every(val => typeof val !== 'string' ||
1856
+ (additionalAllowedKeywords.includes(val) ||
1857
+ !functionExpressionOperatorsRequireParentheses.includes(val.toLowerCase())));
1858
+ }
1859
+
1860
+ /**
1861
+ * Special functions may have special parser rules, such as SAP HANA RegEx functions.
1862
+ * They allow certain keywords in their arguments.
1863
+ *
1864
+ * This function is used to determine if arguments need to be put in parentheses or not.
1865
+ * See {@link isSimpleFunctionExpression}.
1866
+ *
1867
+ * @param {string} funcName
1868
+ * @param {number} argumentIndex
1869
+ * @returns {string[]}
1870
+ */
1871
+ function getKeywordsForSpecialFunctionArgument(funcName, argumentIndex) {
1872
+ const f = specialFunctions[funcName] && specialFunctions[funcName][argumentIndex];
1873
+ if (!f)
1874
+ return [];
1875
+ const additionalKeywords = [];
1876
+ if (f.intro)
1877
+ additionalKeywords.push(...f.intro);
1878
+ if (f.expr)
1879
+ additionalKeywords.push(...f.expr);
1880
+ if (f.separator)
1881
+ additionalKeywords.push(...f.separator);
1882
+ return additionalKeywords;
1883
+ }
1884
+
1885
+ /**
1886
+ * @param {string[]} includes
1887
+ * @return {string}
1888
+ */
1889
+ function renderIncludes(includes) {
1890
+ return ` : ${includes.map(name => quotePathIfRequired(name)).join(', ')}`;
1723
1891
  }
1724
1892
 
1725
1893
  /**
@@ -1800,17 +1968,9 @@ function isSimpleString(str) {
1800
1968
  * @typedef CdlRenderEnvironment Rendering environment used throughout the render process.
1801
1969
  *
1802
1970
  * @property {string} indent Current indentation as a string, e.g. ' ' for two spaces.
1803
- * @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
1806
- * @property {{[name: string]: {
1807
- quotedName: string,
1808
- quotedAlias: string
1809
- }}} topLevelAliases Dictionary of aliases for used artifact names
1810
- *
1811
- * @property {string} namePrefix Current name prefix (including trailing dot if not empty)
1812
- * @property {boolean} [skipKeys]
1813
- * @property {CSN.Artifact} [_artifact]
1971
+ * @property {string[]} [path] CSN path to the current artifact
1972
+ * @property {string} [artifactName] Name of the artifact - set in renderArtifact
1973
+ * @property {string} [elementName] Name of the element being rendered - set in renderElement
1814
1974
  */
1815
1975
 
1816
- module.exports = { toCdsSourceCsn };
1976
+ module.exports = { csnToCdl };