@sap/cds-compiler 3.6.2 → 3.8.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 (89) hide show
  1. package/CHANGELOG.md +109 -1
  2. package/README.md +3 -0
  3. package/bin/cdsc.js +12 -5
  4. package/doc/CHANGELOG_ARCHIVE.md +6 -6
  5. package/doc/CHANGELOG_BETA.md +35 -2
  6. package/doc/CHANGELOG_DEPRECATED.md +2 -2
  7. package/doc/DeprecatedOptions_v2.md +1 -1
  8. package/doc/NameResolution.md +1 -1
  9. package/lib/api/main.js +63 -23
  10. package/lib/api/options.js +1 -0
  11. package/lib/api/validate.js +5 -0
  12. package/lib/base/dictionaries.js +15 -3
  13. package/lib/base/keywords.js +2 -0
  14. package/lib/base/message-registry.js +120 -34
  15. package/lib/base/messages.js +51 -27
  16. package/lib/base/model.js +4 -2
  17. package/lib/base/shuffle.js +2 -1
  18. package/lib/checks/arrayOfs.js +1 -1
  19. package/lib/checks/defaultValues.js +1 -1
  20. package/lib/checks/elements.js +29 -1
  21. package/lib/checks/{emptyOrOnlyVirtual.js → hasPersistedElements.js} +10 -6
  22. package/lib/checks/invalidTarget.js +1 -1
  23. package/lib/checks/nonexpandableStructured.js +1 -1
  24. package/lib/checks/onConditions.js +15 -9
  25. package/lib/checks/sql-snippets.js +2 -2
  26. package/lib/checks/types.js +5 -1
  27. package/lib/checks/validator.js +7 -3
  28. package/lib/compiler/assert-consistency.js +42 -26
  29. package/lib/compiler/base.js +50 -4
  30. package/lib/compiler/builtins.js +17 -8
  31. package/lib/compiler/checks.js +241 -246
  32. package/lib/compiler/define.js +113 -146
  33. package/lib/compiler/extend.js +889 -383
  34. package/lib/compiler/finalize-parse-cdl.js +5 -58
  35. package/lib/compiler/index.js +1 -1
  36. package/lib/compiler/kick-start.js +7 -8
  37. package/lib/compiler/populate.js +297 -293
  38. package/lib/compiler/propagator.js +27 -18
  39. package/lib/compiler/resolve.js +146 -463
  40. package/lib/compiler/shared.js +36 -79
  41. package/lib/compiler/tweak-assocs.js +30 -28
  42. package/lib/compiler/utils.js +31 -5
  43. package/lib/edm/annotations/genericTranslation.js +131 -59
  44. package/lib/edm/annotations/preprocessAnnotations.js +3 -0
  45. package/lib/edm/csn2edm.js +22 -5
  46. package/lib/edm/edm.js +6 -4
  47. package/lib/edm/edmAnnoPreprocessor.js +1 -0
  48. package/lib/edm/edmPreprocessor.js +42 -26
  49. package/lib/gen/Dictionary.json +38 -2
  50. package/lib/gen/language.checksum +1 -1
  51. package/lib/gen/language.interp +3 -1
  52. package/lib/gen/languageLexer.js +1 -1
  53. package/lib/gen/languageParser.js +4828 -4472
  54. package/lib/inspect/inspectPropagation.js +20 -34
  55. package/lib/json/from-csn.js +140 -44
  56. package/lib/json/to-csn.js +114 -122
  57. package/lib/language/errorStrategy.js +2 -0
  58. package/lib/language/genericAntlrParser.js +156 -36
  59. package/lib/language/language.g4 +100 -58
  60. package/lib/language/textUtils.js +13 -0
  61. package/lib/main.d.ts +43 -3
  62. package/lib/main.js +4 -2
  63. package/lib/model/csnRefs.js +15 -3
  64. package/lib/model/csnUtils.js +12 -74
  65. package/lib/model/revealInternalProperties.js +4 -2
  66. package/lib/modelCompare/compare.js +2 -1
  67. package/lib/optionProcessor.js +3 -0
  68. package/lib/render/manageConstraints.js +5 -2
  69. package/lib/render/toCdl.js +216 -104
  70. package/lib/render/toHdbcds.js +2 -9
  71. package/lib/render/toRename.js +14 -51
  72. package/lib/render/toSql.js +4 -3
  73. package/lib/render/utils/common.js +9 -5
  74. package/lib/transform/braceExpression.js +6 -0
  75. package/lib/transform/db/assertUnique.js +2 -1
  76. package/lib/transform/db/expansion.js +2 -0
  77. package/lib/transform/db/flattening.js +37 -36
  78. package/lib/transform/db/rewriteCalculatedElements.js +600 -0
  79. package/lib/transform/db/transformExists.js +4 -0
  80. package/lib/transform/db/views.js +40 -37
  81. package/lib/transform/forOdataNew.js +20 -15
  82. package/lib/transform/forRelationalDB.js +58 -41
  83. package/lib/transform/odata/typesExposure.js +50 -15
  84. package/lib/transform/parseExpr.js +16 -8
  85. package/lib/transform/transformUtilsNew.js +42 -14
  86. package/lib/transform/translateAssocsToJoins.js +60 -37
  87. package/lib/transform/universalCsn/coreComputed.js +15 -7
  88. package/lib/transform/universalCsn/universalCsnEnricher.js +4 -4
  89. package/package.json +2 -1
@@ -9,6 +9,7 @@ const { forEachDefinition, normalizeTypeRef } = require('../model/csnUtils');
9
9
  const enrichUniversalCsn = require('../transform/universalCsn/universalCsnEnricher');
10
10
  const { isBetaEnabled } = require('../base/model');
11
11
  const { ModelError } = require('../base/error');
12
+ const { makeMessageFunction } = require('../base/messages.js');
12
13
  const { typeParameters, specialFunctions } = require('../compiler/builtins');
13
14
  const { forEach } = require('../utils/objectUtils');
14
15
  const {
@@ -34,6 +35,8 @@ function csnToCdl( csn, options ) {
34
35
  const special$self = !csn?.definitions?.$self && '$self';
35
36
  timetrace.start('CDL rendering');
36
37
 
38
+ const msg = makeMessageFunction(csn, options, 'to.cdl');
39
+
37
40
  if (options.csnFlavor === 'universal' && isBetaEnabled(options, 'enableUniversalCsn')) {
38
41
  // Since the expander modifies the CSN, we need to clone it first or
39
42
  // toCdl can't guarantee that the input CSN is not modified.
@@ -214,7 +217,6 @@ function csnToCdl( csn, options ) {
214
217
 
215
218
  // Not part of if/else cascade, because it may be in postfix notation.
216
219
  if (ext.actions) {
217
- // TODO: Merge with renderActionsAndFunctions() -> requires removal of static `with actions`
218
220
  const childEnv = increaseIndent(env);
219
221
  let actions = '';
220
222
  forEach(ext.actions, (actionName, action) => {
@@ -260,8 +262,6 @@ function csnToCdl( csn, options ) {
260
262
  if (ext.enum)
261
263
  return 'enum ';
262
264
  if (ext.elements) { // enum/elements ambiguity -> look into elements
263
- // TODO: Check for `type` as well once this is supported by the parser (and identified as elements):
264
- // `extend E with { a = 'string'; b: String }`
265
265
  const isLikelyElement = Object.keys(ext.elements)
266
266
  .find(name => ext.elements[name].value !== undefined);
267
267
  if (isLikelyElement)
@@ -326,7 +326,7 @@ function csnToCdl( csn, options ) {
326
326
  const childEnv = increaseIndent(env);
327
327
  for (const name in ext.actions) {
328
328
  const action = ext.actions[name];
329
- result += renderAnnotationAssignmentsAndDocComment(action, childEnv) + childEnv.indent + quoteIdIfRequired(name);
329
+ result += renderAnnotationAssignmentsAndDocComment(action, childEnv) + childEnv.indent + quoteNonIdentifierOrKeyword(name);
330
330
  // Action parameter annotations
331
331
  if (action.params)
332
332
  result += renderAnnotateParamsInParentheses(action.params, childEnv);
@@ -361,7 +361,7 @@ function csnToCdl( csn, options ) {
361
361
  for (const name in elements) {
362
362
  const elem = elements[name];
363
363
  result += renderAnnotationAssignmentsAndDocComment(elem, childEnv);
364
- result += childEnv.indent + quoteIdIfRequired(name);
364
+ result += childEnv.indent + quoteNonIdentifierOrKeyword(name);
365
365
  if (elem.elements)
366
366
  result += renderAnnotateStatementElements(elem.elements, childEnv);
367
367
  if (elem.enum)
@@ -385,7 +385,7 @@ function csnToCdl( csn, options ) {
385
385
  let result = '(\n';
386
386
  const paramAnnotations = [];
387
387
  forEach(params, (paramName, param) => {
388
- paramAnnotations.push( renderAnnotationAssignmentsAndDocComment(param, childEnv) + childEnv.indent + quoteIdIfRequired(paramName) );
388
+ paramAnnotations.push( renderAnnotationAssignmentsAndDocComment(param, childEnv) + childEnv.indent + quoteNonIdentifierOrKeyword(paramName) );
389
389
  });
390
390
  result += `${paramAnnotations.join(',\n')}\n${env.indent})`;
391
391
  return result;
@@ -541,37 +541,32 @@ function csnToCdl( csn, options ) {
541
541
  * Returns the resulting source string.
542
542
  *
543
543
  * @param {string} elementName
544
- * @param {CSN.Element|CSN.Enum} elm
544
+ * @param {CSN.Element|CSN.Enum} element
545
545
  * @param {CdlRenderEnvironment} env
546
546
  */
547
- function renderElement( elementName, elm, env ) {
547
+ function renderElement( elementName, element, env ) {
548
548
  env = envAddPath(env, [ 'elements', elementName ]);
549
- let result = renderAnnotationAssignmentsAndDocComment(elm, env);
549
+ let result = renderAnnotationAssignmentsAndDocComment(element, env);
550
550
  result += env.indent;
551
- result += elm.virtual ? 'virtual ' : '';
552
- result += elm.key ? 'key ' : '';
551
+ result += element.virtual ? 'virtual ' : '';
552
+ result += element.key ? 'key ' : '';
553
553
  // TODO(v4): Remove once deprecated flag for `masked` is removed.
554
- result += elm.masked ? 'masked ' : '';
555
- result += quoteIdIfRequired(elementName);
556
- if (elm.val !== undefined) { // enum value
557
- result += ` = ${exprRenderer.renderExpr(elm, env)}`;
554
+ result += element.masked ? 'masked ' : '';
555
+ result += quoteNonIdentifierOrKeyword(elementName);
556
+ if (element.val !== undefined) { // enum value
557
+ result += ` = ${exprRenderer.renderExpr(element, env)}`;
558
558
  }
559
- else if (elm['#'] !== undefined) { // enum symbol reference
560
- result += ` = #${elm['#']}`;
559
+ else if (element['#'] !== undefined) { // enum symbol reference
560
+ result += ` = #${element['#']}`;
561
561
  }
562
562
  else {
563
- // TODO: Maybe push the .value check into renderTypeReferenceAndProps?
564
- const type = normalizeTypeRef(elm.items?.type || elm.type);
565
- const isAssoc = type === 'cds.Association' || type === 'cds.Composition';
566
- if (!isAssoc || elm.value === undefined) {
567
- const props = renderTypeReferenceAndProps(elm, env);
568
- if (props !== '')
569
- result += ` : ${props}`;
570
- }
563
+ const props = renderTypeReferenceAndProps(element, env);
564
+ if (props !== '')
565
+ result += ` : ${props}`;
571
566
  }
572
567
 
573
- if (elm.value !== undefined) { // calculated element // @ts-ignore
574
- result += ` = ${exprRenderer.renderExpr(elm.value, env)}`;
568
+ if (element.value !== undefined) { // calculated element // @ts-ignore
569
+ result += ` = ${exprRenderer.renderExpr(element.value, env)}`;
575
570
  }
576
571
 
577
572
  return `${result};\n`;
@@ -697,7 +692,8 @@ function csnToCdl( csn, options ) {
697
692
  function renderViewSource( source, env ) {
698
693
  // Sub-SELECT
699
694
  if (source.SELECT || source.SET) {
700
- let result = `(${renderQuery(source, false, 'view', increaseIndent(env))})`;
695
+ const subEnv = increaseIndent(env);
696
+ let result = `(\n${subEnv.indent}${renderQuery(source, false, 'view', subEnv)}\n${env.indent})`;
701
697
  if (source.as)
702
698
  result += renderAlias(source.as);
703
699
 
@@ -762,9 +758,13 @@ function csnToCdl( csn, options ) {
762
758
  result += `(${renderArguments(path.ref[0], ':', env)})`;
763
759
 
764
760
  if (path.ref[0].where) {
761
+ // TODO: Unify with other filter rendering
765
762
  const cardinality = path.ref[0].cardinality ? (`${path.ref[0].cardinality.max}: `) : '';
766
763
  const expr = exprRenderer.renderExpr(path.ref[0].where, env);
767
- result += `[${cardinality}${expr}]`;
764
+ if (expr.endsWith(']')) // for cases such as [… ![id] ]
765
+ result += `[ ${cardinality}${expr} ]`;
766
+ else
767
+ result += `[${cardinality}${expr}]`;
768
768
  }
769
769
 
770
770
  // Add any path steps (possibly with parameters and filters) that may follow after that
@@ -827,7 +827,7 @@ function csnToCdl( csn, options ) {
827
827
  result += renderDocComment(element, env);
828
828
  }
829
829
  // Note: parentheses are a workaround for #9015
830
- result += renderAnnotationAssignmentsAndDocComment(col, env, { parens: true });
830
+ result += renderAnnotationAssignmentsAndDocComment(col, env, { parentheses: true });
831
831
  result += env.indent;
832
832
 
833
833
  // only if column is virtual, keyword virtual was present in the source text
@@ -1004,7 +1004,8 @@ function csnToCdl( csn, options ) {
1004
1004
  }
1005
1005
 
1006
1006
  if (select.excluding) {
1007
- result += ` excluding {\n${select.excluding.map(id => `${childEnv.indent}${quoteIdIfRequired(id)}`).join(',\n')}\n`;
1007
+ const excludes = select.excluding.map(id => `${childEnv.indent}${quoteNonIdentifierOrKeyword(id)}`).join(',\n');
1008
+ result += ` excluding {\n${excludes}\n`;
1008
1009
  result += `${env.indent}}`;
1009
1010
  }
1010
1011
 
@@ -1178,7 +1179,7 @@ function csnToCdl( csn, options ) {
1178
1179
  function renderParameter( parName, par, env ) {
1179
1180
  env = envAddPath(env, [ 'params', parName ]);
1180
1181
  let result = `${renderAnnotationAssignmentsAndDocComment(par, env)}${env.indent}`;
1181
- result += `${quoteIdIfRequired(parName)} : ${renderTypeReferenceAndProps(par, env)}`;
1182
+ result += `${quoteNonIdentifierOrKeyword(parName)} : ${renderTypeReferenceAndProps(par, env)}`;
1182
1183
  return result;
1183
1184
  }
1184
1185
 
@@ -1211,7 +1212,7 @@ function csnToCdl( csn, options ) {
1211
1212
  * such as `not null` and `default <xpr>`.
1212
1213
  * Allow suppressing rendering of structs such as enums - used in columns for example.
1213
1214
  *
1214
- * @param {object} artifact
1215
+ * @param {CSN.Artifact} artifact
1215
1216
  * @param {CdlRenderEnvironment} env
1216
1217
  * @param {object} [config={}] - `typeRefOnly` Whether to only render type defs, no arrayed/structured/enum.
1217
1218
  * - `noAnnoCollect` Do not collect annotations of sub-elements.
@@ -1220,7 +1221,6 @@ function csnToCdl( csn, options ) {
1220
1221
  function renderTypeReferenceAndProps( artifact, env, config = {} ) {
1221
1222
  let result = '';
1222
1223
  const { typeRefOnly, noAnnoCollect } = config;
1223
- let isTypeDef = env.path?.length === 2; // e.g [ 'definitions', typeDef ];
1224
1224
 
1225
1225
  if (typeRefOnly && !artifact.type)
1226
1226
  throw new ModelError(`Expected artifact to have a type; in: ${env.artifactName}`);
@@ -1229,20 +1229,16 @@ function csnToCdl( csn, options ) {
1229
1229
  result += 'localized ';
1230
1230
 
1231
1231
  if (!artifact.type && artifact.items) {
1232
+ checkArrayedArtifact(artifact, env);
1232
1233
  result += 'many '; // alternative: 'array of'; but not used
1233
- artifact = artifact.items;
1234
- env = envAddPath(env, 'items');
1235
- // element keywords allowed in MANY case; was an oversight when arrays were introduced.
1236
- isTypeDef = false;
1237
- // "many many" does not work in CDL, so we don't check for it.
1234
+ ({ art: artifact, env } = checkInnerMostArray(artifact, env));
1238
1235
  }
1239
1236
 
1240
1237
  const type = normalizeTypeRef(artifact.type);
1241
1238
 
1242
1239
  if (!type && artifact.elements) {
1243
1240
  result += renderElements(artifact, env);
1244
- if (!isTypeDef)
1245
- result += renderNullability(artifact);
1241
+ result += renderNullability(artifact);
1246
1242
  // structured default not possible at the moment
1247
1243
  return result;
1248
1244
  }
@@ -1278,7 +1274,7 @@ function csnToCdl( csn, options ) {
1278
1274
  if (artifact.keys && !artifact.on)
1279
1275
  result += ` { ${Object.keys(artifact.keys).map(name => renderForeignKey(artifact.keys[name], env)).join(', ')} }`;
1280
1276
 
1281
- if (!isTypeDef && !artifact.on) // unmanaged associations can't be followed by "not null"
1277
+ if (artifact.notNull !== undefined && !artifact.on) // unmanaged associations can't be followed by "not null"
1282
1278
  result += renderNullability(artifact);
1283
1279
  // DEFAULT not possible here.
1284
1280
 
@@ -1291,8 +1287,6 @@ function csnToCdl( csn, options ) {
1291
1287
  // get lost if we only render the type name.
1292
1288
  // We only extract annotations of enums, if "typeRefOnly" is true. Otherwise, since
1293
1289
  // the full enum is rendered below, we would have unnecessary annotations.
1294
- // TODO: Can we annotate elements of targetAspect?
1295
- // If so, move this block before the composition rendering.
1296
1290
  if (!noAnnoCollect && (!artifact.enum || typeRefOnly)) {
1297
1291
  const annotate = collectAnnotationsOfElementsAndEnum(artifact, env);
1298
1292
  if (annotate)
@@ -1310,9 +1304,9 @@ function csnToCdl( csn, options ) {
1310
1304
 
1311
1305
  if (artifact.enum && !typeRefOnly)
1312
1306
  result += renderEnum(artifact.enum, env);
1313
- if (!isTypeDef) // NOT NULL not possible for not-arrayed type definitions
1307
+ if (artifact.notNull !== undefined)
1314
1308
  result += renderNullability(artifact);
1315
- if (artifact.default)
1309
+ if (artifact.default !== undefined)
1316
1310
  result += ` default ${exprRenderer.renderExpr(artifact.default, env)}`;
1317
1311
 
1318
1312
  return result;
@@ -1380,53 +1374,94 @@ function csnToCdl( csn, options ) {
1380
1374
  }
1381
1375
 
1382
1376
  /**
1383
- * Render an annotation value (somewhat like a simplified expression, with slightly different
1384
- * representation)
1377
+ * Render an annotation value, which is either
1378
+ * - a normal expressions
1379
+ * - a somewhat simplified expression, with slightly different representation
1385
1380
  *
1386
- * @param {any} x
1381
+ * @param {any} annoValue
1387
1382
  * @param {CdlRenderEnvironment} env
1388
1383
  */
1389
- function renderAnnotationValue( x, env ) {
1390
- if (Array.isArray(x)) {
1391
- // Render array parts as values. Spaces required if last array value is
1392
- // a delimited identifier.
1393
- return `[ ${x.map(item => renderAnnotationValue(item, env)).join(', ')} ]`;
1384
+ function renderAnnotationValue( annoValue, env ) {
1385
+ // TODO: There must be at least one known expression property, otherwise
1386
+ // it could be `type: 'unchecked'`.
1387
+ const isXpr = annoValue?.['='] !== undefined && (Object.keys(annoValue).length > 1) &&
1388
+ isBetaEnabled(options, 'annotationExpressions');
1389
+ if (isXpr) {
1390
+ // Once inside an expression, we stay there.
1391
+ const xpr = exprRenderer.renderExpr(annoValue, env);
1392
+ return `( ${xpr} )`;
1394
1393
  }
1395
- else if (typeof x === 'object' && x !== null) {
1394
+ else if (Array.isArray(annoValue)) {
1395
+ return renderAnnotationArrayValue( annoValue, env );
1396
+ }
1397
+ else if (typeof annoValue === 'object' && annoValue !== null) {
1396
1398
  // Enum symbol
1397
- if (x['#']) {
1398
- return `#${x['#']}`;
1399
+ if (annoValue['#']) {
1400
+ return `#${annoValue['#']}`;
1399
1401
  }
1400
1402
  // Shorthand for absolute path (as string)
1401
- else if (x['=']) {
1402
- return quotePathIfRequired(x['=']);
1403
+ else if (annoValue['=']) {
1404
+ if (annoValue['='].startsWith('@'))
1405
+ return quoteAnnotationPathIfRequired(annoValue['=']);
1406
+ return quotePathIfRequired(annoValue['=']);
1403
1407
  }
1404
1408
  // Shorthand for ellipsis: `... up to <val>`
1405
- else if (x['...']) {
1406
- if (x['...'] === true)
1409
+ else if (annoValue['...']) {
1410
+ if (annoValue['...'] === true)
1407
1411
  return '...';
1408
- return `... up to ${renderAnnotationValue(x['...'], env)}`;
1412
+ return `... up to ${renderAnnotationValue(annoValue['...'], env)}`;
1409
1413
  }
1410
1414
 
1411
1415
  // Struct value (can currently only occur within an array)
1412
1416
  // Render as one-liner if there is at most one key. Render as multi-line
1413
1417
  // struct if there are more and use nicer indentation.
1414
- const keys = Object.keys(x);
1418
+ const keys = Object.keys(annoValue);
1415
1419
  const childEnv = keys.length <= 1 ? env : increaseIndent(env);
1416
- const values = keys.map(key => `${quoteAnnotationPathIfRequired(key)}: ${renderAnnotationValue(x[key], childEnv)}`);
1420
+ const values = keys.map(key => `${quoteAnnotationPathIfRequired(key)}: ${renderAnnotationValue(annoValue[key], childEnv)}`);
1417
1421
  if (values.length <= 1)
1418
1422
  return `{ ${values.join(', ')} }`;
1419
1423
  const valueList = values.join(`,\n${childEnv.indent}`);
1420
1424
  return `{\n${childEnv.indent}${valueList}\n${env.indent}}`;
1421
1425
  }
1422
1426
  // Null
1423
- else if (x === null) {
1427
+ else if (annoValue === null) {
1424
1428
  return 'null';
1425
1429
  }
1426
1430
  // Primitive: string, number, boolean
1427
1431
 
1428
1432
  // Quote strings, leave all others as they are
1429
- return (typeof x === 'string') ? renderString(x, env) : x;
1433
+ return (typeof annoValue === 'string') ? renderString(annoValue, env) : String(annoValue);
1434
+ }
1435
+
1436
+ /**
1437
+ * Renders an array annotation value. Uses a heuristic to put each element on its own line
1438
+ * if a single-line becomes longer than 100 characters or if any sub-expression already
1439
+ * contains a line break. The latter checks makes nested arrays with structures more
1440
+ * readable.
1441
+ *
1442
+ * @param {any[]} annoValue
1443
+ * @param {CdlRenderEnvironment} env
1444
+ * @return {string}
1445
+ */
1446
+ function renderAnnotationArrayValue( annoValue, env ) {
1447
+ const childEnv = increaseIndent(env);
1448
+ // Render array parts as values.
1449
+ let length = 0;
1450
+ let hasLineBreak = false;
1451
+ const items = annoValue.map((item) => {
1452
+ const result = renderAnnotationValue(item, childEnv);
1453
+ length += result.length + 2; // just a heuristic; add 2 for `, `.
1454
+ if (!hasLineBreak && result.includes('\n'))
1455
+ hasLineBreak = true;
1456
+ return result;
1457
+ });
1458
+
1459
+ if (!hasLineBreak && (length + env.indent.length) < 100) {
1460
+ // Spaces required if last array value is a delimited identifier.
1461
+ return `[ ${items.join(', ')} ]`;
1462
+ }
1463
+ const renderedItems = items.join(`,\n${childEnv.indent}`);
1464
+ return `[\n${childEnv.indent}${renderedItems}\n${env.indent}]`;
1430
1465
  }
1431
1466
 
1432
1467
  /**
@@ -1444,7 +1479,7 @@ function csnToCdl( csn, options ) {
1444
1479
  // FIXME: We should rather explicitly recognize quoting somehow
1445
1480
  if (idx === 0 && s.startsWith('$'))
1446
1481
  return s;
1447
- return quoteIdIfRequired(s, env.additionalKeywords);
1482
+ return quoteNonIdentifierOrKeyword(s, env.additionalKeywords);
1448
1483
  }
1449
1484
  // ID with filters or parameters
1450
1485
  else if (typeof s === 'object') {
@@ -1457,7 +1492,7 @@ function csnToCdl( csn, options ) {
1457
1492
  return `${s.func}(${renderArguments(s, '=>', env)})`;
1458
1493
 
1459
1494
  // Path step, possibly with view parameters and/or filters
1460
- let result = `${quoteIdIfRequired(s.id, env.additionalKeywords)}`;
1495
+ let result = `${quoteNonIdentifierOrKeyword(s.id, env.additionalKeywords)}`;
1461
1496
  if (s.args) {
1462
1497
  // View parameters
1463
1498
  result += `(${renderArguments(s, ':', env)})`;
@@ -1466,7 +1501,10 @@ function csnToCdl( csn, options ) {
1466
1501
  // Filter, possibly with cardinality
1467
1502
  const cardinality = s.cardinality ? (`${s.cardinality.max}: `) : '';
1468
1503
  const expr = exprRenderer.renderExpr(s.where, env);
1469
- result += `[${cardinality}${expr}]`;
1504
+ if (expr.endsWith(']')) // for cases such as [… ![id] ]
1505
+ result += `[ ${cardinality}${expr} ]`;
1506
+ else
1507
+ result += `[${cardinality}${expr}]`;
1470
1508
  }
1471
1509
 
1472
1510
  return result;
@@ -1505,7 +1543,7 @@ function csnToCdl( csn, options ) {
1505
1543
  */
1506
1544
  function renderNamedArguments( node, separator, env ) {
1507
1545
  return Object.keys(node.args).map(function renderNamedArgument(key) {
1508
- return `${quoteIdIfRequired(key, env.additionalKeywords)} ${separator} ${renderArgument(node.args[key], env)}`;
1546
+ return `${quoteNonIdentifierOrKeyword(key, env.additionalKeywords)} ${separator} ${renderArgument(node.args[key], env)}`;
1509
1547
  }).join(', ');
1510
1548
  }
1511
1549
 
@@ -1647,7 +1685,7 @@ function csnToCdl( csn, options ) {
1647
1685
  * @return {string}
1648
1686
  */
1649
1687
  function renderAlias( alias ) {
1650
- return ` as ${quoteIdIfRequired(alias)}`;
1688
+ return ` as ${quoteNonIdentifierOrKeyword(alias)}`;
1651
1689
  }
1652
1690
 
1653
1691
  /**
@@ -1685,7 +1723,7 @@ function csnToCdl( csn, options ) {
1685
1723
  *
1686
1724
  * @param {object} obj Object that has annotations
1687
1725
  * @param {CdlRenderEnvironment} env
1688
- * @param {{parens: boolean}} [config] Config for renderAnnotationAssignment()
1726
+ * @param {{parentheses: boolean}} [config] Config for renderAnnotationAssignment()
1689
1727
  * @return {string}
1690
1728
  */
1691
1729
  function renderAnnotationAssignmentsAndDocComment( obj, env, config ) {
@@ -1705,38 +1743,29 @@ function csnToCdl( csn, options ) {
1705
1743
  * @param {any} anno Annotation value
1706
1744
  * @param {string} name Annotation name, e.g. `@A.B.C#foo.C`
1707
1745
  * @param {CdlRenderEnvironment} env
1708
- * @param {object} [config] parens: Whether the annotation assignment must be surrounded by parentheses.
1746
+ * @param {object} [config] parentheses: Whether the annotation assignment must be surrounded by parentheses.
1709
1747
  * @return {string} Rendered annotation, possibly quoted: `@![A.B.C#foo.C]: value`
1710
1748
  */
1711
- function renderAnnotationAssignment( anno, name, env, config = { parens: false } ) {
1749
+ function renderAnnotationAssignment( anno, name, env, config = { parentheses: false } ) {
1712
1750
  name = name.substring(1);
1713
1751
  // Take the annotation assignment apart into <nameBeforeVariant>#<variantAndRest>
1714
1752
  const parts = name.split('#');
1715
1753
  const nameBeforeVariant = parts[0];
1716
1754
  const variant = parts[1];
1717
- const { parens } = config;
1718
-
1719
- // Identifier according to our grammar: /[$_a-zA-Z][$_a-zA-Z0-9]*/
1720
- // We expand this pattern to also include dots after the first character.
1721
- // If the annotation does not follow this pattern `ident(.@ident)*`, it must be quoted:
1722
- // `@identifier@identifier` must be quoted but `@identifier.@identifier` should not.
1723
- // TODO: Use quoteAnnotationPathIfRequired()
1724
- const annoRequiresQuoting = !/^[$_a-zA-Z][$_a-zA-Z0-9.]*(?:\.@[$_a-zA-Z][$_a-zA-Z0-9.]*)*$/.test(nameBeforeVariant);
1725
- // Unfortunately, the compiler does not allow `.` after the first variant identifier,
1726
- // even though that is the result after flattening.
1727
- const variantRequiresQuoting = variant && !/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(variant);
1755
+ const { parentheses } = config;
1728
1756
 
1729
1757
  let result = `${env.indent}@`;
1730
- if (parens)
1758
+ if (parentheses)
1731
1759
  result += '(';
1732
1760
 
1733
- if (annoRequiresQuoting || variantRequiresQuoting)
1734
- result += delimitedId(name);
1735
- else
1736
- result += name;
1737
-
1761
+ result += quoteAnnotationPathIfRequired(nameBeforeVariant);
1762
+ if (variant !== undefined)
1763
+ // Unfortunately, the compiler does not allow `.@` after the first variant identifier,
1764
+ // so we're back at simple paths.
1765
+ result += `#${quotePathIfRequired(variant)}`;
1738
1766
  result += ` : ${renderAnnotationValue(anno, env)}`;
1739
- if (parens)
1767
+
1768
+ if (parentheses)
1740
1769
  result += ')';
1741
1770
  return `${result}\n`;
1742
1771
  }
@@ -1753,7 +1782,7 @@ function csnToCdl( csn, options ) {
1753
1782
 
1754
1783
  /**
1755
1784
  * Render the name of a definition. Ensures the first segment of the name
1756
- * is available in the rendered CDL. Otherwise a USING is added.
1785
+ * is available in the rendered CDL. Otherwise, a USING is added.
1757
1786
  *
1758
1787
  * @param {string} name
1759
1788
  * @return {string}
@@ -1814,6 +1843,8 @@ function csnToCdl( csn, options ) {
1814
1843
  if (keywords.cdl_functions.includes(x.func.toUpperCase()) && !x.args)
1815
1844
  return x.func;
1816
1845
  const name = smartFunctionId(x.func);
1846
+ if (!x.args) // e.g. for methods without arguments, `args` is not set at all.
1847
+ return `${name}`;
1817
1848
  return `${name}(${renderArguments( x, '=>', this.env )})`;
1818
1849
  },
1819
1850
  xpr(x) {
@@ -1833,6 +1864,62 @@ function csnToCdl( csn, options ) {
1833
1864
  },
1834
1865
  });
1835
1866
  }
1867
+
1868
+ // checks -------------------------------------------------------------------
1869
+ // The CDL backend has very few checks, but we need to tell the user if
1870
+ // something can't be rendered.
1871
+
1872
+ /**
1873
+ * to.cdl() can only render one nesting level of `items`. `items` inside `items, etc.
1874
+ * can't be represented in CDL, hence can't be rendered.
1875
+ * However, it's possible that due to CSN expansion because of type-ofs, we have
1876
+ * nested `.items` with a `.type` next to it. In that case, return the node with `.type`.
1877
+ *
1878
+ * Returns the most deeply nested `.items`. Upper bound are 100 nesting levels.
1879
+ *
1880
+ * @param {CSN.Artifact} art
1881
+ * @param {CdlRenderEnvironment} env
1882
+ * @return {{art: CSN.Artifact, env: CdlRenderEnvironment}} `art` and new `env` with adapted `env.path`.
1883
+ */
1884
+ function checkInnerMostArray( art, env ) {
1885
+ env = envNewPath(env, env.path); // copy path, so we can modify it directly
1886
+
1887
+ let nesting = 0;
1888
+ while (art.items && nesting < 100) {
1889
+ art = art.items;
1890
+ env.path.push( 'items');
1891
+ ++nesting;
1892
+ if (art.type)
1893
+ break; // after first `.items`, break at nesting level that has a type.
1894
+ }
1895
+
1896
+ if (nesting >= 100) {
1897
+ msg.error('def-invalid-nesting', env.path, { count: nesting, prop: 'items' },
1898
+ 'Property $(PROP) is nested more than $(COUNT) levels and can\'t be rendered');
1899
+ }
1900
+ else if (nesting > 1) {
1901
+ msg.warning('def-unexpected-nesting', env.path, { prop: 'items' },
1902
+ 'Property $(PROP) is nested more than one level; only rendering deepest level');
1903
+ }
1904
+ return { art, env };
1905
+ }
1906
+
1907
+ /**
1908
+ * If an artifact is an array via `.items`, some properties on `art` can't be rendered,
1909
+ * for example "not null", because there is no CDL representation for it. Only "not null"
1910
+ * on `.items` can be rendered.
1911
+ *
1912
+ * @param {CSN.Artifact} art
1913
+ * @param {CdlRenderEnvironment} env
1914
+ */
1915
+ function checkArrayedArtifact( art, env ) {
1916
+ if (!art.items)
1917
+ return;
1918
+ if (art.notNull !== undefined) {
1919
+ msg.warning('def-unexpected-nullability', env.path, { prop: 'not null', otherprop: 'items' },
1920
+ 'Property $(PROP) not rendered, because it can only be rendered inside $(OTHERPROP) for arrayed artifacts');
1921
+ }
1922
+ }
1836
1923
  }
1837
1924
 
1838
1925
  /**
@@ -1870,24 +1957,32 @@ function increaseIndent( env ) {
1870
1957
  }
1871
1958
 
1872
1959
  /**
1873
- * Quote the path steps with `![]` if necessary. For simple ids such as
1874
- * `elem` use `quoteIdIfRequired` instead.
1960
+ * Quote simple path steps with `![]` if necessary. For simple ids such as
1961
+ * `elem` use `quoteNonIdentifierOrKeyword` instead.
1875
1962
  *
1876
- * In contrast to quoteIdIfRequired, does not handle additional keywords,
1963
+ * In contrast to quoteNonIdentifierOrKeyword, does not handle additional keywords,
1877
1964
  * because it was not required, yet.
1878
1965
  *
1966
+ * Due to token rewrite, all keywords after a dot (`.`) are rewritten to
1967
+ * identifiers, i.e. we only need to check for the identifier RegEx.
1968
+ *
1879
1969
  * @param {string} path
1880
1970
  * @returns {string}
1881
- *
1882
- * @todo For paths such as `E.key`, `key` does not have to be in quotes.
1883
1971
  */
1884
1972
  function quotePathIfRequired( path ) {
1885
- return path.split('.').map(step => quoteIdIfRequired(step)).join('.');
1973
+ return path.split('.').map((step, index) => {
1974
+ if (index === 0)
1975
+ return quoteNonIdentifierOrKeyword(step);
1976
+ else if (!identifierRegex.test(step))
1977
+ return delimitedId(step);
1978
+ return step;
1979
+ }).join('.');
1886
1980
  }
1887
1981
 
1888
1982
  /**
1889
1983
  * Quote the id with `![]` if necessary. For paths such as `E.key` use
1890
1984
  * `quotePathIfRequired` instead.
1985
+ * See quoteNonIdentifier() if you want to ignore keywords.
1891
1986
  *
1892
1987
  * Set additionalKeywords to an array of UPPERCASE keywords
1893
1988
  * that also need quoting, e.g. in special functions.
@@ -1896,13 +1991,30 @@ function quotePathIfRequired( path ) {
1896
1991
  * @param {string[]} [additionalKeywords]
1897
1992
  * @return {string}
1898
1993
  */
1899
- function quoteIdIfRequired( id, additionalKeywords ) {
1994
+ function quoteNonIdentifierOrKeyword( id, additionalKeywords ) {
1900
1995
  // Quote if required for CDL
1901
1996
  if (requiresQuotingForCdl(id, additionalKeywords || []))
1902
1997
  return delimitedId(id);
1903
1998
  return id;
1904
1999
  }
1905
2000
 
2001
+ /**
2002
+ * Quote the id with `![]` if necessary. For paths such as `E.key` use
2003
+ * `quotePathIfRequired` instead.
2004
+ * See quoteNonIdentifierOrKeyword() if you want to quote identifiers
2005
+ * that are keywords as well.
2006
+ *
2007
+ * Does not quote the given id if it is a keyword.
2008
+ *
2009
+ * @param {string} id
2010
+ * @return {string}
2011
+ */
2012
+ function quoteNonIdentifier( id ) {
2013
+ if (!identifierRegex.test(id))
2014
+ return delimitedId(id);
2015
+ return id;
2016
+ }
2017
+
1906
2018
  /**
1907
2019
  * Quote an annotation path, e.g. `@My.@Anno.Description` if necessary.
1908
2020
  * `anno` can start with `@` but is not required to be.
@@ -1915,8 +2027,8 @@ function quoteIdIfRequired( id, additionalKeywords ) {
1915
2027
  function quoteAnnotationPathIfRequired( anno ) {
1916
2028
  return anno.split('.').map((segment) => {
1917
2029
  if (segment.startsWith('@'))
1918
- return `@${quoteIdIfRequired(segment.slice(1))}`;
1919
- return quoteIdIfRequired(segment);
2030
+ return `@${quoteNonIdentifier(segment.slice(1))}`;
2031
+ return quoteNonIdentifier(segment);
1920
2032
  }).join('.');
1921
2033
  }
1922
2034
 
@@ -2141,8 +2253,8 @@ function availableFirstPathSteps( csn ) {
2141
2253
  function smartId( id, insideFunction = null ) {
2142
2254
  insideFunction = insideFunction?.toUpperCase();
2143
2255
  if (!insideFunction || !specialFunctions[insideFunction])
2144
- return quoteIdIfRequired(id);
2145
- return quoteIdIfRequired(id, getAllKeywordsForSpecialFunction(insideFunction));
2256
+ return quoteNonIdentifierOrKeyword(id);
2257
+ return quoteNonIdentifierOrKeyword(id, getAllKeywordsForSpecialFunction(insideFunction));
2146
2258
  }
2147
2259
 
2148
2260
  /**
@@ -632,7 +632,6 @@ function toHdbcdsSource( csn, options ) {
632
632
  let result = `(${renderQuery(source, false, increaseIndent(env))})`;
633
633
  if (source.as)
634
634
  result += ` as ${formatIdentifier(source.as)}`;
635
-
636
635
  return result;
637
636
  }
638
637
  // JOIN
@@ -1684,25 +1683,19 @@ function toHdbcdsSource( csn, options ) {
1684
1683
  }
1685
1684
 
1686
1685
  /**
1687
- * Return an id 'id' with appropriate "-quotes
1686
+ * Return an id 'id' with appropriate double-quotes
1688
1687
  *
1689
1688
  * @param {string} id Identifier to quote
1690
1689
  * @returns {string} Properly quoted identifier
1691
1690
  */
1692
1691
  function quoteId( id ) {
1693
- // Should only ever be called for real IDs (i.e. no dots inside)
1694
- if (id.indexOf('.') !== -1)
1695
- throw new ModelError(`HDBCDS: Tried to quote id with dot: ${id}`);
1696
-
1697
-
1698
1692
  switch (options.sqlMapping) {
1699
1693
  case 'plain':
1700
1694
  return smartId(id, 'hdbcds');
1701
1695
  case 'quoted':
1702
1696
  case 'hdbcds':
1703
- return delimitedId(id, 'hdbcds');
1704
1697
  default:
1705
- return null;
1698
+ return delimitedId(id, 'hdbcds');
1706
1699
  }
1707
1700
  }
1708
1701