UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

1,293 lines (1,137 loc) 69.5 kB
'use strict'; const { getLastPartOf, getLastPartOfRef, hasValidSkipOrExists, getNormalizedQuery, getRootArtifactName, getResultingName, getNamespace, forEachMember, getVariableReplacement, pathName, hasPersistenceSkipAnnotation, } = require('../model/csnUtils'); const { isBuiltinType, isMagicVariable } = require('../base/builtins'); const keywords = require('../base/keywords'); const { renderFunc, createExpressionRenderer, getRealName, addContextMarkers, addIntermediateContexts, hasHanaComment, getHanaComment, funcWithoutParen, getSqlSnippets, cdsToSqlTypes, cdsToHdbcdsTypes, withoutCast, variableForDialect, isVariableReplacementRequired, } = require('./utils/common'); const { renderReferentialConstraint, } = require('./utils/sql'); const DuplicateChecker = require('./DuplicateChecker'); const { forEachDefinition, isDeprecatedEnabled } = require('../base/model'); const { checkCSNVersion } = require('../json/csnVersion'); const { timetrace } = require('../utils/timetrace'); const { smartId, delimitedId } = require('../sql-identifier'); const { ModelError, CompilerAssertion } = require('../base/error'); const { pathId } = require('../model/csnRefs'); const $PROJECTION = '$projection'; const $SELF = '$self'; // TODO: Unify with other RenderEnvironments class HdbcdsRenderEnvironment { indent = ''; path = null; /** * Dictionary of aliases for used artifact names, each entry like 'name' : { quotedName, quotedAlias } * @type {{[name: string]: { * quotedName: string, * quotedAlias: string * }}} */ topLevelAliases = Object.create(null); // Current name prefix (including trailing dot if not empty) namePrefix = ''; // Skip rendering keys in subqueries skipKeys = false; currentArtifactName = null; // The original view artifact, used when rendering queries _artifact = null; constructor(values) { Object.assign(this, values); } withIncreasedIndent() { return new HdbcdsRenderEnvironment({ ...this, namePrefix: '', indent: ` ${ this.indent }` }); } withSubPath(path) { return new HdbcdsRenderEnvironment({ ...this, path: [ ...this.path, ...path ] }); } cloneWith(values) { return Object.assign(new HdbcdsRenderEnvironment(this), values); } } /** * Get the comment and in addition escape \n and `'` so SAP HANA CDS can handle it. * * @param {CSN.Artifact} obj * @returns {string} */ function getEscapedHanaComment( obj ) { return getHanaComment(obj) .replace(/\n/g, '\\n') .replace(/'/g, "''"); } /** * Render a string for HDBCDS, i.e. put it in quotes and escape single quotes. * * @param {string} str * @returns {string} */ function renderStringForHdbcds( str ) { return `'${ str.replace(/'/g, '\'\'') }'`; } /** * Render the CSN model 'model' to CDS source text. * * @param {CSN.Model} csn HANA transformed CSN * @param {CSN.Options} [options] Transformation options * @param {object} messageFunctions Message functions such as `error()`, `info()`, … * @returns {object} Dictionary of filename: content */ function toHdbcdsSource( csn, options, messageFunctions ) { timetrace.start('HDBCDS rendering'); const plainNames = options.sqlMapping === 'plain'; const quotedNames = options.sqlMapping === 'quoted'; const hdbcdsNames = options.sqlMapping === 'hdbcds'; const { info, warning, error, throwWithAnyError, message, } = messageFunctions; const reportedMissingReplacements = Object.create(null); const exprRenderer = createExpressionRenderer({ finalize: x => x, typeCast(x) { let typeRef = renderTypeReference(x.cast, this.env.withSubPath([ 'cast' ])); // inside a cast expression, the cds and hana cds types need to be mapped to hana sql types const hanaSqlType = cdsToSqlTypes.hana[x.cast.type] || cdsToSqlTypes.standard[x.cast.type]; if (hanaSqlType) { const typeRefWithoutParams = typeRef.substring(0, typeRef.indexOf('(')) || typeRef; typeRef = typeRef.replace(typeRefWithoutParams, hanaSqlType); } return `CAST(${ this.renderExpr(withoutCast(x)) } AS ${ typeRef })`; }, val: renderExpressionLiteral, enum: x => `#${ x['#'] }`, ref: renderExpressionRef, windowFunction: renderExpressionFunc, func: renderExpressionFunc, xpr(x) { const xprEnv = this.env.withSubPath([ 'xpr' ]); if (this.isNestedXpr && !x.cast) return `(${ this.renderSubExpr(x.xpr, xprEnv) })`; return this.renderSubExpr(x.xpr, xprEnv); }, SELECT(x) { return `(${ renderQuery(x, false, this.env.withIncreasedIndent()) })`; }, SET(x) { return `${ renderQuery(x, false, this.env.withIncreasedIndent()) }`; }, }); function renderExpr( x, env ) { return exprRenderer.renderExpr(x, env); } checkCSNVersion(csn, options); const hdbcdsResult = Object.create(null); const globalDuplicateChecker = new DuplicateChecker(options.sqlMapping); // registry for all artifact names and element names const killList = []; if (quotedNames) addContextMarkers(csn, killList); if (!plainNames) addIntermediateContexts(csn, killList); // Render each top-level artifact on its own const hdbcds = Object.create(null); for (const artifactName in getTopLevelArtifacts()) { const art = csn.definitions[artifactName]; // This environment is passed down the call hierarchy, for dealing with // indentation and name resolution issues const env = createEnv(); const sourceStr = renderDefinition(artifactName, art, env); // Must come first because it populates 'env.topLevelAliases' if (sourceStr !== '') { const name = plainNames ? artifactName.replace(/\./g, '_').toUpperCase() : artifactName; hdbcds[name] = [ renderNamespaceDeclaration(name, env), renderUsings(name, env), sourceStr, ].join(''); } } // render .hdbconstraint into result const hdbconstraint = Object.create(null); forEachDefinition(csn, (art) => { if (art.$tableConstraints && art.$tableConstraints.referential) { const referentialConstraints = {}; Object.entries(art.$tableConstraints.referential) .forEach(([ fileName, referentialConstraint ]) => { referentialConstraints[fileName] = renderReferentialConstraint( referentialConstraint, createEnv().withIncreasedIndent().indent, false, csn, options ); }); Object.entries(referentialConstraints) .forEach(([ fileName, constraint ]) => { hdbconstraint[fileName] = constraint; }); } }); hdbcdsResult.hdbcds = hdbcds; hdbcdsResult.hdbconstraint = hdbconstraint; if (globalDuplicateChecker) globalDuplicateChecker.check(error, options); // perform duplicates check killList.forEach(fn => fn()); throwWithAnyError(); timetrace.stop('HDBCDS rendering'); return options.testMode ? sort(hdbcdsResult) : hdbcdsResult; /** * Sort the given object alphabetically * * @param {Object} obj Object to sort * @returns {Object} With keys sorted */ function sort( obj ) { const keys = Object.keys(obj).sort((a, b) => a.localeCompare(b)); const sortedResult = Object.create(null); for (const key of keys) sortedResult[key] = obj[key]; return sortedResult; } /** * Render a definition. Return the resulting source string. * * @param {string} artifactName Name of the artifact to render * @param {CSN.Artifact} art Content of the artifact to render * @param {HdbcdsRenderEnvironment} env Environment * @returns {string} The rendered artifact */ function renderDefinition( artifactName, art, env ) { // We're always a top-level artifact. env.path = [ 'definitions', artifactName ]; // Ignore whole artifacts if toHana says so if (art.abstract || hasValidSkipOrExists(art)) return ''; switch (art.kind) { case 'entity': // FIXME: For HANA CDS, we need to replace $self at the beginning of paths in association ON-condition // by the full name of the artifact we are rendering (should actually be done by forRelationalDB, but that is // somewhat difficult because this kind of absolute path is quite unusual). In order not to have to pass // the current artifact name down through the stack to renderExpr, we just put it into the env. env.currentArtifactName = artifactName; if (art.query || art.projection) return renderView(artifactName, art, env); return renderEntity(artifactName, art, env); case 'context': case 'service': return renderContext(artifactName, art, env, false); case 'namespace': return renderNamespace(artifactName, art, env); case 'type': case 'aspect': return renderType(artifactName, art, env); case 'annotation': case 'action': case 'function': case 'event': return ''; default: throw new ModelError(`Unknown artifact kind: ${ art.kind }`); } } /** * Return a dictionary with the direct sub-artifacts of the artifact with name 'artifactName' in the csn * * @param {string} artifactName Find all children of this artifact * @returns {object} Dictionary with direct sub-artifacts */ function getSubArtifacts( artifactName ) { const prefix = `${ artifactName }.`; const result = Object.create(null); for (const name in csn.definitions) { // We have a direct child if its name starts with prefix and contains no more dots if (name.startsWith(prefix) && !name.substring(prefix.length).includes('.')) { result[getLastPartOf(name)] = csn.definitions[name]; } else if (name.startsWith(prefix) && !isContainedInOtherContext(name, artifactName)) { const prefixPlusNextPart = name.substring(0, name.substring(prefix.length).indexOf('.') + prefix.length); if (csn.definitions[prefixPlusNextPart]) { const art = csn.definitions[prefixPlusNextPart]; if (![ 'service', 'context', 'namespace' ].includes(art.kind)) { const nameWithoutPrefix = name.substring(prefix.length); result[nameWithoutPrefix] = csn.definitions[name]; } } else { result[name.substring(prefix.length)] = csn.definitions[name]; } } } return options && options.testMode ? sort(result) : result; } /** * Check whether the given context is the direct parent of the containee. * * @param {string} containee Name of the contained artifact * @param {string} contextName Name of the (grand?)parent context * @returns {boolean} True if there is another context in between */ function isContainedInOtherContext( containee, contextName ) { const parts = containee.split('.'); const prefixLength = contextName.split('.').length; for (let i = parts.length - 1; i > prefixLength; i--) { const prefix = parts.slice(0, i).join('.'); const art = csn.definitions[prefix]; if (art && (art.kind === 'context' || art.kind === 'service')) return true; } return false; } /** * Render a context or service. Return the resulting source string. * * If the context is shadowed by another entity, the context itself is not rendered, * but any contained (and transitively contained) entities and views are. * * @param {string} artifactName Name of the context/service * @param {CSN.Artifact} art Content of the context/service * @param {HdbcdsRenderEnvironment} env Environment * @param {boolean} isShadowed * @returns {string} The rendered context/service */ function renderContext( artifactName, art, env, isShadowed ) { let result = ''; if (!isShadowed) isShadowed = contextIsShadowed(artifactName); if (isShadowed) { const subArtifacts = getSubArtifacts(artifactName); for (const name in subArtifacts) result += renderDefinition(`${ artifactName }.${ name }`, subArtifacts[name], env); return `${ result }\n`; } const childEnv = env.withIncreasedIndent(); result += `${ env.indent }context ${ renderArtifactName(artifactName, env, true) }`; result += ' {\n'; const subArtifacts = getSubArtifacts(artifactName); let renderedSubArtifacts = ''; for (const name in subArtifacts) renderedSubArtifacts += renderDefinition(`${ artifactName }.${ name }`, subArtifacts[name], updatePrefixForDottedName(childEnv, name)); if (renderedSubArtifacts === '') return ''; return `${ result + renderedSubArtifacts + env.indent }};\n`; } /** * Check whether the given context is shadowed, i.e. part of his name prefix is shared by a * non-context/service/namespace definition * * @param {string} artifactName * @returns {boolean} */ function contextIsShadowed( artifactName ) { if (artifactName.indexOf('.') === -1) return false; const parts = artifactName.split('.'); for (let i = 0; i < parts.length; i++) { const art = csn.definitions[parts.slice(0, i).join('.')]; if (art && art.kind !== 'context' && art.kind !== 'service' && art.kind !== 'namespace') return true; } return false; } /** * In case of an artifact with . in the name (that are not a namespace/context part), * we need to update the env to correctly render the artifact name. * * @param {HdbcdsRenderEnvironment} env Environment * @param {string} name Possibly dotted artifact name * @returns {HdbcdsRenderEnvironment} Updated env or original instance */ function updatePrefixForDottedName( env, name ) { if (plainNames) { let innerEnv = env; if (name.indexOf('.') !== -1) { const parts = name.split('.'); for (let i = 0; i < parts.length - 1; i++) innerEnv = addNamePrefix(innerEnv, parts[i]); } return innerEnv; } return env; } /** * Render a namespace. Return the resulting source string. * * @param {string} artifactName Name of the namespace * @param {CSN.Artifact} art Content of the namespace * @param {HdbcdsRenderEnvironment} env Environment * @returns {string} The rendered children of the namespace */ function renderNamespace( artifactName, art, env ) { // We currently do not render anything for a namespace, we just append its id to // the environment's current name prefix and descend into its children let result = ''; const childEnv = addNamePrefix(env, getLastPartOf(artifactName)); const subArtifacts = getSubArtifacts(artifactName); for (const name in subArtifacts) result += renderDefinition(`${ artifactName }.${ name }`, subArtifacts[name], updatePrefixForDottedName(childEnv, name)); return result; } /** * Render a non-query entity. Return the resulting source string. * * @param {string} artifactName Name of the entity * @param {CSN.Artifact} art Content of the entity * @param {HdbcdsRenderEnvironment} env Environment * @returns {string} The rendered entity */ function renderEntity( artifactName, art, env ) { let result = ''; const childEnv = env.withIncreasedIndent(); const normalizedArtifactName = renderArtifactName(artifactName, env); globalDuplicateChecker.addArtifact(art['@cds.persistence.name'], env.path, artifactName); if (hasHanaComment(art, options)) result += `${ env.indent }@Comment: '${ getEscapedHanaComment(art) }'\n`; // tables can have @sql.prepend and @sql.append const { front, back } = getSqlSnippets(options, art); if (front) // attach @sql.prepend after adding @Comment annotation result += front; result += `${ env.indent + (art.abstract ? 'abstract ' : '') }entity ${ normalizedArtifactName }`; if (art.includes) { // Includes are never flattened (don't exist in HANA) result += ` : ${ art.includes.map((name, i) => renderAbsoluteNameWithQuotes(name, env.withSubPath([ 'includes', i ]))).join(', ') }`; } result += ' {\n'; const duplicateChecker = new DuplicateChecker(); // registry for all artifact names and element names duplicateChecker.addArtifact(artifactName, env.path, artifactName); // calculate __aliases which must be used in case an association // has the same identifier as it's target createTopLevelAliasesForArtifact(artifactName, art, env); for (const name in art.elements) result += renderElement(name, art.elements[name], childEnv.withSubPath([ 'elements', name ]), duplicateChecker); duplicateChecker.check(error); result += `${ env.indent }}`; result += `${ renderTechnicalConfiguration(art.technicalConfig, env) }`; if (back) result += back; return `${ result };\n`; } /** * If an association/composition has the same identifier as it's target * we must render a "using target as __target" and use the alias to refer to the target * * @param {string} artName * @param {CSN.Artifact} art * @param {HdbcdsRenderEnvironment} env */ function createTopLevelAliasesForArtifact( artName, art, env ) { forEachMember(art, (element) => { if (!element.target) return; if (uppercaseAndUnderscore(element.target) === element['@cds.persistence.name']) { let alias = createTopLevelAliasName(element['@cds.persistence.name']); // calculate new alias if it would conflict with other csn.Artifact while (csn.definitions[alias]) alias = createTopLevelAliasName(alias); env.topLevelAliases[element['@cds.persistence.name']] = { quotedName: formatIdentifier(element['@cds.persistence.name']), quotedAlias: formatIdentifier(alias), }; } }); } /** * Render the 'technical configuration { ... }' section 'tc' of an entity. * * @param {object} tc content of the technical configuration * @param {HdbcdsRenderEnvironment} env Environment * @returns {string} Return the resulting source string. */ function renderTechnicalConfiguration( tc, env ) { if (!tc) return ''; let result = ''; const childEnv = env.withIncreasedIndent(); // FIXME: How to deal with non-HANA technical configurations? We should probably just iterate all entries // in 'tc' that we find and render them all (is it syntactically allowed yet to have more than one?) tc = tc.hana; if (!tc) throw new ModelError('Expecting a SAP HANA technical configuration'); result += `\n${ env.indent }technical ${ tc.calculated ? '' : 'hana ' }configuration {\n`; // Store type (must be separate because SQL wants it between 'CREATE' and 'TABLE') if (tc.storeType) result += `${ tc.storeType } store;\n`; // Fixed parts belonging to the table (includes migration, unload prio, extended storage, // auto merge, partitioning, ...) if (tc.tableSuffix) { // Unlike SQL, CDL and HANA CDS require a semicolon after each table-suffix part // (e.g. `migration enabled; row store; ...`). In order to keep both // the simplicity of "the whole bandwurm is just one expression that can be // rendered to SQL without further knowledge" and at the same time telling // CDS about the boundaries, the compactor has put each part into its own `xpr` // object. Semantically equivalent because a "trivial" SQL renderer would just // concatenate them. for (const xpr of tc.tableSuffix) result += `${ childEnv.indent + renderExpr(xpr, childEnv) };\n`; } // Indices and full-text indices for (const idxName in tc.indexes || {}) { if (Array.isArray(tc.indexes[idxName][0])) { // FIXME: Should we allow multiple indices with the same name at all? for (const index of tc.indexes[idxName]) result += `${ childEnv.indent + renderExpr(index, childEnv) };\n`; } else { result += `${ childEnv.indent + renderExpr(tc.indexes[idxName], childEnv) };\n`; } } // Fuzzy search indices for (const columnName in tc.fzindexes || {}) { if (Array.isArray(tc.fzindexes[columnName][0])) { // FIXME: Should we allow multiple fuzzy search indices on the same column at all? // And if not, why do we wrap this into an array? for (const index of tc.fzindexes[columnName]) result += `${ childEnv.indent + renderExpr(fixFuzzyIndex(index, columnName), childEnv) };\n`; } else { result += `${ childEnv.indent + renderExpr(fixFuzzyIndex(tc.fzindexes[columnName], columnName), childEnv) };\n`; } } result += `${ env.indent }}`; return result; /** * Fuzzy indices are stored in compact CSN as they would appear in SQL after the column name, * i.e. the whole line in SQL looks somewhat like this: * s nvarchar(10) FUZZY SEARCH INDEX ON FUZZY SEARCH MODE 'ALPHANUM' * But in CDL, we don't write fuzzy search indices together with the table column, so we need * to insert the name of the column after 'ON' in CDS syntax, making it look like this: * fuzzy search mode on (s) search mode 'ALPHANUM' * This function expects an array with the original expression and returns an array with the modified expression * * @param {Array} fuzzyIndex Expression array representing the fuzzy index * @param {string} columnName Name of the SQL column * @returns {Array} Modified expression array */ function fixFuzzyIndex( fuzzyIndex, columnName ) { return fuzzyIndex.map(token => (token === 'on' ? { xpr: [ 'on', { xpr: { ref: columnName.split('.') } } ] } : token)); } } /** * Render an element (of an entity, type or annotation, not a projection or view). * Return the resulting source string. * * @param {string} elementName Name of the element * @param {CSN.Element} elm Content of the element * @param {HdbcdsRenderEnvironment} env Environment * @param {DuplicateChecker} [duplicateChecker] Utility for detecting duplicates * @param {boolean} [isSubElement] Whether the given element is a subelement or not - subelements cannot be key! * @returns {string} The rendered element */ function renderElement( elementName, elm, env, duplicateChecker, isSubElement ) { // Ignore if toHana says so if (elm.virtual) return ''; // Special handling for HANA CDS: Must omit the ':' before anonymous structured types (for historical reasons) const omitColon = (!elm.type && elm.elements); let result = ''; const quotedElementName = formatIdentifier(elementName); if (duplicateChecker) duplicateChecker.addElement(quotedElementName, env.path, elementName); if (hasHanaComment(elm, options)) result += `${ env.indent }@Comment: '${ getEscapedHanaComment(elm) }'\n`; result += env.indent + (elm.key && !isSubElement ? 'key ' : '') + (elm.masked ? 'masked ' : '') + quotedElementName + (omitColon ? ' ' : ' : ') + renderTypeReference(elm, env); // GENERATED AS ALWAYS() can't have a trailing "[not] null" nor "default". // Because we already emit an error that calc-on-write is not supported, just ignore nullability/default. if (!elm.value?.stored) { result += renderNullability(elm); if (elm.default && !elm.target) result += ` default ${ renderExpr(elm.default, env.withSubPath([ 'default' ])) }`; } // (table) elements can only have a @sql.append const { back } = getSqlSnippets(options, elm); if (back) result += back; return `${ result };\n`; } /** * Render the source of a query, which may be a path reference, possibly with an alias, * or a subselect, or a join operation, as seen from artifact 'art'. * Returns the source as a string. * * @param {object} source Source to render * @param {HdbcdsRenderEnvironment} env Environment * @returns {string} Rendered view source */ function renderViewSource( source, env ) { // Sub-SELECT if (source.SELECT || source.SET) { let result = `(${ renderQuery(source, false, env.withIncreasedIndent()) })`; if (source.as) result += ` as ${ formatIdentifier(source.as) }`; return result; } // JOIN else if (source.join) { // One join operation, possibly with ON-condition let result = `${ renderViewSource(source.args[0], env.withSubPath([ 'args', 0 ])) }`; for (let i = 1; i < source.args.length; i++) { result = `(${ result } ${ source.join } `; result += `join ${ renderViewSource(source.args[i], env.withSubPath([ 'args', i ])) }`; if (source.on) result += ` on ${ renderExpr(source.on, env.withSubPath([ 'on' ])) }`; result += ')'; } return result; } // Ordinary path, possibly with an alias return renderAbsolutePathWithAlias(source, env); } /** * Render a path that starts with an absolute name (as used e.g. for the source of a query), * with plain or quoted names, depending on options. Expects an object 'path' that has a 'ref'. * Returns the name as a string. * * @param {object} path Path to render * @param {HdbcdsRenderEnvironment} env Environment * @returns {string} Rendered path */ function renderAbsolutePath( path, env ) { // Sanity checks if (!path.ref) throw new ModelError(`Expecting ref in path: ${ JSON.stringify(path) }`); // Determine the absolute name of the first artifact on the path (before any associations or element traversals) const firstArtifactName = path.ref[0].id || path.ref[0]; let result = ''; // Render the first path step (absolute name, with different quoting/naming ..) if (plainNames) result += renderAbsoluteNamePlain(firstArtifactName, env); else result += renderAbsoluteNameWithQuotes(firstArtifactName, env); // Even the first step might have parameters and/or a filter if (path.ref[0].args) result += `(${ renderArgs(path.ref[0], ':', env.withSubPath([ 'ref', 0 ])) })`; if (path.ref[0].where) { const cardinality = path.ref[0].cardinality ? (`${ path.ref[0].cardinality.max }: `) : ''; result += `[${ cardinality }${ renderExpr(path.ref[0].where, env.withSubPath([ 'ref', 0, 'where' ])) }]`; } // Add any path steps (possibly with parameters and filters) that may follow after that if (path.ref.length > 1) result += `.${ renderTypeRef({ ref: path.ref.slice(1) }, env) }`; return result; } /** * Render a path that starts with an absolute name (as used for the source of a query), * possibly with an alias, with plain or quoted names, depending on options. Expects an object 'path' that has a * 'ref' and (in case of an alias) an 'as'. If necessary, an artificial alias * is created to the original implicit name. * Returns the name and alias as a string. * * @param {object} path Path to render * @param {HdbcdsRenderEnvironment} env Environment * @returns {string} Rendered path including alias */ function renderAbsolutePathWithAlias( path, env ) { let result = renderAbsolutePath(path, env); // Take care of aliases - for artifact references, use the resulting name (multi-dot joined with _) const implicitAlias = path.ref.length === 0 ? getLastPartOf(getResultingName(csn, options.sqlMapping, path.ref[0])) : getLastPartOfRef(path.ref); if (path.as) { // Source had an alias - render it result += ` as ${ formatIdentifier(path.as) }`; } else if (getLastPartOf(result) !== formatIdentifier(implicitAlias)) { // Render an artificial alias if the result would produce a different one result += ` as ${ formatIdentifier(implicitAlias) }`; } return result; } /** * Render a single view or projection column 'col', as it occurs in a select list or * projection list within 'art', possibly with annotations. * Return the resulting source string (no trailing LF). * * @param {object} col Column to render * @param {CSN.Elements} elements where column exists * @param {HdbcdsRenderEnvironment} env Environment * @returns {string} Rendered column */ function renderViewColumn( col, elements, env ) { const leaf = col.as || col.ref && col.ref[col.ref.length - 1] || col.func; const element = elements[leaf]; // Render 'null as <alias>' only for database and if element is virtual if (element?.virtual) { if (isDeprecatedEnabled(options, '_renderVirtualElements')) return `${ env.indent }null as ${ formatIdentifier(leaf) }`; return ''; } return renderNonVirtualColumn(); function renderNonVirtualColumn() { let result = env.indent; // only if column is virtual, keyword virtual was present in the source text if (col.virtual) result += 'virtual '; // If key is explicitly set in a non-leading query, issue an error. if (col.key && env.skipKeys) error(null, env.path, { keyword: 'key', $reviewed: true }, 'Unexpected $(KEYWORD) in subquery'); const key = (!env.skipKeys && (col.key || element?.key) ? 'key ' : ''); result += key + renderExpr(withoutCast(col), env); let alias = col.as || (!col.args && col.func); // func: e.g. CURRENT_TIMESTAMP // HANA requires an alias for 'key' columns just for syntactical reasons // FIXME: This will not complain for non-refs (but that should be checked in forRelationalDB) // Explicit or implicit alias? // Shouldn't we simply generate an alias all the time? if ((key || col.cast) && !alias) alias = leaf; if (alias) result += ` as ${ formatIdentifier(alias) }`; // Explicit type provided for the view element? if (col.cast?.target) { // Special case: Explicit association type is actually a redirect // Redirections are never flattened (don't exist in HANA) result += ` : redirected to ${ renderAbsoluteNameWithQuotes(col.cast.target, env.withSubPath([ 'cast', 'target' ])) }`; if (col.cast.on) result += ` on ${ renderExpr(col.cast.on, env.withSubPath([ 'cast', 'on' ])) }`; } return result; } } /** * Render a view. If '$syntax' is set (to 'projection', 'view', 'entity'), * the view query is rendered in the requested syntax style, otherwise it * is rendered as a view. * * @param {string} artifactName Name of the artifact * @param {CSN.Artifact} art Content of the artifact * @param {HdbcdsRenderEnvironment} env Environment * @returns {string} The rendered view */ function renderView( artifactName, art, env ) { let result = ''; const artifactPath = [ 'definitions', artifactName ]; globalDuplicateChecker.addArtifact(art['@cds.persistence.name'], artifactPath, artifactName); if (hasHanaComment(art, options)) result += `${ env.indent }@Comment: '${ getEscapedHanaComment(art) }'\n`; result += `${ env.indent }${ art.abstract ? 'abstract ' : '' }view ${ renderArtifactName(artifactName, env) }`; if (art.params) { const childEnv = env.withIncreasedIndent(); const parameters = Object.keys(art.params) .map(name => renderParameter(name, art.params[name], childEnv.withSubPath([ 'params', name ]))) .join(',\n'); // SAP HANA only understands the 'with parameters' syntax' result += ` with parameters\n${ parameters }\n${ env.indent }as `; } else { result += ' as '; } env._artifact = art; result += renderQuery(getNormalizedQuery(art).query, true, env.withSubPath([ art.projection ? 'projection' : 'query' ]), art.elements); // views can only have a @sql.append const { back } = getSqlSnippets(options, art); if (back) result += back; result += ';\n'; return result; } /** * Render a query 'query', i.e. a select statement with where-condition etc. * If 'isLeadingQuery' is true, mixins, actions and functions of 'art' are * also rendered into the query. Use 'syntax' style ('projection', 'view', * or 'entity') * * @param {CSN.Query} query Query object * @param {boolean} isLeadingQuery Whether the query is the leading query or not * @param {HdbcdsRenderEnvironment} env Environment * @param {object} [elements] For leading query, the elements of the artifact * @returns {string} The rendered query */ function renderQuery( query, isLeadingQuery, env, elements = null ) { const isProjection = env.path[env.path.length - 1] === 'projection'; let result = ''; env.skipKeys = !isLeadingQuery; // Set operator, like UNION, INTERSECT, ... if (query.SET) { // First arg may be leading query result += `(${ renderQuery(query.SET.args[0], isLeadingQuery, env.withSubPath([ 'SET', 'args', 0 ]), elements || query.SET.elements) }`; // FIXME: Clarify if set operators can be n-ary (assuming binary here) if (query.SET.op) { // Loop over all other arguments, i.e. for A UNION B UNION C UNION D ... for (let i = 1; i < query.SET.args.length; i++) result += `\n${ env.indent }${ query.SET.op }${ query.SET.all ? ' all' : '' } ${ renderQuery(query.SET.args[i], false, env.withSubPath([ 'SET', 'args', i ]), elements || query.SET.elements) }`; } result += ')'; // Set operation may also have an ORDER BY and LIMIT/OFFSET (in contrast to the ones belonging to // each SELECT) if (query.SET.orderBy) result += `${ continueIndent(result, env) }order by ${ query.SET.orderBy.map((entry, i) => renderOrderByEntry(entry, env.withSubPath([ 'SET', 'orderBy', i ]))).join(', ') }`; if (query.SET.limit) result += `${ continueIndent(result, env) }${ renderLimit(query.SET.limit, env.withSubPath([ 'SET', 'limit' ])) }`; return result; } // Otherwise must have a SELECT else if (!query.SELECT) { throw new ModelError(`Unexpected query operation ${ JSON.stringify(query) } at ${ JSON.stringify(env.path) }`); } if (!isProjection) env = env.withSubPath([ 'SELECT' ]); const select = query.SELECT; result += `select from ${ renderViewSource(select.from, env.withSubPath([ 'from' ])) }`; const childEnv = env.withIncreasedIndent(); childEnv.currentArtifactName = $PROJECTION; // $self to be replaced by $projection if (select.mixin) { let elems = ''; for (const name in select.mixin) elems += renderElement(name, select.mixin[name], childEnv.withSubPath([ 'mixin', name ])); if (elems) { result += ' mixin {\n'; result += elems; result += `${ env.indent }} into`; } } result += select.distinct ? ' distinct' : ''; if (select.columns) { result += ' {\n'; result += select.columns .map((col, i) => renderViewColumn(col, elements || select.elements, childEnv.withSubPath([ 'columns', i ]))) .filter(s => s !== '') .join(',\n'); result += `\n${ env.indent }}`; } if (select.excluding) { const excludingList = select.excluding.map(id => `${ childEnv.indent }${ formatIdentifier(id) }`).join(',\n'); result += ` excluding {\n${ excludingList }\n`; result += `${ env.indent }}`; } return renderSelectProperties(select, result, env); } /** * Render WHERE, GROUP BY, HAVING, ORDER BY and LIMIT clause * * @param {CSN.QuerySelect} select * @param {string} alreadyRendered The query as it has been rendered so far * @param {HdbcdsRenderEnvironment} env Environment * @returns {string} The query with WHERE etc. added */ function renderSelectProperties( select, alreadyRendered, env ) { if (select.where) alreadyRendered += `${ continueIndent(alreadyRendered, env) }where ${ renderExpr(select.where, env.withSubPath([ 'where' ])) }`; if (select.groupBy) alreadyRendered += `${ continueIndent(alreadyRendered, env) }group by ${ select.groupBy.map((expr, i) => renderExpr(expr, env.withSubPath([ 'groupBy', i ]))).join(', ') }`; if (select.having) alreadyRendered += `${ continueIndent(alreadyRendered, env) }having ${ renderExpr(select.having, env.withSubPath([ 'having' ])) }`; if (select.orderBy) alreadyRendered += `${ continueIndent(alreadyRendered, env) }order by ${ select.orderBy.map((entry, i) => renderOrderByEntry(entry, env.withSubPath([ 'orderBy', i ]))).join(', ') }`; if (select.limit) alreadyRendered += `${ continueIndent(alreadyRendered, env) }${ renderLimit(select.limit, env.withSubPath([ 'limit' ])) }`; return alreadyRendered; } /** * Utility function to make sure that we continue with the same indentation in WHERE, GROUP BY, ... after a closing curly brace and beyond * * @param {string} result Result of a previous render step * @param {HdbcdsRenderEnvironment} env Environment * @returns {string} String to join with */ function continueIndent( result, env ) { if (result.endsWith('}') || result.endsWith('})')) { // The preceding clause ended with '}', just append after that return ' '; } // Otherwise, start new line and indent normally return `\n${ env.withIncreasedIndent().indent }`; } /** * Render a query's LIMIT clause, which may also have OFFSET. * * @param {CSN.QueryLimit} limit CSN limit clause * @param {HdbcdsRenderEnvironment} env Environment * @returns {string} Rendered limit clause */ function renderLimit( limit, env ) { let result = ''; if (limit.rows !== undefined) result += `limit ${ renderExpr(limit.rows, env.withSubPath([ 'rows' ])) }`; if (limit.offset !== undefined) { const indent = result !== '' ? `\n${ env.withIncreasedIndent().indent }` : ''; result += `${ indent }offset ${ renderExpr(limit.offset, env.withSubPath([ 'offset' ])) }`; } return result; } /** * Render one entry of a query's ORDER BY clause (which always has a 'value' expression, and may * have a 'sort' property for ASC/DESC and a 'nulls' for FIRST/LAST * * @param {object} entry CSN order by * @param {HdbcdsRenderEnvironment} env Environment * @returns {string} Rendered order by */ function renderOrderByEntry( entry, env ) { let result = renderExpr(entry, env); if (entry.sort) result += ` ${ entry.sort }`; if (entry.nulls) result += ` nulls ${ entry.nulls }`; return result; } /** * Render a view parameter. * * @param {string} parName Name of the parameter * @param {object} par CSN parameter * @param {HdbcdsRenderEnvironment} env Environment * @returns {string} The resulting parameter as source string (no trailing LF). */ function renderParameter( parName, par, env ) { if (par.notNull === true || par.notNull === false) info('query-ignoring-param-nullability', env.path, { '#': 'std' }); return `${ env.indent + formatParamIdentifier(parName, env.path) } : ${ renderTypeReference(par, env) }`; } /** * Render a type (derived or structured). * Return the resulting source string. * * @param {string} artifactName Name of the artifact * @param {CSN.Artifact} art Content of the artifact * @param {HdbcdsRenderEnvironment} env Environment * @returns {string} Rendered type/annotation */ function renderType( artifactName, art, env ) { if (art.kind === 'aspect' || art.kind === 'type' && !hdbcdsNames || art.kind === 'type' && hdbcdsNames && !art.elements) return ''; let result = ''; result += `${ env.indent + (art.kind) } ${ renderArtifactName(artifactName, env, true) }`; if (art.includes) { // Includes are never flattened (don't exist in HANA) result += ` : ${ art.includes.map(name => renderAbsoluteNameWithQuotes(name, env)).join(', ') }`; } if (art.elements && !art.type) { const childEnv = env.withIncreasedIndent(); // Structured type or annotation with anonymous struct type result += ' {\n'; for (const name in art.elements) result += renderElement(name, art.elements[name], childEnv.withSubPath([ 'elements', name ])); result += `${ env.indent }};\n`; } else { // Derived type or annotation with non-anonymous type result += ` : ${ renderTypeReference(art, env) };\n`; } return result; } /** * Render a reference to a type used by 'elm' (named or inline) * Allow suppressing enum-rendering - used in columns for example * * @param {object} elm Element using the type reference * @param {HdbcdsRenderEnvironment} env Environment * @returns {string} Rendered type reference */ function renderTypeReference( elm, env ) { let result = ''; // Array type: Render items instead if (elm.items && !elm.type) { // HANA CDS does not support keyword many let rc = `array of ${ renderTypeReference(elm.items, env.withSubPath([ 'items' ])) }`; if (elm.items.notNull != null) rc += elm.items.notNull ? ' not null' : ' null'; return rc; } // FIXME: Is this a type attribute? result += (elm.localized ? 'localized ' : ''); // Anonymous structured type if (!elm.type && !elm.value) { if (!elm.elements) throw new ModelError(`Missing type of: ${ JSON.stringify(elm) }`); result += '{\n'; const childEnv = env.withIncreasedIndent(); // omit "key" keyword for nested elements, as this will result in a deployment error in naming mode 'hdbcds' const dontRenderKeyForNestedElement = hdbcdsNames; for (const name in elm.elements) result += renderElement(name, elm.elements[name], childEnv.withSubPath([ 'elements', name ]), null, dontRenderKeyForNestedElement); result += `${ env.indent }}`; return result; } // Association type if ([ 'cds.Association', 'cds.Composition' ].includes(elm.type)) return result + renderAssociationType(elm, env); if (elm.type?.ref) { // Reference to another element // For HANA CDS, we need a 'type of' result += `type of ${ renderAbsolutePath(elm.type, env.withSubPath([ 'type' ])) }`; } else if (isBuiltinType(elm.type)) { // If we get here, it must be a named type result += renderBuiltinType(elm); } else { // Simple absolute name // Type names are never flattened (derived types are unraveled in HANA) result += renderAbsoluteNameWithQuotes(elm.type, env.withSubPath([ 'type' ])); } if (elm.value) { if (!elm.value.stored) throw new CompilerAssertion('Found calculated element on-read in rendering; should have been replaced!'); message('def-unsupported-calc-elem', env.path, { '#': 'hdbcds' }); result += ` GENERATED ALWAYS AS ${ renderExpr(elm.value, env.withSubPath([ 'value' ])) }`; return result; } return result; } /** * @param {CSN.Element} elm * @param {HdbcdsRenderEnvironment} env * @returns {string} */ function renderAssociationType( elm, env ) { // Type, cardinality and target let result = `association${ renderCardinality(elm.cardinality) } to `; // normal target or named aspect if (elm.target || elm.targetAspect && typeof elm.targetAspect === 'string') { // we might have a "using target as __target" const targetArtifact = csn.definitions[elm.target]; const targetAlias = env.topLevelAliases[targetArtifact['@cds.persistence.name']]; if (targetAlias) { result += targetAlias.quotedAlias; } else { const target = elm.target || elm.targetAspect; const childEnv = env.withSubPath([ elm.target ? 'target' : 'targetAspect' ]); result += plainNames ? renderAbsoluteNamePlain(target, childEnv) : renderAbsoluteNameWithQuotes(target, childEnv); } } // ON-condition (if any) if (elm.on) { result += ` on ${ renderExpr(elm.on, env.withSubPath([ 'on' ])) }`; } else if (elm.targetAspect?.elements) { // anonymous aspect const childEnv = env.withIncreasedIndent(); result += '{\n'; for (const name in elm.targetAspect.elements) result += renderElement(name, elm.targetAspect.elements[name], childEnv.withSubPath([ 'targetAspect', 'elements', name ])); result += `${ env.indent }}`; } // Foreign keys (if any, unless we also have an ON_condition (which means we have been transformed from managed to unmanaged) if (elm.keys && !elm.on) result += ` { ${ Object.keys(elm.keys).map(name => renderForeignKey(elm.keys[name], env.withSubPath([ 'keys', name ]))).join(', ') } }`; return result; } /** * Render a builtin type. cds.Integer => render as Integer (no quotes) * Map Decimal (w/o Prec/Scale) to cds.DecimalFloat for HANA CDS * * @param {CSN.Element} elm Element with the type * @returns {string} The rendered type */ function renderBuiltinType( elm ) { if (elm.type === 'cds.Decimal' && elm.scale === undefined && elm.precision === undefined) return 'DecimalFloat'; const type = cdsToHdbcdsTypes[elm.type] || elm.type; return type.replace(/^cds\./, '') + renderTypeParameters(elm); } /** * Render a single path step 's' at path position 'idx', which can have filters or parameters or be a function * * @param {string|object} s Path step * @param {number} idx Path position * @param {HdbcdsRenderEnvironment} env * @returns {string} Rendered path step */ function renderPathStep( s, idx, env ) { // Simple id or absolute name if (typeof s === 'string') { // HANA-specific extra magic (should actually be in forRelationalDB) // In HANA, we replace leading $self by the absolute name of the current artifact // (see FIXME at renderDefinition) if (idx === 0 && s === $SELF) { // do not produce USING for $projection if (env.currentArtifactName === $PROJECTION) return env.currentArtifactName; return plainNames ? renderAbsoluteNamePlain(env.currentArtifactName, env) : renderAbsoluteNameWithQuotes(env.currentArtifactName, env); } // TODO: quote $parameters if it doesn't reference a parameter, this requires knowledge about the kind // Example: both views are correct in HANA CDS // entity E { key id: Integer; } // view EV with parameters P1: Integer as select from E { id, $parameters.P1 }; // view EVp as select from E as "$parameters" { "$parameters".id }; if (idx === 0 && [ $SELF, $PROJECTION, '$session' ].includes(s)) return s; return formatIdentifier(s); } // ID with filters or parameters else if (typeof s === 'object') { // Sanity check if (!s.func && !s.id) throw new ModelError(`Unknown path step object: ${ JSON.stringify(s) }`); // Not really a path step but an object-like function call if (s.func) return `${ s.func }(${ renderArgs(s, '=>', env) })`; // Path step, possibly with view parameters and/or filters let result = `${ formatIdentifier(s.id) }`; if (s.args) { // View parameters result += `(${ renderArgs(s, ':', env) })`; } if (s.where) { // Filter, possibly with cardinality const cardinality = s.cardinality ? `${ s.cardinality.max }: ` : ''; result += `[${ cardinality }${ renderExpr(s.where, env.withSubPath([ 'where' ])) }]`; } return result; } throw new ModelError(`Unknown path step: ${ JSON.stringify(s) } at ${ JSON.stringify(env.path) }`); } /** * @param {object} x Expression with a val and/or literal property * @returns {string} Rendered expression */ function renderExpressionLiteral( x ) { // Literal value, possibly with explicit 'literal' property switch (x.literal || typeof x.val) { case 'number': case 'boolean': case 'null': return x.val; case 'x': case 'date': case 'time': case 'timestamp': return `${ x.literal }'${ x.val }'`; case 'string': return renderStringForHdbcds(x.val); case 'object': if (x.val === null) return 'null'; // otherwise fall through to default: throw new ModelError(`Unknown literal or type: ${ JSON.stringify(x) }`); } } /** * Render the given expression x - which has a .func property * * @param {object} x * @returns {string} */ function renderExpressionFunc( x ) { const regex = RegExp(/^[a-zA-Z][\w#$]*$/, 'g'); const funcName = regex.test(x.func) ? x.func : quoteId(x.func); // we can't quote functions with parens, issue warning if it is a reserved keyword if (!funcWithoutParen(x, 'hana') && keywords.hdbcds.includes(uppercaseAndUnderscore(funcName))) warning(null, this.env.path, { id: uppercaseAndUnderscore(funcName) }, 'The identifier $(ID) is a SAP HANA keyword'); return renderFunc(funcName, x, a => renderArgs(a, '=>', this.env), { options }); } /** * Render a magic variable. Values are determined in following order: * 1. User defined replacement in options.variableReplacements * 2. Predefined fallback values * 3. Rendering of the variable as a string (i.e. its name) + warning * * @param {CSN.Path} ref * @param {object} env * @return {string} */ function renderMagicVariable( ref, env ) { const magicReplacement = getVariableReplacement(ref, options); if (magicReplacement !== null) return renderStringForHdbcds(magicReplacement); const name = pathName(ref); const result = variableForDialect(options, name); if (result) return result; if (isVariableReplacementRequired(name)) { reportedMissingReplacements[name] = true; error('ref-