@sap/cds-compiler 6.3.6 → 6.4.6

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 (62) hide show
  1. package/CHANGELOG.md +101 -3
  2. package/LICENSE +32 -0
  3. package/README.md +14 -2
  4. package/bin/cdsse.js +0 -3
  5. package/doc/CHANGELOG_BETA.md +1 -1
  6. package/doc/CHANGELOG_DEPRECATED.md +1 -1
  7. package/lib/base/message-registry.js +9 -2
  8. package/lib/base/messages.js +1 -1
  9. package/lib/base/model.js +2 -0
  10. package/lib/checks/existsExpressionsOnlyForeignKeys.js +16 -10
  11. package/lib/checks/existsMustEndInAssoc.js +1 -1
  12. package/lib/checks/existsMustNotStartWithDollarSelf.js +31 -0
  13. package/lib/checks/validator.js +4 -2
  14. package/lib/compiler/assert-consistency.js +3 -2
  15. package/lib/compiler/builtins.js +5 -6
  16. package/lib/compiler/checks.js +37 -26
  17. package/lib/compiler/define.js +1 -1
  18. package/lib/compiler/extend.js +39 -50
  19. package/lib/compiler/finalize-parse-cdl.js +1 -1
  20. package/lib/compiler/lsp-api.js +1 -1
  21. package/lib/compiler/populate.js +2 -2
  22. package/lib/compiler/propagator.js +29 -6
  23. package/lib/compiler/resolve.js +13 -3
  24. package/lib/compiler/shared.js +157 -133
  25. package/lib/compiler/tweak-assocs.js +87 -29
  26. package/lib/compiler/xpr-rewrite.js +164 -160
  27. package/lib/edm/annotations/edmJson.js +206 -37
  28. package/lib/edm/csn2edm.js +13 -0
  29. package/lib/edm/edmUtils.js +2 -2
  30. package/lib/gen/BaseParser.js +106 -72
  31. package/lib/gen/CdlGrammar.checksum +1 -1
  32. package/lib/gen/CdlParser.js +1501 -1509
  33. package/lib/json/to-csn.js +8 -5
  34. package/lib/language/genericAntlrParser.js +0 -0
  35. package/lib/main.js +19 -16
  36. package/lib/model/csnRefs.js +589 -521
  37. package/lib/model/csnUtils.js +8 -5
  38. package/lib/model/enrichCsn.js +1 -0
  39. package/lib/parsers/AstBuildingParser.js +73 -28
  40. package/lib/render/toCdl.js +2 -1
  41. package/lib/render/toHdbcds.js +6 -3
  42. package/lib/render/toSql.js +5 -0
  43. package/lib/transform/db/applyTransformations.js +1 -1
  44. package/lib/transform/db/assertUnique.js +4 -1
  45. package/lib/transform/db/assocsToQueries/transformExists.js +3 -10
  46. package/lib/transform/db/assocsToQueries/utils.js +0 -5
  47. package/lib/transform/db/cdsPersistence.js +17 -18
  48. package/lib/transform/db/expansion.js +179 -3
  49. package/lib/transform/db/flattening.js +16 -5
  50. package/lib/transform/db/rewriteCalculatedElements.js +79 -283
  51. package/lib/transform/effective/main.js +8 -1
  52. package/lib/transform/forOdata.js +1 -1
  53. package/lib/transform/forRelationalDB.js +21 -80
  54. package/lib/transform/localized.js +75 -127
  55. package/lib/transform/odata/foreignKeyRefsInXprAnnos.js +89 -63
  56. package/lib/transform/transformUtils.js +23 -21
  57. package/lib/transform/translateAssocsToJoins.js +7 -5
  58. package/lib/transform/tupleExpansion.js +16 -3
  59. package/package.json +3 -3
  60. package/doc/DeprecatedOptions_v2.md +0 -150
  61. package/doc/NameResolution.md +0 -837
  62. package/lib/transform/parseExpr.js +0 -415
@@ -170,26 +170,46 @@
170
170
  // any reference in the entity is inspected: with data for the query
171
171
  // hierarchy, query number, table aliases and links from a column to its
172
172
  // respective inferred element.
173
- // - TODO: some `name` property would also be useful (set with `initDefinition`)
174
173
 
175
- // Properties in cache:
174
+ // Property name convention in cache:
176
175
  //
177
- // - _effectiveType on def/member/items: cached result of effectiveType()
178
- // - _origin on def/member/items: the "prototype"
179
- // - $origin on def/member/items: whether implicit _origin refs have been set for direct members
180
- // - _parent: currently just use to allow ref to `up_` in anonymous aspect
181
- // for managed compositions;
182
- // in queries always the main artifact (see `$next` for name resolution)
183
- // - _env on non-string path item: environment provided by the ref so far,
184
- // next path item is element in it
185
- // - _ref on non-string `type` or `from` ref, or on alias: the referred def/elem
176
+ // - $PropName: to other cache object (with proto), dictionary (w/o proto), or scalar
177
+ // - _PropName, PropName: to CSN object value or dictionary of CSN node (_PropName),
178
+ // or CSN dictionary (PropName)
179
+
180
+ // Property `_parent` in cache set with initDefinition() is set for members:
186
181
  //
187
- // - $queries on def: array of all queries of an entity
182
+ // - for an action, param or element: the node containing the dictionary of …,
183
+ // → elements of the leading/other query have main/query as _parent
184
+ // - for a `returns`/`items` or anonymous `target`/`targetAspect`: the node containing
185
+ // the object (remark: in XSN, the _parent link of elements/items in items differs)
186
+ // - for any query: use main as _parent
187
+ // - for a mixin or table alias: use query select as _parent
188
+ // - for a column: TODO
189
+
190
+ // Additional properties in cache set with initDefinition():
191
+ //
192
+ // - $name: name for a main artifact (in `definitions`) or a column
193
+ // - _keys: the foreign keys as dictionary
194
+ // - $origin$step: for Univeral CSN (TODO: describe further)
195
+ // - $queries on def: array of caches for all queryies, null in anonymous aspects
188
196
  // - $queryNumber: the index position +1 of a query inside the $queries array
189
- // - $aliases on query: dictionary of alias names to cache with _ref/_select and elements
190
- // - _select: value of the `SELECT` property of a query (or value of `projection`)
197
+ // - _select: value of the `SELECT` or `projection` property of a query
191
198
  // - elements: the elements of the query (original CSN elements from query or main)
199
+ // - $next: the “upper” query cache, used when searching for a table alias
200
+ // - $aliases: dictionary of caches for table aliases
201
+ // (currently not stored as cache of table alias, has _ref/_select and elements)
192
202
  // - _element on query column: the corresponding element
203
+ // - _column on element of a query entity: the column “creating” it
204
+
205
+ // Other properties set on-demand:
206
+ //
207
+ // - _ref on object with a `ref` property: cached return value of artifactRef{,.from}
208
+ // - _env on non-string path item: environment provided by the ref so far,
209
+ // next path item is element in it
210
+ // - _effectiveType on def/member/items: cached result of effectiveType()
211
+ // - _origin on def/member/items: the "prototype" (via type or relevant ref)
212
+
193
213
 
194
214
  'use strict';
195
215
 
@@ -250,7 +270,7 @@ function csnRefs( csn, universalReady ) {
250
270
  // const { definitions } = csn;
251
271
  const cache = new WeakMap();
252
272
  setCache( BUILTIN_TYPE, '_origin', null );
253
- if (universalReady === 'init-all')
273
+ if (universalReady === 'init-all') // TODO: remove
254
274
  initAllDefinitions();
255
275
 
256
276
  // Functions which set the new `baseEnv`:
@@ -263,79 +283,241 @@ function csnRefs( csn, universalReady ) {
263
283
  return getCache( pathItem, '_env' );
264
284
  } );
265
285
  };
266
- artifactRef.from = fromArtifactRef;
286
+ artifactRef.from = artifactRefFrom;
267
287
  return {
288
+ // init and drop caches: ----------------------------------------------------
289
+ initAllDefinitions, // TODO: delete
290
+ initDefinition,
291
+ dropDefinitionCache,
292
+ // artifact references, effective type, and origin: -------------------------
293
+ artifactRef, // with prop `from`, see above
268
294
  effectiveType,
269
- artifactRef,
270
- getOrigin,
271
- queryForElements,
295
+ getOrigin: art => cached( art, '_origin', getOriginRaw ),
296
+ // the main API function: ---------------------------------------------------
272
297
  inspectRef,
273
- queryOrMain,
298
+ // file and semantic location for CSN path: ---------------------------------
299
+ msgLocations,
300
+ // small cache functions ----------------------------------------------------
274
301
  getColumn: elem => getCache( elem, '_column' ),
275
302
  getElement: col => getCache( col, '_element' ),
276
303
  /** Returns the column's name; either explicit, implicit or internal one. */
277
- getColumnName: col => getCache( col, '$as' ),
304
+ getColumnName: col => getCache( col, '$name' ),
305
+ /** Return the query cache (with `elements`) for the query. */
306
+ queryForElements: query => query && cache.get( query.projection || query ),
278
307
  $getQueries: def => getCache( def, '$queries' ), // unstable API
279
- initDefinition,
280
- initAllDefinitions,
281
- dropDefinitionCache,
282
- targetAspect,
283
- msgLocations,
284
308
  __getCache_forEnrichCsnDebugging: obj => cache.get( obj ),
309
+ // functions which we could return top-level: -------------------------------
310
+ targetAspect,
311
+ queryOrMain,
285
312
  };
286
313
 
314
+ // Init and drop caches: ------------------------------------------------------
315
+
316
+ function initAllDefinitions() { // TODO: delete
317
+ for (const name of Object.keys( csn.definitions || {}))
318
+ initDefinition( name );
319
+ }
320
+
287
321
  /**
288
- * Return the type relevant for name resolution, i.e. the object which has a
289
- * `target`, `elements`, `enum` property, or no `type` property.
290
- * (This function could be simplified if we would use JS prototypes for type refs.)
322
+ * Initialize the cache for the definition named `main` and all its members,
323
+ * including queries and embedded anonymous aspects.
291
324
  *
292
- * @param {CSN.ArtifactWithRefs} art
325
+ * Return the initialized artifact.
293
326
  */
294
- function effectiveType( art ) {
295
- const cachedType = getCache( art, '_effectiveType' );
296
- if (cachedType !== undefined)
297
- return cachedType;
327
+ function initDefinition( main ) {
328
+ const name = typeof main === 'string' && main;
329
+ if (name) {
330
+ main = csn.definitions[name];
331
+ setCache( main, '$name', name );
332
+ }
333
+ // TODO: some --test-mode check that the argument is in ‹csn›.definitions ?
334
+ if (!main || getCache( main, '$queries' ) !== undefined) // already computed
335
+ return main;
298
336
 
299
- const chain = [];
300
- let origin;
301
- while (getCache( art, '_effectiveType' ) === undefined &&
302
- (origin = cached( art, '_origin', getOriginRaw )) &&
303
- !art.elements && !art.target && !art.targetAspect && !art.enum && !art.items) {
304
- chain.push( art );
305
- setCache( art, '_effectiveType', 0 ); // initial setting in case of cycles
306
- art = origin;
337
+ traverseDef( main, null, null, null, initNode );
338
+ const queries = cached( main, '$queries', allQueries );
339
+ for (const qcache of queries || []) {
340
+ const { _select } = qcache;
341
+ const { elements } = _select;
342
+ if (elements) {
343
+ for (const n of Object.keys( elements ))
344
+ traverseDef( elements[n], _select, n, 'element', initNode );
345
+ }
346
+ if (_select.mixin) {
347
+ for (const n of Object.keys( _select.mixin ))
348
+ setCache( _select.mixin[n], '_parent', _select ); // relevant initNode() part
349
+ }
307
350
  }
308
- if (!chain.length)
309
- return setCache( art, '_effectiveType', art );
351
+ return main;
352
+ }
310
353
 
311
- if (getCache( art, '_effectiveType' ) === 0)
312
- throw new ModelError( 'Circular type reference');
313
- const type = getCache( art, '_effectiveType' ) || art;
314
- chain.forEach( a => setCache( a, '_effectiveType', type ) );
315
- return type;
354
+ function initNode( art, parent, name, kind ) {
355
+ setCache( art, '_parent', parent );
356
+ if (art.keys) // TODO: what about managed w/o keys in Univeral CSN?
357
+ setCache( art, '_keys', getKeysDict( art ) );
358
+ if (kind === 'target') {
359
+ // Prevent re-initialization of anonymous aspect with initDefinition():
360
+ // (that would be with parent: null which would be wrong)
361
+ setCache( art, '$queries', null );
362
+ return;
363
+ }
364
+ if (art.type || !kind) // with type, top-level, query or mixin
365
+ return;
366
+ const { $origin } = art;
367
+ if (typeof $origin === 'object') // null, […], {…}
368
+ return;
369
+ const step = $origin || name;
370
+ if (parent.$origin ||
371
+ parent.type && kind !== 'enum' && parent.$origin !== null ||
372
+ getCache( parent, '$origin$step' ))
373
+ setCache( art, '$origin$step', (kind === 'element' ? step : { [kind]: step }) );
316
374
  }
317
375
 
318
376
  /**
319
- * @param {CSN.Artifact} art
377
+ * Get the array of all (sub-)queries (value of the `SELECT`/`projection`
378
+ * property) inside the given `main` artifact (of `main.query`).
379
+ *
380
+ * @param {CSN.Definition} main
381
+ * @returns {CSN.Query[]}
320
382
  */
321
- function navigationEnv( art, staticAssoc ) {
322
- let env = effectiveType( art );
323
- // here, we do not care whether it is semantically ok to navigate into sub
324
- // elements of array items (that is the task of the core compiler /
325
- // semantic check)
326
- while (env.items)
327
- env = effectiveType( env.items );
328
- if (env.elements) // shortcut
329
- return env;
330
- const target = (staticAssoc ? targetAspect( env ) : env.target || env.targetAspect);
331
- if (typeof target !== 'string')
332
- return target || env;
333
- return initDefinition( target );
383
+ function allQueries( main ) {
384
+ const all = [];
385
+ const projection = main.query || main.projection && main;
386
+ if (!projection)
387
+ return null;
388
+ traverseQuery( projection, null, null, function memorize( query, fromSelect, parentQuery ) {
389
+ if (query.ref) { // ref in from
390
+ // console.log('SQ:',query,cache.get(query))
391
+ const as = query.as || implicitAs( query.ref );
392
+ const _ref = artifactRefFrom( query );
393
+ // TODO: really eval from refs here and not on-demand?
394
+ // if not, just have this (w/o elements) as cache elem on the CSN node
395
+ getCache( fromSelect, '$aliases' )[as] = { _ref, elements: _ref.elements, _parent: query };
396
+ }
397
+ else {
398
+ const qcache = getQueryCache( parentQuery );
399
+ if (query !== main)
400
+ cache.set( query, qcache );
401
+
402
+ if (fromSelect) {
403
+ const $queryNumber = all.length + 1;
404
+ const alias = query.as || `$_select_${ $queryNumber }__`;
405
+ getCache( fromSelect, '$aliases' )[alias] = qcache;
406
+ }
407
+
408
+ const select = query.SELECT || query.projection;
409
+ if (select) {
410
+ cache.set( select, qcache ); // query and query.SELECT have the same cache qcache
411
+ qcache._select = select;
412
+ qcache._parent = main;
413
+ all.push( qcache );
414
+ }
415
+ }
416
+ } );
417
+ all.forEach( function initElements( qcache, index ) {
418
+ qcache._parent = main;
419
+ qcache.$queryNumber = index + 1;
420
+ const { elements } = (index ? qcache._select : main);
421
+ qcache.elements = elements;
422
+ const { columns } = qcache._select;
423
+ if (elements && columns)
424
+ columns.map( (col, colIndex) => initColumnElement( col, colIndex, qcache ) );
425
+ else if (columns && !elements)
426
+ throw new ModelError( `Query elements not available: ${ Object.keys( (index ? qcache._select : main) ).join('+') }`);
427
+ } );
428
+ return all;
429
+ }
430
+
431
+ /**
432
+ * Return the cache object for a new query.
433
+ * Might re-use cache object with the `parentQuery`, or use `parentQuery`
434
+ * for link to next lexical environment.
435
+ */
436
+
437
+ function getQueryCache( parentQuery ) {
438
+ if (!parentQuery)
439
+ return { $aliases: Object.create(null) };
440
+ const pcache = cache.get( parentQuery.projection || parentQuery );
441
+ if (!parentQuery.SET) // SELECT / projection: real sub query
442
+ return { $aliases: Object.create(null), $next: pcache };
443
+ // the parent query is a SET: that is not a sub query
444
+ // (works, as no sub queries are allowed in ORDER BY)
445
+ return (!pcache._select) // no leading query yet
446
+ ? pcache // share cache with parent query
447
+ : { $aliases: Object.create(null), $next: pcache.$next };
448
+ }
449
+
450
+ /**
451
+ * TODO: comment
452
+ *
453
+ * In the OData backend, the sub elements from a column with expand might
454
+ * have been “externalized” into a named type. No backward `_column` link
455
+ * from the (now the type) element is possible this way, of course...
456
+ */
457
+ function initColumnElement( col, colIndex, parentElementOrQueryCache, externalElements ) {
458
+ if (col === '*')
459
+ return;
460
+ if (col.inline) {
461
+ col.inline.map( c => initColumnElement( c, null, parentElementOrQueryCache, externalElements ) );
462
+ return;
463
+ }
464
+ setCache( col, '_parent', // not set for query (has property _select)
465
+ !parentElementOrQueryCache._select && parentElementOrQueryCache );
466
+ // TODO: why set _parent suddently to a cache and not the SELECT?
467
+ let as = columnAlias( col );
468
+ if (!as && colIndex !== null)
469
+ as = `$_column_${ colIndex + 1 }`;
470
+ setCache( col, '$name', as );
471
+ let type = parentElementOrQueryCache;
472
+ if (col.cast)
473
+ traverseType( col.cast, col, colIndex, 'column', initNode );
474
+
475
+ while (type.items)
476
+ type = type.items;
477
+ if (!type.elements) {
478
+ // in OData backend, the sub elements from a column with expand might have
479
+ // been “externalized” into a named type. No backward _column link is
480
+ // possible this way, of course...
481
+ type = artifactRef( type.type );
482
+ externalElements = true;
483
+ }
484
+ const elem = setCache( col, '_element', type.elements[as] );
485
+ if (elem && !externalElements) // TODO to.sql: something is strange if `elem` is not set
486
+ setCache( elem, '_column', col );
487
+ if (col.expand)
488
+ col.expand.map( c => initColumnElement( c, null, elem, externalElements ) );
489
+ }
490
+
491
+ function dropDefinitionCache( main ) {
492
+ const queries = getCache( main, '$queries' );
493
+ if (!queries) // not yet initialized
494
+ return;
495
+ if (!cache.delete( main )) // not yet initialized
496
+ return;
497
+ for (const qcache of queries || []) {
498
+ const { _select } = qcache;
499
+ for (const n of Object.keys( _select.mixin || {} ))
500
+ cache.delete( _select.mixin[n] );
501
+ dropColumnsCache( _select.columns );
502
+ traverseDef( _select, null, null, null, a => cache.delete( a ) ); // elements
503
+ }
504
+ traverseDef( main, null, null, null, a => cache.delete( a ) );
505
+ }
506
+
507
+ function dropColumnsCache( select ) {
508
+ if (!select)
509
+ return;
510
+ for (const col of select.columns || select.expand || select.inline || []) {
511
+ dropColumnsCache( col );
512
+ cache.delete( select );
513
+ }
334
514
  }
335
515
 
516
+ // Artifact references, effective type, and origin: ---------------------------
517
+
336
518
  /**
337
519
  * Return the object pointing to by the artifact reference (in 'type',
338
- * 'includes', 'target', raw 'from').
520
+ * 'includes', 'target'). For `from`, use artifactRefFrom()!
339
521
  *
340
522
  * @param {CSN.ArtifactReferencePath|string} ref
341
523
  * @param {any} [notFound] Value that is returned in case the artifact reference
@@ -362,12 +544,11 @@ function csnRefs( csn, universalReady ) {
362
544
  throw new ModelError( `Unknown artifact reference: ${ typeof ref !== 'string' ? JSON.stringify(ref.ref) : ref }` );
363
545
  }
364
546
 
365
- function fromArtifactRef( ref ) {
366
- // do not cache while there is second param
367
- const art = artifactFromRef( ref );
547
+ function artifactRefFrom( ref ) {
548
+ const art = cached( ref, '_ref', r => navigationEnv( artifactPathRef( r ) ) );
368
549
  if (art)
369
550
  return art;
370
- throw new ModelError( `Unknown artifact reference: ${ typeof ref !== 'string' ? JSON.stringify(ref.ref) : ref }` );
551
+ throw new ModelError( `Unknown artifact reference: ${ JSON.stringify(ref.ref) }` );
371
552
  }
372
553
 
373
554
  function artifactPathRef( ref ) {
@@ -380,35 +561,53 @@ function csnRefs( csn, universalReady ) {
380
561
  return art;
381
562
  }
382
563
 
383
- function artifactFromRef( ref, noLast ) {
384
- const [ head, ...tail ] = ref.ref;
385
- let art = initDefinition( pathId( head ) );
386
- for (const elem of tail) {
387
- const env = navigationEnv( art );
388
- art = env.elements[pathId( elem )];
389
- }
390
- if (noLast) // TODO: delete that param
391
- return art;
392
- return navigationEnv( art );
564
+ /**
565
+ * @param {CSN.Artifact} art
566
+ */
567
+ function navigationEnv( art, staticAssoc ) {
568
+ let env = effectiveType( art );
569
+ // here, we do not care whether it is semantically ok to navigate into sub
570
+ // elements of array items (that is the task of the core compiler /
571
+ // semantic check)
572
+ while (env.items)
573
+ env = effectiveType( env.items );
574
+ if (env.elements) // shortcut
575
+ return env;
576
+ const target = (staticAssoc ? targetAspect( env ) : env.target || env.targetAspect);
577
+ if (typeof target !== 'string')
578
+ return target || env;
579
+ return initDefinition( target );
393
580
  }
394
581
 
395
- // Return target when resolving references in 'keys'
396
- function assocTarget( art, refCtx ) {
397
- // Call contexts:
398
- // 1. normal definition of association with explicit foreign keys
399
- // 2. auto-redirected association with renaming of foreign keys
400
- // (currently: `keys` always available on inherited associations)
401
- // 3. user-induced redirection (in 'cast') with explicit foreign keys
402
- // 4. original provided association def inside $origin with explicit foreign keys
403
- // (outside $origin like 2)
404
- const targetName = refCtx !== 'keys_origin' && art.target ||
405
- art.$origin && art.$origin.target ||
406
- art.cast.target;
407
- return initDefinition( targetName );
408
- }
582
+ /**
583
+ * Return the type relevant for name resolution, i.e. the object which has a
584
+ * `target`, `elements`, `enum` property, or no `type` property.
585
+ * (This function could be simplified if we would use JS prototypes for type refs.)
586
+ *
587
+ * @param {CSN.ArtifactWithRefs} art
588
+ */
589
+ function effectiveType( art ) {
590
+ const cachedType = getCache( art, '_effectiveType' );
591
+ if (cachedType !== undefined)
592
+ return cachedType;
593
+
594
+ const chain = [];
595
+ let origin;
596
+ while (getCache( art, '_effectiveType' ) === undefined &&
597
+ (origin = cached( art, '_origin', getOriginRaw )) &&
598
+ !art.elements && !art.target && !art.targetAspect && !art.enum && !art.items) {
599
+ chain.push( art );
600
+ setCache( art, '_effectiveType', 0 ); // initial setting in case of cycles
601
+ art = origin;
602
+ }
603
+ if (!chain.length)
604
+ return setCache( art, '_effectiveType', art );
409
605
 
410
- function getOrigin( art ) {
411
- return cached( art, '_origin', getOriginRaw );
606
+ if (getCache( art, '_effectiveType' ) === 0)
607
+ throw new ModelError( 'Circular type reference');
608
+ const type = getCache( art, '_effectiveType' ) || art;
609
+ chain.forEach( a => setCache( a, '_effectiveType', type ) );
610
+ return type;
412
611
  }
413
612
 
414
613
  function getOriginRaw( art ) {
@@ -435,6 +634,16 @@ function csnRefs( csn, universalReady ) {
435
634
  return originNavigation( origin, step );
436
635
  }
437
636
 
637
+ function boundActionOrMain( art ) {
638
+ while (art.kind !== 'action' && art.kind !== 'function') {
639
+ const p = getCache( art, '_parent' );
640
+ if (!p)
641
+ return art;
642
+ art = p;
643
+ }
644
+ return art;
645
+ }
646
+
438
647
  function getOriginExplicit( $origin ) { // null, […], {…}
439
648
  if (!$origin)
440
649
  return null;
@@ -455,117 +664,28 @@ function csnRefs( csn, universalReady ) {
455
664
  return navigationEnv( art, true ).elements[step];
456
665
 
457
666
  if (step.action)
458
- return effectiveArtFor( art, 'actions' )[step.action];
667
+ return effectiveArtifactFor( art, 'actions' )[step.action];
459
668
  if (step.param)
460
- return effectiveArtFor( art, 'params' )[step.param];
669
+ return effectiveArtifactFor( art, 'params' )[step.param];
461
670
  if (step.returns)
462
- return effectiveArtFor( art, 'returns' );
671
+ return effectiveArtifactFor( art, 'returns' );
463
672
  if (step.enum)
464
673
  return navigationEnv( art, true ).enum[step.enum];
465
674
  if (step.items)
466
675
  return effectiveType( art ).items;
467
676
  if (step.target)
468
677
  return targetAspect( effectiveType( art ) );
469
- throw new CompilerAssertion( `Illegal navigation step ${ Object.keys(step)[0] }` );
678
+ const [ prop, val ] = Object.entries( step )[0];
679
+ throw new CompilerAssertion( `Illegal navigation step {${ prop }: "${ val }"}` );
470
680
  }
471
681
 
472
- function effectiveArtFor( art, property ) {
682
+ function effectiveArtifactFor( art, property ) {
473
683
  while (!art[property])
474
- art = getOrigin( art );
684
+ art = cached( art, '_origin', getOriginRaw );
475
685
  return art[property];
476
686
  }
477
687
 
478
- function boundActionOrMain( art ) {
479
- while (art.kind !== 'action' && art.kind !== 'function') {
480
- const p = getCache( art, '_parent' );
481
- if (!p)
482
- return art;
483
- art = p;
484
- }
485
- return art;
486
- }
487
-
488
- function queryForElements( query ) {
489
- return query && cache.get( query.projection || query );
490
- }
491
-
492
- function initAllDefinitions() {
493
- for (const name of Object.keys( csn.definitions || {}))
494
- initDefinition( name );
495
- }
496
-
497
- function initDefinition( main ) {
498
- const name = typeof main === 'string' && main;
499
- if (name) {
500
- main = csn.definitions[name];
501
- setCache( main, '$name', name );
502
- }
503
- // TODO: some --test-mode check that the argument is in ‹csn›.definitions ?
504
- if (!main || getCache( main, '$queries' ) !== undefined) // already computed
505
- return main;
506
- traverseDef( main, null, null, null, initNode );
507
- const queries = cached( main, '$queries', allQueries );
508
- for (const qcache of queries || []) {
509
- const { _select } = qcache;
510
- const { elements } = _select;
511
- if (elements) {
512
- for (const n of Object.keys( elements ))
513
- traverseDef( elements[n], _select, 'element', n, initNode );
514
- }
515
- if (_select.mixin) {
516
- for (const n of Object.keys( _select.mixin ))
517
- setCache( _select.mixin[n], '_parent', _select ); // relevant initNode() part
518
- }
519
- }
520
- return main;
521
- }
522
-
523
- function initNode( art, parent, kind, name ) {
524
- setCache( art, '_parent', parent );
525
- if (art.keys)
526
- setCache(art, '_keys', getKeysDict( art ));
527
- if (kind === 'target') {
528
- // Prevent re-initialization of anonymous aspect with initDefinition():
529
- // (that would be with parent: null which would be wrong)
530
- setCache( art, '$queries', null );
531
- return;
532
- }
533
- if (art.type || !kind) // with type, top-level, query or mixin
534
- return;
535
- const { $origin } = art;
536
- if (typeof $origin === 'object') // null, […], {…}
537
- return;
538
- const step = $origin || name;
539
- if (parent.$origin ||
540
- parent.type && kind !== 'enum' && parent.$origin !== null ||
541
- getCache( parent, '$origin$step' ))
542
- setCache( art, '$origin$step', (kind === 'element' ? step : { [kind]: step }) );
543
- }
544
-
545
- function dropDefinitionCache( main ) {
546
- const queries = getCache( main, '$queries' );
547
- if (!queries) // not yet initialized
548
- return;
549
- if (!cache.delete( main )) // not yet initialized
550
- return;
551
- for (const qcache of queries || []) {
552
- const { _select } = qcache;
553
- for (const n of Object.keys( _select.mixin || {} ))
554
- cache.delete( _select.mixin[n] );
555
- dropColumnsCache( _select.columns );
556
- traverseDef( _select, null, null, null, a => cache.delete( a ) ); // elements
557
- }
558
- traverseDef( main, null, null, null, a => cache.delete( a ) );
559
- }
560
-
561
- function dropColumnsCache( select ) {
562
- if (!select)
563
- return;
564
- for (const col of select.columns || select.expand || select.inline || []) {
565
- dropColumnsCache( col );
566
- cache.delete( select );
567
- }
568
- }
688
+ // ----------------------------------------------------------------------------
569
689
 
570
690
  /**
571
691
  * @param {CSN.Path} csnPath
@@ -663,12 +783,10 @@ function csnRefs( csn, universalReady ) {
663
783
  return resolvePath( path, elemParent.elements[head], null, 'query' );
664
784
  }
665
785
  if (!query) { // outside queries - TODO: items?
666
- let art = parent.elements?.[head];
667
- if (parent.keys) {
668
- const keysDict = getCache( parent, '_keys' );
669
- art = keysDict[head];
670
- } // Ref to up_ in anonymous aspect
671
- else if (!art && head === 'up_') {
786
+ // refs in annos on foreign keys use fk name, not fk ref name:
787
+ const dict = parent.elements ?? getCache( parent, '_keys' );
788
+ let art = dict?.[head];
789
+ if (!art && head === 'up_') { // ref to up_ in anonymous aspect
672
790
  const up = getCache( parent, '_parent' );
673
791
  const target = up && typeof up.target === 'string' && csn.definitions[up.target];
674
792
  if (target && target.elements) {
@@ -757,6 +875,23 @@ function csnRefs( csn, universalReady ) {
757
875
  };
758
876
  }
759
877
 
878
+ // Return target when resolving references in 'keys'
879
+ function assocTarget( art, refCtx ) {
880
+ // Call contexts:
881
+ // 1. normal definition of association with explicit foreign keys
882
+ // 2. auto-redirected association with renaming of foreign keys
883
+ // (currently: `keys` always available on inherited associations)
884
+ // 3. user-induced redirection (in 'cast') with explicit foreign keys
885
+ // 4. original provided association def inside $origin with explicit foreign keys
886
+ // (outside $origin like 2)
887
+ const targetName = refCtx !== 'keys_origin' && art.target ||
888
+ art.$origin && art.$origin.target ||
889
+ art.cast.target;
890
+ return initDefinition( targetName );
891
+ }
892
+
893
+ // File and semantic location for CSN path: -----------------------------------
894
+
760
895
  /**
761
896
  * Return [ Location, SemanticLocation ] from `csnPath`.
762
897
  */
@@ -855,336 +990,56 @@ function csnRefs( csn, universalReady ) {
855
990
  }
856
991
  artifact.element.push( elem || name + 1);
857
992
  artifact.innerKind = kind || undefined;
858
- ++index;
859
- }
860
- function suffix( prop ) {
861
- artifact.suffix = prop;
862
- obj = null; // stop
863
- }
864
- }
865
- /**
866
- * Get the array of all (sub-)queries (value of the `SELECT`/`projection`
867
- * property) inside the given `main` artifact (of `main.query`).
868
- *
869
- * @param {CSN.Definition} main
870
- * @returns {CSN.Query[]}
871
- */
872
- function allQueries( main ) {
873
- const all = [];
874
- const projection = main.query || main.projection && main;
875
- if (!projection)
876
- return null;
877
- traverseQuery( projection, null, null, function memorize( query, fromSelect, parentQuery ) {
878
- if (query.ref) { // ref in from
879
- // console.log('SQ:',query,cache.get(query))
880
- const as = query.as || implicitAs( query.ref );
881
- const _ref = cached( query, '_from', artifactFromRef );
882
- getCache( fromSelect, '$aliases' )[as] = { _ref, elements: _ref.elements, _parent: query };
883
- }
884
- else {
885
- const qcache = getQueryCache( parentQuery );
886
- if (query !== main)
887
- cache.set( query, qcache );
888
-
889
- if (fromSelect) {
890
- const $queryNumber = all.length + 1;
891
- const alias = query.as || `$_select_${ $queryNumber }__`;
892
- getCache(fromSelect, '$aliases')[alias] = qcache;
893
- }
894
-
895
- const select = query.SELECT || query.projection;
896
- if (select) {
897
- cache.set( select, qcache ); // query and query.SELECT have the same cache qcache
898
- qcache._select = select;
899
- qcache._parent = main;
900
- all.push( qcache );
901
- }
902
- }
903
- } );
904
- all.forEach( function initElements( qcache, index ) {
905
- qcache._parent = main;
906
- qcache.$queryNumber = index + 1;
907
- const { elements } = (index ? qcache._select : main);
908
- qcache.elements = elements;
909
- const { columns } = qcache._select;
910
- if (elements && columns)
911
- columns.map( (col, colIndex) => initColumnElement( col, colIndex, qcache ) );
912
- else if (columns && !elements)
913
- throw new ModelError( `Query elements not available: ${ Object.keys( (index ? qcache._select : main) ).join('+') }`);
914
- } );
915
- return all;
916
- }
917
-
918
- /**
919
- * Return the cache object for a new query.
920
- * Might re-use cache object with the `parentQuery`, or use `parentQuery`
921
- * for link to next lexical environment.
922
- */
923
-
924
- function getQueryCache( parentQuery ) {
925
- if (!parentQuery)
926
- return { $aliases: Object.create(null) };
927
- const pcache = cache.get( parentQuery.projection || parentQuery );
928
- if (!parentQuery.SET) // SELECT / projection: real sub query
929
- return { $aliases: Object.create(null), $next: pcache };
930
- // the parent query is a SET: that is not a sub query
931
- // (works, as no sub queries are allowed in ORDER BY)
932
- return (!pcache._select) // no leading query yet
933
- ? pcache // share cache with parent query
934
- : { $aliases: Object.create(null), $next: pcache.$next };
935
- }
936
-
937
- function initColumnElement( col, colIndex, parentElementOrQueryCache, externalElements ) {
938
- if (col === '*')
939
- return;
940
- if (col.inline) {
941
- col.inline.map( c => initColumnElement( c, null, parentElementOrQueryCache, externalElements ) );
942
- return;
943
- }
944
- setCache( col, '_parent', // not set for query (has property _select)
945
- !parentElementOrQueryCache._select && parentElementOrQueryCache );
946
- let as = columnAlias( col );
947
- if (!as && colIndex !== null)
948
- as = `$_column_${ colIndex + 1 }`;
949
- setCache( col, '$as', as );
950
- let type = parentElementOrQueryCache;
951
- if (col.cast)
952
- traverseType( col.cast, col, 'column', colIndex, initNode );
953
-
954
- while (type.items)
955
- type = type.items;
956
- if (!type.elements) {
957
- // in OData backend, the sub elements from a column with expand might have
958
- // been “externalized” into a named type. No backward _column link is
959
- // possible this way, of course...
960
- type = artifactRef( type.type );
961
- externalElements = true;
962
- }
963
- const elem = setCache( col, '_element', type.elements[as] );
964
- if (elem && !externalElements) // TODO to.sql: something is strange if `elem` is not set
965
- setCache( elem, '_column', col );
966
- if (col.expand)
967
- col.expand.map( c => initColumnElement( c, null, elem, externalElements ) );
968
- }
969
-
970
- // property name convention in cache:
971
- // - $name: to other cache object (with proto), dictionary (w/o proto), or scalar
972
- // - _name, name: to CSN object value (_name) or dictionary (name)
973
-
974
- function setCache( obj, prop, val ) {
975
- let hidden = cache.get( obj );
976
- if (!hidden) {
977
- hidden = {};
978
- cache.set( obj, hidden );
979
- }
980
- // TODO: we might keep the following with --test-mode
981
- // if (hidden[prop] !== undefined) {
982
- // console.log('RS:',prop,hidden[prop],val,obj)
983
- // throw Error('RESET')
984
- // }
985
- hidden[prop] = val;
986
- return val;
987
- }
988
-
989
- function getCache( obj, prop ) {
990
- const hidden = cache.get( obj );
991
- return hidden && hidden[prop];
992
- }
993
-
994
- function cached( obj, prop, calc ) {
995
- let hidden = cache.get( obj );
996
- if (!hidden) {
997
- hidden = {};
998
- cache.set( obj, hidden );
999
- }
1000
- else if (hidden[prop] !== undefined) {
1001
- return hidden[prop];
1002
- }
1003
- const val = calc( obj );
1004
- hidden[prop] = val;
1005
- return val;
1006
- }
1007
- }
1008
-
1009
- /**
1010
- * Foreign keys are stored in an array; for easier name resolution, create
1011
- * a dictionary of them.
1012
- */
1013
- function getKeysDict( art ) {
1014
- const dict = Object.create(null);
1015
- for (const key of art.keys)
1016
- dict[key.as || implicitAs( key.ref )] = key;
1017
- return dict;
1018
- }
1019
-
1020
- /**
1021
- * Return value of a query SELECT for the query node, or the main artifact,
1022
- * i.e. a value with an `elements` property.
1023
- * TODO: only used in forRelationalDB - move somewhere else
1024
- *
1025
- * @param {object} query node (object with SET or SELECT property)
1026
- * @param {object} main definition
1027
- */
1028
- function queryOrMain( query, main ) {
1029
- while (query.SET)
1030
- query = query.SET.args[0];
1031
- if (query.SELECT && query.SELECT.elements)
1032
- return query.SELECT;
1033
- let leading = main.query || main;
1034
- while (leading.SET)
1035
- leading = leading.SET.args[0];
1036
- // If an entity has both a projection and query property, the param `query`
1037
- // can be the entity itself (when inspect is called with a csnPath containing
1038
- // 'projection'), but `leading` can be its `query` property:
1039
- if ((leading === query || leading === query.query) && main.elements)
1040
- return main;
1041
- throw new ModelError( `Query elements not available: ${ Object.keys( query ).join('+') }`);
1042
- }
1043
-
1044
- /**
1045
- * Traverse query in pre-order
1046
- *
1047
- * The callback is called on the following XSN nodes inside the query `query`:
1048
- * - a query node, which has property `SET` or `SELECT` (or `projection`),
1049
- * - a query source node inside `from` if it has property `ref`,
1050
- * - NOT on a `join` node inside `from`.
1051
- *
1052
- * @param {CSN.Query} query
1053
- * @param {CSN.QuerySelect} fromSelect for query in `from`
1054
- * @param {CSN.Query} parentQuery for a sub query (ex those in `from`)
1055
- * @param {(query: CSN.Query&CSN.QueryFrom, select: CSN.QuerySelectEnriched, parentQuery: CSN.Query) => void} callback
1056
- */
1057
- function traverseQuery( query, fromSelect, parentQuery, callback ) {
1058
- const select = query.SELECT || query.projection;
1059
- if (select) {
1060
- callback( query, fromSelect, parentQuery );
1061
- traverseFrom( select.from, select, parentQuery, callback );
1062
- for (const prop of [ 'columns', 'where', 'having' ]) {
1063
- // all properties which can have sub queries (`join-on` also can)
1064
- const expr = select[prop];
1065
- if (expr)
1066
- expr.forEach( q => traverseExpr( q, query, callback ) );
1067
- }
1068
- }
1069
- else if (query.SET) {
1070
- callback( query, fromSelect, parentQuery );
1071
- const { args } = query.SET;
1072
- for (const q of args || [])
1073
- traverseQuery( q, null, query, callback );
1074
- }
1075
- }
1076
-
1077
- /**
1078
- * @param {CSN.QueryFrom} from
1079
- * @param {CSN.QuerySelect} fromSelect
1080
- * @param {CSN.Query} parentQuery
1081
- * @param {(from: CSN.QueryFrom, select: CSN.QuerySelect, parentQuery: CSN.Query) => void} callback
1082
- */
1083
- function traverseFrom( from, fromSelect, parentQuery, callback ) {
1084
- if (from.ref) {
1085
- callback( from, fromSelect, parentQuery );
1086
- }
1087
- else if (from.args) { // join
1088
- from.args.forEach( arg => traverseFrom( arg, fromSelect, parentQuery, callback ) );
1089
- if (from.on) // join-on, potentially having a sub query (in xpr)
1090
- from.on.forEach(arg => traverseExpr( arg, fromSelect, callback ));
1091
- }
1092
- else { // sub query in FROM
1093
- traverseQuery( from, fromSelect, parentQuery, callback );
1094
- }
1095
- }
1096
-
1097
- function traverseExpr( expr, parentQuery, callback ) {
1098
- if (expr.SELECT || expr.SET)
1099
- traverseQuery( expr, null, parentQuery, callback );
1100
- for (const prop of [ 'args', 'xpr' ]) {
1101
- // all properties which could have sub queries (directly or indirectly),
1102
- const val = expr[prop];
1103
- if (val && typeof val === 'object') {
1104
- const args = Array.isArray( val ) ? val : Object.values( val );
1105
- args.forEach( e => traverseExpr( e, parentQuery, callback ) );
1106
- }
1107
- }
1108
- }
1109
-
1110
- function traverseDef( node, parent, kind, name, callback ) {
1111
- callback( node, parent, kind, name );
1112
- if (node.params) {
1113
- for (const n of Object.keys( node.params ))
1114
- traverseType( node.params[n], node, 'param', n, callback );
1115
- }
1116
- if (node.returns)
1117
- traverseType( node.returns, node, 'returns', true, callback );
1118
- traverseType( node, true, kind, name, callback );
1119
- if (node.actions) {
1120
- for (const n of Object.keys( node.actions ))
1121
- traverseDef( node.actions[n], node, 'action', n, callback );
1122
- }
1123
- }
1124
-
1125
- function traverseType( node, parent, kind, name, callback ) {
1126
- if (parent !== true)
1127
- callback( node, parent, kind, name );
1128
- const target = targetAspect( node );
1129
- if (target && typeof target === 'object' && target.elements) {
1130
- callback( target, node, 'target', true );
1131
- node = target;
1132
- }
1133
- else if (node.items) {
1134
- let items = 0;
1135
- while (node.items) {
1136
- callback( node.items, node, 'items', ++items );
1137
- node = node.items;
993
+ ++index;
994
+ }
995
+ function suffix( prop ) {
996
+ artifact.suffix = prop;
997
+ obj = null; // stop
1138
998
  }
1139
999
  }
1140
- if (node.elements) {
1141
- for (const n of Object.keys( node.elements ))
1142
- traverseDef( node.elements[n], node, 'element', n, callback );
1143
- }
1144
- if (node.enum) {
1145
- for (const n of Object.keys( node.enum ))
1146
- traverseDef( node.enum[n], node, 'enum', n, callback );
1147
- }
1148
- }
1149
1000
 
1150
- function targetAspect( art ) {
1151
- const { $origin } = art;
1152
- return art.targetAspect ||
1153
- $origin && typeof $origin === 'object' && !Array.isArray( $origin ) && $origin.target ||
1154
- art.target;
1155
- }
1001
+ // Low-level cache functions: -------------------------------------------------
1156
1002
 
1157
- function pathId( item ) {
1158
- return (typeof item === 'string') ? item : item.id;
1159
- }
1003
+ function setCache( obj, prop, val ) {
1004
+ let hidden = cache.get( obj );
1005
+ if (!hidden) {
1006
+ hidden = {};
1007
+ cache.set( obj, hidden );
1008
+ }
1009
+ // TODO: we might keep the following with --test-mode
1010
+ // if (hidden[prop] !== undefined) {
1011
+ // console.log('RS:',prop,hidden[prop],val,obj)
1012
+ // throw Error('RESET')
1013
+ // }
1014
+ hidden[prop] = val;
1015
+ return val;
1016
+ }
1160
1017
 
1161
- function implicitAs( ref ) {
1162
- if (typeof ref !== 'string')
1163
- ref = ref[ref.length - 1];
1164
- const id = (typeof ref === 'string') ? ref : ref.id; // inlined `pathId`
1165
- return id.substring( id.lastIndexOf('.') + 1 );
1166
- }
1018
+ function getCache( obj, prop ) {
1019
+ const hidden = cache.get( obj );
1020
+ return hidden && hidden[prop];
1021
+ }
1167
1022
 
1168
- function startCsnPath( csnPath, csn ) {
1169
- const head = csnPath[0];
1170
- if (typeof head !== 'string') {
1171
- const {
1172
- main, parent, art, query,
1173
- } = head;
1174
- return {
1175
- index: 1, main, parent, art, query,
1176
- };
1023
+ function cached( obj, prop, calc ) {
1024
+ let hidden = cache.get( obj );
1025
+ if (!hidden) {
1026
+ hidden = {};
1027
+ cache.set( obj, hidden );
1028
+ }
1029
+ else if (hidden[prop] !== undefined) {
1030
+ return hidden[prop];
1031
+ }
1032
+ const val = calc( obj );
1033
+ hidden[prop] = val;
1034
+ return val;
1177
1035
  }
1178
- if (csnPath.length < 2 || head !== 'definitions' && head !== 'vocabularies')
1179
- throw new CompilerAssertion( 'References outside definitions and vocabularies not supported yet' );
1180
- const art = csn[head][csnPath[1]];
1181
- return {
1182
- index: 2, main: art, parent: art, art, query: null,
1183
- };
1184
1036
  }
1185
1037
 
1038
+ // CSN path analysing: ----------------------------------------------------------
1186
1039
 
1187
1040
  /**
1041
+ * Analyse path `csnPath` in the CSN `csn` and call `resolve`.
1042
+ *
1188
1043
  * @param {CSN.Path} csnPath
1189
1044
  * @param {CSN.Model} csn
1190
1045
  * @param {any} resolve
@@ -1255,6 +1110,8 @@ function analyseCsnPath( csnPath, csn, resolve ) {
1255
1110
  }
1256
1111
  else if (prop === 'items' || prop === 'returns') {
1257
1112
  art = obj[prop];
1113
+ // TODO: what about parent here?, see cache property _parent
1114
+ // BTW, still necessary to calculate parent here if we have _parent?
1258
1115
  }
1259
1116
  else if (prop === 'args') {
1260
1117
  isName = true; // for named arguments
@@ -1276,6 +1133,7 @@ function analyseCsnPath( csnPath, csn, resolve ) {
1276
1133
  baseEnv = resolve.expandInline( obj, baseCtx, main, query, parent, baseEnv );
1277
1134
  refCtx = prop;
1278
1135
  }
1136
+ // TODO: for on condition in expand, also set an environment
1279
1137
  isName = prop;
1280
1138
  }
1281
1139
  else if (prop === 'on') {
@@ -1309,8 +1167,160 @@ function analyseCsnPath( csnPath, csn, resolve ) {
1309
1167
  return resolve( obj, refCtx, main, query, parent, baseEnv );
1310
1168
  }
1311
1169
 
1170
+ function startCsnPath( csnPath, csn ) {
1171
+ const head = csnPath[0];
1172
+ if (typeof head !== 'string') {
1173
+ const {
1174
+ main, parent, art, query,
1175
+ } = head;
1176
+ return {
1177
+ index: 1, main, parent, art, query,
1178
+ };
1179
+ }
1180
+ if (csnPath.length < 2 || head !== 'definitions' && head !== 'vocabularies')
1181
+ throw new CompilerAssertion( 'References outside definitions and vocabularies not supported yet' );
1182
+ const art = csn[head][csnPath[1]];
1183
+ return {
1184
+ index: 2, main: art, parent: art, art, query: null,
1185
+ };
1186
+ }
1187
+
1188
+ // CSN traversal functions: -----------------------------------------------------
1189
+
1190
+ /**
1191
+ * Traverse an artifact `node` and its members, but without queries, in pre-order.
1192
+ *
1193
+ * Call `callback` on all nodes with the following arguments:
1194
+ *
1195
+ * - `node`: the current node
1196
+ * - `parent`: the parent node (containing the dictionary),
1197
+ * see the initial file comment for cache property _parent for details
1198
+ * - `name`: the name of the current node in the parent dictionary, or
1199
+ * true for `returns` parameter, and increasing number for `items` object
1200
+ * - `kind`
1201
+ *
1202
+ *
1203
+ * The callback is called on the following CSN nodes inside the query `query`:
1204
+ * - a query node, which has property `SET` or `SELECT` (or `projection`),
1205
+ * - a query source node inside `from` if it has property `ref`,
1206
+ * - NOT on a `join` node inside `from`.
1207
+ *
1208
+ * @param {CSN.Query} query
1209
+ * @param {CSN.QuerySelect} fromSelect for query in `from`
1210
+ * @param {CSN.Query} parentQuery for a sub query (ex those in `from`)
1211
+ * @param {(query: CSN.Query&CSN.QueryFrom, select: CSN.QuerySelectEnriched, parentQuery: CSN.Query) => void} callback
1212
+ */
1213
+
1214
+ function traverseDef( node, parent, name, kind, callback ) {
1215
+ callback( node, parent, name, kind );
1216
+ if (node.params) {
1217
+ for (const n of Object.keys( node.params ))
1218
+ traverseType( node.params[n], node, n, 'param', callback );
1219
+ }
1220
+ if (node.returns)
1221
+ traverseType( node.returns, node, true, 'returns', callback );
1222
+ traverseType( node, true, name, kind, callback );
1223
+ if (node.actions) {
1224
+ for (const n of Object.keys( node.actions ))
1225
+ traverseDef( node.actions[n], node, n, 'action', callback );
1226
+ }
1227
+ }
1228
+
1229
+ function traverseType( node, parent, name, kind, callback ) {
1230
+ if (parent !== true)
1231
+ callback( node, parent, name, kind );
1232
+ const target = targetAspect( node );
1233
+ if (target && typeof target === 'object' && target.elements) {
1234
+ callback( target, node, true, 'target' );
1235
+ node = target;
1236
+ }
1237
+ else if (node.items) {
1238
+ let items = 0;
1239
+ while (node.items) {
1240
+ callback( node.items, node, ++items, 'items' );
1241
+ node = node.items;
1242
+ }
1243
+ }
1244
+ if (node.elements) {
1245
+ for (const n of Object.keys( node.elements ))
1246
+ traverseDef( node.elements[n], node, n, 'element', callback );
1247
+ }
1248
+ if (node.enum) {
1249
+ for (const n of Object.keys( node.enum ))
1250
+ traverseDef( node.enum[n], node, n, 'enum', callback );
1251
+ }
1252
+ }
1253
+
1254
+ /**
1255
+ * Traverse query in pre-order
1256
+ *
1257
+ * The callback is called on the following CSN nodes inside the query `query`:
1258
+ * - a query node, which has property `SET` or `SELECT` (or `projection`),
1259
+ * - a query source node inside `from` if it has property `ref`,
1260
+ * - NOT on a `join` node inside `from`.
1261
+ *
1262
+ * @param {CSN.Query} query
1263
+ * @param {CSN.QuerySelect} fromSelect for query in `from`
1264
+ * @param {CSN.Query} parentQuery for a sub query (ex those in `from`)
1265
+ * @param {(query: CSN.Query&CSN.QueryFrom, select: CSN.QuerySelectEnriched, parentQuery: CSN.Query) => void} callback
1266
+ */
1267
+ function traverseQuery( query, fromSelect, parentQuery, callback ) {
1268
+ const select = query.SELECT || query.projection;
1269
+ if (select) {
1270
+ callback( query, fromSelect, parentQuery );
1271
+ traverseFrom( select.from, select, parentQuery, callback );
1272
+ for (const prop of [ 'columns', 'where', 'having' ]) {
1273
+ // all properties which can have sub queries (`join-on` also can)
1274
+ const expr = select[prop];
1275
+ if (expr)
1276
+ expr.forEach( q => traverseExpr( q, query, callback ) );
1277
+ }
1278
+ }
1279
+ else if (query.SET) {
1280
+ callback( query, fromSelect, parentQuery );
1281
+ const { args } = query.SET;
1282
+ for (const q of args || [])
1283
+ traverseQuery( q, null, query, callback );
1284
+ }
1285
+ }
1286
+
1287
+ /**
1288
+ * @param {CSN.QueryFrom} from
1289
+ * @param {CSN.QuerySelect} fromSelect
1290
+ * @param {CSN.Query} parentQuery
1291
+ * @param {(from: CSN.QueryFrom, select: CSN.QuerySelect, parentQuery: CSN.Query) => void} callback
1292
+ */
1293
+ function traverseFrom( from, fromSelect, parentQuery, callback ) {
1294
+ if (from.ref) {
1295
+ callback( from, fromSelect, parentQuery );
1296
+ }
1297
+ else if (from.args) { // join
1298
+ from.args.forEach( arg => traverseFrom( arg, fromSelect, parentQuery, callback ) );
1299
+ if (from.on) // join-on, potentially having a sub query (in xpr)
1300
+ from.on.forEach(arg => traverseExpr( arg, fromSelect, callback ));
1301
+ }
1302
+ else { // sub query in FROM
1303
+ traverseQuery( from, fromSelect, parentQuery, callback );
1304
+ }
1305
+ }
1306
+
1307
+ function traverseExpr( expr, parentQuery, callback ) {
1308
+ if (expr.SELECT || expr.SET)
1309
+ traverseQuery( expr, null, parentQuery, callback );
1310
+ for (const prop of [ 'args', 'xpr' ]) {
1311
+ // all properties which could have sub queries (directly or indirectly),
1312
+ const val = expr[prop];
1313
+ if (val && typeof val === 'object') {
1314
+ const args = Array.isArray( val ) ? val : Object.values( val );
1315
+ args.forEach( e => traverseExpr( e, parentQuery, callback ) );
1316
+ }
1317
+ }
1318
+ }
1319
+
1320
+ // Small CSN utility functions: -------------------------------------------------
1321
+
1312
1322
  // A SELECT which is (unnecessarily) put into parentheses, the CSN
1313
- // representation uses SET without `op` and args of length 1:
1323
+ // representation uses SET without `op` and args of length 1 in compiler <v6:
1314
1324
  function isSelectQuery( query ) {
1315
1325
  while (query.SET) {
1316
1326
  const { args } = query.SET;
@@ -1332,13 +1342,71 @@ function columnAlias( col ) {
1332
1342
  return col.as || (!col.args && col.func) || (col.ref && implicitAs( col.ref ));
1333
1343
  }
1334
1344
 
1345
+ function implicitAs( ref ) {
1346
+ if (typeof ref !== 'string')
1347
+ ref = ref[ref.length - 1];
1348
+ const id = (typeof ref === 'string') ? ref : ref.id; // inlined `pathId`
1349
+ return id.substring( id.lastIndexOf('.') + 1 );
1350
+ }
1351
+
1352
+ function pathId( item ) {
1353
+ return (typeof item === 'string') ? item : item.id;
1354
+ }
1355
+
1356
+ /**
1357
+ * Foreign keys are stored in an array; for easier name resolution, create
1358
+ * a dictionary of them.
1359
+ */
1360
+ function getKeysDict( art ) {
1361
+ const dict = Object.create(null);
1362
+ for (const key of art.keys)
1363
+ dict[key.as || implicitAs( key.ref )] = key;
1364
+ return dict;
1365
+ }
1366
+
1367
+ /**
1368
+ * Return the target aspect (or target entity if that does not exist) of `art`.
1369
+ * Works with both CSN flavors `client` and `universal`.
1370
+ */
1371
+ function targetAspect( art ) {
1372
+ const { $origin } = art;
1373
+ return ($origin
1374
+ ? typeof $origin === 'object' && !Array.isArray( $origin ) && $origin.target
1375
+ : art.targetAspect
1376
+ ) || art.target;
1377
+ }
1378
+
1379
+ /**
1380
+ * Return value of a query SELECT for the query node, or the main artifact,
1381
+ * i.e. a value with an `elements` property.
1382
+ * TODO: only used in transformViewOrEntity() - make it use queryForElements()
1383
+ *
1384
+ * @param {object} query node (object with SET or SELECT property)
1385
+ * @param {object} main definition
1386
+ */
1387
+ function queryOrMain( query, main ) {
1388
+ while (query.SET)
1389
+ query = query.SET.args[0];
1390
+ if (query.SELECT?.elements)
1391
+ return query.SELECT;
1392
+ let leading = main.query || main;
1393
+ while (leading.SET)
1394
+ leading = leading.SET.args[0];
1395
+ // If an entity has both a projection and query property, the param `query`
1396
+ // can be the entity itself (when inspect is called with a csnPath containing
1397
+ // 'projection'), but `leading` can be its `query` property:
1398
+ if ((leading === query || leading === query.query) && main.elements)
1399
+ return main;
1400
+ throw new ModelError( `Query elements not available: ${ Object.keys( query ).join('+') }`);
1401
+ }
1402
+
1335
1403
  module.exports = {
1404
+ artifactProperties,
1336
1405
  csnRefs,
1406
+ analyseCsnPath,
1337
1407
  traverseQuery,
1338
- artifactProperties,
1408
+ columnAlias,
1339
1409
  implicitAs,
1340
- getKeysDict,
1341
- analyseCsnPath,
1342
1410
  pathId,
1343
- columnAlias,
1411
+ getKeysDict,
1344
1412
  };