@sap/cds-compiler 4.4.2 → 4.5.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 (60) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/bin/cdsc.js +5 -0
  3. package/bin/cdsv2m.js +7 -5
  4. package/doc/CHANGELOG_BETA.md +16 -0
  5. package/lib/api/main.js +68 -47
  6. package/lib/api/options.js +10 -6
  7. package/lib/api/validate.js +1 -1
  8. package/lib/base/message-registry.js +28 -6
  9. package/lib/base/messages.js +18 -13
  10. package/lib/base/model.js +3 -0
  11. package/lib/checks/annotationsOData.js +49 -0
  12. package/lib/checks/validator.js +6 -4
  13. package/lib/compiler/assert-consistency.js +38 -16
  14. package/lib/compiler/builtins.js +10 -49
  15. package/lib/compiler/checks.js +16 -8
  16. package/lib/compiler/cycle-detector.js +1 -4
  17. package/lib/compiler/define.js +4 -1
  18. package/lib/compiler/extend.js +21 -7
  19. package/lib/compiler/generate.js +3 -0
  20. package/lib/compiler/populate.js +5 -1
  21. package/lib/compiler/propagator.js +46 -9
  22. package/lib/compiler/resolve.js +68 -14
  23. package/lib/compiler/shared.js +44 -27
  24. package/lib/compiler/tweak-assocs.js +158 -37
  25. package/lib/compiler/utils.js +9 -0
  26. package/lib/edm/annotations/edmJson.js +35 -61
  27. package/lib/edm/annotations/genericTranslation.js +13 -5
  28. package/lib/edm/annotations/preprocessAnnotations.js +2 -3
  29. package/lib/edm/csn2edm.js +4 -1
  30. package/lib/edm/edmInboundChecks.js +59 -15
  31. package/lib/edm/edmPreprocessor.js +1 -7
  32. package/lib/gen/Dictionary.json +8 -0
  33. package/lib/gen/language.checksum +1 -1
  34. package/lib/gen/language.interp +12 -2
  35. package/lib/gen/languageParser.js +6095 -5195
  36. package/lib/json/from-csn.js +4 -5
  37. package/lib/json/to-csn.js +22 -3
  38. package/lib/language/errorStrategy.js +7 -3
  39. package/lib/language/genericAntlrParser.js +120 -24
  40. package/lib/language/textUtils.js +16 -0
  41. package/lib/model/csnUtils.js +9 -8
  42. package/lib/model/revealInternalProperties.js +5 -2
  43. package/lib/modelCompare/compare.js +10 -4
  44. package/lib/optionProcessor.js +2 -3
  45. package/lib/render/toCdl.js +31 -13
  46. package/lib/render/toHdbcds.js +20 -30
  47. package/lib/render/toSql.js +33 -54
  48. package/lib/render/utils/common.js +24 -6
  49. package/lib/transform/db/applyTransformations.js +59 -2
  50. package/lib/transform/db/backlinks.js +13 -1
  51. package/lib/transform/db/expansion.js +24 -3
  52. package/lib/transform/db/flattening.js +2 -2
  53. package/lib/transform/db/killAnnotations.js +37 -0
  54. package/lib/transform/db/rewriteCalculatedElements.js +46 -6
  55. package/lib/transform/forOdata.js +13 -46
  56. package/lib/transform/forRelationalDB.js +2 -1
  57. package/lib/transform/translateAssocsToJoins.js +13 -4
  58. package/lib/transform/universalCsn/coreComputed.js +1 -1
  59. package/lib/transform/universalCsn/universalCsnEnricher.js +4 -4
  60. package/package.json +7 -6
@@ -4,12 +4,13 @@ const {
4
4
  getLastPartOf, getLastPartOfRef,
5
5
  hasValidSkipOrExists, isBuiltinType, generatedByCompilerVersion, getNormalizedQuery,
6
6
  getRootArtifactName, getResultingName, getNamespace, forEachMember, getVariableReplacement, hasAnnotationValue,
7
+ pathName,
7
8
  } = require('../model/csnUtils');
8
9
  const keywords = require('../base/keywords');
9
10
  const {
10
11
  renderFunc, createExpressionRenderer, getRealName, addContextMarkers, addIntermediateContexts,
11
12
  hasHanaComment, getHanaComment, funcWithoutParen, getSqlSnippets,
12
- cdsToSqlTypes, cdsToHdbcdsTypes, withoutCast,
13
+ cdsToSqlTypes, cdsToHdbcdsTypes, withoutCast, variableForDialect,
13
14
  } = require('./utils/common');
14
15
  const {
15
16
  renderReferentialConstraint,
@@ -1209,11 +1210,10 @@ function toHdbcdsSource( csn, options ) {
1209
1210
  *
1210
1211
  * @param {string|object} s Path step
1211
1212
  * @param {number} idx Path position
1212
- * @param {any[]} ref
1213
1213
  * @param {HdbcdsRenderEnvironment} env
1214
1214
  * @returns {string} Rendered path step
1215
1215
  */
1216
- function renderPathStep( s, idx, ref, env ) {
1216
+ function renderPathStep( s, idx, env ) {
1217
1217
  // Simple id or absolute name
1218
1218
  if (typeof s === 'string') {
1219
1219
  // HANA-specific extra magic (should actually be in forRelationalDB)
@@ -1227,12 +1227,6 @@ function toHdbcdsSource( csn, options ) {
1227
1227
  return plainNames ? renderAbsoluteNamePlain(env.currentArtifactName, env)
1228
1228
  : renderAbsoluteNameWithQuotes(env.currentArtifactName, env);
1229
1229
  }
1230
- // HANA-specific translation of '$now' and '$user'
1231
- if (s === '$now' && ref.length === 1)
1232
- return 'CURRENT_TIMESTAMP';
1233
-
1234
- // In first path position, do not quote $projection and magic $-variables like CURRENT_DATE, $now etc.
1235
- // FIXME: We should rather explicitly recognize quoting somehow
1236
1230
 
1237
1231
  // TODO: quote $parameters if it doesn't reference a parameter, this requires knowledge about the kind
1238
1232
  // Example: both views are correct in HANA CDS
@@ -1319,39 +1313,35 @@ function toHdbcdsSource( csn, options ) {
1319
1313
  /**
1320
1314
  * @param {object} x Expression with a ref property
1321
1315
  * @returns {string} Rendered expression
1322
- * @todo no extra magic with x.param or x.global
1316
+ * @todo no extra magic with x.param
1323
1317
  */
1324
1318
  function renderExpressionRef( x ) {
1325
- if (!x.param && !x.global) {
1319
+ if (!x.param) {
1326
1320
  const magicReplacement = getVariableReplacement(x.ref, options);
1327
- if (x.ref[0] === '$user') {
1321
+ if (x.ref[0] === '$user' || x.ref[0] === '$tenant') {
1328
1322
  if (magicReplacement !== null)
1329
1323
  return renderStringForHdbcds(magicReplacement);
1330
1324
 
1331
- // Note: The compiler already transforms $user into $user.id.
1332
-
1333
- // FIXME: this is all not enough: we might need an explicit select item alias (?)
1334
- if (x.ref[1] === 'id')
1335
- return 'SESSION_CONTEXT(\'APPLICATIONUSER\')';
1336
-
1337
- else if (x.ref[1] === 'locale')
1338
- return 'SESSION_CONTEXT(\'LOCALE\')';
1339
-
1340
- else if (x.ref[1] === 'tenant')
1341
- return 'SESSION_CONTEXT(\'TENANT\')';
1325
+ const name = pathName(x.ref);
1326
+ const result = variableForDialect(options, name);
1327
+ // Invalid second path step doesn't cause a return
1328
+ if (result)
1329
+ return result;
1342
1330
  }
1343
- else if (x.ref[0] === '$at' || x.ref[0] === '$valid') {
1344
- if (x.ref[1] === 'from')
1345
- return 'TO_TIMESTAMP(SESSION_CONTEXT(\'VALID-FROM\'))';
1346
-
1347
- else if (x.ref[1] === 'to')
1348
- return 'TO_TIMESTAMP(SESSION_CONTEXT(\'VALID-TO\'))';
1331
+ else if (x.ref[0] === '$at' || x.ref[0] === '$valid' || x.ref[0] === '$now') {
1332
+ const name = pathName(x.ref);
1333
+ const result = variableForDialect(options, name);
1334
+ // Invalid second path step doesn't cause a return
1335
+ if (result)
1336
+ return result;
1349
1337
  }
1350
1338
  else if (x.ref[0] === '$session' && magicReplacement !== null) {
1351
1339
  return renderStringForHdbcds(magicReplacement);
1352
1340
  }
1353
1341
  }
1354
- return `${(x.param || x.global) ? ':' : ''}${x.ref.map((step, index) => renderPathStep(step, index, x.ref, this.env.withSubPath([ 'ref', index ]))).join('.')}`;
1342
+ const prefix = x.param ? ':' : '';
1343
+ const ref = x.ref.map((step, index) => renderPathStep(step, index, this.env.withSubPath([ 'ref', index ]))).join('.');
1344
+ return `${prefix}${ref}`;
1355
1345
  }
1356
1346
 
1357
1347
  /**
@@ -4,7 +4,7 @@
4
4
  const {
5
5
  getLastPartOf, getLastPartOfRef,
6
6
  hasValidSkipOrExists, isBuiltinType, generatedByCompilerVersion, getNormalizedQuery,
7
- forEachDefinition, getResultingName, getVariableReplacement,
7
+ forEachDefinition, getResultingName, getVariableReplacement, pathName,
8
8
  } = require('../model/csnUtils');
9
9
  const { forEach, forEachValue, forEachKey } = require('../utils/objectUtils');
10
10
  const {
@@ -608,7 +608,7 @@ function toSqlDdl( csn, options ) {
608
608
  result += renderTechnicalConfiguration(art.technicalConfig, childEnv);
609
609
 
610
610
 
611
- if (options.sqlDialect === 'hana') {
611
+ if (options.sqlDialect === 'hana' && options.withHanaAssociations) {
612
612
  const associations = Object.keys(art.elements)
613
613
  .map(name => renderAssociationElement(name, art.elements[name], childEnv))
614
614
  .filter(s => s !== '')
@@ -761,7 +761,6 @@ function toSqlDdl( csn, options ) {
761
761
  * is not an association.
762
762
  * Any change to the cardinality rendering must be reflected in A2J mapAssocToJoinCardinality() as well.
763
763
  *
764
- * @todo Duplicate check
765
764
  * @param {string} elementName Name of the element to render
766
765
  * @param {CSN.Element} elm CSN element
767
766
  * @param {SqlRenderEnvironment} env Render environment
@@ -905,12 +904,11 @@ function toSqlDdl( csn, options ) {
905
904
  *
906
905
  * Returns the source as a string.
907
906
  *
908
- * @todo Misleading name, should be something like 'renderQueryFrom'. All the query parts should probably also be rearranged.
909
907
  * @param {object} source Query source
910
908
  * @param {SqlRenderEnvironment} env Render environment
911
909
  * @returns {string} Rendered view source
912
910
  */
913
- function renderViewSource( source, env ) {
911
+ function renderQuerySource( source, env ) {
914
912
  // Sub-SELECT
915
913
  if (source.SELECT || source.SET) {
916
914
  let result = `(${renderQuery(source, env.withIncreasedIndent())})`;
@@ -922,12 +920,12 @@ function toSqlDdl( csn, options ) {
922
920
  // JOIN
923
921
  else if (source.join) {
924
922
  // One join operation, possibly with ON-condition
925
- let result = `${renderViewSource(source.args[0], env.withSubPath([ 'args', 0 ]))}`;
923
+ let result = `${renderQuerySource(source.args[0], env.withSubPath([ 'args', 0 ]))}`;
926
924
  for (let i = 1; i < source.args.length; i++) {
927
925
  result = `(${result} ${source.join.toUpperCase()} `;
928
926
  if (options.sqlDialect === 'hana')
929
927
  result += renderJoinCardinality(source.cardinality);
930
- result += `JOIN ${renderViewSource(source.args[i], env.withSubPath([ 'args', i ]))}`;
928
+ result += `JOIN ${renderQuerySource(source.args[i], env.withSubPath([ 'args', i ]))}`;
931
929
  if (source.on)
932
930
  result += ` ON ${renderExpr(source.on, env.withSubPath([ 'on' ]))}`;
933
931
 
@@ -1150,7 +1148,7 @@ function toSqlDdl( csn, options ) {
1150
1148
  .map(name => renderAssociationElement(name, art.elements[name], childEnv))
1151
1149
  .filter(s => s !== '')
1152
1150
  .join(',\n');
1153
- if (associations !== '' && options.sqlDialect === 'hana') {
1151
+ if (associations !== '' && options.sqlDialect === 'hana' && options.withHanaAssociations) {
1154
1152
  result += `${env.indent}\nWITH ASSOCIATIONS (\n${associations}\n`;
1155
1153
  result += `${env.indent})`;
1156
1154
  }
@@ -1257,7 +1255,7 @@ function toSqlDdl( csn, options ) {
1257
1255
  })
1258
1256
  .filter(s => s !== '')
1259
1257
  .join(',\n')}\n`;
1260
- result += `${env.indent}FROM ${renderViewSource( select.from, env.withSubPath([ 'from' ]))}`;
1258
+ result += `${env.indent}FROM ${renderQuerySource( select.from, env.withSubPath([ 'from' ]))}`;
1261
1259
  if (select.where)
1262
1260
  result += `\n${env.indent}WHERE ${renderExpr(select.where, env.withSubPath([ 'where' ]))}`;
1263
1261
 
@@ -1335,23 +1333,15 @@ function toSqlDdl( csn, options ) {
1335
1333
  function renderTypeReference( elm, env ) {
1336
1334
  let result = '';
1337
1335
 
1338
- // Anonymous structured type: Not supported with SQL (but shouldn't happen anyway after forHana flattened them)
1339
1336
  if (!elm.type && !elm.value) {
1340
- if (!elm.elements)
1337
+ // Anonymous structured type: Not supported with SQL, but doesn't happen anyway after flattening.
1338
+ if (options.testMode)
1341
1339
  throw new ModelError(`to.sql(): Missing type of: ${JSON.stringify(env.path)}`);
1342
-
1343
- // TODO: Signal is not covered by tests
1344
- error(null, env.path,
1345
- 'Anonymous structured types are not supported for conversion to SQL');
1346
1340
  return result;
1347
1341
  }
1348
-
1349
- // Association type
1350
- if (elm.target) {
1351
- // TODO: Signal is not covered by tests
1352
- // We can't do associations yet
1353
- error(null, env.path,
1354
- 'Association and composition types are not yet supported for conversion to SQL');
1342
+ else if (elm.target) {
1343
+ if (options.testMode)
1344
+ throw new ModelError(`to.sql(): Unexpected association in: ${JSON.stringify(env.path)}`);
1355
1345
  return result;
1356
1346
  }
1357
1347
 
@@ -1370,7 +1360,6 @@ function toSqlDdl( csn, options ) {
1370
1360
  if (elm.value) {
1371
1361
  if (!elm.value.stored)
1372
1362
  throw new CompilerAssertion('Found calculated element on-read in rendering; should have been replaced!');
1373
- // TODO: Properly implement calculated elements on-write:
1374
1363
  // The SQL standard 2016 describes the syntax in section 11.3 - 11.4
1375
1364
  // of the SQL Foundation spec (for 2003 in 5WD-02-Foundation-2003-09.pdf). Summarized:
1376
1365
  // <generation clause> ::= GENERATED ALWAYS AS '(' <value expression> ')'
@@ -1383,7 +1372,6 @@ function toSqlDdl( csn, options ) {
1383
1372
  return result;
1384
1373
  }
1385
1374
 
1386
-
1387
1375
  /**
1388
1376
  * Render the name of a builtin CDS type
1389
1377
  *
@@ -1487,7 +1475,7 @@ function toSqlDdl( csn, options ) {
1487
1475
  * @return {string}
1488
1476
  */
1489
1477
  function renderExpressionRef( x, env ) {
1490
- if (!x.param && !x.global) {
1478
+ if (!x.param) {
1491
1479
  const magicReplacement = getVariableReplacement(x.ref, options);
1492
1480
 
1493
1481
  if (x.ref[0] === '$user') {
@@ -1499,6 +1487,15 @@ function toSqlDdl( csn, options ) {
1499
1487
  if (result)
1500
1488
  return result;
1501
1489
  }
1490
+ else if (x.ref[0] === '$tenant') {
1491
+ if (magicReplacement !== null)
1492
+ return renderStringForSql(magicReplacement, options.sqlDialect);
1493
+
1494
+ const name = pathName(x.ref);
1495
+ const result = variableForDialect(options, name);
1496
+ if (result)
1497
+ return result;
1498
+ }
1502
1499
  else if (x.ref[0] === '$at' || x.ref[0] === '$valid') {
1503
1500
  const result = render$at(x);
1504
1501
  // Invalid second path step doesn't cause a return
@@ -1508,18 +1505,12 @@ function toSqlDdl( csn, options ) {
1508
1505
  else if (x.ref[0] === '$session' && magicReplacement !== null) {
1509
1506
  return renderStringForSql(magicReplacement, options.sqlDialect);
1510
1507
  }
1511
- else if (x.ref[0] === '$now') { // TODO: Can there be cases where $now is followed by something?
1512
- switch (options.sqlDialect) {
1513
- case 'plain':
1514
- case 'sqlite':
1515
- case 'hana':
1516
- return 'CURRENT_TIMESTAMP';
1517
- case 'h2':
1518
- case 'postgres':
1519
- return 'current_timestamp';
1520
- default:
1521
- return quoteSqlId(x.ref[0]);
1522
- }
1508
+ else if (x.ref[0] === '$now') {
1509
+ const name = pathName(x.ref);
1510
+ const result = variableForDialect(options, name);
1511
+ if (result)
1512
+ return result;
1513
+ return quoteSqlId(x.ref[0]);
1523
1514
  }
1524
1515
  }
1525
1516
  // FIXME: We currently cannot distinguish whether '$parameters' was quoted or not - we
@@ -1544,7 +1535,7 @@ function toSqlDdl( csn, options ) {
1544
1535
  if (x.ref.length > 2 || typeof x.ref[1] !== 'string')
1545
1536
  return null; // `$user` can only have two path steps
1546
1537
 
1547
- const name = `$user.${x.ref[1]}`;
1538
+ const name = pathName(x.ref);
1548
1539
  const result = variableForDialect(options, name);
1549
1540
  if (result)
1550
1541
  return result;
@@ -1565,7 +1556,7 @@ function toSqlDdl( csn, options ) {
1565
1556
  if (x.ref.length > 2 || typeof x.ref[1] !== 'string')
1566
1557
  return null; // `$at` can only have two path steps
1567
1558
 
1568
- const name = `$at.${x.ref[1]}`;
1559
+ const name = pathName(x.ref);
1569
1560
  const config = variableForDialect(options, name);
1570
1561
  if (config)
1571
1562
  return config;
@@ -1586,22 +1577,10 @@ function toSqlDdl( csn, options ) {
1586
1577
  function renderPathStep( s, idx, env ) {
1587
1578
  // Simple id or absolute name
1588
1579
  if (typeof (s) === 'string') {
1589
- // TODO: When is this actually executed and not handled already in renderExpr?
1590
- const magicForHana = {
1591
- '$user.id': 'SESSION_CONTEXT(\'APPLICATIONUSER\')',
1592
- '$user.locale': 'SESSION_CONTEXT(\'LOCALE\')',
1593
- };
1594
1580
  // Some magic for first path steps
1595
- if (idx === 0) {
1596
- // HANA-specific translation of '$now' and '$user'
1597
- // FIXME: this is all not enough: we might need an explicit select item alias
1598
- if (options.sqlDialect === 'hana' && magicForHana[s])
1599
- return magicForHana[s];
1600
-
1601
- // Ignore initial $projection and initial $self
1602
- if (s === '$projection' || s === '$self')
1603
- return '';
1604
- }
1581
+ // Ignore initial $projection and initial $self
1582
+ if (idx === 0 && (s === '$projection' || s === '$self'))
1583
+ return '';
1605
1584
  return quoteSqlId(s);
1606
1585
  }
1607
1586
  // ID with filters or parameters
@@ -344,30 +344,39 @@ function getDefaultTypeLengths( sqlDialect ) {
344
344
  */
345
345
  const variablesToSql = {
346
346
  fallback: {
347
- // no fallback for $user.id and $user.tenant -> warning in call-site
347
+ // no fallback for $user.id and $tenant -> warning in call-site
348
348
  '$user.locale': '\'en\'',
349
- // $at.* are handled in all dialects -> there is no need for a fallback
349
+ // $at.*/$now are handled in all dialects -> there is no need for a fallback
350
350
  },
351
351
  hana: {
352
352
  '$user.id': "SESSION_CONTEXT('APPLICATIONUSER')",
353
353
  '$user.locale': "SESSION_CONTEXT('LOCALE')",
354
- '$user.tenant': "SESSION_CONTEXT('TENANT')",
354
+ $tenant: "SESSION_CONTEXT('APPLICATIONTENANT')",
355
355
  '$at.from': "TO_TIMESTAMP(SESSION_CONTEXT('VALID-FROM'))",
356
356
  '$at.to': "TO_TIMESTAMP(SESSION_CONTEXT('VALID-TO'))",
357
+ '$valid.from': "TO_TIMESTAMP(SESSION_CONTEXT('VALID-FROM'))",
358
+ '$valid.to': "TO_TIMESTAMP(SESSION_CONTEXT('VALID-TO'))",
359
+ $now: 'CURRENT_TIMESTAMP',
357
360
  },
358
361
  postgres: {
359
362
  '$user.id': "current_setting('cap.applicationuser')",
360
363
  '$user.locale': "current_setting('cap.locale')",
361
- '$user.tenant': "current_setting('cap.tenant')",
364
+ $tenant: "current_setting('cap.tenant')",
362
365
  '$at.from': "current_setting('cap.valid_from')::timestamp",
363
366
  '$at.to': "current_setting('cap.valid_to')::timestamp",
367
+ '$valid.from': "current_setting('cap.valid_from')::timestamp",
368
+ '$valid.to': "current_setting('cap.valid_to')::timestamp",
369
+ $now: 'current_timestamp',
364
370
  },
365
371
  'better-sqlite': {
366
372
  '$user.id': "session_context( '$user.id' )",
367
373
  '$user.locale': "session_context( '$user.locale' )",
368
- '$user.tenant': "session_context( '$user.tenant' )",
374
+ $tenant: "session_context( '$tenant' )",
369
375
  '$at.from': "session_context( '$valid.from' )",
370
376
  '$at.to': "session_context( '$valid.to' )",
377
+ '$valid.from': "session_context( '$valid.from' )",
378
+ '$valid.to': "session_context( '$valid.to' )",
379
+ $now: 'CURRENT_TIMESTAMP',
371
380
  },
372
381
  sqlite: {
373
382
  // For sqlite, we render the string-format-time (strftime) function.
@@ -377,17 +386,26 @@ const variablesToSql = {
377
386
  '$at.from': "strftime('%Y-%m-%dT%H:%M:%S.000Z', 'now')",
378
387
  // + 1ms compared to $at.from
379
388
  '$at.to': "strftime('%Y-%m-%dT%H:%M:%S.001Z', 'now')",
389
+ '$valid.from': "strftime('%Y-%m-%dT%H:%M:%S.000Z', 'now')",
390
+ '$valid.to': "strftime('%Y-%m-%dT%H:%M:%S.001Z', 'now')",
391
+ $now: 'CURRENT_TIMESTAMP',
380
392
  },
381
393
  plain: {
382
394
  '$at.from': 'current_timestamp',
383
395
  '$at.to': 'current_timestamp',
396
+ '$valid.from': 'current_timestamp',
397
+ '$valid.to': 'current_timestamp',
398
+ $now: 'CURRENT_TIMESTAMP',
384
399
  },
385
400
  h2: {
386
401
  '$user.id': '@applicationuser',
387
402
  '$user.locale': '@locale',
388
- '$user.tenant': '@tenant',
403
+ $tenant: '@tenant',
389
404
  '$at.from': '@valid_from',
390
405
  '$at.to': '@valid_to',
406
+ '$valid.from': '@valid_from',
407
+ '$valid.to': '@valid_to',
408
+ $now: 'current_timestamp',
391
409
  },
392
410
  };
393
411
 
@@ -85,6 +85,8 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
85
85
  const trans = transformers[name] || transformers[name.charAt(0)] || standard;
86
86
  if (customTransformers[name])
87
87
  customTransformers[name](node, name, node[name], csnPath, _parent, _prop);
88
+ else if (options.processAnnotations && customTransformers['@'] && name.charAt(0) === '@')
89
+ customTransformers['@'](node, name, node[name], csnPath, _parent, _prop);
88
90
  trans( node, name, node[name], csnPath );
89
91
  }
90
92
  }
@@ -109,6 +111,8 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
109
111
  const trans = transformers[name] || transformers[name.charAt(0)] || standard;
110
112
  if (customTransformers[name])
111
113
  customTransformers[name](node, name, node[name], csnPath, dict);
114
+ else if (options.processAnnotations && customTransformers['@'] && name.charAt(0) === '@')
115
+ customTransformers['@'](node, name, node[name], csnPath, dict);
112
116
  trans( node, name, node[name], csnPath );
113
117
  }
114
118
  csnPath.pop();
@@ -150,8 +154,13 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
150
154
  else if (node && typeof node === 'object') {
151
155
  csnPath.push(_prop);
152
156
 
153
- for (const name of Object.getOwnPropertyNames( node ))
154
- annotation( node, name, node[name] );
157
+ if (Array.isArray(node)) {
158
+ node.forEach( (n, i) => annotation( node, i, n ) );
159
+ }
160
+ else {
161
+ for (const name of Object.getOwnPropertyNames( node ))
162
+ annotation( node, name, node[name] );
163
+ }
155
164
 
156
165
  csnPath.pop();
157
166
  }
@@ -287,7 +296,55 @@ function applyTransformationsOnDictionary( dictionary, customTransformers = {},
287
296
  return applyTransformationsInternal(dictionary, null, customTransformers, [], { directDict: true, ...options }, path);
288
297
  }
289
298
 
299
+ /**
300
+ * transformExpression is a lightweight version of applyTransformations
301
+ * used primarily to transform annotation expressions.
302
+ * @param {object} parent Start node
303
+ * @param {object} customTransformers Map of callback functions
304
+ * @param {CSN.Path} path Path to parent
305
+ * @returns {object} transformed parent
306
+ */
307
+ function transformExpression( parent, customTransformers, path = [] ) {
308
+ const csnPath = [ ...path ];
309
+ /**
310
+ *
311
+ * @param {object} _parent
312
+ * @param {string} _prop
313
+ * @param {object} node
314
+ */
315
+ function standard( _parent, _prop, node ) {
316
+ if (!node || typeof node !== 'object' ||
317
+ !{}.propertyIsEnumerable.call( _parent, _prop ) ||
318
+ (typeof _prop === 'string' && _prop.startsWith('@')))
319
+ return;
320
+
321
+
322
+ csnPath.push(_prop);
323
+ if (Array.isArray(node)) {
324
+ node.forEach( (n, i) => standard( node, i, n ) );
325
+ }
326
+ else {
327
+ for (const name of Object.getOwnPropertyNames( node )) {
328
+ const ct = customTransformers[name];
329
+ if (ct) {
330
+ if (Array.isArray(ct))
331
+ ct.forEach(cti => cti(node, name, node[name], csnPath, _parent, _prop));
332
+ else
333
+ ct(node, name, node[name], csnPath, _parent, _prop);
334
+ }
335
+ standard(node, name, node[name]);
336
+ }
337
+ }
338
+ csnPath.pop();
339
+ }
340
+
341
+ for (const name of Object.getOwnPropertyNames( parent ))
342
+ standard( parent, name, parent[name] );
343
+ return parent;
344
+ }
345
+
290
346
  module.exports = {
347
+ transformExpression,
291
348
  applyTransformations,
292
349
  applyTransformationsOnNonDictionary,
293
350
  applyTransformationsOnDictionary,
@@ -18,6 +18,7 @@ const { forEach } = require('../../utils/objectUtils');
18
18
  * @returns {import('../../model/csnUtils').genericCallback} callback for forEachDefinition
19
19
  */
20
20
  function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimiter, doA2J = true ) {
21
+ let prepend$self = false;
21
22
  return transformSelfInBacklinks;
22
23
  /**
23
24
  * @param {CSN.Artifact} artifact
@@ -26,12 +27,15 @@ function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimi
26
27
  * @param {CSN.Path} path
27
28
  */
28
29
  function transformSelfInBacklinks( artifact, artifactName, dummy, path ) {
30
+ prepend$self = false;
29
31
  // Fixme: For toHana mixins must be transformed, for toSql -d hana
30
32
  // mixin elements must be transformed, why can't toSql also use mixins?
31
33
  if (options.transformation === 'effective' && artifact.elements || artifact.kind === 'entity' || artifact.query || (options.forHana && options.sqlMapping === 'hdbcds' && artifact.kind === 'type'))
32
34
  processDict(artifact.elements, path.concat([ 'elements' ]));
33
- if (artifact.query?.SELECT?.mixin)
35
+ if (artifact.query?.SELECT?.mixin) {
36
+ prepend$self = options.transformation === 'effective';
34
37
  processDict(artifact.query.SELECT.mixin, path.concat([ 'query', 'SELECT', 'mixin' ]));
38
+ }
35
39
 
36
40
  /**
37
41
  * Loop over the dict and start the processing.
@@ -237,6 +241,10 @@ function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimi
237
241
  { ref: k.ref },
238
242
  ];
239
243
 
244
+ if (prepend$self)
245
+ a[1].ref = [ '$self', ...a[1].ref ];
246
+
247
+
240
248
  conditions.push([ a[0], '=', a[1] ]);
241
249
  });
242
250
 
@@ -273,11 +281,15 @@ function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimi
273
281
  // we are in the "path" from the forwarding assoc => need to remove the first part of the path
274
282
  if (ref[0] === assocName) {
275
283
  ref.shift();
284
+ if (prepend$self)
285
+ ref.unshift('$self');
276
286
  }
277
287
  else if (ref.length > 1 && ref[0] === '$self' && ref[1] === assocName) {
278
288
  // We could also have a $self in front of the assoc name - so we would need to shift twice
279
289
  ref.shift();
280
290
  ref.shift();
291
+ if (prepend$self)
292
+ ref.unshift('$self');
281
293
  }
282
294
  else { // we are in the backlink assoc "path" => need to push at the beginning the association's id
283
295
  ref.unshift(elemName);
@@ -10,6 +10,7 @@ const {
10
10
  const { implicitAs, columnAlias, pathId } = require('../../model/csnRefs');
11
11
  const { setProp } = require('../../base/model');
12
12
  const { forEach } = require('../../utils/objectUtils');
13
+ const { killNonrequiredAnno } = require('./killAnnotations');
13
14
 
14
15
  /**
15
16
  * For keys, columns, groupBy and orderBy, expand structured things.
@@ -27,7 +28,7 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
27
28
 
28
29
  rewriteExpandInline();
29
30
 
30
- applyTransformations(csn, {
31
+ const transformers = {
31
32
  keys: (parent, name, keys, path) => {
32
33
  parent.keys = expand(keys, path.concat('keys'), true);
33
34
  },
@@ -53,7 +54,13 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
53
54
  orderBy: (parent, name, orderBy, path) => {
54
55
  parent.orderBy = expand(orderBy, path.concat('orderBy'));
55
56
  },
56
- }, [], iterateOptions);
57
+ };
58
+
59
+ // To not have a whole model loop for such a "small" thing, we kill all non-sql-backend relevant annotations here
60
+ if (options.transformation === 'sql' || options.transformation === 'hdbcds')
61
+ transformers['@'] = killNonrequiredAnno;
62
+
63
+ applyTransformations(csn, transformers, [], iterateOptions);
57
64
 
58
65
  /**
59
66
  * Turn .expand/.inline into normal refs. @cds.persistence.skip .expand with to-many (and all transitive views).
@@ -529,7 +536,7 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
529
536
  else
530
537
  newThing.push(col);
531
538
  }
532
- else if (col.ref && col.$scope === '$magic' && ( col.ref[0] === '$user' || col.ref[0] === '$session' ) && !col.as) {
539
+ else if (col.ref && col.$scope === '$magic' && ( col.ref[0] === '$user' || col.ref[0] === '$tenant' || col.ref[0] === '$session' ) && !col.as) {
533
540
  col.as = implicitAs(col.ref);
534
541
  newThing.push(col);
535
542
  }
@@ -618,6 +625,20 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
618
625
  setProp(obj, '$implicitAlias', true);
619
626
  }
620
627
 
628
+ // If our column/thing was cast to a structured type, we need to keep the "cast" insync with the
629
+ // flattened out leaf elements that we turn the ref into
630
+ if (obj.cast?.type) {
631
+ const addedRef = currentRef.slice(root.ref.length);
632
+ if (addedRef.length > 0) {
633
+ // Decouple from other leafs
634
+ obj.cast = { ...obj.cast };
635
+ if (!obj.cast.type.ref)
636
+ obj.cast.type = { ref: [ obj.cast.type ] };
637
+
638
+ obj.cast.type.ref = [ ...obj.cast.type.ref, ...addedRef ];
639
+ }
640
+ }
641
+
621
642
  // The Java runtime, as of 2023-09-13, assumes that for _simple projections_, all references
622
643
  // are relative to the query source. To avoid breaking that assumption unless necessary,
623
644
  // we only add the table alias if:
@@ -17,7 +17,7 @@ const { cardinality2str } = require('../../model/csnUtils');
17
17
  * @param {CSN.Model} csn
18
18
  */
19
19
  function removeLeadingSelf( csn ) {
20
- const magicVars = [ '$now', '$self', '$projection', '$user', '$session', '$at' ];
20
+ const magicVars = [ '$now', '$self', '$projection', '$user', '$tenant', '$session', '$at' ];
21
21
  applyTransformations(csn, {
22
22
  elements: (parent, prop, elements) => {
23
23
  for (const [ elementName, element ] of Object.entries(elements)) {
@@ -184,7 +184,7 @@ function flattenAllStructStepsInRefs( csn, options, resolved, pathDelimiter, ite
184
184
  * For each step of the links, check if there is a type reference.
185
185
  * If there is, resolve it and store the result in a WeakMap.
186
186
  *
187
- * @param {Array} [links=[]]
187
+ * @param {Array} [links]
188
188
  * @todo seems too hacky
189
189
  * @returns {WeakMap} A WeakMap where a link is the key and the type is the value
190
190
  */
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const requiredAnnos = {
4
+ '@cds.persistence.skip': true,
5
+ '@cds.persistence.exists': true,
6
+ '@cds.persistence.table': true,
7
+ '@cds.persistence.journal': true, // Build checks on it
8
+ '@sql.append': true,
9
+ '@sql.prepend': true,
10
+ '@sql.replace': true, // We do a check on this, no real function
11
+ '@assert.unique': true, // We do a check on this, no real function
12
+ '@assert.integrity': true,
13
+ '@cds.valid.from': true,
14
+ '@cds.valid.to': true,
15
+ '@cds.valid.key': true,
16
+ '@odata.draft.enabled': true,
17
+ '@fiori.draft.enabled': true,
18
+ '@cds.persistence.calcview': true,
19
+ '@cds.persistence.udf': true,
20
+ '@cds.autoexpose': true,
21
+ '@cds.autoexposed': true,
22
+ '@cds.redirection.target': true,
23
+ '@Core.Computed': true,
24
+ };
25
+
26
+ /**
27
+ *
28
+ * @param {object} carrier
29
+ * @param {string} annoKey
30
+ */
31
+ function killNonrequiredAnno( carrier, annoKey ) {
32
+ if (!requiredAnnos[annoKey] && !annoKey.startsWith('@assert.unique.'))
33
+ delete carrier[annoKey];
34
+ }
35
+
36
+
37
+ module.exports = { killNonrequiredAnno };