UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

777 lines (726 loc) 26.1 kB
// Common render functions for toCdl.js, toHdbcds.js and toSql.js 'use strict'; const { ModelError } = require('../../base/error'); const { standardDatabaseFunctions } = require('./standardDatabaseFunctions'); const functionsWithoutParams = { hana: { CURRENT_CONNECTION: {}, CURRENT_SCHEMA: {}, CURRENT_TRANSACTION_ISOLATION_LEVEL: {}, CURRENT_UTCDATE: {}, CURRENT_UTCTIME: {}, CURRENT_UTCTIMESTAMP: {}, SYSUUID: {}, }, }; const { hasValidSkipOrExists, forEachDefinition, getNamespace, getUnderscoredName, } = require('../../model/csnUtils'); const { implicitAs } = require('../../model/csnRefs'); /** * Render the given function * * @param {string} funcName Name of the function * @param {object} node Content of the function * @param {(a: string) => string} renderArgs Function to render function arguments * @param {object} utils Utility object containing options, path, and message functions * @returns {string} Function string */ function renderFunc( funcName, node, renderArgs, utils ) { const { options, path, messageFunctions } = utils || {}; const { sqlDialect } = options; if (funcWithoutParen( node, sqlDialect )) return funcName; const rewriteStandardFunctions = options.transformation !== 'hdbcds' && sqlDialect !== 'plain' && options.standardDatabaseFunctions !== false; if (rewriteStandardFunctions) { // we check function arguments for correctness const { error } = messageFunctions; const that = { renderArgs, error, path }; if (standardDatabaseFunctions[sqlDialect]?.[funcName]) return standardDatabaseFunctions[sqlDialect][funcName].call(that, node); else if (standardDatabaseFunctions.common[funcName]) return standardDatabaseFunctions.common[funcName].call(that, node); } return `${ funcName }(${ renderArgs( node ) })`; } /** * Checks whether the given function is to be rendered without parentheses * * @param {object} node Content of the function * @param {string} dialect One of 'hana', 'cap' or 'sqlite' - only 'hana' is relevant atm * @returns {boolean} True if without */ function funcWithoutParen( node, dialect ) { if (!node.args) return true; if (!Array.isArray( node.args ) || node.args.length) return false; const specials = functionsWithoutParams[dialect]; return specials && specials[node.func.toUpperCase()]; } /** * Process already rendered expression parts by joining them nicely. * For example, it adds spaces around operators such as `+`, but not around `.`. * * @param {any[]} tokens Array of expression tokens * @returns {string} The rendered xpr */ function beautifyExprArray( tokens ) { // Simply concatenate array parts with spaces (with a tiny bit of beautification) let result = ''; for (let i = 0; i < tokens.length; i++) { result += tokens[i]; // No space after last token, after opening parentheses, before closing parentheses, before comma, before and after dot if (i !== tokens.length - 1 && // current token tokens[i] !== '.' && // next token tokens[i + 1] !== '.' && tokens[i + 1] !== ',') result += ' '; } return result; } /** * Get the part that is really the name of this artifact and not just prefix caused by a context/service * * @param {CSN.Model} csn CSN model * @param {string} artifactName Artifact name to use * @returns {string} non-prefix part of the artifact name */ function getRealName( csn, artifactName ) { const parts = artifactName.split('.'); // Length of 1 -> There can be no prefix if (parts.length === 1) return artifactName; const namespace = getNamespace(csn, artifactName); const startIndex = namespace ? namespace.split('.').length : 0; let indexOfLastParent = startIndex; const realParts = getUnderscoredName(startIndex, parts, csn); if (realParts) return realParts[realParts.length - 1]; // With this loop, we find the name if the art is part of a context for (let i = startIndex; i < parts.length; i++) { const possibleParentName = parts.slice(0, i).join('.'); const art = csn.definitions[possibleParentName]; if (art && art.kind !== 'context' && art.kind !== 'service') return parts.slice(i).join('_'); else if (art && (art.kind === 'context' || art.kind === 'service')) indexOfLastParent = i; } // With this, we find the name if it is shadowed by another definition or similar return parts.slice(indexOfLastParent, parts.length).join('.'); } /** * For given artifact, return the name of the topmost context it is contained in (if any). * * Given context A and artifact A.B.C (with no context A.B) -> return A. * * Given context Namespace.A and artifact Namespace.A.B.C -> return Namespace.A. * * Given entity A and artifact * * @param {string} artifactName Name of the artifact to check for * @returns {string | null} Name of the topmost context or null */ function getParentContextName( csn, artifactName ) { const parts = artifactName.split('.'); for (let i = 0; i < parts.length - 1; i++) { const name = parts.slice(0, i).join('.'); const art = csn.definitions[name]; if (art && (art.kind === 'context' || art.kind === 'service')) return name; } return null; } /** * If there is a namespace A.B.C, create context A and a context A.B.C. * * Context A.B will be created by addIntermediateContexts * * @param {CSN.Model} csn * @param {Function[]} killList Array to add cleanup functions to */ function addContextMarkers( csn, killList ) { const contextsToCreate = Object.create(null); forEachDefinition(csn, (art, artifactName) => { const namespace = getNamespace(csn, artifactName); if (namespace && !(art.$ignore || hasValidSkipOrExists(art))) { const parts = namespace.split('.'); contextsToCreate[parts[0]] = true; if (parts.length > 1) contextsToCreate[namespace] = true; } }); Object.keys(contextsToCreate).forEach((contextName) => { if (!csn.definitions[contextName]) { csn.definitions[contextName] = { kind: 'context', }; killList.push(() => delete csn.definitions[contextName]); } }); } /** * For the given parent context and the current context, calculate all missing intermediate context names. * I.e. all the artifact names inbetween, that do not have a csn.definition. * * A and A.B.C.D -> A.B and A.B.C are possible candidates * * @param {CSN.Model} csn * @param {string} parentName Name of the parent context * @param {string} artifactName Name of the current context * @returns {string[]} All possible context names inbetween */ function getIntermediateContextNames( csn, parentName, artifactName ) { const parentLength = parentName.split('.').length; const parts = artifactName.split('.'); const names = []; for (let i = parentLength + 1; i < parts.length; i++) { const name = parts.slice(0, i).join('.'); const art = csn.definitions[name]; if (!art) names.push(name); } return names; } /** * For context A and entity A.B.C, create context A.B * * @param {CSN.Model} csn * @param {string} artifactName * @param {Function[]} killList Array to add cleanup functions to */ function addMissingChildContexts( csn, artifactName, killList ) { // Get all other definitions sharing the same prefix, sorted by shortest first const possibleNames = Object.keys(csn.definitions).filter(name => name.startsWith(`${ artifactName }.`)).sort((a, b) => a.length - b.length); for (const name of possibleNames) { const artifact = csn.definitions[name]; if (!artifact.$ignore && !hasValidSkipOrExists(artifact)) addPossibleGaps(name.slice(artifactName.length + 1).split('.'), artifactName); } function addPossibleGaps( possibleGaps, gapArtifactName ) { for (const gap of possibleGaps) { gapArtifactName += `.${ gap }`; if (!csn.definitions[gapArtifactName]) { const contextName = gapArtifactName; csn.definitions[contextName] = { kind: 'context', }; killList.push(() => delete csn.definitions[contextName]); } else { return; } } } } // Type mapping from cds type names to DB type names: // (in the future, we would introduce an option for the mapping table) const cdsToSqlTypes = { standard: { // characters and binaries 'cds.String': 'NVARCHAR', 'cds.hana.NCHAR': 'NCHAR', 'cds.LargeString': 'NCLOB', 'cds.hana.VARCHAR': 'VARCHAR', 'cds.hana.CHAR': 'CHAR', 'cds.hana.CLOB': 'CLOB', 'cds.Binary': 'VARBINARY', // not a Standard SQL type, but HANA and MS SQL Server 'cds.hana.BINARY': 'BINARY', 'cds.LargeBinary': 'BLOB', // numbers: exact and approximate 'cds.Decimal': 'DECIMAL', 'cds.DecimalFloat': 'DECIMAL', 'cds.Integer64': 'BIGINT', 'cds.Integer': 'INTEGER', 'cds.Int64': 'BIGINT', 'cds.Int32': 'INTEGER', 'cds.Int16': 'SMALLINT', 'cds.UInt8': 'TINYINT', 'cds.hana.SMALLINT': 'SMALLINT', 'cds.hana.TINYINT': 'TINYINT', // not a Standard SQL type 'cds.Double': 'DOUBLE', 'cds.hana.REAL': 'REAL', // other: date/time, boolean 'cds.Date': 'DATE', 'cds.Time': 'TIME', 'cds.DateTime': 'TIMESTAMP', // cds-compiler#2758 'cds.Timestamp': 'TIMESTAMP', 'cds.Boolean': 'BOOLEAN', // (TODO: do it later; TODO: why not CHAR or at least VARCHAR?) 'cds.UUID': 'NVARCHAR', // changed to cds.String earlier 'cds.hana.ST_POINT': 'CHAR', // CHAR is implicit fallback used in toSql - make it explicit here 'cds.hana.ST_GEOMETRY': 'CHAR', // CHAR is implicit fallback used in toSql - make it explicit here 'cds.Vector': 'NVARCHAR', // Not supported; see #11725 'cds.Map': 'NCLOB', // Not supported; see #13149 }, hana: { 'cds.hana.SMALLDECIMAL': 'SMALLDECIMAL', 'cds.DateTime': 'SECONDDATE', 'cds.hana.ST_POINT': 'ST_POINT', 'cds.hana.ST_GEOMETRY': 'ST_GEOMETRY', 'cds.Vector': 'REAL_VECTOR', // FIXME: test me 'cds.Map': 'NCLOB', }, sqlite: { 'cds.Date': 'DATE_TEXT', 'cds.Time': 'TIME_TEXT', 'cds.Timestamp': 'TIMESTAMP_TEXT', 'cds.DateTime': 'DATETIME_TEXT', 'cds.Binary': 'BINARY_BLOB', 'cds.hana.BINARY': 'BINARY_BLOB', 'cds.hana.SMALLDECIMAL': 'SMALLDECIMAL', 'cds.Vector': 'BINARY_BLOB', // Not supported; see #11725 'cds.Map': 'JSON_TEXT', // '_TEXT' suffix required for text affinity }, plain: { 'cds.Binary': 'VARBINARY', 'cds.hana.BINARY': 'BINARY', 'cds.hana.SMALLDECIMAL': 'DECIMAL', }, h2: { 'cds.Binary': 'VARBINARY', // same as for plain 'cds.LargeBinary': 'BINARY LARGE OBJECT', // BLOB would require a length! // 'cds.Decimal': 'DECFLOAT', // Decimal and Decimal(p) is mapped to cds.DecimalFloat 'cds.DecimalFloat': 'DECFLOAT', 'cds.DateTime': 'TIMESTAMP(0)', 'cds.Timestamp': 'TIMESTAMP(7)', 'cds.Map': 'JSON', 'cds.UInt8': 'SMALLINT', // See #13870; not equivalent, but smallint can hold >byte }, postgres: { // See <https://www.postgresql.org/docs/current/datatype.html> 'cds.String': 'VARCHAR', 'cds.LargeString': 'TEXT', 'cds.LargeBinary': 'BYTEA', 'cds.Binary': 'BYTEA', 'cds.Double': 'FLOAT8', 'cds.UInt8': 'SMALLINT', // See #13870; not equivalent, but smallint can hold >byte 'cds.Vector': 'VARCHAR', // Not supported; see #11725 'cds.Map': 'JSONB', }, }; // Type mapping from cds type names to HDBCDS type names: // Only those types, that need mapping, are listed. const cdsToHdbcdsTypes = { 'cds.UInt8': 'cds.hana.TINYINT', 'cds.Int16': 'cds.hana.SMALLINT', 'cds.Int32': 'cds.Integer', 'cds.Int64': 'cds.Integer64', 'cds.Timestamp': 'cds.UTCTimestamp', 'cds.DateTime': 'cds.UTCDateTime', 'cds.Date': 'cds.LocalDate', 'cds.Time': 'cds.LocalTime', }; /** * Default lengths for CDS types. */ const sqlDefaultLengths = { hana: { 'cds.String': 5000, }, default: { 'cds.String': 255, 'cds.Binary': 5000, }, }; function getDefaultTypeLengths( sqlDialect ) { if (!sqlDefaultLengths[sqlDialect]) return sqlDefaultLengths.default; return { ...sqlDefaultLengths.default, ...sqlDefaultLengths[sqlDialect] }; } /** * Maps $-variables per SQL dialect to a renderable expression. * Callers can use `.fallback` in case the wanted dialect is not found. * * IMPORTANT: There is no sqlDialect better-sqlite. This "fake" dialect is * set in variableForDialect() below. * * @type {object} */ const variablesToSql = { fallback: { // no fallback for $user.id and $tenant -> warning in call-site '$user.locale': '\'en\'', // $at.*/$now are handled in all dialects -> there is no need for a fallback }, hana: { '$user.id': "SESSION_CONTEXT('APPLICATIONUSER')", '$user.locale': "SESSION_CONTEXT('LOCALE')", $tenant: "SESSION_CONTEXT('APPLICATIONTENANT')", '$at.from': "TO_TIMESTAMP(SESSION_CONTEXT('VALID-FROM'))", '$at.to': "TO_TIMESTAMP(SESSION_CONTEXT('VALID-TO'))", '$valid.from': "TO_TIMESTAMP(SESSION_CONTEXT('VALID-FROM'))", '$valid.to': "TO_TIMESTAMP(SESSION_CONTEXT('VALID-TO'))", $now: "TO_TIMESTAMP(SESSION_CONTEXT('NOW'))", // special handling, see variableForDialect() '$now.deprecated': 'CURRENT_TIMESTAMP', // special handling, see variableForDialect() // in DEFAULT, we can't render SESSION_CONTEXT '$now.default': 'CURRENT_TIMESTAMP', }, postgres: { '$user.id': "current_setting('cap.applicationuser')", '$user.locale': "current_setting('cap.locale')", $tenant: "current_setting('cap.tenant')", '$at.from': "current_setting('cap.valid_from')::timestamp", '$at.to': "current_setting('cap.valid_to')::timestamp", '$valid.from': "current_setting('cap.valid_from')::timestamp", '$valid.to': "current_setting('cap.valid_to')::timestamp", $now: "current_setting('cap.now')::timestamp", '$now.deprecated': 'current_timestamp', '$now.default': 'current_timestamp', }, sqlite: { '$user.id': "session_context( '$user.id' )", '$user.locale': "session_context( '$user.locale' )", $tenant: "session_context( '$tenant' )", '$at.from': "session_context( '$valid.from' )", '$at.to': "session_context( '$valid.to' )", '$valid.from': "session_context( '$valid.from' )", '$valid.to': "session_context( '$valid.to' )", $now: "session_context( '$now' )", '$now.deprecated': 'CURRENT_TIMESTAMP', '$now.default': 'CURRENT_TIMESTAMP', }, 'old-sqlite': { // For sqlite, we render the string-format-time (strftime) function. // Because the format of `current_timestamp` is like that: '2021-05-14 09:17:19' whereas // the format for timestamps (at least in Node.js) is like that: '2021-01-01T00:00:00.000Z' // --> Therefore the comparison in the temporal where clause doesn't work properly. '$at.from': "strftime('%Y-%m-%dT%H:%M:%S.000Z', 'now')", // + 1ms compared to $at.from '$at.to': "strftime('%Y-%m-%dT%H:%M:%S.001Z', 'now')", '$valid.from': "strftime('%Y-%m-%dT%H:%M:%S.000Z', 'now')", '$valid.to': "strftime('%Y-%m-%dT%H:%M:%S.001Z', 'now')", $now: 'CURRENT_TIMESTAMP', }, plain: { // better-sqlite defaults '$user.id': "session_context( '$user.id' )", '$user.locale': "session_context( '$user.locale' )", $tenant: "session_context( '$tenant' )", '$at.from': "session_context( '$valid.from' )", '$at.to': "session_context( '$valid.to' )", '$valid.from': "session_context( '$valid.from' )", '$valid.to': "session_context( '$valid.to' )", $now: "session_context( '$now' )", '$now.deprecated': 'CURRENT_TIMESTAMP', '$now.default': 'CURRENT_TIMESTAMP', }, h2: { '$user.id': '@applicationuser', '$user.locale': '@locale', $tenant: '@tenant', '$at.from': '@valid_from', '$at.to': '@valid_to', '$valid.from': '@valid_from', '$valid.to': '@valid_to', $now: '@now', '$now.deprecated': 'current_timestamp', '$now.default': 'current_timestamp', }, }; /** * Get a renderable string for given variable for the given options.sqlDialect. * Note that this function does not handle `variableReplacements`. Callers should * first check if the user has specified them and use them instead. * * @param {SqlOptions} options * Used for `sqlDialect` and better-sqlite option. * @param {string} variable * Variable to render, e.g. `$user.id`. * @param {null|'default'} [context = null] * A context in which the variable is used. Some variables are translated * context-dependent, such as '$now'. * * @return {string|null} * `null` if the variable could not be found for the given dialect and in the fallback values. */ function variableForDialect( options, variable, context = null ) { const dialect = options.sqlDialect === 'sqlite' && options.betterSqliteSessionVariables === false ? 'old-sqlite' : options.sqlDialect; if (options.dollarNowAsTimestamp === true && variable === '$now') return variablesToSql.hana['$now.deprecated'] ?? variablesToSql.hana.$now; return variablesToSql[dialect]?.[`${ variable }.${ context }`] || variablesToSql[dialect]?.[variable] || variablesToSql.fallback[variable] || null; } /** * Whether a replacement is required for the given variable (e.g. '$user.id'). * Some variables such as `$user.id` are not required to have replacement values, even if * there is no proper fallback via `variableForDialect(…)` (for example in sqlDialect 'plain'). * * @param {string} name * @return {boolean} */ function isVariableReplacementRequired( name ) { const notRequired = [ '$user.id', '$user.locale', '$tenant' ]; return !notRequired.includes(name); } /** * Get the element matching the column * * @param {CSN.Elements} elements Elements of a query * @param {CSN.Column} column Column from the same query * @returns {CSN.Element} */ function findElement( elements, column ) { if (!elements) return undefined; if (column.as) return elements[column.as]; else if (column.ref) return elements[implicitAs(column.ref)]; else if (column.func) return elements[column.func]; return undefined; } /** * If there is a context A and a context A.B.C without a definition A.B, create an * intermediate context A.B to keep the context hierarchy intact. * * @param {Function[]} killList Array to add cleanup functions to */ function addIntermediateContexts( csn, killList ) { for (const artifactName in csn.definitions) { const artifact = csn.definitions[artifactName]; if ((artifact.kind === 'context') && !artifact.$ignore) { // If context A.B.C and entity A exist, we still need generate context A_B. // But if no entity A exists, A.B is just a namespace. // For case 1 and 2, getParentContextName returns undefined - we then use the namespace as our "off-limits" // starting point for finding the intermediates. const parentContextName = getParentContextName(csn, artifactName) || getNamespace(csn, artifactName) || ''; getIntermediateContextNames(csn, parentContextName, artifactName).forEach((name) => { if (!csn.definitions[name]) { csn.definitions[name] = { kind: 'context', }; killList.push(() => delete csn.definitions[name]); } }); addMissingChildContexts(csn, artifactName, killList); } } } /** * Check whether the given artifact or element has a comment that needs to be rendered. * Things annotated with @cds.persistence.journal (for HANA SQL), should not get a comment. * * @param {CSN.Artifact} obj * @param {CSN.Options} options To check for `disableHanaComments` * @returns {boolean} */ function hasHanaComment( obj, options ) { return !options.disableHanaComments && typeof obj.doc === 'string'; } /** * Return the comment of the given artifact or element. * Uses the first block (everything up to the first empty line (double \n)). * Remove leading/trailing whitespace. * Does not escape any characters, use e.g. `getEscapedHanaComment()` for HDBCDS. * * @param {CSN.Artifact|CSN.Element} obj * @returns {string} */ function getHanaComment( obj ) { return obj.doc.split('\n\n')[0].trim(); } /** * Get the @sql.prepend/append if set - already add a space after/before. * If no value is set, use ''; * * @param {CSN.Options} options * @param {object} obj * @returns {object} object with .front and .back */ function getSqlSnippets( options, obj ) { const front = obj['@sql.prepend'] ? `${ obj['@sql.prepend'] } ` : ''; const back = obj['@sql.append'] ? ` ${ obj['@sql.append'] }` : ''; return { front, back }; } /** * A function used to render a certain part of an expression object * * @callback renderPart * @param {object|array} expression * @this {ExpressionRenderer} * @returns {string} */ /** * The object containing the concrete rendering functions for the different parts * of an expression * * @typedef {object} ExpressionConfiguration * @property {(x: any) => string} finalize The final function to call on the expression(-string) before returning * @property {renderPart} typeCast * @property {renderPart} val * @property {renderPart} enum * @property {renderPart} ref * @property {renderPart} windowFunction * @property {renderPart} func * @property {renderPart} xpr * @property {renderPart} SELECT * @property {renderPart} SET * @property {Function} [visitExpr] * @property {Function} [renderExpr] * @property {Function} [renderSubExpr] * @property {boolean} [isNestedXpr] * @property {object} [env] */ /** * @typedef {object} ExpressionRenderer * @property {(x: any) => string} finalize The final function to call on the expression(-string) before returning * @property {renderPart} typeCast * @property {renderPart} val * @property {renderPart} enum * @property {renderPart} ref * @property {renderPart} windowFunction * @property {renderPart} func * @property {renderPart} xpr * @property {renderPart} SELECT * @property {renderPart} SET * @property {Function} visitExpr * @property {Function} renderExpr * @property {Function} renderSubExpr * @property {boolean} isNestedXpr * @property {object} env */ /** * If `xpr` has a `cast` property, return a copy without it, otherwise return `xpr`. * Useful for removing e.g. top-level CDL-style casts that should not be rendered as CAST(). * * @param xpr */ function withoutCast( xpr ) { return !xpr.cast ? xpr : { ...xpr, cast: undefined }; } /** * Render an expression (including paths and values) or condition 'x'. * (no trailing LF, don't indent if inline) * * @param {ExpressionConfiguration} rendererBase * @returns {ExpressionRenderer} Expression rendering utility */ function createExpressionRenderer( rendererBase ) { const renderer = Object.create(rendererBase); renderer.visitExpr = visitExpr; /** * @param {any} x * @param {object} env */ renderer.renderExpr = function renderExpr(x, env) { /** @type {ExpressionRenderer} */ const renderObj = Object.create(renderer); renderObj.env = env || this?.env; // The outermost expression is not nested. All `.xpr` inside `expr` // are nested. This information is used for adding parentheses around // expressions (see `this.xpr()`). renderObj.isNestedXpr = false; return renderObj.visitExpr(x); }; /** * @param {any} x * @param {object} env */ renderer.renderSubExpr = function renderSubExpr(x, env) { /** @type {ExpressionRenderer} */ const renderObj = Object.create(renderer); renderObj.env = env || this?.env; renderObj.isNestedXpr = true; return renderObj.visitExpr(x); }; return renderer; } /** * Render an expression (including paths and values) or condition 'x'. * (no trailing LF, don't indent if inline) * * `this` must refer to an object of type `ExpressionRenderer`, see * `createExpressionRenderer()` * * @param {any} x (Sub-)Expression to render * * @this ExpressionRenderer * @returns {string} Rendered expression */ function visitExpr( x ) { if (Array.isArray(x)) { // Compound expression, e.g. for on- or where-conditions. // If xpr is part of an array, it's always a nested xpr, // e.g. CSN for `(1=1 or 2=2) and 3=3`. const tokens = x.map((item, i) => { this.env.path.push( i ); const result = this.renderSubExpr(item, this.env); this.env.path.length -= 1; return result; }); return beautifyExprArray(tokens); } else if (typeof x !== 'object' || x === null) { // Not a literal value but part of an operator, function etc - just leave as it is return this.finalize(x); } else if (x.cast?.type && !x.cast.target) { return this.typeCast(x); } else if (x.list) { // Render as non-nested expr. return `(${ x.list.map((item, i) => { this.env.path.push('list', i); const result = this.renderExpr(item, this.env); this.env.path.length -= 2; return result; }).join(', ') })`; } else if (x['#']) { // Enum symbol return this.enum(x); } else if (x.val !== undefined) { return this.val(x); } else if (x.ref) { // Reference: Array of path steps, possibly preceded by ':' return this.ref(x); } else if (x.func) { // Function call, possibly with args (use '=>' for named args) if (x.xpr) return this.windowFunction(x); return this.func(x); } else if (x.xpr) { return this.xpr(x); } else if (x.SELECT) { return this.SELECT(x); } else if (x.SET) { return this.SET(x); } else if (x.as) { return ''; } throw new ModelError(`renderExpr(): Unknown expression: ${ JSON.stringify(x) }`); } module.exports = { renderFunc, createExpressionRenderer, getNamespace, getRealName, addIntermediateContexts, addContextMarkers, cdsToSqlTypes, cdsToHdbcdsTypes, variableForDialect, isVariableReplacementRequired, hasHanaComment, getHanaComment, findElement, funcWithoutParen, getSqlSnippets, withoutCast, getDefaultTypeLengths, };