@sap/cds-compiler 4.1.2 → 4.2.4

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 (74) hide show
  1. package/CHANGELOG.md +107 -1
  2. package/bin/cdsc.js +6 -3
  3. package/doc/CHANGELOG_BETA.md +5 -0
  4. package/doc/CHANGELOG_DEPRECATED.md +15 -0
  5. package/lib/api/main.js +2 -2
  6. package/lib/api/options.js +2 -2
  7. package/lib/api/validate.js +24 -24
  8. package/lib/base/message-registry.js +41 -6
  9. package/lib/base/messages.js +7 -0
  10. package/lib/base/model.js +38 -8
  11. package/lib/checks/elements.js +11 -10
  12. package/lib/checks/manyNavigations.js +33 -0
  13. package/lib/checks/onConditions.js +5 -2
  14. package/lib/checks/queryNoDbArtifacts.js +2 -3
  15. package/lib/checks/selectItems.js +4 -55
  16. package/lib/checks/utils.js +3 -2
  17. package/lib/checks/validator.js +3 -1
  18. package/lib/compiler/.eslintrc.json +2 -1
  19. package/lib/compiler/assert-consistency.js +27 -24
  20. package/lib/compiler/base.js +6 -2
  21. package/lib/compiler/builtins.js +34 -34
  22. package/lib/compiler/checks.js +179 -208
  23. package/lib/compiler/classes.js +2 -2
  24. package/lib/compiler/cycle-detector.js +6 -6
  25. package/lib/compiler/define.js +66 -45
  26. package/lib/compiler/extend.js +81 -72
  27. package/lib/compiler/finalize-parse-cdl.js +26 -26
  28. package/lib/compiler/generate.js +61 -45
  29. package/lib/compiler/index.js +47 -49
  30. package/lib/compiler/kick-start.js +8 -7
  31. package/lib/compiler/moduleLayers.js +1 -1
  32. package/lib/compiler/populate.js +42 -35
  33. package/lib/compiler/propagator.js +6 -6
  34. package/lib/compiler/resolve.js +170 -126
  35. package/lib/compiler/shared.js +122 -45
  36. package/lib/compiler/tweak-assocs.js +93 -40
  37. package/lib/compiler/utils.js +15 -12
  38. package/lib/edm/.eslintrc.json +40 -1
  39. package/lib/edm/annotations/genericTranslation.js +721 -707
  40. package/lib/edm/annotations/preprocessAnnotations.js +88 -77
  41. package/lib/edm/csn2edm.js +389 -378
  42. package/lib/edm/edm.js +678 -772
  43. package/lib/edm/edmAnnoPreprocessor.js +132 -146
  44. package/lib/edm/edmInboundChecks.js +29 -27
  45. package/lib/edm/edmPreprocessor.js +686 -646
  46. package/lib/edm/edmUtils.js +277 -296
  47. package/lib/gen/language.checksum +1 -1
  48. package/lib/gen/language.interp +1 -1
  49. package/lib/gen/languageParser.js +1253 -1276
  50. package/lib/json/from-csn.js +34 -4
  51. package/lib/json/to-csn.js +4 -4
  52. package/lib/language/language.g4 +2 -5
  53. package/lib/main.d.ts +61 -1
  54. package/lib/model/csnUtils.js +31 -2
  55. package/lib/model/revealInternalProperties.js +1 -1
  56. package/lib/modelCompare/compare.js +37 -2
  57. package/lib/modelCompare/utils/filter.js +1 -1
  58. package/lib/optionProcessor.js +15 -3
  59. package/lib/render/toCdl.js +30 -4
  60. package/lib/render/toSql.js +5 -9
  61. package/lib/render/utils/common.js +8 -6
  62. package/lib/transform/db/applyTransformations.js +1 -1
  63. package/lib/transform/db/cdsPersistence.js +1 -1
  64. package/lib/transform/db/constraints.js +47 -17
  65. package/lib/transform/db/expansion.js +133 -50
  66. package/lib/transform/db/flattening.js +75 -7
  67. package/lib/transform/forOdata.js +4 -1
  68. package/lib/transform/forRelationalDB.js +80 -62
  69. package/lib/transform/localized.js +91 -54
  70. package/lib/transform/transformUtils.js +9 -10
  71. package/lib/utils/file.js +7 -7
  72. package/lib/utils/moduleResolve.js +210 -121
  73. package/lib/utils/objectUtils.js +1 -1
  74. package/package.json +5 -5
@@ -9,7 +9,7 @@ const { cloneCsnNonDict,
9
9
  const { makeMessageFunction } = require('../base/messages');
10
10
  const transformUtils = require('./transformUtils');
11
11
  const { translateAssocsToJoinsCSN } = require('./translateAssocsToJoins');
12
- const { csnRefs, pathId, traverseQuery } = require('../model/csnRefs');
12
+ const { csnRefs, pathId, traverseQuery, columnAlias} = require('../model/csnRefs');
13
13
  const { checkCSNVersion } = require('../json/csnVersion');
14
14
  const validate = require('../checks/validator');
15
15
  const { rejectManagedAssociationsAndStructuresForHdbcdsNames } = require('../checks/selectItems');
@@ -163,6 +163,8 @@ function transformForRelationalDBWithCsn(csn, options, moduleName) {
163
163
  // Needs to happen before tuple expansion, so the newly generated WHERE-conditions have it applied
164
164
  handleExists(csn, options, error, csnUtils.inspectRef, csnUtils.initDefinition, csnUtils.dropDefinitionCache);
165
165
 
166
+ doA2J && flattening.linkForeignKeyAnnotationExtensionsToAssociation(csn, options);
167
+
166
168
  // Check if structured elements and managed associations are compared in an expression
167
169
  // and expand these structured elements. This tuple expansion allows all other
168
170
  // subsequent procession steps (especially a2j) to see plain paths in expressions.
@@ -463,70 +465,71 @@ function transformForRelationalDBWithCsn(csn, options, moduleName) {
463
465
  Object.assign(csnUtils, csnRefApi);
464
466
  }
465
467
 
468
+ // For non-A2J only
466
469
  function handleMixinOnConditions(artifact, artifactName) {
467
- if (!artifact.query)
470
+ if (!artifact.query) // projections can't have mixins
468
471
  return;
469
472
  forAllQueries(artifact.query, (query, path) => {
470
- const { mixin } = query.SELECT || {};
471
- if(mixin) {
473
+ const { mixin } = query.SELECT || {};
474
+ if (mixin) {
472
475
  query.SELECT.columns
473
- // filter for associations which are used in the SELECT
474
- .filter((c) => {
475
- return c.ref && c.ref.length > 1;
476
- })
477
- .forEach((usedAssoc) => {
478
- const assocName = pathId(usedAssoc.ref[0]);
479
- const mixinAssociation = mixin[assocName];
480
- if(mixinAssociation){
481
- mixinAssociation.on = getResolvedMixinOnCondition(csn, mixinAssociation, query, assocName, path.concat(['mixin', assocName]));
482
- }
483
- })
476
+ // filter for associations which are used in the SELECT
477
+ .filter((c) => {
478
+ return c.ref && c.ref.length > 1;
479
+ })
480
+ .forEach((usedAssoc) => {
481
+ const assocName = pathId(usedAssoc.ref[0]);
482
+ const mixinAssociation = mixin[assocName];
483
+ if (mixinAssociation)
484
+ mixinAssociation.on = getResolvedMixinOnCondition(mixinAssociation, query, assocName, path.concat(['mixin', assocName]));
485
+ })
484
486
  }
485
- }, [ 'definitions', artifactName, 'query' ]);
486
-
487
- function getResolvedMixinOnCondition(csn, mixinAssociation, query, assocName, path){
488
- const { inspectRef } = csnRefs(csn);
489
- const referencedThroughStar = query.SELECT.columns.some((column) => column === '*');
490
- return mixinAssociation.on
491
- .map((onConditionPart, i) => {
492
- let columnToReplace;
493
- if(onConditionPart.ref && (onConditionPart.ref[0] === '$projection' || onConditionPart.ref[0] === '$self')){
494
- const { links } = inspectRef(path.concat(['on', i]));
495
- if(links){
496
- columnToReplace = onConditionPart.ref[links.length - 1];
497
- }
498
- }
499
- if (!columnToReplace)
500
- return onConditionPart;
501
-
502
- const replaceWith = query.SELECT.columns.find((column) =>
503
- column.as && column.as === columnToReplace ||
504
- column.ref && column.ref[0] === columnToReplace
505
- );
506
- if (!replaceWith && referencedThroughStar) {
507
- // not explicitly in column list, check query sources
508
- // get$combined also includes elements which are part of "excluding {}"
509
- // this shouldn't be an issue here, as such references get rejected
510
- const elementsOfQuerySources = csnUtils.get$combined(query);
511
- forEach(elementsOfQuerySources, (id, element) => {
512
- // if the ref points to an element which is not explicitly exposed in the column list,
513
- // but through the '*' operator -> replace the $projection / $self with the correct source entity
514
- if(id === columnToReplace)
515
- onConditionPart.ref[0] = element[0].parent;
516
- });
517
- }
487
+ }, ['definitions', artifactName, 'query']);
488
+ }
518
489
 
519
- // No implicit CAST in on-condition
520
- if(replaceWith && replaceWith.cast) {
521
- const clone = cloneCsnNonDict(replaceWith, options);
522
- delete clone.cast;
523
- return clone;
524
- }
525
- return replaceWith || onConditionPart;
526
- });
490
+ // For non-A2J only
491
+ function getResolvedMixinOnCondition(mixinAssociation, query, assocName, path) {
492
+ const referencedThroughStar = query.SELECT.columns.some((column) => column === '*');
493
+ return mixinAssociation.on.map(handeMixinOnConditionPart);
494
+
495
+ function handeMixinOnConditionPart(onConditionPart, i) {
496
+ let columnToReplace;
497
+ if (onConditionPart.ref && (onConditionPart.ref[0] === '$projection' || onConditionPart.ref[0] === '$self')){
498
+ const { links } = csnUtils.inspectRef(path.concat(['on', i]));
499
+ if (links) {
500
+ columnToReplace = onConditionPart.ref[links.length - 1];
501
+ }
502
+ }
503
+ if (!columnToReplace)
504
+ return onConditionPart;
505
+
506
+ const replaceWith = query.SELECT.columns.find(col => columnAlias(col) === columnToReplace);
507
+ if (!replaceWith && referencedThroughStar) {
508
+ // not explicitly in column list, check query sources
509
+ // get$combined also includes elements which are part of "excluding {}"
510
+ // this shouldn't be an issue here, as such references get rejected
511
+ const elementsOfQuerySources = csnUtils.get$combined(query);
512
+ forEach(elementsOfQuerySources, (id, element) => {
513
+ // if the ref points to an element which is not explicitly exposed in the column list,
514
+ // but through the '*' operator -> replace the $projection / $self with the correct source entity
515
+ if(id === columnToReplace)
516
+ onConditionPart.ref[0] = element[0].parent;
517
+ });
518
+ return onConditionPart;
519
+ }
520
+ else if (replaceWith) {
521
+ const clone = cloneCsnNonDict(replaceWith, options);
522
+ delete clone.cast; // No implicit CAST in on-condition
523
+ delete clone.as;
524
+ return clone;
525
+ }
526
+ else {
527
+ return onConditionPart
528
+ }
527
529
  }
528
530
  }
529
531
 
532
+
530
533
  /**
531
534
  * @param {CSN.Artifact} artifact
532
535
  * @param {string} artifactName
@@ -701,7 +704,9 @@ function transformForRelationalDBWithCsn(csn, options, moduleName) {
701
704
  throwWithAnyError();
702
705
  // the augmentor isn't able to deal with technical configurations and since assoc2join can ignore it we
703
706
  // simply make it invisible and copy it over to the result csn
704
- forEachDefinition(csn, art => art.technicalConfig && setProp(art, 'technicalConfig', art.technicalConfig));
707
+ forEachDefinition(csn,
708
+ art => art.technicalConfig && setProp(art, 'technicalConfig',
709
+ art.technicalConfig));
705
710
 
706
711
  const newCsn = translateAssocsToJoinsCSN(csn, options);
707
712
 
@@ -714,6 +719,23 @@ function transformForRelationalDBWithCsn(csn, options, moduleName) {
714
719
  newCsn.definitions[artName].technicalConfig = art.technicalConfig;
715
720
 
716
721
  });
722
+ // restore $fkExtensions and $structRef for foreign key annotations
723
+ if (isBetaEnabled(options, 'annotateForeignKeys')) {
724
+ forEachDefinition(csn, (oldDef, artName) => {
725
+ const newDef = newCsn.definitions[artName];
726
+ if(oldDef?.elements) {
727
+ Object.entries(oldDef.elements).forEach(([eltName, oldElt]) => {
728
+ const newElt = newDef.elements[eltName];
729
+ if(oldElt.$fkExtensions)
730
+ setProp(newElt, '$fkExtensions', oldElt.$fkExtensions);
731
+ oldElt.keys?.forEach((fk, i) => {
732
+ if(fk.$structRef && newElt.keys?.[i])
733
+ setProp(newElt.keys[i], '$structRef', fk.$structRef);
734
+ })
735
+ })
736
+ }
737
+ });
738
+ }
717
739
  csn = newCsn;
718
740
  }
719
741
 
@@ -748,11 +770,7 @@ function transformForRelationalDBWithCsn(csn, options, moduleName) {
748
770
  function renamePrimitiveTypesAndUuid(val, node, key) {
749
771
  // assert key === 'type'
750
772
  const hanaNamesMap = createDict({
751
- 'cds.DateTime': 'cds.UTCDateTime',
752
- 'cds.Timestamp': 'cds.UTCTimestamp',
753
- 'cds.Date': 'cds.LocalDate',
754
- 'cds.Time': 'cds.LocalTime',
755
- 'cds.UUID': 'cds.String',
773
+ 'cds.UUID': 'cds.String'
756
774
  });
757
775
  node[key] = hanaNamesMap[val] || val;
758
776
  if (val === 'cds.UUID' && !node.length) {
@@ -13,6 +13,7 @@ const {
13
13
  forAllQueries,
14
14
  sortCsnDefinitionsForTests,
15
15
  } = require('../model/csnUtils');
16
+ const {CompilerAssertion} = require('../base/error');
16
17
 
17
18
  /**
18
19
  * Indicator that a definition is localized and has a convenience view.
@@ -34,10 +35,11 @@ const _isViewForEntity = Symbol('_isViewForEntity'); // $inferred = 'LOCALIZED-H
34
35
  * Used to transitively create convenience views.
35
36
  */
36
37
  const _targetFor = Symbol('_targetFor');
38
+ const annoPersistenceSkip = '@cds.persistence.skip';
37
39
 
38
40
  /**
39
41
  * Callback function returning `true` if the localization view should be created.
40
- * @callback acceptLocalizedView
42
+ * @callback AcceptLocalizedViewCallback
41
43
  * @param {string} viewName localization view name
42
44
  * @param {string} originalName Artifact name of the original view
43
45
  */
@@ -45,20 +47,22 @@ const _targetFor = Symbol('_targetFor');
45
47
  /**
46
48
  * Create transitive localized convenience views.
47
49
  *
48
- * A convenience view is created if the entity/view has a localized element
50
+ * A convenience view is created if the entity/view has a localized element[^1]
49
51
  * or if it exposes an association leading to a localized-tagged target.
50
52
  *
51
53
  * INTERNALS:
52
54
  * We have three kinds of localized convenience views:
53
55
  *
54
56
  * 1. "direct ones" using coalesce() for the table entities with localized
55
- * elements: as projection on the original (created in extend.js)
56
- * 2. for table entities with associations to entities which have a localized
57
- * convenience views or redirections thereon: as projection on the original
58
- * 3. for view entities with associations to entities which have a localized
59
- * convenience views or redirections thereon: as entity using the same
60
- * query as the original, but replacing all sources by their localized
61
- * convenience view variant if present
57
+ * elements[^1]: as projection on the original and '.texts' entity (created in extend.js)
58
+ * 2. for _table_ entities with associations to entities which have a localized
59
+ * convenience: as projection on the original
60
+ * 3. for _view_ entities with either localized elements[^1] or associations
61
+ * to entities which have a localized convenience view:
62
+ * as view using a copy of the original query, but replacing all sources by
63
+ * their localized convenience view variant if present
64
+ *
65
+ * [^1]: That is, the element has `localized: true`.
62
66
  *
63
67
  * First, all "direct ones" are built (1). Then we build all 2 and 3
64
68
  * transitively (i.e. as long as an entity has an association which directly or
@@ -68,21 +72,46 @@ const _targetFor = Symbol('_targetFor');
68
72
  * variant if present.
69
73
  *
70
74
  * @param {CSN.Model} csn
71
- * @param {CSN.Options} options
72
- * @param {boolean} useJoins If true, rewrite the "localized" association to a
73
- * join in direct convenience views.
75
+ * Input CSN model. Should not have existing convenience views.
76
+ *
77
+ * @param {object} options
78
+ * CSN options. Only few options are used, see below for important ones.
79
+ * Options such as `testMode` or `testSortCsn` can also be set.
80
+ *
81
+ * @param {string} [options.localizedLanguageFallback]
82
+ * Valid values (if set): 'none', 'coalesce' (default)
83
+ * Whether to use a `coalesce()` function when selecting from `.texts` entities.
84
+ * If not set, untranslated strings may not return any value. If 'coalesce'
85
+ * is used, it will fall back to the original string.
86
+ *
87
+ * @param {boolean} [options.localizedWithoutCoalesce]
88
+ * Deprecated version of localizedLanguageFallback. Do not use.
89
+ *
90
+ * @param {boolean} [options.fewerLocalizedViews]
91
+ *
74
92
  * @param {object} config
93
+ * Configuration for creating convenience views. Non-user visible options.
94
+ *
95
+ * @param {boolean} [config.useJoins]
96
+ * If true, rewrite the "localized" association to a join in direct convenience views.
97
+ *
98
+ * @param {AcceptLocalizedViewCallback} [config.acceptLocalizedView]
99
+ * A callback that can be used to suppress the creation of localized convenience views
100
+ * if desired. For example, if you want to know which definitions get a convenience view
101
+ * but don't actually want to create them.
102
+ *
103
+ * @param {boolean} [config.ignoreUnknownExtensions]
104
+ * If true, do not emit a warning for annotations on unknown `localized.*` views.
75
105
  */
76
- function _addLocalizationViews(csn, options, useJoins, config) {
106
+ function _addLocalizationViews(csn, options, config) {
77
107
  const messageFunctions = makeMessageFunction(csn, options);
78
- if (checkExistingLocalizationViews(csn, options, messageFunctions)) {
79
- messageFunctions.throwWithError();
108
+ if (checkExistingLocalizationViews(csn, options, messageFunctions))
80
109
  return csn;
81
- }
82
110
 
83
- const { acceptLocalizedView, ignoreUnknownExtensions } = config;
111
+ const { useJoins, acceptLocalizedView, ignoreUnknownExtensions } = config;
84
112
  const noCoalesce = (options.localizedLanguageFallback === 'none' ||
85
113
  options.localizedWithoutCoalesce);
114
+ const ignoreAssocToLocalized = !!options.fewerLocalizedViews;
86
115
 
87
116
  createDirectConvenienceViews(); // 1
88
117
  createTransitiveConvenienceViews(); // 2 + 3
@@ -179,10 +208,10 @@ function _addLocalizationViews(csn, options, useJoins, config) {
179
208
  copyLocation(convenienceView.query, entity);
180
209
 
181
210
  if (shouldUseJoin)
182
- // Expand elements:
211
+ // Expand elements; (variant 1)
183
212
  columns.push( ...columnsForEntityWithExcludeList( entity, 'L_0', textElements ) )
184
213
  else
185
- columns.push( '*' );
214
+ columns.push( '*' ); // (variant 2)
186
215
 
187
216
  for (const originalElement of textElements) {
188
217
  const elem = entity.elements[originalElement];
@@ -330,7 +359,7 @@ function _addLocalizationViews(csn, options, useJoins, config) {
330
359
  let keyCount = 0;
331
360
  let textElements = [];
332
361
 
333
- forEachGeneric(art, 'elements', (elem, elemName , _prop, path) => {
362
+ forEachGeneric(art, 'elements', (elem, elemName , _prop) => {
334
363
  if (elem.$ignore) // from SAP HANA backend
335
364
  return;
336
365
 
@@ -339,11 +368,6 @@ function _addLocalizationViews(csn, options, useJoins, config) {
339
368
 
340
369
  if (elem.key || elem.$key || elem.localized)
341
370
  textElements.push( elemName );
342
-
343
- if ((elem.key|| elem.$key) && elem.localized) {
344
- messageFunctions.warning('def-ignoring-localized', path, { keyword: 'localized' },
345
- 'Keyword $(KEYWORD) is ignored for primary keys');
346
- }
347
371
  }, artPath);
348
372
 
349
373
  if (textElements.length <= keyCount || keyCount <= 0)
@@ -364,12 +388,17 @@ function _addLocalizationViews(csn, options, useJoins, config) {
364
388
  'Skipped creation of convenience view for $(NAME) because its texts entity could not be found' );
365
389
  return null;
366
390
  }
367
-
368
391
  if (!isValidTextsEntity( textsEntity )) {
369
392
  messageFunctions.info( null, [ 'definitions', textsName ], { name: artName },
370
393
  'Skipped creation of convenience view for $(NAME) because its texts entity does not appear to be valid' );
371
394
  return null;
372
395
  }
396
+ if (!art[annoPersistenceSkip] && textsEntity[annoPersistenceSkip]) {
397
+ messageFunctions.message( 'anno-unexpected-localized-skip', artPath,
398
+ { name: textsName, art: artName, anno: annoPersistenceSkip },
399
+ 'Compiler generated entity $(NAME) must not be annotated with $(ANNO) if $(ART) is not skipped' );
400
+ return null;
401
+ }
373
402
 
374
403
  // There may be keys in the original artifact that were added by the core compiler,
375
404
  // for example elements that are marked @cds.valid.from.
@@ -391,22 +420,23 @@ function _addLocalizationViews(csn, options, useJoins, config) {
391
420
 
392
421
  /**
393
422
  * Transitively create convenience views for entities/views that have
394
- * associations to localized entities or to views that themselves have such
395
- * a dependency.
423
+ * associations to localized entities, views that themselves have such
424
+ * a dependency or views that contain projections on localized elements.
396
425
  *
397
426
  * The algorithm is as follows:
398
427
  *
399
- * 1. For each view/entity with associations:
400
- * - If target is NOT localized => add view/entity to target's `_targetFor` property
401
- * - If target is localized => add view/entity to array `entities`
428
+ * 1. For each view with elements that have `localized: true` markers:
429
+ * => add view to array `entities`
430
+ * For each view/entity with associations:
431
+ * - If target is NOT localized => add view/entity to target's `_targetFor` property
432
+ * - If target is localized => add view/entity to array `entities`
402
433
  * 2. As long as `entities` has entries:
403
- * a. For each entry in `entities`
404
- * - Create a convenience view
405
- * - If the entry has a `_targetFor` property, add its entries to
406
- * `nextEntities` because they now have a transitive dependency on a
407
- * localized view.
408
- * b. Copy all entries from `nextEntities` to `entities`.
409
- * c. Clear `nextEntities`.
434
+ * a. For each entry in `entities`
435
+ * - Create a convenience view
436
+ * - If the entry has a `_targetFor` property, add its entries to `nextEntities`
437
+ * because they now have a transitive dependency on a localized view.
438
+ * b. Copy all entries from `nextEntities` to `entities`.
439
+ * c. Clear `nextEntities`.
410
440
  * 3. Rewrite all references to the localized variants.
411
441
  */
412
442
  function createTransitiveConvenienceViews() {
@@ -448,10 +478,9 @@ function _addLocalizationViews(csn, options, useJoins, config) {
448
478
  // if the artifact is an entity (already processed in (1))
449
479
  entities.push(artName);
450
480
  }
451
- else if (elem.target) {
481
+ else if (!ignoreAssocToLocalized && elem.target) {
452
482
  // If the target has a localized view then we are localized as well.
453
483
  const def = csn.definitions[elem.target];
454
- // TODO: What if elem.target cannot be found? Could this happen after flattening, ...?
455
484
  if (!def)
456
485
  continue;
457
486
 
@@ -490,7 +519,7 @@ function _addLocalizationViews(csn, options, useJoins, config) {
490
519
 
491
520
  addLocalizedView(artName);
492
521
 
493
- if (art[_targetFor])
522
+ if (!ignoreAssocToLocalized && art[_targetFor])
494
523
  nextEntities.push(...art[_targetFor]);
495
524
  delete art[_targetFor];
496
525
  }
@@ -578,14 +607,22 @@ function _addLocalizationViews(csn, options, useJoins, config) {
578
607
  if (!obj || !obj.ref)
579
608
  return;
580
609
  const ref = Array.isArray(obj.ref) ? obj.ref[0] : obj.ref;
581
- if (typeof ref !== 'string')
582
- return;
583
- const def = csn.definitions[ref];
584
- if (def && def[_hasLocalizedView]) {
585
- if (Array.isArray(obj.ref))
586
- obj.ref[0] = def[_hasLocalizedView];
587
- else
610
+ if (typeof ref === 'string') {
611
+ const def = csn.definitions[ref];
612
+ if (def && def[_hasLocalizedView]) {
613
+ if (Array.isArray(obj.ref))
614
+ obj.ref[0] = def[_hasLocalizedView];
615
+ else
588
616
  obj.ref = def[_hasLocalizedView];
617
+ }
618
+
619
+ } else if (ref.id) {
620
+ const def = csn.definitions[ref.id];
621
+ if (def && def[_hasLocalizedView])
622
+ obj.ref[0].id = def[_hasLocalizedView];
623
+
624
+ } else if (options.testMode) {
625
+ throw new CompilerAssertion('Debug me: Unhandled reference during localized-rewrite!');
589
626
  }
590
627
  }
591
628
 
@@ -593,7 +630,7 @@ function _addLocalizationViews(csn, options, useJoins, config) {
593
630
  * @param {string} artName
594
631
  */
595
632
  function textsEntityName(artName) {
596
- // We can assume, that the element exists. This is checked in isEntityPreprocessed()
633
+ // We can assume that the element exists. This is checked in isEntityPreprocessed().
597
634
  return csn.definitions[artName].elements.texts.target;
598
635
  }
599
636
 
@@ -626,23 +663,23 @@ function _addLocalizationViews(csn, options, useJoins, config) {
626
663
  *
627
664
  * @param {CSN.Model} csn
628
665
  * @param {CSN.Options} options
629
- * @param [config] config.acceptLocalizedView: optional callback function returning true if the localized view name and its parent name provided as parameter should be created
666
+ * @param [config]
630
667
  */
631
668
  function addLocalizationViews(csn, options, config = {}) {
632
- return _addLocalizationViews(csn, options, false, config);
669
+ return _addLocalizationViews(csn, options, { ...config, useJoins: false });
633
670
  }
634
671
 
635
672
  /**
636
673
  * Create transitive localized convenience views to the given CSN but
637
674
  * rewrite the "localized" association to joins in direct entity convenience
638
- * views. This is needed by e.g. SQL for SQLite where A2J is used.
675
+ * views. This is needed e.g. by SQL for SQLite where A2J is used.
639
676
  *
640
677
  * @param {CSN.Model} csn
641
678
  * @param {CSN.Options} options
642
- * @param [config] config.acceptLocalizedView: optional callback function returning true if the localized view name and its parent name provided as parameter should be created
679
+ * @param [config]
643
680
  */
644
681
  function addLocalizationViewsWithJoins(csn, options, config = {}) {
645
- return _addLocalizationViews(csn, options, true, config);
682
+ return _addLocalizationViews(csn, options, { ...config, useJoins: true });
646
683
  }
647
684
 
648
685
  /**
@@ -265,7 +265,7 @@ function getTransformers(model, options, pathDelimiter = '_') {
265
265
  } else {
266
266
  // Primitive child - clone it and restore its cross references
267
267
  let flatElemName = elemName + pathDelimiter + childName;
268
- let flatElem = cloneCsnNonDict(childElem, options);
268
+ let flatElem = cloneCsnNonDict(childElem, { ...options, hiddenPropertiesToClone: [ '$structRef', '$fkExtensions' ] } );
269
269
  // Don't take over notNull from leaf elements
270
270
  delete flatElem.notNull;
271
271
  setProp(flatElem, '_flatElementNameWithDots', elementPath.concat(childName).join('.'));
@@ -536,7 +536,7 @@ function getTransformers(model, options, pathDelimiter = '_') {
536
536
  addElement(draftUuid, artifact, artifactName);
537
537
 
538
538
  // CreationDateTime : Timestamp;
539
- const creationDateTime = createScalarElement('CreationDateTime', hanaMode ? 'cds.UTCTimestamp' : 'cds.Timestamp');
539
+ const creationDateTime = createScalarElement('CreationDateTime', 'cds.Timestamp');
540
540
  creationDateTime.CreationDateTime['@Common.Label'] = '{i18n>Draft_CreationDateTime}';
541
541
  addElement(creationDateTime, artifact, artifactName);
542
542
 
@@ -553,7 +553,7 @@ function getTransformers(model, options, pathDelimiter = '_') {
553
553
  addElement(draftIsCreatedByMe, artifact, artifactName);
554
554
 
555
555
  // LastChangeDateTime : Timestamp;
556
- const lastChangeDateTime = createScalarElement('LastChangeDateTime', hanaMode ? 'cds.UTCTimestamp' : 'cds.Timestamp');
556
+ const lastChangeDateTime = createScalarElement('LastChangeDateTime', 'cds.Timestamp');
557
557
  lastChangeDateTime.LastChangeDateTime['@Common.Label'] = '{i18n>Draft_LastChangeDateTime}';
558
558
  addElement(lastChangeDateTime, artifact, artifactName);
559
559
 
@@ -1175,13 +1175,12 @@ function getTransformers(model, options, pathDelimiter = '_') {
1175
1175
  // do the paths match?
1176
1176
  if(op !== 'like' && !(x.lhs && x.rhs)) {
1177
1177
  if(xn.length) {
1178
- error(null, location,
1179
- {
1180
- prefix: prefix(lhs, op, rhs),
1181
- name: xn,
1182
- alias: (x.lhs ? rhs : lhs).ref.join('.')
1183
- },
1184
- '$(PREFIX): Sub path $(NAME) not found in $(ALIAS)');
1178
+ error('expr-invalid-expansion', location, {
1179
+ value: prefix(lhs, op, rhs),
1180
+ name: xn,
1181
+ alias: (x.lhs ? rhs : lhs).ref.join('.')
1182
+ },
1183
+ 'Missing sub path $(NAME) in $(ALIAS) for tuple expansion of $(VALUE); both sides must expand to the same sub paths');
1185
1184
  }
1186
1185
  else {
1187
1186
  error(null, location,
package/lib/utils/file.js CHANGED
@@ -101,22 +101,22 @@ function cdsFs( fileCache, enableTrace ) {
101
101
  if (body && body.stack && body.message) {
102
102
  // NOTE: checks for instanceof Error are not reliable if error
103
103
  // created in different execution env
104
- traceFS( 'READFILE:cache-error:', filename, body.message );
104
+ traceFS( 'READFILE:cache-err:', filename, body.message );
105
105
  cb( body ); // no need for process.nextTick( cb, body ) with moduleResolve
106
106
  }
107
107
  else {
108
- traceFS( 'READFILE:cache:', filename, body );
108
+ traceFS( 'READFILE:cache: ', filename, body );
109
109
  cb( null, body );
110
110
  }
111
111
  }
112
112
  else {
113
- traceFS( 'READFILE:start:', filename );
113
+ traceFS( 'READFILE:start: ', filename );
114
114
  // TODO: set cache directly to some "delay" - store error differently?
115
115
  // e.g. an error of callback functions!
116
116
  try {
117
117
  reader(filename, enc, (err, data) => {
118
118
  fileCache[filename] = err || data;
119
- traceFS('READFILE:data:', filename, err || data);
119
+ traceFS('READFILE:data: ', filename, err || data);
120
120
  cb(err, data);
121
121
  });
122
122
  }
@@ -137,14 +137,14 @@ function cdsFs( fileCache, enableTrace ) {
137
137
  return ( filename, cb ) => {
138
138
  let body = fileCache[filename];
139
139
  if (body !== undefined) {
140
- traceFS( 'ISFILE:cache:', filename, body );
140
+ traceFS( 'ISFILE:cache: ', filename, body );
141
141
  if (body instanceof Error)
142
142
  cb( body ); // no need for process.nextTick( cb, body ) with moduleResolve
143
143
  else // body could be empty string
144
144
  cb( null, !!body || typeof body === 'string');
145
145
  }
146
146
  else {
147
- traceFS( 'ISFILE:start:', filename, body );
147
+ traceFS( 'ISFILE:start: ', filename, body );
148
148
  // in the future (if we do module resolve ourselves with just readFile),
149
149
  // we avoid parallel readFile by storing having an array of `cb`s in
150
150
  // fileCache[ filename ] before starting fs.readFile().
@@ -156,7 +156,7 @@ function cdsFs( fileCache, enableTrace ) {
156
156
  body = !!(stat.isFile() || stat.isFIFO());
157
157
  if (fileCache[filename] === undefined) // parallel readFile() has been processed
158
158
  fileCache[filename] = body;
159
- traceFS('ISFILE:data:', filename, body);
159
+ traceFS('ISFILE:data: ', filename, body);
160
160
  if (body instanceof Error)
161
161
  cb(err);
162
162
  else