@sap/cds-compiler 6.8.0 → 6.9.1

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 (44) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +4 -0
  3. package/bin/cdshi.js +1 -0
  4. package/lib/api/main.js +8 -1
  5. package/lib/api/options.js +3 -1
  6. package/lib/base/builtins.js +13 -9
  7. package/lib/base/csnRefs.js +8 -10
  8. package/lib/base/message-registry.js +61 -3
  9. package/lib/base/messages.js +2 -0
  10. package/lib/base/optionProcessor.js +2 -0
  11. package/lib/base/specialOptions.js +1 -1
  12. package/lib/compiler/assert-consistency.js +11 -9
  13. package/lib/compiler/base.js +5 -1
  14. package/lib/compiler/define.js +1 -1
  15. package/lib/compiler/dictionaries.js +2 -3
  16. package/lib/compiler/extend.js +137 -27
  17. package/lib/compiler/lsp-api.js +3 -3
  18. package/lib/compiler/populate.js +4 -5
  19. package/lib/compiler/resolve.js +50 -35
  20. package/lib/compiler/shared.js +33 -14
  21. package/lib/compiler/tweak-assocs.js +2 -2
  22. package/lib/compiler/utils.js +26 -23
  23. package/lib/compiler/xpr-rewrite.js +2 -2
  24. package/lib/edm/EdmPrimitiveTypeDefinitions.js +4 -1
  25. package/lib/edm/annotations/genericTranslation.js +49 -6
  26. package/lib/edm/csn2edm.js +4 -2
  27. package/lib/edm/edm.js +6 -3
  28. package/lib/gen/BaseParser.js +59 -97
  29. package/lib/gen/CdlGrammar.checksum +1 -1
  30. package/lib/gen/CdlParser.js +2055 -1969
  31. package/lib/gen/Dictionary.json +67 -7
  32. package/lib/json/from-csn.js +7 -12
  33. package/lib/json/to-csn.js +59 -35
  34. package/lib/parsers/AstBuildingParser.js +43 -30
  35. package/lib/render/toCdl.js +46 -27
  36. package/lib/render/toSql.js +9 -0
  37. package/lib/render/utils/common.js +3 -2
  38. package/lib/tool-lib/enrichCsn.js +1 -0
  39. package/lib/transform/effective/flattening.js +6 -5
  40. package/lib/transform/effective/main.js +5 -0
  41. package/lib/transform/forOdata.js +20 -2
  42. package/lib/transform/forRelationalDB.js +8 -4
  43. package/lib/transform/tupleExpansion.js +40 -0
  44. package/package.json +3 -40
@@ -11,7 +11,7 @@ const {
11
11
  setLink,
12
12
  setArtifactLink,
13
13
  copyExpr,
14
- setExpandStatusAnnotate,
14
+ setExpandStatus,
15
15
  linkToOrigin,
16
16
  initItemsLinks,
17
17
  setMemberParent,
@@ -84,6 +84,8 @@ function extend( model ) {
84
84
  const includesNonShadowedFirst
85
85
  = isDeprecatedEnabled( model.options, '_includesNonShadowedFirst' );
86
86
 
87
+ const includeCollisions = [];
88
+
87
89
  forEachGeneric( model, 'definitions', tagCompositionTargets );
88
90
  dictForEach( model.$collectedExtensions, e => e._extensions.forEach( tagCompositionTargets ) );
89
91
  // remark: tagging on extensions works _before_ running extendArtifactBefore() on each artifact
@@ -174,7 +176,7 @@ function extend( model ) {
174
176
  // elements etc. TODO: do that more specifically on the dicts (via symbol)
175
177
  // Probably better: we could use the _extensions dict prop directly in to-csn
176
178
  if (art.$inferred)
177
- setExpandStatusAnnotate( art, 'annotate' );
179
+ setExpandStatus( art, 'annotate' );
178
180
  if (Array.isArray( art._extensions )) {
179
181
  checkExtensionsKind( art._extensions, art );
180
182
  transformArtifactExtensions( art );
@@ -258,7 +260,8 @@ function extend( model ) {
258
260
  checkReturnsExtension( ext, art );
259
261
  }
260
262
  // if (art.elements || art.enum || art.kind === 'annotate')
261
- moveDictExtensions( art, extensionsMap, (art.enum ? 'enum' : 'elements'), 'elements' );
263
+ moveDictExtensions( art, extensionsMap,
264
+ (art.enum ? 'enum' : 'elements'), false );
262
265
  }
263
266
  }
264
267
 
@@ -399,7 +402,9 @@ function extend( model ) {
399
402
  pushTo$add( dict, ext );
400
403
  }
401
404
  else if (prop.charAt(0) === '@' || prop === 'doc' || prop === 'columns' ||
402
- prop === 'length' || prop === 'scale' || prop === 'precision' || prop === 'srid') {
405
+ prop === 'groupBy' || prop === 'where' || prop === 'having' ||
406
+ prop === 'orderBy' || prop === 'limit' || prop === 'length' ||
407
+ prop === 'scale' || prop === 'precision' || prop === 'srid') {
403
408
  if (!isAutoItemsOrReturns)
404
409
  pushToDict( dict, prop, ext );
405
410
  }
@@ -577,7 +582,7 @@ function extend( model ) {
577
582
  else if (prop === 'columns') {
578
583
  const { query } = art;
579
584
  for (const col of ext.columns)
580
- col.$extended = true;
585
+ col.$extended = 'columns';
581
586
 
582
587
  if (art.kind === 'annotate' && art.$inferred === '')
583
588
  return; // internal super-annotate for unknown artifacts
@@ -593,6 +598,10 @@ function extend( model ) {
593
598
  query.columns.push( ...ext.columns );
594
599
  ext.columns.forEach( col => changeParentLinks( col, query ) );
595
600
  }
601
+ else if (prop === 'groupBy' || prop === 'where' || prop === 'having' ||
602
+ prop === 'orderBy' || prop === 'limit') {
603
+ applyQueryClause( prop, ext, art );
604
+ }
596
605
  else if (typeParameters.list.includes( prop )) {
597
606
  const typeExts = art.$typeExts || (art.$typeExts = {});
598
607
  typeExts[prop] = ext;
@@ -603,6 +612,39 @@ function extend( model ) {
603
612
  }
604
613
  }
605
614
 
615
+ function applyQueryClause( prop, ext, art ) {
616
+ const { query } = art;
617
+ const clause = ext[prop];
618
+ const isArray = Array.isArray( clause );
619
+
620
+ if (prop !== 'limit') {
621
+ const items = isArray ? clause : [ clause ];
622
+ for (const item of items) {
623
+ item.$extended = prop;
624
+ setLink( item, '_block', ext._block );
625
+ setLink( item, '_outer', query );
626
+ }
627
+ }
628
+ if (!query?.from?.path) {
629
+ const variant = (query?.from || query)?.op?.val || 'std';
630
+ const loc = isArray ? clause[$location] : clause.location;
631
+ error( `extend-${ prop.toLowerCase() }`, [ loc, ext ], { '#': variant, art } );
632
+ return;
633
+ }
634
+ if (isArray) {
635
+ if (!query[prop])
636
+ query[prop] = [];
637
+ query[prop].push( ...clause );
638
+ }
639
+ else {
640
+ if (query[prop]) {
641
+ error( 'ext-unexpected-sql-clause', [ clause.location, ext ], { art, keyword: prop } );
642
+ return;
643
+ }
644
+ query[prop] = clause;
645
+ }
646
+ }
647
+
606
648
  function changeParentLinks( art, queryOrMain ) {
607
649
  // TODO: we might also change the implicit name (if name.id is a number,
608
650
  // adding the previous column lenght - 1) for better error messages
@@ -838,18 +880,18 @@ function extend( model ) {
838
880
  }
839
881
 
840
882
  function moveDictExtensions( art, extensionsMap, artProp, extProp = artProp ) {
841
- // TODO: setExpandStatusAnnotate
842
- const extensions = extensionsMap[extProp];
883
+ // TODO: setExpandStatus
884
+ const extensions = extensionsMap[extProp || 'elements'];
843
885
  if (!extensions)
844
886
  return;
845
887
 
846
888
  for (const ext of extensions) {
847
889
  let dictCheck = (art.kind !== 'annotate'); // no check in super annotate statement
848
- forEachGeneric( ext, extProp, ( elemExt, name ) => {
890
+ forEachGeneric( ext, extProp || (ext.enum ? 'enum' : 'elements'), ( elemExt, name ) => {
849
891
  if (elemExt.kind !== 'annotate' && elemExt.kind !== 'extend') // TODO: specified elems
850
892
  return; // definitions inside extend, already handled
851
893
  dictCheck = dictCheck && checkRemainingMemberExtensions( art, elemExt, artProp, name );
852
- const elem = art[artProp]?.[name] || annotateFor( art, extProp, name );
894
+ const elem = art[artProp]?.[name] || annotateFor( art, extProp || 'elements', name );
853
895
  setLink( elemExt.name, '_artifact', (elem.kind !== 'annotate' ? elem : null ) );
854
896
  // TODO: why null for annotate?
855
897
  ensureArtifactNotProcessed( elem );
@@ -1055,11 +1097,23 @@ function extend( model ) {
1055
1097
 
1056
1098
  function checkRemainingMainExtensions( art, ext ) {
1057
1099
  const refCtx = extensionRefContext( ext );
1058
- if (!resolvePath( ext.name, refCtx, ext )) // error for extend, info for annotate
1059
- return;
1060
-
1061
- if (art?.builtin) { // TODO: do via accept
1062
- info( 'anno-builtin', [ ext.name.location, ext ], {} ); // TODO: better location?
1100
+ if (resolvePath( ext.name, refCtx, ext ) && art?.builtin) {
1101
+ if (ext.kind === 'extend') {
1102
+ // extending built-ins with elements/enums already gives an error
1103
+ warning( 'ext-unexpected-builtin', [ ext.name.location, ext ], {}, // error v8?
1104
+ 'Built-in types should not be extended' ); // keep the text general
1105
+ const typeProp = typeParameters.list.find( p => ext[p] );
1106
+ if (typeProp) {
1107
+ const location = ext.$typeArgs?.[$location] || ext[typeProp].location;
1108
+ message( 'ext-unexpected-type-property', [ location, ext ], {}, // error v7
1109
+ 'Built-in types can\'t be extended with type properties' );
1110
+ // see also 'ext-invalid-type-property'
1111
+ }
1112
+ }
1113
+ else {
1114
+ info( 'anno-builtin', [ ext.name.location, ext ], {} ); // TODO: better location?
1115
+ }
1116
+ // TODO: remove built-ins as CC candidates via accept property of ./shared.js
1063
1117
  }
1064
1118
  }
1065
1119
 
@@ -1119,7 +1173,8 @@ function extend( model ) {
1119
1173
  }
1120
1174
  if (art._extensions?.$add)
1121
1175
  extendArtifact( art._extensions.$add, art );
1122
- // TODO: for proper shadowing: first collect defined element & action names
1176
+ checkRedefinitionThroughIncludes( art, 'elements' );
1177
+ checkRedefinitionThroughIncludes( art, 'actions' );
1123
1178
  }
1124
1179
 
1125
1180
  /**
@@ -1142,10 +1197,33 @@ function extend( model ) {
1142
1197
  // TODO: complain if $inferred
1143
1198
  // checkExtensionsKind( extensions, art );
1144
1199
  extendMembers( extensions, art );
1200
+ reportIncludeCollisions( art );
1145
1201
  // TODO: complain about element extensions inside projection
1146
1202
  return true;
1147
1203
  }
1148
1204
 
1205
+ function reportIncludeCollisions( art ) {
1206
+ const grouped = Object.create( null );
1207
+ for (const {
1208
+ prop, name, existing, elem,
1209
+ } of includeCollisions) {
1210
+ const key = `${ prop }:${ name }`;
1211
+ if (!grouped[key])
1212
+ grouped[key] = { prop, name, collisions: new Set( [ existing ] ) };
1213
+ grouped[key].collisions.add( elem );
1214
+ }
1215
+ for (const key in grouped) {
1216
+ const { prop, name, collisions } = grouped[key];
1217
+ const member = art[prop]?.[name];
1218
+ if (member?.$inferred === 'include') {
1219
+ const arts = [ ...collisions ].map( m => m._origin._main );
1220
+ message( 'ext-duplicate-include', [ art.name.location, member ],
1221
+ { '#': prop, name, sorted_arts: arts } );
1222
+ }
1223
+ }
1224
+ includeCollisions.length = 0;
1225
+ }
1226
+
1149
1227
  function extendMembers( extensions, art ) {
1150
1228
  // TODO: do the whole extension stuff lazily if the elements are requested
1151
1229
  const elemExtensions = [];
@@ -1154,7 +1232,11 @@ function extend( model ) {
1154
1232
  // TODO: use same sequence as in chooseAssignment() - better: use common code with that fn
1155
1233
  // console.log('EM:',art.name,extensions,art._extensions)
1156
1234
  for (const ext of extensions) { // those in extMap.includes
1157
- if (art.$inferred) {
1235
+ if (art.$inferred === 'include') {
1236
+ error( 'ref-expected-direct-structure', [ ext.name.location, ext ],
1237
+ { '#': 'elements', art } );
1238
+ }
1239
+ else if (art.$inferred) {
1158
1240
  error( 'extend-for-generated', [ ext.name.location, ext ], { art, keyword: 'extend' },
1159
1241
  'You can\'t use $(KEYWORD) on the generated $(ART)' ); // or with inferred elements
1160
1242
  }
@@ -1298,6 +1380,8 @@ function extend( model ) {
1298
1380
 
1299
1381
  if (obj !== parent && obj.elements && parent.enum) { // applying the extension
1300
1382
  initElementsAsEnum();
1383
+ if (parent.enum[$inferred])
1384
+ setExpandStatus( parent, 'extend' );
1301
1385
  }
1302
1386
  else {
1303
1387
  if (checkDefinitions( construct, parent, 'elements', obj.elements || false ))
@@ -1341,7 +1425,11 @@ function extend( model ) {
1341
1425
  e.kind = 'enum';
1342
1426
  hasElement = true; // warning with CDL input or `name: {}` in CSN input
1343
1427
  }
1344
- if (hasElement) {
1428
+ if (!parent.type || parent.type.$inferred || (parent._main || parent).query) {
1429
+ error( 'extend-type', [ obj.elements[$location], construct ], {},
1430
+ 'Only structures or enum types can be extended with elements/enums' );
1431
+ }
1432
+ else if (hasElement) {
1345
1433
  warning( 'ext-expecting-enum', [ obj.elements[$location], construct ],
1346
1434
  { code: 'extend … with enum' }, 'Use $(CODE) when extending enums' );
1347
1435
  }
@@ -1404,26 +1492,45 @@ function extend( model ) {
1404
1492
 
1405
1493
  if (isQueryExtension && elem.kind === 'element') {
1406
1494
  error( 'extend-query', [ elem.location, construct ], // TODO: searchName ?
1407
- { code: 'extend projection' },
1495
+ { code: 'extend … with columns' },
1408
1496
  'Use $(CODE) to add select items to the query entity' );
1409
1497
  return;
1410
1498
  }
1411
1499
 
1412
1500
  const existing = parent[prop]?.[name];
1501
+ const shadowsIncludeMember = construct !== parent &&
1502
+ elem.$inferred !== 'include' &&
1503
+ existing?.$inferred === 'include';
1413
1504
  const add = construct !== parent && (!existing || elem.$inferred !== 'include');
1505
+ if (!add && existing?.$inferred === 'include' && elem.$inferred === 'include') {
1506
+ includeCollisions.push( {
1507
+ prop, name, existing, elem,
1508
+ } );
1509
+ }
1414
1510
  // don't dump with `entity T {}; extend T with { extend e {}; e {}; e {} };`:
1415
1511
  const { $duplicates } = elem;
1416
1512
  if ($duplicates === true && add)
1417
1513
  elem.$duplicates = null;
1418
- setMemberParent( elem, name, parent, add && prop );
1419
- if (!$duplicates) // not already reported
1420
- checkRedefinition( elem );
1514
+ if (add && elem.$inferred === 'include' && Array.isArray( $duplicates ))
1515
+ elem.$duplicates = null;
1516
+ if (shadowsIncludeMember) {
1517
+ parent[prop][name] = elem;
1518
+ setMemberParent( elem, name, parent );
1519
+ }
1520
+ else {
1521
+ setMemberParent( elem, name, parent, add && prop );
1522
+ if (!$duplicates) // not already reported
1523
+ checkRedefinition( elem );
1524
+ }
1421
1525
  initMembers( elem, elem, elem._block );
1422
1526
  if (elem.kind === 'action' || elem.kind === 'function')
1423
1527
  initBoundSelfParam( elem.params, elem._main );
1424
1528
 
1425
1529
  // for a correct home path, setMemberParent needed to be called
1426
1530
 
1531
+ if (parent[prop]?.[$inferred])
1532
+ setExpandStatus( parent, 'extend' );
1533
+
1427
1534
  if (!elem.value || elem.kind !== 'element')
1428
1535
  return;
1429
1536
  // remark: potential enum elements have already been turned into enums
@@ -1502,10 +1609,14 @@ function extend( model ) {
1502
1609
  error( 'ref-expected-direct-structure', [ location, construct ],
1503
1610
  { '#': variant, art: parent } );
1504
1611
  }
1505
- else {
1612
+ else if (prop !== 'enum' || !parent.enum ||
1613
+ !parent.type || parent.type.$inferred || (parent._main || parent).query) {
1506
1614
  error( 'extend-type', [ location, construct ], {},
1507
1615
  'Only structures or enum types can be extended with elements/enums' );
1508
1616
  }
1617
+ else {
1618
+ return true;
1619
+ }
1509
1620
  }
1510
1621
  else if (prop === 'elements') {
1511
1622
  error( 'def-unexpected-elements', [ location, construct ], {},
@@ -1585,6 +1696,7 @@ function extend( model ) {
1585
1696
  // TODO two kind of messages:
1586
1697
  // Error 'More than one include defines element "A"' (at include ref)
1587
1698
  // Warning 'Overwrites definition from include "I" (at elem def)
1699
+ const propagateKeys = art.kind !== 'type' || !model.options.v7KeyPropagation;
1588
1700
  const parent = ext === art && art;
1589
1701
  const members = ext[prop];
1590
1702
  // if (members)console.log( 'EXT:', prop, art.kind, art.name.id, ...Object.keys(members));
@@ -1624,7 +1736,7 @@ function extend( model ) {
1624
1736
  elem.$inferred = 'include';
1625
1737
  if (origin.masked) // TODO(v6): remove 'masked'
1626
1738
  elem.masked = Object.assign( { $inferred: 'include' }, origin.masked );
1627
- if (origin.key)
1739
+ if (origin.key && propagateKeys)
1628
1740
  elem.key = Object.assign( { $inferred: 'include' }, origin.key );
1629
1741
  if (origin.value && origin.$syntax === 'calc') {
1630
1742
  // TODO: If paths become invalid in the new artifact, should we mark
@@ -1640,9 +1752,6 @@ function extend( model ) {
1640
1752
  } );
1641
1753
  }
1642
1754
  }
1643
-
1644
- checkRedefinitionThroughIncludes( parent, prop );
1645
-
1646
1755
  if (!hasNewElement && members) {
1647
1756
  ext[prop] = members;
1648
1757
  }
@@ -1688,7 +1797,8 @@ function extend( model ) {
1688
1797
 
1689
1798
  /**
1690
1799
  * Report duplicates in parent[prop] that happen due to multiple includes having the
1691
- * same member. Covers `entity G : E, G {};` but not `entity G : E {}; extend G with F;`.
1800
+ * same member. Run after includes and extends so shadowing members have already
1801
+ * replaced any include-derived survivors in-place.
1692
1802
  */
1693
1803
  function checkRedefinitionThroughIncludes( parent, prop ) {
1694
1804
  if (!parent[prop])
@@ -7,7 +7,7 @@
7
7
  //
8
8
  // This files includes an iterator over "semantic tokens" in an XSN model.
9
9
  // "Semantic tokens" are identifiers, but also the "return" parameter.
10
- // See <../../internalDoc/lsp/IdentifierCrawling.md> for details.
10
+ // See <../../internalDoc/lsp/SemanticTokenCrawling.md> for details.
11
11
 
12
12
  const { CompilerAssertion } = require('../base/error');
13
13
  const $inferred = Symbol.for( 'cds.$inferred' );
@@ -190,7 +190,7 @@ function* artifactTokens( art ) {
190
190
  * @returns {Generator<LspSemanticTokenEvent>}
191
191
  */
192
192
  function* extensionTokens( ext ) {
193
- if (ext.kind !== 'extend' && ext.kind !== 'annotate')
193
+ if (ext.kind !== 'extend' && ext.kind !== 'annotate' || !ext.name)
194
194
  return null;
195
195
 
196
196
  const wasApplied = ext.name._artifact && !ext.name._artifact.$inferred;
@@ -203,7 +203,7 @@ function* extensionTokens( ext ) {
203
203
 
204
204
  // We need to traverse all dictionaries that could themselves contain
205
205
  // extensions. Enum extensions or columns don't need to be traversed,
206
- // for example, because there can't be inner extensions.
206
+ // for example, because there can't be inner extensions. TODO: still?
207
207
  yield* dictOf( extensionTokens )( ext.params );
208
208
  yield* dictOf( extensionTokens )( ext.actions );
209
209
  yield* dictOf( extensionTokens )( ext.elements );
@@ -36,7 +36,6 @@ const {
36
36
  dependsOn,
37
37
  proxyCopyMembers,
38
38
  setExpandStatus,
39
- setExpandStatusAnnotate,
40
39
  dependsOnSilent,
41
40
  columnRefStartsWithSelf,
42
41
  forEachDefinition,
@@ -218,8 +217,8 @@ function populate( model ) {
218
217
  populateGeneratedEntity( a );
219
218
  if (a.includes)
220
219
  a.includes.forEach( i => resolveInclude( i, a ) );
221
- extendArtifactAdd( a );
222
220
  art = populateArtifact( a, art ) || a;
221
+ extendArtifactAdd( a );
223
222
  setLink( a, '_effectiveType', art );
224
223
  a.$effectiveSeqNo = ++effectiveSeqNo;
225
224
  // console.log('PE:',require('../model/revealInternalProperties').ref(a))
@@ -614,7 +613,7 @@ function populate( model ) {
614
613
  }
615
614
 
616
615
  if (wasAnnotated)
617
- setExpandStatusAnnotate( art, 'annotate' );
616
+ setExpandStatus( art, 'annotate' );
618
617
 
619
618
  // TODO: We don't check enum$, yet! We first need to fix expansion for
620
619
  // `cast(elem as EnumType)` (see #9421)
@@ -668,7 +667,7 @@ function populate( model ) {
668
667
  }
669
668
  }
670
669
  if (wasAnnotated)
671
- setExpandStatusAnnotate( art, 'annotate' );
670
+ setExpandStatus( art, 'annotate' );
672
671
 
673
672
  for (const id in art.foreignKeys$) {
674
673
  const specifiedElement = art.foreignKeys$[id];
@@ -767,7 +766,7 @@ function populate( model ) {
767
766
  const { targetMax } = path[path.length - 1].cardinality ||
768
767
  getInheritedProp( assoc, 'cardinality' ) || {};
769
768
  if (targetMax && (targetMax.val === '*' || targetMax.val > 1)) {
770
- elem.items = { location: elem.expand[$location] };
769
+ elem.items = { location: elem.expand[$location], $inferred: 'query' };
771
770
  setLink( elem.items, '_outer', elem );
772
771
  }
773
772
  return initFromColumns( elem, elem.expand );
@@ -39,7 +39,7 @@
39
39
  'use strict';
40
40
 
41
41
  const { isDeprecatedEnabled } = require('../base/specialOptions');
42
- const { dictAdd } = require('./dictionaries');
42
+ const { dictAdd, pushToDict } = require('./dictionaries');
43
43
  const { weakLocation } = require('../base/location');
44
44
  const { combinedLocation } = require('../base/location');
45
45
  const { typeParameters } = require('./builtins');
@@ -285,9 +285,9 @@ function resolve( model ) {
285
285
  // we don't propagate keys to type projections, see #13575
286
286
  return;
287
287
  }
288
- // Second argument true ensure that `key` is only propagated along simple
288
+ // Second argument controls whether `key` is only propagated along simple
289
289
  // view, i.e. ref or subquery in FROM, not UNION or JOIN.
290
- traverseQueryPost( view.query, true, ( query ) => {
290
+ traverseQueryPost( view.query, !options.v7KeyPropagation, ( query ) => {
291
291
  if (!withExplicitKeys( query ) && inheritKeyProp( query ) &&
292
292
  withKeyPropagation( query )) // now the part with messages
293
293
  inheritKeyProp( query, true );
@@ -331,47 +331,47 @@ function resolve( model ) {
331
331
  return head?.kind === '$tableAlias' && item._artifact?.key;
332
332
  }
333
333
 
334
- function primarySourceNavigation( aliases ) {
335
- for (const name in aliases)
336
- return aliases[name].elements;
337
- return undefined;
338
- }
339
-
340
334
  function withKeyPropagation( query ) {
341
- const { from } = query;
335
+ const { from, $tableAliases } = query;
342
336
  if (!from) // parse error SELECT FROM <EOF>
343
337
  return false;
344
-
345
338
  let propagateKeys = true; // used instead early RETURN to get more messages
346
- const toMany = withAssociation( from, targetMaxNotOne, true );
347
- if (toMany) {
348
- propagateKeys = false;
349
- info( 'query-from-many', [ toMany.location, query ], { art: toMany }, {
350
- std: 'Key properties are not propagated because a to-many association $(ART) is selected',
351
- // eslint-disable-next-line @stylistic/max-len
352
- element: 'Key properties are not propagated because a to-many association $(MEMBER) of $(ART) is selected',
353
- } );
354
- }
339
+
355
340
  // Check that all keys from the source are projected:
356
- const notProjected = []; // we actually push to the array
357
- const navElems = primarySourceNavigation( query.$tableAliases );
341
+ const notProjected = Object.create( null );
342
+ const navElems = query._combined;
358
343
  for (const name in navElems) {
359
344
  const nav = navElems[name];
360
- if (nav.$duplicates)
361
- continue;
362
- const { key } = nav._origin;
363
- if (key?.val && !nav._projections?.length)
364
- notProjected.push( nav.name.id );
345
+ if (Array.isArray( nav ))
346
+ nav.forEach( pushNonProjected );
347
+ else
348
+ pushNonProjected( nav );
365
349
  }
366
- if (notProjected.length) {
350
+ for (const [ alias, names ] of Object.entries( notProjected )) {
367
351
  propagateKeys = false;
368
- info( 'query-missing-keys', [ from.location, query ], { names: notProjected },
352
+ // TODO: mention alias
353
+ const location = $tableAliases[alias].name?.location || from.location;
354
+ info( 'query-missing-keys', [ location, query ], { names },
369
355
  {
370
356
  std: 'Keys $(NAMES) have not been projected - key properties are not propagated',
371
357
  one: 'Key $(NAMES) has not been projected - key properties are not propagated',
372
358
  } );
373
359
  }
374
- // Check that there is no to-many assoc used in select item:
360
+ if (options.v7KeyPropagation)
361
+ return propagateKeys;
362
+
363
+ // Check that there is no to-many assoc in the FROM reference or in the select
364
+ // item reference (references in expressions are not checked,
365
+ // withAssociation() will see no _artifact links anyway)
366
+ const toMany = withAssociation( from, targetMaxNotOne, true );
367
+ if (toMany) {
368
+ propagateKeys = false;
369
+ info( 'query-from-many', [ toMany.location, query ], { art: toMany }, {
370
+ std: 'Key properties are not propagated because a to-many association $(ART) is selected',
371
+ // eslint-disable-next-line @stylistic/max-len
372
+ element: 'Key properties are not propagated because a to-many association $(MEMBER) of $(ART) is selected',
373
+ } );
374
+ }
375
375
  for (const name in query.elements) {
376
376
  const elem = query.elements[name];
377
377
 
@@ -383,6 +383,14 @@ function resolve( model ) {
383
383
  }
384
384
  return propagateKeys;
385
385
 
386
+ function pushNonProjected( nav ) {
387
+ if (nav.$duplicates)
388
+ return;
389
+ const { key } = nav._origin;
390
+ if (key?.val && !nav._projections?.length)
391
+ pushToDict( notProjected, nav._parent.name.id, nav.name.id );
392
+ }
393
+
386
394
  function selectTest( expr, user ) {
387
395
  const art = withAssociation( expr, targetMaxNotOne );
388
396
  if (art) {
@@ -1004,11 +1012,16 @@ function resolve( model ) {
1004
1012
  forEachGeneric( query, 'elements', resolveRefs );
1005
1013
  if (query.from)
1006
1014
  resolveJoinOn( query.from );
1007
- if (query.where)
1008
- resolveExpr( query.where, 'where', query );
1015
+ if (query.where) {
1016
+ const user = query.where._block ? query.where : query;
1017
+ resolveExpr( query.where, 'where', user );
1018
+ }
1009
1019
  if (query.groupBy)
1010
1020
  resolveBy( query.groupBy, 'groupBy', 'groupBy' );
1011
- resolveExpr( query.having, 'having', query );
1021
+ if (query.having) {
1022
+ const user = query.having._block ? query.having : query;
1023
+ resolveExpr( query.having, 'having', user );
1024
+ }
1012
1025
  if (query.$orderBy) // ORDER BY from UNION:
1013
1026
  // TODO clarify: can I access the tab alias of outer queries? If not:
1014
1027
  // 4th arg query._main instead query._parent.
@@ -1051,8 +1064,10 @@ function resolve( model ) {
1051
1064
  */
1052
1065
  function resolveBy( array, refMode, exprMode ) {
1053
1066
  for (const value of array ) {
1054
- if (value)
1055
- resolveExpr( value, (value.path ? refMode : exprMode), query );
1067
+ if (value) {
1068
+ const user = value._block ? value : query;
1069
+ resolveExpr( value, (value.path ? refMode : exprMode), user );
1070
+ }
1056
1071
  }
1057
1072
  }
1058
1073
 
@@ -790,6 +790,8 @@ function fns( model ) {
790
790
  return undefined; // parse error
791
791
  if (head._artifact !== undefined)
792
792
  return head._artifact;
793
+ if (user.$extended && user._outer && !semantics.isMainRef)
794
+ user = user._outer;
793
795
  let ruser = user._user || user; // TODO: nicer name if we keep this
794
796
  // TODO: re-think _user link
795
797
  if (ruser._outer && !semantics.isMainRef) {
@@ -982,8 +984,6 @@ function fns( model ) {
982
984
  art.kind === '$self' && path[0].id === '$projection') {
983
985
  // Rewrite $projection to $self
984
986
  path[0].id = '$self';
985
- warning( 'ref-expecting-$self', [ path[0].location, user ],
986
- { code: '$projection', newcode: '$self' });
987
987
  }
988
988
  return art.name?.$inferred !== '$internal'; // not a compiler-generated internal alias
989
989
  }
@@ -1035,13 +1035,23 @@ function fns( model ) {
1035
1035
  return null;
1036
1036
  }
1037
1037
  case 'mixin': {
1038
- // use a source element having that name if in `extend … with columns`:
1039
- const elem = (user._user || user).$extended &&
1040
- art._parent._combined[head.id];
1038
+ // use a source element having that name if in `extend … with` (columns or groupBy):
1039
+ const $extended = user._user?.$extended ?? user.$extended;
1040
+ const elem = $extended && art._parent._combined[head.id];
1041
1041
  if (elem) {
1042
1042
  path.$prefix = elem._parent.name.id; // prepend alias name
1043
- info( 'ref-special-in-extend', [ head.location, user ],
1044
- { '#': 'mixin', id: head.id, art: elem._origin._main } );
1043
+ if ($extended === 'columns') {
1044
+ warning( 'ref-special-in-extend', [ head.location, user ],
1045
+ { '#': 'mixin', id: head.id, art: elem._origin._main } );
1046
+ }
1047
+ else {
1048
+ error( 'ref-unexpected-in-extend', [ head.location, user ], {
1049
+ '#': 'mixin',
1050
+ keyword: $extended,
1051
+ id: head.id,
1052
+ art: elem._origin._main,
1053
+ } );
1054
+ }
1045
1055
  setLink( head, '_navigation', elem );
1046
1056
  return setArtifactLink( head, elem._origin );
1047
1057
  }
@@ -1052,21 +1062,30 @@ function fns( model ) {
1052
1062
  return setArtifactLink( head, art._origin );
1053
1063
  }
1054
1064
  case '$tableAlias': {
1055
- // use a source element having that name if in `extend … with columns`:
1056
- const { $extended } = user._user || user;
1065
+ // use a source element having that name if in `extend … with` (columns or groupBy):
1066
+ const $extended = user._user?.$extended ?? user.$extended;
1057
1067
  // if query source has duplicates, table alias has no elements
1058
1068
  const elem = $extended && art.elements?.[head.id];
1059
1069
  if (elem) {
1060
1070
  path.$prefix = art.name.id; // prepend alias name
1061
- info( 'ref-special-in-extend', [ head.location, user ],
1062
- { '#': 'alias', id: head.id, art: elem._origin._main } );
1071
+ if ($extended === 'columns') {
1072
+ warning( 'ref-special-in-extend', [ head.location, user ],
1073
+ { '#': 'alias', id: head.id, art: elem._origin._main } );
1074
+ }
1075
+ else {
1076
+ error( 'ref-unexpected-in-extend', [ head.location, user ], {
1077
+ '#': 'alias',
1078
+ keyword: $extended,
1079
+ id: head.id,
1080
+ art: elem._origin._main,
1081
+ } );
1082
+ }
1063
1083
  setLink( head, '_navigation', elem );
1064
1084
  return setArtifactLink( head, elem._origin );
1065
1085
  }
1066
1086
  else if ($extended && art.elements) {
1067
- warning( 'ref-deprecated-in-extend', [ head.location, user ], { id: head.id },
1068
- // eslint-disable-next-line @stylistic/max-len
1069
- 'In an added column, do not use the table alias $(ID) to refer to source elements' );
1087
+ warning( 'ref-deprecated-in-extend', [ head.location, user ],
1088
+ { '#': $extended, id: head.id } );
1070
1089
  }
1071
1090
  }
1072
1091
  /* FALLTHROUGH */
@@ -87,7 +87,7 @@ function tweakAssocs( model ) {
87
87
  forEachGeneric( art, 'elements', complainAboutTargetOutsideService );
88
88
 
89
89
  if (art.query) {
90
- traverseQueryPost(art.query, false, (query) => {
90
+ traverseQueryPost(art.query, null, (query) => {
91
91
  forEachGeneric( query, 'elements', handleQueryElements );
92
92
  });
93
93
  }
@@ -103,7 +103,7 @@ function tweakAssocs( model ) {
103
103
  // Check explicit ON / keys with REDIRECTED TO
104
104
  // TODO: run on all queries, but this is potentially incompatible
105
105
  // function rewriteViewCheck( view ) {
106
- // traverseQueryPost( view.query, false, ( query ) => {
106
+ // traverseQueryPost( view.query, null, ( query ) => {
107
107
  // forEachGeneric( query, 'elements', rewriteAssociationCheck );
108
108
  // } );
109
109
  // }