UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

1,251 lines (1,166 loc) 52.7 kB
// CSN functionality for resolving references // Resolving references in a CSN can be a bit tricky, because the semantics of // a reference is context-dependent, especially if queries are involved. This // module provides the corresponding resolve/inspect functions. // // This module should work with both client-style CSN and Universal CSN. // // See below for preconditions / things to consider – the functions in this // module do not issue user-friendly messages for invalid references in a CSN, // such messages are (hopefully) issued by the compile() function. // The main export function `csnRefs` of this module is called with a CSN as // input and returns functions which analyse references in the provided CSN: // // const { csnRefs } = require('../model/csnRefs'); // function myCsnAnalyser( csn ) { // const { inspectRef } = csnRefs( csn ); // … // const { links, art } = inspectRef( csnPath ); // // → art is the CSN node which is referred to by the reference // // → links provides some info about each reference path step // … // } // // You can see the results of the CSN refs functions by using our client tool: // cdsc --enrich-csn MyModel.cds // It is also used by our references tests, for details see ./enrichCsn.js. // Terminology used in this file: // // - ref (reference): a { ref: <path> } object (or sometimes also a string) // referring an artifact or member (element, …) // - path: an array of strings or { id: … } objects for the dot-connected names // used as reference // - csnPath: an array of strings and numbers (e.g. ['definitions', 'S.E', // 'query', 'SELECT', 'from', 'ref', 0, 'where', 2]); they are the property // names and array indexes which navigate from the CSN root to the reference. // ## PRECONDITIONS / THINGS TO CONSIDER ------------------------------------- // The functions in this module expect // // 1. a well-formed CSN with valid references; // 2. a compiled model, i.e. a CSN with all inferred information provided by // the compile() function for the CSN flavors `client` or `universal` // (including the (non-)enumerable `elements` property in `client` CSN); // 3. no (relevant) CSN changes between the calls of the same instance of // inspectRef() - to enable caching. // // If any of these conditions are not given, our functions usually simply // throw an exception (which might even be a plain TypeError), but it might // also just return any value. CSN processors can provide user-friendly error // messages by calling the Core Compiler in case of exceptions. For details, // see internalDoc/CoreCompiler.md#use-of-the-core-compiler-for-csn-processors. // During a transformation, care must be taken to adhere to these conditions. // E.g. a structure flattening function cannot create an element `s_x` and // delete `s` and then still expect inspectRef() to be able to resolve a // reference `['s', 'x']`. // There are currently 3 (SQL) backend issues for which we provide a workaround: // // - function `resolvePath`: issue with argument `arg` being falsy // - function `artifactRef`: issue with non-string ref without definition // - function `initColumnElement`: issue with column which is neither `*` nor // a `ref` with sibling `inline`, but still has no corresponding element // The functions in this module also use an internal cache, which can be dropped // for a single definition (main artifact) with function dropDefinitionCache(). // When modifying the CSN, caches might need to be invalidated. In the following // example, the second call of inspectRef() might lead to a wrong result or an // exception if the assignment to `inspectRef` is not uncommented: // // let { inspectRef } = csnRefs( csn ); // const csnPath = ['definitions','P','projection','columns',0]; // const subElement = inspectRef( csnPath ); // type T is involved // csn.definitions.T.type = 'some.other.type'; // // ({ inspectRef } = csnRefs( csn )); // drop caches // … = inspectRef( csnPath ); // type T - using the cached or the new? // // On request, we might add a functions for individual cache invalidations or // low-level versions of inspectRef() for performance. // ## NAME RESOLUTION OVERVIEW ----------------------------------------------- // The most interesting part of a reference is always: where to search for the // name in its first path item? The general search is always as follows, with // the exact behavior being dependent on the “reference context” (e.g. “reference // in a `on` condition of a `mixin` definition”): // // 1. We search in environments constructed by “defining” names “around” the // lexical position of the reference. In a CSN, these could be the // (explicit and implicit) table alias names and `mixin` definitions of the // current query and its parent queries (according to the query hierarchy). // 2. If the search according to (1) was not successful and the name starts // with a `$`, we could consider the name to be a “magic” variable with // `$self` (and `$projection`) being a special magic variable. // 3. Otherwise, we would search in a “dynamic” environment, which could be // `‹csn›.definitions` for global references like `type`, the elements of // the current element's parent, the combined elements of the query source // entities, the resulting elements of the current query, or something // special (elements of the association's target, …). // // The names in further path items are searched in the “navigation” environment // of the path so far - it does not need to depend on the reference context (as // we do not check the validity here): // // 1. We search in the elements of the target entity for associations and // compositions, and in the elements of the current object otherwise. // 2. If there is an `items`, we check for `elements`/`target` inside `items`. // 3. `elements`/`target`/`items` inherited from the “effective type” are also // considered. // For details about the name resolution in CSN, see // internalDoc/CsnSyntax.md#helper-property-for-simplified-name-resolution // and doc/NameResolution.md. Here comes a summary. // ## IMPLEMENTATION OVERVIEW ------------------------------------------------ // The main function `inspectRef` works as follows: // // 1. For ease of use, the input is the “CSN path” as explained above, e.g. // ['definitions', 'P', 'query', 'SELECT', 'from', 'ref', 0, 'where', 2] // 2. This is condensed into a “reference context” string, e.g. `ref_where`; // that might also depend on sibling properties along the way, e.g. // ['definitions', 'P', 'query', 'SELECT', 'columns', 0, 'expand', 0] leads // to `expand` if there is a `‹csn›.definitions.P.query.SELECT.columns[0].ref` // and to `columns` otherwise. // 3. Additionally, other useful CSN nodes are collected like the current query; // the queries of a definition are also prepared for further inspection. // 4. If applicable, a “base environment” is calculated; e.g. references in // `ref_where` are resolved against the elements of the entity referred to // by the outer `ref`. // 5. We look up the “reference semantics” in constant `referenceSemantics` // using the “reference context” string as key. // 6. The property `lexical` determines whether to search in “lexical // environments” (table aliases and `mixin`s) starting from which query, and // whether to do something special for names starting with `$`. // 7. The property `dynamic` determines where to search if the lexical search // was not successful. // 8. The remaining reference path is resolved as well - the final referred CSN // node is returned as well as information about each path step. // We usually cache calculated data. For the following reasons, we now use a // WeakMap as cache instead of adding non-enumerable properties to the CSN: // // - CSN consumers should not have access to the cached data, as we might // change the way how we calculate things. // - Avoid memory leaks. // - Natural cache invalidation if there is no handle anymore to the functions // returned by `csnRefs`. // Our cache looks like follows: // - Each object in the CSN could have an cache entry which itself is an object // which contains cached data. Such data can be a link to a CSN node (like // `_effectiveType`/`elements`), scalar (like `$queryNumber`) or link to // another cache object (like `$next`). // - A cache entry must not link to a cache object of another main definition; // otherwise, individual cache invalidation does not work. // - Usually, each CSN object has an individual cache object. // - For CSN queries nodes, cache objects are _shared_: both the CSN nodes // `‹query› = { SELECT: ‹select›, … }` and `‹select›` share the same cache // object; a UNION `‹set_query› = { SET: args: [‹query1›, …] }` and ‹query1› // (which can itself be a `SELECT` or `SET`) share also the same cache // object; this way, the relevant query elements are directly available. // - The cache objects for all queries of an entity are initialized as soon as // any reference in the entity is inspected: with data for the query // hierarchy, query number, table aliases and links from a column to its // respective inferred element. // - TODO: some `name` property would also be useful (set with `initDefinition`) // Properties in cache: // // - _effectiveType on def/member/items: cached result of effectiveType() // - _origin on def/member/items: the "prototype" // - $origin on def/member/items: whether implicit _origin refs have been set for direct members // - _parent: currently just use to allow ref to `up_` in anonymous aspect // for managed compositions; // in queries always the main artifact (see `$next` for name resolution) // - _env on non-string path item: environment provided by the ref so far, // next path item is element in it // - _ref on non-string `type` or `from` ref, or on alias: the referred def/elem // // - $queries on def: array of all queries of an entity // - $queryNumber: the index position +1 of a query inside the $queries array // - $aliases on query: dictionary of alias names to cache with _ref/_select and elements // - _select: value of the `SELECT` property of a query (or value of `projection`) // - elements: the elements of the query (original CSN elements from query or main) // - _element on query column: the corresponding element 'use strict'; const BUILTIN_TYPE = {}; const { SemanticLocation, locationString } = require('../base/location'); const { ModelError, CompilerAssertion } = require('../base/error'); const { isAnnotationExpression } = require('../base/builtins'); // Properties in which artifact or members are defined - next property in the // "csnPath" is the name or index of that property; 'args' (its value can be a // dictionary) is handled extra here, also 'expand' and 'inline' const artifactProperties = [ 'elements', 'columns', 'keys', 'mixin', 'enum', 'params', 'actions', 'definitions', 'extensions' ]; // + 'args', see above // Mapping the “reference context string” to the reference semantics // - lexical: false | Function - determines where to look first for “lexical names” // - dynamic: String - describes the dynamic environment (if in query) // - assoc: String, with dynamic: 'global' - what to do with assoc steps // * 'target': always follow target, including last ref item // * other (& not provided) = follow target (targetAspect if no target) if not last ref item const referenceSemantics = { $init: { $initOnly: true }, type: { lexical: false, dynamic: 'global' }, includes: { lexical: false, dynamic: 'global' }, target: { lexical: false, dynamic: 'global' }, targetAspect: { lexical: false, dynamic: 'global' }, from: { lexical: false, dynamic: 'global', assoc: 'target' }, keys: { lexical: false, dynamic: 'target' }, keys_origin: { lexical: false, dynamic: 'target' }, excluding: { lexical: false, dynamic: 'source' }, expand: { lexical: justDollar, dynamic: 'expand' }, // ...using baseEnv inline: { lexical: justDollar, dynamic: 'inline' }, // ...using baseEnv ref_where: { lexical: justDollar, dynamic: 'ref-target' }, // ...using baseEnv on: { lexical: justDollar, dynamic: 'query' }, // assoc defs, redirected to annotation: { lexical: justDollar, dynamic: 'query' }, // anno top-level `ref` annotationExpr: { lexical: justDollar, dynamic: 'query' }, // annotation assignment // there are also 'on_join' and 'on_mixin' with default semantics orderBy_ref: { lexical: query => query, dynamic: 'query' }, orderBy_expr: { lexical: query => query, dynamic: 'source' }, // ref in ORDER BY expression orderBy_set_ref: { lexical: query => query.$next, dynamic: 'query' }, // to outer SELECT (from UNION) // refs in ORDER BY expr in UNION not really allowed // only with table alias (of outer queries) or $self orderBy_set_expr: { lexical: query => query.$next, dynamic: false }, // default: { lexical: query => query, dynamic: 'source' } }; function justDollar() { return null; } /** * @param {CSN.Model} csn * @param {boolean|string} [universalReady] */ function csnRefs( csn, universalReady ) { // some users exchange the dict while using csn-refs !?! see test/testDraft.js // const { definitions } = csn; const cache = new WeakMap(); setCache( BUILTIN_TYPE, '_origin', null ); if (universalReady === 'init-all') { for (const name of Object.keys( csn.definitions || {})) initDefinition( name ); } // Functions which set the new `baseEnv`: resolveRef.expandInline = function resolveExpandInline( ref, ...args ) { return cached( ref, '_env', () => navigationEnv( resolveRef( ref, ...args ).art ) ); }; resolveRef.ref_where = function resolveRefWhere( pathItem, baseRef, ...args ) { return cached( pathItem, '_env', () => { resolveRef( baseRef, ...args ); // sets _env cache for non-string ref items return getCache( pathItem, '_env' ); } ); }; artifactRef.from = fromArtifactRef; return { effectiveType, artifactRef, getOrigin, queryForElements, inspectRef, queryOrMain, getColumn: elem => getCache( elem, '_column' ), getElement: col => getCache( col, '_element' ), /** Returns the column's name; either explicit, implicit or internal one. */ getColumnName: col => getCache( col, '$as' ), $getQueries: def => getCache( def, '$queries' ), // unstable API initDefinition, dropDefinitionCache, targetAspect, msgLocations, __getCache_forEnrichCsnDebugging: obj => cache.get( obj ), }; /** * Return the type relevant for name resolution, i.e. the object which has a * `target`, `elements`, `enum` property, or no `type` property. * (This function could be simplified if we would use JS prototypes for type refs.) * * @param {CSN.ArtifactWithRefs} art */ function effectiveType( art ) { const cachedType = getCache( art, '_effectiveType' ); if (cachedType !== undefined) return cachedType; const chain = []; let origin; while (getCache( art, '_effectiveType' ) === undefined && (origin = cached( art, '_origin', getOriginRaw )) && !art.elements && !art.target && !art.targetAspect && !art.enum && !art.items) { chain.push( art ); setCache( art, '_effectiveType', 0 ); // initial setting in case of cycles art = origin; } if (!chain.length) return setCache( art, '_effectiveType', art ); if (getCache( art, '_effectiveType' ) === 0) throw new ModelError( 'Circular type reference'); const type = getCache( art, '_effectiveType' ) || art; chain.forEach( a => setCache( a, '_effectiveType', type ) ); return type; } /** * @param {CSN.Artifact} art */ function navigationEnv( art, staticAssoc ) { let env = effectiveType( art ); // here, we do not care whether it is semantically ok to navigate into sub // elements of array items (that is the task of the core compiler / // semantic check) while (env.items) env = effectiveType( env.items ); if (env.elements) // shortcut return env; const target = (staticAssoc ? targetAspect( env ) : env.target || env.targetAspect); if (typeof target !== 'string') return target || env; return initDefinition( target ); } /** * Return the object pointing to by the artifact reference (in 'type', * 'includes', 'target', raw 'from'). * * @param {CSN.ArtifactReferencePath|string} ref * @param {any} [notFound] Value that is returned in case the artifact reference * could not be found. */ function artifactRef( ref, notFound ) { // TODO: what about type ref? if (typeof ref === 'string') { const main = csn.definitions[ref]; if (main) return initDefinition( ref ); // notFound only meant for builtins and $self if (notFound !== undefined) return notFound; } else { const art = cached( ref, '_ref', artifactPathRef ); if (art) return art; // Backend bug workaround, TODO: delete next 2 lines if (notFound !== undefined) return notFound; } throw new ModelError( `Unknown artifact reference: ${ typeof ref !== 'string' ? JSON.stringify(ref.ref) : ref }` ); } function fromArtifactRef( ref ) { // do not cache while there is second param const art = artifactFromRef( ref ); if (art) return art; throw new ModelError( `Unknown artifact reference: ${ typeof ref !== 'string' ? JSON.stringify(ref.ref) : ref }` ); } function artifactPathRef( ref ) { const [ head, ...tail ] = ref.ref; let art = initDefinition( pathId( head ) ); for (const elem of tail) { const env = navigationEnv( art ); art = env.elements[pathId( elem )]; } return art; } function artifactFromRef( ref, noLast ) { const [ head, ...tail ] = ref.ref; let art = initDefinition( pathId( head ) ); for (const elem of tail) { const env = navigationEnv( art ); art = env.elements[pathId( elem )]; } if (noLast) // TODO: delete that param return art; return navigationEnv( art ); } // Return target when resolving references in 'keys' function assocTarget( art, refCtx ) { // Call contexts: // 1. normal definition of association with explicit foreign keys // 2. auto-redirected association with renaming of foreign keys // (currently: `keys` always available on inherited associations) // 3. user-induced redirection (in 'cast') with explicit foreign keys // 4. original provided association def inside $origin with explicit foreign keys // (outside $origin like 2) const targetName = refCtx !== 'keys_origin' && art.target || art.$origin && art.$origin.target || art.cast.target; return initDefinition( targetName ); } function getOrigin( art ) { return cached( art, '_origin', getOriginRaw ); } function getOriginRaw( art ) { if (art.type) { // TODO: make robust against "linked" = only direct return (art.type !== '$self' || csn.definitions.$self) ? artifactRef( art.type, BUILTIN_TYPE ) : getCache( boundActionOrMain( art ), '_parent' ); } if (typeof art.$origin === 'object') // null, […], {…} return getOriginExplicit( art.$origin ); const parent = getCache( art, '_parent' ); if (parent === undefined && universalReady) { const { $location } = art; const location = $location && (typeof $location === 'string' ? $location : locationString( $location )); const def = Object.keys( art ).join('+') + (location ? `:${ location }` : ''); throw new CompilerAssertion( `Inspecting non-initialized CSN node {${ def }}` ); } const step = getCache( art, '$origin$step' ); if (!step) return null; const origin = cached( parent, '_origin', getOriginRaw ); return originNavigation( origin, step ); } function getOriginExplicit( $origin ) { // null, […], {…} if (!$origin) return null; if (!Array.isArray( $origin )) // anonymous prototype in $origin return getOriginExplicit( $origin.$origin ); const [ head, ...tail ] = $origin; // if (!main) throw Error(JSON.stringify({$origin,csn})) const main = initDefinition( head ); return tail.reduce( originNavigation, main ); } function originNavigation( art, step ) { if (!step) return null; if (!effectiveType( art )) throw new ModelError( 'Cyclic type definition' ); if (typeof step === 'string') return navigationEnv( art, true ).elements[step]; if (step.action) return effectiveArtFor( art, 'actions' )[step.action]; if (step.param) return effectiveArtFor( art, 'params' )[step.param]; if (step.returns) return effectiveArtFor( art, 'returns' ); if (step.enum) return navigationEnv( art, true ).enum[step.enum]; if (step.items) return effectiveType( art ).items; if (step.target) return targetAspect( effectiveType( art ) ); throw new CompilerAssertion( `Illegal navigation step ${ Object.keys(step)[0] }` ); } function effectiveArtFor( art, property ) { while (!art[property]) art = getOrigin( art ); return art[property]; } function boundActionOrMain( art ) { while (art.kind !== 'action' && art.kind !== 'function') { const p = getCache( art, '_parent' ); if (!p) return art; art = p; } return art; } function queryForElements( query ) { return query && cache.get( query.projection || query ); } function initDefinition( main ) { const name = typeof main === 'string' && main; if (name) { main = csn.definitions[name]; setCache( main, '$name', name ); } // TODO: some --test-mode check that the argument is in ‹csn›.definitions ? if (!main || getCache( main, '$queries' ) !== undefined) // already computed return main; traverseDef( main, null, null, null, initNode ); const queries = cached( main, '$queries', allQueries ); for (const qcache of queries || []) { const { _select } = qcache; const { elements } = _select; if (elements) { for (const n of Object.keys( elements )) traverseDef( elements[n], _select, 'element', n, initNode ); } if (_select.mixin) { for (const n of Object.keys( _select.mixin )) setCache( _select.mixin[n], '_parent', _select ); // relevant initNode() part } } return main; } function initNode( art, parent, kind, name ) { setCache( art, '_parent', parent ); if (art.keys) setCache(art, '_keys', getKeysDict( art )); if (kind === 'target') { // Prevent re-initialization of anonymous aspect with initDefinition(): // (that would be with parent: null which would be wrong) setCache( art, '$queries', null ); return; } if (art.type || !kind) // with type, top-level, query or mixin return; const { $origin } = art; if (typeof $origin === 'object') // null, […], {…} return; const step = $origin || name; if (parent.$origin || parent.type && kind !== 'enum' && parent.$origin !== null || getCache( parent, '$origin$step' )) setCache( art, '$origin$step', (kind === 'element' ? step : { [kind]: step }) ); } function dropDefinitionCache( main ) { const queries = getCache( main, '$queries' ); if (!queries) // not yet initialized return; if (!cache.delete( main )) // not yet initialized return; for (const qcache of queries || []) { const { _select } = qcache; for (const n of Object.keys( _select.mixin || {} )) cache.delete( _select.mixin[n] ); dropColumnsCache( _select.columns ); traverseDef( _select, null, null, null, a => cache.delete( a ) ); // elements } traverseDef( main, null, null, null, a => cache.delete( a ) ); } function dropColumnsCache( select ) { if (!select) return; for (const col of select.columns || select.expand || select.inline || []) { dropColumnsCache( col ); cache.delete( select ); } } /** * @param {CSN.Path} csnPath * * - return value `art`: the “resulting” CSN node of the reference * * - return value `links`: array of { art, env } in length of ref.path where * art = the definition or element reached by the ref path so far * env = the “navigation environment” provided by `art` * (not set for last item, except for `from` reference or with filter) * * - return value `scope` * global: first item is name of definition * param: first item is parameter of definition (with param: true) * parent: first item is elem of parent (definition or outer elem) * target: first item is elem in target (for keys of assocs) * $magic: magic variable (path starts with $magic, see also $self) * $self: first item is $self or $projection * // now values only in queries: * mixin: first item is mixin * alias: first item is table alias * source: first item is element in a source of the current query * query: first item is element of current query * ref-target: first item is element of target of outer ref item * (used for filter condition) * expand: ref is "path continuation" of a ref with EXPAND * inline: ref is "path continuation" of a ref with INLINE * * - return value `$env` is set with certain values of `scope`: * with 'alias': the query number _n_ (the _n_th SELECT) * with 'source': the table alias name for the source entity */ function inspectRef( csnPath ) { return analyseCsnPath( csnPath, csn, resolveRef ); } function resolveRef( ref, refCtx, main, query, parent, baseEnv ) { const path = (typeof ref === 'string') ? [ ref ] : ref.ref; if (!Array.isArray( path )) throw new ModelError( 'References must look like {ref:[...]}' ); if (main) // TODO: improve, for csnpath starting with art initDefinition( main ); const head = pathId( path[0] ); if (ref.param) { const boundOrMain = (query || !main.actions || parent === main) ? main // shortcut (would also have been return by function) : boundActionOrMain( parent ); return resolvePath( path, boundOrMain.params[head], boundOrMain, 'param' ); } const semantics = referenceSemantics[refCtx] || {}; if (semantics.$initOnly) return undefined; if (semantics.dynamic === 'global' || ref.global) return resolvePath( path, csn.definitions[head], null, 'global', semantics.assoc ); const qcache = query && cache.get( query.projection || query ); // first the lexical scopes (due to query hierarchy) and $magic: --------- if (semantics.lexical !== false) { const tryAlias = path.length > 1 || ref.expand || ref.inline; let ncache = qcache && (semantics.lexical ? semantics.lexical( qcache ) : qcache); while (ncache) { const alias = tryAlias && ncache.$aliases[head]; if (alias) { return resolvePath( path, alias._select || alias._ref, null, 'alias', ncache.$queryNumber ); } const mixin = ncache._select.mixin?.[head]; if (mixin && {}.hasOwnProperty.call( ncache._select.mixin, head )) { setCache( mixin, '_parent', qcache._select ); return resolvePath( path, mixin, null, 'mixin', ncache.$queryNumber ); } ncache = ncache.$next; } if (head.charAt(0) === '$') { if (head !== '$self' && head !== '$projection') return { scope: '$magic' }; const self = qcache && qcache.$queryNumber > 1 ? qcache._select : main; return resolvePath( path, self, null, '$self' ); } } // now the dynamic environment: ------------------------------------------ if (semantics.dynamic !== false) { if (semantics.dynamic === 'target') { // ref in keys const target = assocTarget( parent, refCtx ); return resolvePath( path, target.elements[head], target, 'target' ); } if (baseEnv) { // ref-target (filter condition), expand, inline if (semantics.dynamic !== 'query') return resolvePath( path, baseEnv.elements[head], baseEnv, semantics.dynamic ); // in an ON condition of an association inside inner expand/inline: const elemParent = getCache( parent, '_element' ); if (elemParent) // expand in expand return resolvePath( path, elemParent.elements[head], null, 'query' ); } if (!query) { // outside queries - TODO: items? let art = parent.elements?.[head]; if (parent.keys) { const keysDict = getCache( parent, '_keys' ); art = keysDict[head]; } // Ref to up_ in anonymous aspect else if (!art && head === 'up_') { const up = getCache( parent, '_parent' ); const target = up && typeof up.target === 'string' && csn.definitions[up.target]; if (target && target.elements) { initDefinition( up.target ); art = target.elements.up_; } } return resolvePath( path, art, parent, 'parent' ); } if (!qcache) throw new CompilerAssertion( `Query not in cache at: ${ locationString(query.$location) }` ); if (semantics.dynamic === 'query') { // TODO: for ON condition in expand, would need to use cached _element // TODO: test and implement - Issue #11792! return resolvePath( path, qcache.elements[head], null, 'query' ); } for (const name in qcache.$aliases) { const alias = qcache.$aliases[name]; const found = alias.elements[head]; if (found) return resolvePath( path, found, alias._ref, 'source', name ); } } // console.log(query.SELECT,qcache,qcache.$next,main) throw new ModelError( `Path item 0=${ head } refers to nothing, refCtx: ${ refCtx }` ); } /** * @param {CSN.Path} path * @param {CSN.Artifact} art * @param {CSN.Artifact} parent * @param {string} [scope] * @param [extraInfo] */ function resolvePath( path, art, parent, scope, extraInfo ) { if (!art && path.length > 1) { // TODO: For path.length===1, it may be that `art` is undefined, e.g. for CSN paths such // as `[…, 'on', 1]` where the path segment refers to `=`. // TODO: Check the call-side. const loc = locationString(parent?.$location); throw new ModelError(`Path item 0='${ pathId(path[0]) }' refers to nothing; in ${ loc }; path=${ JSON.stringify(path) }`); } const staticAssoc = extraInfo === 'static' && scope === 'global'; /** @type {{idx, art?, env?}[]} */ const links = path.map( (_v, idx) => ({ idx }) ); // TODO: backends should be changed to enable uncommenting: // if (!art) // does not work with test3/Associations/KeylessManagedAssociation/ // throw new ModelError( `Path item 0=${ pathId( path[0] ) // } refers to nothing, scope: ${ scope }`); links[0].art = art; for (let i = 1; i < links.length; ++i) { // yes, starting at 1, links[0] is set above parent = navigationEnv( art, staticAssoc ); links[i - 1].env = parent; if (typeof path[i - 1] !== 'string') setCache( path[i - 1], '_env', parent ); if (!parent.elements) throw new ModelError( `${ parent.from ? 'Query ' : '' }elements not available: ${ Object.keys( parent ).join('+') }`); art = parent.elements[pathId( path[i] )]; if (!art) { const { env } = links[i - 1]; const loc = env.name && env.name.$location || env.$location; throw new ModelError( `Path item ${ i }=${ pathId( path[i] ) } refers to nothing; in ${ locationString( loc ) }; path=${ JSON.stringify(path) }` ); } links[i].art = art; } const last = path[path.length - 1]; const fromRef = scope === 'global' && extraInfo === 'target'; if (fromRef || typeof last !== 'string') { const env = navigationEnv( art ); links[links.length - 1].env = env; if (fromRef) { art = env; parent = null; } if (typeof last !== 'string') setCache( last, '_env', env ); } return (extraInfo && scope !== 'global') ? { links, art, parent, scope, $env: extraInfo, } : { links, art, parent, scope, }; } /** * Return [ Location, SemanticLocation ] from `csnPath`. */ function msgLocations( csnPath ) { let location = csn?.$location; const artifact = new SemanticLocation(); /** @type object */ let obj = csn; let index = 0; let inlinePathIndex = null; if (typeof csnPath[0] === 'object') startPath( csnPath[0] ); /* eslint-disable no-return-assign */ const pathFunctions = { definitions: name => absolute( name, 'type' ), vocabularies: name => absolute( name, 'annotation' ), extensions, projection, SELECT: projection, // TODO: alias mixin: name => nameInProp( name, 'mixin' ), actions: name => nameInProp( name, 'action' ), params: name => nameInProp( name, 'param' ), returns: () => (artifact.param = ''), elements: name => elements( name, artifact.select == null ? null : 'element' ), columns: elements, expand: elements, inline: elements, keys: pos => elements( pos, 'key' ), enum: name => elements( name, 'enum' ), item: () => (artifact.innerKind = 'item'), // targetAspect: () => (artifact.innerKind = 'aspect') '@': suffix, }; /* eslint-enable no-return-assign */ while (obj && index < csnPath.length) { const step = csnPath[index++]; obj = obj[step]; const fn = pathFunctions[step] || pathFunctions[step.charAt( 0 )]; if (fn) fn( csnPath[index] ); if (obj?.$location) location = obj.$location; } return [ location, artifact ]; function startPath( art ) { const parent = getCache( art, '_parent' ); if (parent) { if (!art.SELECT && !art.projection) throw new CompilerAssertion( 'CSN path starts with object other than def or query' ); } obj = csn.definitions; absolute( getCache( parent || art, '$name' ), 'type' ); obj = art; location = art.$location || parent?.$location || csn.$location; } function absolute( name, defaultKind ) { obj = obj[name]; artifact.mainKind = obj.kind || defaultKind; artifact.absolute = name; ++index; } function extensions( pos ) { obj = obj[pos]; artifact.mainKind = obj.annotate ? 'annotate' : 'extend'; artifact.absolute = obj.annotate || obj.extend; ++index; } function projection() { let select = getCache( obj, '$queryNumber' ); if (select === 1) { const parent = getCache( obj, '_parent' ); if (parent && getCache( parent, '$queries' )?.length === 1) select = 0; } artifact.select = select; } function nameInProp( name, prop ) { obj = obj[name]; artifact[prop] = name; ++index; } function elements( name, kind ) { obj = obj[name]; const elem = (typeof name === 'string') ? name : !obj.inline && columnAlias( obj ); if (obj.inline) { // inline inlinePathIndex ??= artifact.element.length; } else if (inlinePathIndex != null) { // inline before: remove inline col indexes if (elem) artifact.element.length = inlinePathIndex; inlinePathIndex = null; } artifact.element.push( elem || name + 1); artifact.innerKind = kind || undefined; ++index; } function suffix( prop ) { artifact.suffix = prop; obj = null; // stop } } /** * Get the array of all (sub-)queries (value of the `SELECT`/`projection` * property) inside the given `main` artifact (of `main.query`). * * @param {CSN.Definition} main * @returns {CSN.Query[]} */ function allQueries( main ) { const all = []; const projection = main.query || main.projection && main; if (!projection) return null; traverseQuery( projection, null, null, function memorize( query, fromSelect, parentQuery ) { if (query.ref) { // ref in from // console.log('SQ:',query,cache.get(query)) const as = query.as || implicitAs( query.ref ); const _ref = cached( query, '_from', artifactFromRef ); getCache( fromSelect, '$aliases' )[as] = { _ref, elements: _ref.elements, _parent: query }; } else { const qcache = getQueryCache( parentQuery ); if (query !== main) cache.set( query, qcache ); if (fromSelect) { const $queryNumber = all.length + 1; const alias = query.as || `$_select_${ $queryNumber }__`; getCache(fromSelect, '$aliases')[alias] = qcache; } const select = query.SELECT || query.projection; if (select) { cache.set( select, qcache ); // query and query.SELECT have the same cache qcache qcache._select = select; qcache._parent = main; all.push( qcache ); } } } ); all.forEach( function initElements( qcache, index ) { qcache._parent = main; qcache.$queryNumber = index + 1; const { elements } = (index ? qcache._select : main); qcache.elements = elements; const { columns } = qcache._select; if (elements && columns) columns.map( (col, colIndex) => initColumnElement( col, colIndex, qcache ) ); else if (columns && !elements) throw new ModelError( `Query elements not available: ${ Object.keys( (index ? qcache._select : main) ).join('+') }`); } ); return all; } /** * Return the cache object for a new query. * Might re-use cache object with the `parentQuery`, or use `parentQuery` * for link to next lexical environment. */ function getQueryCache( parentQuery ) { if (!parentQuery) return { $aliases: Object.create(null) }; const pcache = cache.get( parentQuery.projection || parentQuery ); if (!parentQuery.SET) // SELECT / projection: real sub query return { $aliases: Object.create(null), $next: pcache }; // the parent query is a SET: that is not a sub query // (works, as no sub queries are allowed in ORDER BY) return (!pcache._select) // no leading query yet ? pcache // share cache with parent query : { $aliases: Object.create(null), $next: pcache.$next }; } function initColumnElement( col, colIndex, parentElementOrQueryCache, externalElements ) { if (col === '*') return; if (col.inline) { col.inline.map( c => initColumnElement( c, null, parentElementOrQueryCache, externalElements ) ); return; } setCache( col, '_parent', // not set for query (has property _select) !parentElementOrQueryCache._select && parentElementOrQueryCache ); let as = columnAlias( col ); if (!as && colIndex !== null) as = `$_column_${ colIndex + 1 }`; setCache( col, '$as', as ); let type = parentElementOrQueryCache; if (col.cast) traverseType( col.cast, col, 'column', colIndex, initNode ); while (type.items) type = type.items; if (!type.elements) { // in OData backend, the sub elements from a column with expand might have // been “externalized” into a named type. No backward _column link is // possible this way, of course... type = artifactRef( type.type ); externalElements = true; } const elem = setCache( col, '_element', type.elements[as] ); if (elem && !externalElements) // TODO to.sql: something is strange if `elem` is not set setCache( elem, '_column', col ); if (col.expand) col.expand.map( c => initColumnElement( c, null, elem, externalElements ) ); } // property name convention in cache: // - $name: to other cache object (with proto), dictionary (w/o proto), or scalar // - _name, name: to CSN object value (_name) or dictionary (name) function setCache( obj, prop, val ) { let hidden = cache.get( obj ); if (!hidden) { hidden = {}; cache.set( obj, hidden ); } // TODO: we might keep the following with --test-mode // if (hidden[prop] !== undefined) { // console.log('RS:',prop,hidden[prop],val,obj) // throw Error('RESET') // } hidden[prop] = val; return val; } function getCache( obj, prop ) { const hidden = cache.get( obj ); return hidden && hidden[prop]; } function cached( obj, prop, calc ) { let hidden = cache.get( obj ); if (!hidden) { hidden = {}; cache.set( obj, hidden ); } else if (hidden[prop] !== undefined) { return hidden[prop]; } const val = calc( obj ); hidden[prop] = val; return val; } } /** * Foreign keys are stored in an array; for easier name resolution, create * a dictionary of them. */ function getKeysDict( art ) { const dict = Object.create(null); for (const key of art.keys) dict[key.as || implicitAs( key.ref )] = key; return dict; } /** * Return value of a query SELECT for the query node, or the main artifact, * i.e. a value with an `elements` property. * TODO: only used in forRelationalDB - move somewhere else * * @param {object} query node (object with SET or SELECT property) * @param {object} main definition */ function queryOrMain( query, main ) { while (query.SET) query = query.SET.args[0]; if (query.SELECT && query.SELECT.elements) return query.SELECT; let leading = main.query || main; while (leading.SET) leading = leading.SET.args[0]; // If an entity has both a projection and query property, the param `query` // can be the entity itself (when inspect is called with a csnPath containing // 'projection'), but `leading` can be its `query` property: if ((leading === query || leading === query.query) && main.elements) return main; throw new ModelError( `Query elements not available: ${ Object.keys( query ).join('+') }`); } /** * Traverse query in pre-order * * The callback is called on the following XSN nodes inside the query `query`: * - a query node, which has property `SET` or `SELECT` (or `projection`), * - a query source node inside `from` if it has property `ref`, * - NOT on a `join` node inside `from`. * * @param {CSN.Query} query * @param {CSN.QuerySelect} fromSelect for query in `from` * @param {CSN.Query} parentQuery for a sub query (ex those in `from`) * @param {(query: CSN.Query&CSN.QueryFrom, select: CSN.QuerySelectEnriched, parentQuery: CSN.Query) => void} callback */ function traverseQuery( query, fromSelect, parentQuery, callback ) { const select = query.SELECT || query.projection; if (select) { callback( query, fromSelect, parentQuery ); traverseFrom( select.from, select, parentQuery, callback ); for (const prop of [ 'columns', 'where', 'having' ]) { // all properties which can have sub queries (`join-on` also can) const expr = select[prop]; if (expr) expr.forEach( q => traverseExpr( q, query, callback ) ); } } else if (query.SET) { callback( query, fromSelect, parentQuery ); const { args } = query.SET; for (const q of args || []) traverseQuery( q, null, query, callback ); } } /** * @param {CSN.QueryFrom} from * @param {CSN.QuerySelect} fromSelect * @param {CSN.Query} parentQuery * @param {(from: CSN.QueryFrom, select: CSN.QuerySelect, parentQuery: CSN.Query) => void} callback */ function traverseFrom( from, fromSelect, parentQuery, callback ) { if (from.ref) { callback( from, fromSelect, parentQuery ); } else if (from.args) { // join from.args.forEach( arg => traverseFrom( arg, fromSelect, parentQuery, callback ) ); if (from.on) // join-on, potentially having a sub query (in xpr) from.on.forEach(arg => traverseExpr( arg, fromSelect, callback )); } else { // sub query in FROM traverseQuery( from, fromSelect, parentQuery, callback ); } } function traverseExpr( expr, parentQuery, callback ) { if (expr.SELECT || expr.SET) traverseQuery( expr, null, parentQuery, callback ); for (const prop of [ 'args', 'xpr' ]) { // all properties which could have sub queries (directly or indirectly), const val = expr[prop]; if (val && typeof val === 'object') { const args = Array.isArray( val ) ? val : Object.values( val ); args.forEach( e => traverseExpr( e, parentQuery, callback ) ); } } } function traverseDef( node, parent, kind, name, callback ) { callback( node, parent, kind, name ); if (node.params) { for (const n of Object.keys( node.params )) traverseType( node.params[n], node, 'param', n, callback ); } if (node.returns) traverseType( node.returns, node, 'returns', true, callback ); traverseType( node, true, kind, name, callback ); if (node.actions) { for (const n of Object.keys( node.actions )) traverseDef( node.actions[n], node, 'action', n, callback ); } } function traverseType( node, parent, kind, name, callback ) { if (parent !== true) callback( node, parent, kind, name ); const target = targetAspect( node ); if (target && typeof target === 'object' && target.elements) { callback( target, node, 'target', true ); node = target; } else if (node.items) { let items = 0; while (node.items) { callback( node.items, node, 'items', ++items ); node = node.items; } } if (node.elements) { for (const n of Object.keys( node.elements )) traverseDef( node.elements[n], node, 'element', n, callback ); } if (node.enum) { for (const n of Object.keys( node.enum )) traverseDef( node.enum[n], node, 'enum', n, callback ); } } function targetAspect( art ) { const { $origin } = art; return art.targetAspect || $origin && typeof $origin === 'object' && !Array.isArray( $origin ) && $origin.target || art.target; } function pathId( item ) { return (typeof item === 'string') ? item : item.id; } function implicitAs( ref ) { if (typeof ref !== 'string') ref = ref[ref.length - 1]; const id = (typeof ref === 'string') ? ref : ref.id; // inlined `pathId` return id.substring( id.lastIndexOf('.') + 1 ); } function startCsnPath( csnPath, csn ) { const head = csnPath[0]; if (typeof head !== 'string') { const { main, parent, art, query, } = head; return { index: 1, main, parent, art, query, }; } if (csnPath.length < 2 || head !== 'definitions' && head !== 'vocabularies') throw new CompilerAssertion( 'References outside definitions and vocabularies not supported yet' ); const art = csn[head][csnPath[1]]; return { index: 2, main: art, parent: art, art, query: null, }; } /** * @param {CSN.Path} csnPath * @param {CSN.Model} csn * @param {any} resolve */ function analyseCsnPath( csnPath, csn, resolve ) { /** @type {any} */ let refCtx = null; /** @type {boolean|string|number} */ let isName = false; let baseRef = null; let baseCtx = null; let baseEnv = null; let { index, main, parent, art, query, } = startCsnPath( csnPath, csn ); let obj = art; for (; index < csnPath.length; index++) { if (!obj && !resolve) // For the semantic location, use current object as best guess break; const prop = csnPath[index]; if (refCtx === 'annotation' && typeof obj === 'object') { // we do not know yet whether the annotation value is an expression or not → // loop over outer array and records (structure values): if (Array.isArray( obj ) || !isAnnotationExpression( obj )) { obj = obj[prop]; continue; } refCtx = 'annotationExpr'; } // array item, name/index of artifact/member, (named) argument if (isName || Array.isArray( obj ) || prop === 'returns') { // TODO: call some kind of resolve.setOrigin() if (isName === 'actions') { art = obj[prop]; parent = art; // param refs in annos for actions are based on the action, not the entity } else if (typeof isName === 'string' || prop === 'returns') { parent = art; art = obj[prop]; } else if (refCtx === 'orderBy') { const isSelect = isSelectQuery( query ); // use _query_ elements with direct refs (consider sub-optimal CSN, // representation of the CAST function), otherwise source elements: if (obj[prop].ref && !obj[prop].cast) refCtx = (isSelect ? 'orderBy_ref' : 'orderBy_set_ref'); else refCtx = (isSelect ? 'orderBy_expr' : 'orderBy_set_expr'); } isName = false; } else if (artifactProperties.includes( String(prop) )) { if (refCtx === 'target' || refCtx === 'targetAspect') { // with 'elements' // $self refers to the anonymous aspect if (resolve) resolve( '', '$init', main ); main = obj; art = obj; parent = obj; } isName = prop; // if we want to allow auto-redirect of user-provided target with renamed keys: // (TODO: no, we do not allow that anymore) refCtx = (refCtx === '$origin' && prop === 'keys') ? 'keys_origin' : prop; } else if (prop