UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

1,212 lines (1,126 loc) 57.4 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. // Property name convention in cache: // // - $PropName: to other cache object (with proto), dictionary (w/o proto), or scalar // - _PropName, PropName: to CSN object value or dictionary of CSN node (_PropName), // or CSN dictionary (PropName) // Property `_parent` in cache set with initDefinition() is set for members: // // - for an action, param or element: the node containing the dictionary of …, // → elements of the leading/other query have main/query as _parent // - for a `returns`/`items` or anonymous `target`/`targetAspect`: the node containing // the object (remark: in XSN, the _parent link of elements/items in items differs) // - for any query: use main as _parent // - for a mixin or table alias: use query select as _parent // - for a column: TODO // Additional properties in cache set with initDefinition(): // // - $name: name for a main artifact (in `definitions`) or a column // - _keys: the foreign keys as dictionary // - $origin$step: for Univeral CSN (TODO: describe further) // - $queries on def: array of caches for all queryies, null in anonymous aspects // - $queryNumber: the index position +1 of a query inside the $queries array // - _select: value of the `SELECT` or `projection` property of a query // - elements: the elements of the query (original CSN elements from query or main) // - $next: the “upper” query cache, used when searching for a table alias // - $aliases: dictionary of caches for table aliases // (currently not stored as cache of table alias, has _ref/_select and elements) // - _element on query column: the corresponding element // - _column on element of a query entity: the column “creating” it // Other properties set on-demand: // // - _ref on object with a `ref` property: cached return value of artifactRef{,.from} // - _env on non-string path item: environment provided by the ref so far, // next path item is element in it // - _effectiveType on def/member/items: cached result of effectiveType() // - _origin on def/member/items: the "prototype" (via type or relevant ref) '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 // there are also 'on_join' and 'on_mixin' with default semantics $calc: { lexical: justDollar, dynamic: 'query' }, // calculation for draft annotation: { lexical: justDollar, dynamic: 'query' }, // anno top-level `ref` annotationExpr: { lexical: justDollar, dynamic: 'query' }, // annotation assignment 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') // TODO: remove initAllDefinitions(); // 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 = artifactRefFrom; return { // init and drop caches: ---------------------------------------------------- initAllDefinitions, // TODO: delete initDefinition, dropDefinitionCache, // artifact references, effective type, and origin: ------------------------- artifactRef, // with prop `from`, see above effectiveType, getOrigin: art => cached( art, '_origin', getOriginRaw ), // the main API function: --------------------------------------------------- inspectRef, // file and semantic location for CSN path: --------------------------------- msgLocations, // small cache functions ---------------------------------------------------- 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, '$name' ), /** Return the query cache (with `elements`) for the query. */ queryForElements: query => query && cache.get( query.projection || query ), $getQueries: def => getCache( def, '$queries' ), // unstable API __getCache_forEnrichCsnDebugging: obj => cache.get( obj ), // functions which we could return top-level: ------------------------------- targetAspect, queryOrMain, }; // Init and drop caches: ------------------------------------------------------ function initAllDefinitions() { // TODO: delete for (const name of Object.keys( csn.definitions || {})) initDefinition( name ); } /** * Initialize the cache for the definition named `main` and all its members, * including queries and embedded anonymous aspects. * * Return the initialized artifact. */ 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, n, 'element', 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, name, kind ) { setCache( art, '_parent', parent ); if (art.keys) // TODO: what about managed w/o keys in Univeral CSN? 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 }) ); } /** * 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 = artifactRefFrom( query ); // TODO: really eval from refs here and not on-demand? // if not, just have this (w/o elements) as cache elem on the CSN node 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 }; } /** * TODO: comment * * In the OData backend, the sub elements from a column with expand might * have been “externalized” into a named type. No backward `_column` link * from the (now the type) element is possible this way, of course... */ 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 ); // TODO: why set _parent suddently to a cache and not the SELECT? let as = columnAlias( col ); if (!as && colIndex !== null) as = `$_column_${ colIndex + 1 }`; setCache( col, '$name', as ); let type = parentElementOrQueryCache; if (col.cast) traverseType( col.cast, col, colIndex, 'column', 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 ) ); } 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 ); } } // Artifact references, effective type, and origin: --------------------------- /** * Return the object pointing to by the artifact reference (in 'type', * 'includes', 'target'). For `from`, use artifactRefFrom()! * * Warning: do not use it for the binding parameter if the ref is `$self`. * * @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 artifactRefFrom( ref ) { const art = cached( ref, '_ref', r => navigationEnv( artifactPathRef( r ) ) ); if (art) return art; throw new ModelError( `Unknown artifact reference: ${ JSON.stringify(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; } /** * @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 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; } function getOriginRaw( art ) { if (art.type) { // TODO: make robust against "linked" = only direct if (art.type !== '$self') return artifactRef( art.type, BUILTIN_TYPE ); const action = boundActionOrMain( art ); const binding = action?.params && Object.values( action.params )[0]; // binding parameter must be typed with `$self` or `many $self`: const entity = binding && (art === binding || art === binding.items) && getCache( action, '_parent' ); // if name-deprecated-$self is a non-config error, this could be simplified to: // const entity = action?.params && getCache( action, '_parent' ); return entity || artifactRef( art.type, BUILTIN_TYPE ); } 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 boundActionOrMain( art ) { while (art.kind !== 'action' && art.kind !== 'function') { const p = getCache( art, '_parent' ); if (!p) return art; art = p; } return art; } 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 effectiveArtifactFor( art, 'actions' )[step.action]; if (step.param) return effectiveArtifactFor( art, 'params' )[step.param]; if (step.returns) return effectiveArtifactFor( 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 ) ); const [ prop, val ] = Object.entries( step )[0]; throw new CompilerAssertion( `Illegal navigation step {${ prop }: "${ val }"}` ); } function effectiveArtifactFor( art, property ) { while (!art[property]) art = cached( art, '_origin', getOriginRaw ); return art[property]; } // ---------------------------------------------------------------------------- /** * @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? // refs in annos on foreign keys use fk name, not fk ref name: const dict = parent.elements ?? getCache( parent, '_keys' ); let art = dict?.[head]; if (!art && head === 'up_') { // ref to up_ in anonymous aspect 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( `For semantics '${ refCtx }', 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 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 ); } // File and semantic location for CSN path: ----------------------------------- /** * 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 } } // Low-level cache functions: ------------------------------------------------- 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; } } // CSN path analysing: ---------------------------------------------------------- /** * Analyse path `csnPath` in the CSN `csn` and call `resolve`. * * @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): // TODO: add test → `@(A: [{target: ($user)}])` on query column 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 === 'items' || prop === 'returns') { art = obj[prop]; // TODO: what about parent here?, see cache property _parent // BTW, still necessary to calculate parent here if we have _parent? } else if (prop === 'args') { isName = true; // for named arguments } else if (prop === 'SELECT' || prop === 'SET' || prop === 'projection') { query = obj; parent = null; baseEnv = null; refCtx = prop; } else if (prop === 'where' && refCtx === 'ref') { if (resolve) baseEnv = resolve.ref_where( obj, baseRef, baseCtx, main, query, parent, baseEnv ); refCtx = 'ref_where'; } else if (prop === 'expand' || prop === 'inline') { if (obj.ref) { if (resolve) baseEnv = resolve.expandInline( obj, baseCtx, main, query, parent, baseEnv ); refCtx = prop; } // TODO: for on condition in expand, also set an environment isName = prop; } else if (prop === 'on') { if (refCtx === 'from') refCtx = 'on_join'; else if (refCtx === 'mixin') refCtx = 'on_mixin'; else refCtx = 'on'; // will use query elements with REDIRECTED TO } else if (prop === 'ref') { baseRef = obj; // needs to be inspected for filter conditions baseCtx = refCtx; refCtx = prop; } else if (prop === 'orderBy') { refCtx = 'orderBy'; } else if (prop[0] === '@') { refCtx = 'annotation'; } else if (prop === '$calc') { refCtx = '$calc'; } else if (prop !== 'xpr' && prop !== 'list') { // 'xpr' and 'list' do not change the ref context, all other props do: refCtx = prop; } obj = obj[prop]; } // console.log( 'CPATH:', csnPath, refCtx, obj, parent.$location ); if (!resolve) return { query }; // for constructSemanticLocationFromCsnPath return resolve( obj, refCtx, main, query, parent, baseEnv ); } 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, }; } // CSN traversal functions: ----------------------------------------------------- /** * Traverse an artifact `node` and its members, but without queries, in pre-order. * * Call `callback` on all nodes with the following arguments: * * - `node`: the current node * - `parent`: the parent node (containing the dictionary), * see the initial file comment for cache property _parent for details * - `name`: the name of the current node in the