@cap-js/db-service
Advanced tools
+19
-0
@@ -7,2 +7,21 @@ # Changelog | ||
| ## [2.9.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.8.2...db-service-v2.9.0) (2026-03-09) | ||
| ### Added | ||
| * runtime views ([#1410](https://github.com/cap-js/cds-dbs/issues/1410)) ([5242675](https://github.com/cap-js/cds-dbs/commit/5242675c97472b86b81b3dc5fe0906141d276b02)) | ||
| * support calculated elements in hierarchies ([#1456](https://github.com/cap-js/cds-dbs/issues/1456)) ([97c6f66](https://github.com/cap-js/cds-dbs/commit/97c6f6661f0ac4043245e021f2bf182f4e5d406f)) | ||
| ### Fixed | ||
| * **`exists`:** detect join relevant path after exists ([#1412](https://github.com/cap-js/cds-dbs/issues/1412)) ([c5bad06](https://github.com/cap-js/cds-dbs/commit/c5bad06724ce6761379f91748490c6caac84153a)), closes [#1407](https://github.com/cap-js/cds-dbs/issues/1407) | ||
| * **cqn2sql:** Relied on inconstistent behavior of cds.ql.cloned queries ([#1500](https://github.com/cap-js/cds-dbs/issues/1500)) ([f9cb201](https://github.com/cap-js/cds-dbs/commit/f9cb2011219a86ae22f22fcc105e597b23209adf)) | ||
| * enable expressions for `inline` ([#1512](https://github.com/cap-js/cds-dbs/issues/1512)) ([65f78e1](https://github.com/cap-js/cds-dbs/commit/65f78e1f3af83188462e9d44db67daa5d743ceb0)) | ||
| * path expressions for scoped queries ([#1507](https://github.com/cap-js/cds-dbs/issues/1507)) ([0f1e234](https://github.com/cap-js/cds-dbs/commit/0f1e234b373f26a6244c715c9ca9d4a207a0faed)) | ||
| * reject duplicated wildcards ([#1511](https://github.com/cap-js/cds-dbs/issues/1511)) ([b483062](https://github.com/cap-js/cds-dbs/commit/b483062e2ff5a8d0960dc2e7b71880af87ee8f78)) | ||
| * the combination of `iterator` and `SELECT.one` ([#1514](https://github.com/cap-js/cds-dbs/issues/1514)) ([4b28579](https://github.com/cap-js/cds-dbs/commit/4b2857920a7a57bcfc09a9b5fb765283cf8bd70b)) | ||
| * wildcard on inlined assoc ([#1513](https://github.com/cap-js/cds-dbs/issues/1513)) ([e520b97](https://github.com/cap-js/cds-dbs/commit/e520b97fd30394825b937b3613370c32c36c24a4)) | ||
| ## [2.8.2](https://github.com/cap-js/cds-dbs/compare/db-service-v2.8.1...db-service-v2.8.2) (2026-02-03) | ||
@@ -9,0 +28,0 @@ |
+124
-42
| const cds = require('@sap/cds') | ||
| const cds_infer = require('./infer') | ||
| const cqn4sql = require('./cqn4sql') | ||
| const { resolveTable } = require('./utils') | ||
| const _simple_queries = cds.env.features.sql_simple_queries | ||
@@ -29,3 +31,4 @@ const _strict_booleans = _simple_queries < 2 | ||
| const e = name.id || name | ||
| return (query?._target || this.model?.definitions[e])?.['@cds.persistence.name'] || e | ||
| const entity = query?._target || this.model?.definitions[e] | ||
| return (!entity?.['@cds.persistence.skip'] && entity?.['@cds.persistence.name']) || e | ||
| } | ||
@@ -80,2 +83,3 @@ this.class.prototype.quote = (s) => `"${String(s).replace(/"/g, '""')}"` | ||
| const kind = q.kind || Object.keys(q)[0] // SELECT, INSERT, ... | ||
| if (q._with) this._with = q._with | ||
| /** | ||
@@ -95,3 +99,2 @@ * @type {string} the rendered SQL string | ||
| if (DEBUG && (LOG_SQL._debug || LOG_SQLITE._debug)) { | ||
@@ -263,9 +266,11 @@ let values = sanitize_values && (this.entries || this.values?.length > 0) ? ['***'] : this.entries || this.values || [] | ||
| if (!where && from?.ref?.length === 1 && from.ref[0]?.where) where = from.ref[0]?.where | ||
| const columns = this.SELECT_columns(q) | ||
| let sql = `SELECT` | ||
| if (distinct) sql += ` DISTINCT` | ||
| if (!_empty(columns)) sql += ` ${columns}` | ||
| if (recurse) sql += ` FROM ${this.SELECT_recurse(q)}` | ||
| else if (!_empty(from)) sql += ` FROM ${this.from(from, q)}` | ||
| else sql += this.from_dummy() | ||
| if (recurse) sql += this.SELECT_recurse(q) | ||
| else { | ||
| sql += ` ${this.SELECT_columns(q)}` | ||
| if (!_empty(from)) sql += ` FROM ${this.from(from, q)}` | ||
| else sql += this.from_dummy() | ||
| } | ||
| if (!recurse && !_empty(where)) sql += ` WHERE ${this.where(where)}` | ||
@@ -303,3 +308,3 @@ if (!recurse && !_empty(groupBy)) sql += ` GROUP BY ${this.groupBy(groupBy)}` | ||
| const clone = q.clone() | ||
| clone.columns(keys) | ||
| clone.SELECT.columns = keys | ||
| clone.SELECT.recurse = undefined | ||
@@ -355,6 +360,12 @@ clone.SELECT.limit = undefined | ||
| const element = target.elements[name] | ||
| if (element.virtual || element.value || element.isAssociation) continue | ||
| if (element['@Core.Computed'] && name in availableComputedColumns) continue | ||
| if (element.virtual || element.isAssociation) continue | ||
| if (name in availableComputedColumns) continue | ||
| if (name.toUpperCase() in reservedColumnNames) ref.as = `$$${name}$$` | ||
| columnsIn.push(ref) | ||
| // This only supports calculated elements within the scope of the own entity | ||
| if ('value' in element) { | ||
| const requested = columnsFiltered.find(c => this.column_name(c) === element.name) | ||
| if (requested) columnsIn.push(requested) | ||
| else continue | ||
| } | ||
| else columnsIn.push(ref) | ||
| const foreignkey4 = element._foreignKey4 | ||
@@ -389,3 +400,3 @@ if ( | ||
| if (orderBy) { | ||
| orderBy = orderBy.map(r => { | ||
| orderBy = orderBy.filter(o => o.ref).map(r => { | ||
| let col = r.ref.at(-1) | ||
@@ -507,9 +518,15 @@ if (col.toUpperCase() in reservedColumnNames) col = `$$${col}$$` | ||
| const columnsQuery = cds.ql(q).clone() | ||
| columnsQuery.SELECT.columns = columns.map(x => { | ||
| if (x.element && 'value' in x.element) return { element: x.element, ref: [this.column_name(x)] } | ||
| return x | ||
| }) | ||
| const recurseColumns = this.SELECT_columns(columnsQuery) | ||
| // Only apply result join if the columns contain a references which doesn't start with the source alias | ||
| if (from.args && columns.find(c => c.ref?.[0] === alias)) { | ||
| graph.as = alias | ||
| return this.from(setStableFrom(from, graph)) | ||
| return ` ${recurseColumns} FROM ${this.from(setStableFrom(from, graph))}` | ||
| } | ||
| return `(${this.SELECT(graph)})${alias ? ` AS ${this.quote(alias)}` : ''} ` | ||
| return ` ${recurseColumns} FROM (${this.SELECT(graph)})${alias ? ` AS ${this.quote(alias)}` : ''} ` | ||
@@ -742,2 +759,34 @@ function collectDistanceTo(where, innot = false) { | ||
| /** | ||
| * Renders a transformed where clause that maps the query target view to the source table | ||
| * @param {import('./infer/cqn').source} alias | ||
| * @param {import('./infer/cqn').predicate} where | ||
| * @param {import('./infer/cqn').query} q | ||
| * @returns SQL | ||
| */ | ||
| where_resolved(alias, where, q) { | ||
| const transitions = this.srv.resolve.transitions(q) | ||
| if (transitions.target === transitions.queryTarget) return this.where(where) | ||
| // view and table column refs to be matched | ||
| const viewCols = [] | ||
| const tableCols = [] | ||
| // Only match key columns when possible | ||
| const elements = q._target.keys || q._target.elements | ||
| for (const c in elements) { | ||
| if ( | ||
| c in elements | ||
| && transitions.mapping.has(c) | ||
| && this.physical_column(elements, c) | ||
| ) { | ||
| viewCols.push({ ref: [c] }) | ||
| tableCols.push(transitions.mapping.get(c)) | ||
| } | ||
| } | ||
| return tableCols.length > 0 | ||
| ? this.where([{ list: tableCols }, 'in', SELECT.from(q._target).alias(alias).columns(viewCols).where(where)]) | ||
| : this.where(where) | ||
| } | ||
| /** | ||
| * Renders a HAVING clause into generic SQL | ||
@@ -847,4 +896,9 @@ * @param {import('./infer/cqn').predicate} xpr | ||
| } | ||
| const transitions = this.srv.resolve.transitions(q) | ||
| const columns = elements | ||
| ? ObjectKeys(elements).filter(c => c in elements && !elements[c].virtual && !elements[c].value && !elements[c].isAssociation) | ||
| ? ObjectKeys(elements).filter(c => this.physical_column(elements, c) | ||
| && (c = transitions.mapping.get(c)?.ref?.[0] || c) | ||
| && c in transitions.target.elements | ||
| && this.physical_column(transitions.target.elements, c) | ||
| ) | ||
| : ObjectKeys(INSERT.entries[0]) | ||
@@ -856,3 +910,3 @@ | ||
| const alias = INSERT.into.as | ||
| const entity = this.name(q._target?.name || INSERT.into.ref[0], q) | ||
| const entity = q._target ? this.table_name(q) : INSERT.into.ref[0] | ||
| if (!elements) { | ||
@@ -879,4 +933,4 @@ this.entries = INSERT.entries.map(e => columns.map(c => e[c])) | ||
| const extractions = this._managed = this.managed(columns.map(c => ({ name: c })), elements) | ||
| return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c)) | ||
| }) SELECT ${extractions.map(c => c.insert)} FROM json_each(?)`) | ||
| return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c)) | ||
| }) SELECT ${extractions.slice(0, columns.length).map(c => c.insert)} FROM json_each(?)`) | ||
| } | ||
@@ -987,3 +1041,3 @@ | ||
| const { INSERT } = q | ||
| const entity = this.name(q._target?.name || INSERT.into.ref[0], q) | ||
| const entity = q._target ? this.table_name(q) : INSERT.into.ref[0] | ||
| const alias = INSERT.into.as | ||
@@ -1013,3 +1067,4 @@ const elements = q.elements || q._target?.elements | ||
| return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c)) | ||
| const transitions = this.srv.resolve.transitions(q) | ||
| return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c)) | ||
| }) SELECT ${extraction} FROM json_each(?)`) | ||
@@ -1035,10 +1090,14 @@ } | ||
| const { INSERT } = q | ||
| const entity = this.name(q._target.name, q) | ||
| const entity = q._target ? this.table_name(q) : INSERT.into.ref[0] | ||
| const alias = INSERT.into.as | ||
| const src = this.cqn4sql(INSERT.from) | ||
| const elements = q.elements || q._target?.elements || {} | ||
| let columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter( | ||
| c => c in elements && !elements[c].virtual && !elements[c].isAssociation, | ||
| )) | ||
| const transitions = this.srv.resolve.transitions(q) | ||
| let columns = (this.columns = (INSERT.columns || src.SELECT.columns?.map(c => this.column_name(c)) || ObjectKeys(src.elements) || ObjectKeys(elements)) | ||
| .filter(c => this.physical_column(elements, c) | ||
| && (c = transitions.mapping.get(c)?.ref?.[0] || c) | ||
| && c in transitions.target.elements | ||
| && this.physical_column(transitions.target.elements, c) | ||
| )) | ||
| const src = this.cqn4sql(INSERT.from) | ||
| const extractions = this._managed = this.managed(columns.map(c => ({ name: c, sql: `NEW.${this.quote(c)}` })), elements) | ||
@@ -1049,3 +1108,3 @@ const sql = extractions.length > columns.length | ||
| if (extractions.length > columns.length) columns = this.columns = extractions.map(c => c.name) | ||
| this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns.map(c => this.quote(c))}) ${sql}` | ||
| this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c))}) ${sql}` | ||
| this.entries = [this.values] | ||
@@ -1104,3 +1163,3 @@ return this.sql | ||
| let columns = this.columns // this.columns is computed as part of this.INSERT | ||
| const entity = this.name(q._target?.name || UPSERT.into.ref[0], q) | ||
| const entity = q._target ? this.table_name(q) : this.name(UPSERT.into.ref[0], q) | ||
| if (UPSERT.entries || UPSERT.rows || UPSERT.values) { | ||
@@ -1141,3 +1200,4 @@ const managed = this._managed.slice(0, columns.length) | ||
| return (this.sql = `INSERT INTO ${this.quote(entity)} (${columns.map(c => this.quote(c))}) ${sql | ||
| const transitions = this.srv.resolve.transitions(q) | ||
| return (this.sql = `INSERT INTO ${this.quote(entity)} (${columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c))}) ${sql | ||
| } WHERE TRUE ON CONFLICT(${keys.map(c => this.quote(c))}) DO ${updateColumns.length ? `UPDATE SET ${updateColumns}` : 'NOTHING'}`) | ||
@@ -1155,15 +1215,16 @@ } | ||
| const { entity, with: _with, data, where } = q.UPDATE | ||
| const transitions = this.srv.resolve.transitions(q) | ||
| const elements = q._target?.elements | ||
| let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity, q))}` | ||
| let sql = `UPDATE ${this.quote(this.table_name(q))}` | ||
| if (entity.as) sql += ` AS ${this.quote(entity.as)}` | ||
| let columns = [] | ||
| if (data) _add(data, val => this.val({ val })) | ||
| if (_with) _add(_with, x => this.expr(x)) | ||
| function _add(data, sql4) { | ||
| for (let c in data) { | ||
| const columnExistsInDatabase = | ||
| elements && c in elements && !elements[c].virtual && !elements[c].isAssociation && !elements[c].value | ||
| const _add = (data, sql4) => { | ||
| for (let col in data) { | ||
| const c = transitions.mapping.get(col)?.ref?.[0] || col | ||
| const columnExistsInDatabase = elements | ||
| && this.physical_column(elements, col) | ||
| && c in transitions.target.elements | ||
| && this.physical_column(transitions.target.elements, c) | ||
| if (!elements || columnExistsInDatabase) { | ||
| columns.push({ name: c, sql: sql4(data[c]) }) | ||
| columns.push({ name: c, sql: sql4(data[col], col) }) | ||
| } | ||
@@ -1173,8 +1234,14 @@ } | ||
| let columns = [] | ||
| if (data) _add(data, val => this.val({ val })) | ||
| if (_with) _add(_with, x => this.expr(x)) | ||
| const extraction = this.managed(columns, elements) | ||
| .filter((c, i) => columns[i] || c.onUpdate) | ||
| .map((c, i) => `${this.quote(c.name)}=${!columns[i] ? c.onUpdate : c.sql}`) | ||
| .filter((c, i) => { | ||
| if (transitions.mapping.get(c.name)?.ref?.length > 1) return false | ||
| return columns[i] || c.onUpdate | ||
| }).map((c, i) => `${this.quote(transitions.mapping.get(c.name)?.ref?.[0] || c.name)}=${!columns[i] ? c.onUpdate : c.sql}`) | ||
| sql += ` SET ${extraction}` | ||
| if (where) sql += ` WHERE ${this.where(where)}` | ||
| if (where) sql += ` WHERE ${this.where_resolved(entity.as, where, q)}` | ||
| return (this.sql = sql) | ||
@@ -1191,4 +1258,5 @@ } | ||
| DELETE(q) { | ||
| const { DELETE: { from, where } } = q | ||
| let sql = `DELETE FROM ${this.from(from, q)}` | ||
| const { DELETE: { where, from } } = q | ||
| let sql = `DELETE FROM ${this.quote(this.table_name(q))}` | ||
| if (from.as) sql += ` AS ${this.quote(from.as)}` | ||
| if (where) sql += ` WHERE ${this.where(where)}` | ||
@@ -1407,2 +1475,12 @@ return (this.sql = sql) | ||
| /** | ||
| * Calculates the Database table name of the query target | ||
| * @param {import('./infer/cqn').Query} query | ||
| * @returns {string} Database table name | ||
| */ | ||
| table_name(q) { | ||
| const table = resolveTable(q._target) | ||
| return this.name(table.name, { _target: table }) | ||
| } | ||
| /** | ||
| * Calculates the Database name of the given name | ||
@@ -1514,2 +1592,6 @@ * @param {string|import('./infer/cqn').ref} name | ||
| physical_column(elements, c) { | ||
| return elements[c] && !elements[c].virtual && !elements[c].value && !elements[c].isAssociation | ||
| } | ||
| managed_extract(name, element, converter) { | ||
@@ -1516,0 +1598,0 @@ const { UPSERT, INSERT } = this.cqn |
+74
-13
@@ -7,3 +7,3 @@ 'use strict' | ||
| const { pseudos } = require('./pseudos') | ||
| const { isCalculatedOnRead, getImplicitAlias, getModelUtils, defineProperty, hasOwnSkip } = require('../utils') | ||
| const { isCalculatedOnRead, getImplicitAlias, getModelUtils, defineProperty, hasOwnSkip, isRuntimeView } = require('../utils') | ||
| const cdsTypes = cds.builtin.types | ||
@@ -195,2 +195,3 @@ /** | ||
| if (col === '*') { | ||
| if (wildcardSelect) throw new Error('Duplicate wildcard "*" in column list') | ||
| wildcardSelect = true | ||
@@ -464,6 +465,6 @@ } else if (col.val !== undefined || col.xpr || col.SELECT || col.func || col.param) { | ||
| const nextStep = arg.ref[1]?.id || arg.ref[1] | ||
| if (isNonForeignKeyNavigation(element, nextStep)) { | ||
| if (isNonForeignKeyNavigation(element, nextStep) || arg.ref[0]?.where) { | ||
| if (inExists) { | ||
| defineProperty($baseLink, 'pathExpressionInsideFilter', true) | ||
| } else { | ||
| } else if (!inFrom) { | ||
| rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep) | ||
@@ -529,6 +530,6 @@ } | ||
| const nextStep = arg.ref[i + 1]?.id || arg.ref[i + 1] | ||
| if (isNonForeignKeyNavigation(element, nextStep)) { | ||
| if (isNonForeignKeyNavigation(element, nextStep) || arg.ref[i-1]?.where) { | ||
| if (inExists) { | ||
| defineProperty($baseLink, 'pathExpressionInsideFilter', true) | ||
| } else { | ||
| } else if (!inFrom) { | ||
| rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep) | ||
@@ -572,3 +573,3 @@ } | ||
| const definition = arg.$refLinks[i].definition | ||
| if ((!definition.target && definition.kind !== 'entity') || (!inFrom && danglingFilter)) | ||
| if ((!definition.target && definition.kind !== 'entity') || (!inFrom && !inCalcElement && danglingFilter)) | ||
| throw new Error('A filter can only be provided when navigating along associations') | ||
@@ -582,5 +583,7 @@ if (!inFrom && !arg.expand)defineProperty(arg, 'isJoinRelevant', true) | ||
| } else if (token.ref || token.xpr || token.list) { | ||
| // For scoped queries (non-dangling filters in FROM), treat filter contents as EXISTS context | ||
| // because they will become part of an EXISTS subquery | ||
| inferArg(token, false, arg.$refLinks[i], { | ||
| ...context, | ||
| inExists: skipJoinsForFilter || inExists, | ||
| inExists: skipJoinsForFilter || inExists || (inFrom && !danglingFilter), | ||
| inXpr: !!token.xpr, | ||
@@ -595,3 +598,3 @@ inInfixFilter: true, | ||
| arg.$refLinks[i], | ||
| { inExists: skipJoinsForFilter || inExists, inXpr: true, inInfixFilter: true, inFrom }, | ||
| { inExists: skipJoinsForFilter || inExists || (inFrom && !danglingFilter), inXpr: true, inInfixFilter: true, inFrom }, | ||
| ]) | ||
@@ -605,3 +608,4 @@ } | ||
| arg.$refLinks[i].alias = !arg.ref[i + 1] && arg.as ? arg.as : id.split('.').pop() | ||
| if (hasOwnSkip(getDefinition(arg.$refLinks[i].definition.target))) isPersisted = false | ||
| const def = getDefinition(arg.$refLinks[i].definition.target) | ||
| if (hasOwnSkip(def) && !isRuntimeView(def)) isPersisted = false | ||
| if (!arg.ref[i + 1]) { | ||
@@ -666,3 +670,7 @@ const flatName = nameSegments.join('_') | ||
| const { $refLinks } = arg | ||
| const skip = $refLinks.some(link => hasOwnSkip(getDefinition(link.definition.target))) | ||
| const skip = $refLinks.some(link => { | ||
| const def = getDefinition(link.definition.target) | ||
| return hasOwnSkip(def) && !isRuntimeView(def) | ||
| }) | ||
| if (skip) { | ||
@@ -720,8 +728,15 @@ $refLinks[$refLinks.length - 1].skipExpand = true | ||
| let elements = {} | ||
| let seenWildcard = false | ||
| inline.forEach(inlineCol => { | ||
| inferArg(inlineCol, null, $leafLink, { inXpr: true, baseColumn: col }) | ||
| if (inlineCol === '*') { | ||
| if (seenWildcard) throw new Error(`Duplicate wildcard "*" in inline of "${col.as || col.ref.map(idOnly).join('_')}"`) | ||
| seenWildcard = true | ||
| const wildCardElements = {} | ||
| // either the `.elements´ of the struct or the `.elements` of the assoc target | ||
| const leafLinkElements = getDefinition($leafLink.definition.target)?.elements || $leafLink.definition.elements | ||
| const targetDef = getDefinition($leafLink.definition.target) | ||
| const leafLinkElements = targetDef?.elements || $leafLink.definition.elements | ||
| const isAssociation = !!$leafLink.definition.target | ||
| const deferredCalcElements = [] | ||
| Object.entries(leafLinkElements).forEach(([k, v]) => { | ||
@@ -732,4 +747,38 @@ const name = namePrefix ? `${namePrefix}_${k}` : k | ||
| // in excluding, the elements are addressed without the prefix | ||
| if (!(name in elements || col.excluding?.includes(k))) wildCardElements[name] = v | ||
| if (!(name in elements || col.excluding?.includes(k))) { | ||
| wildCardElements[name] = v | ||
| if(v.value) { | ||
| // defer linkCalculatedElement calls until after all association joins are registered | ||
| // so that the join tree order is correct | ||
| deferredCalcElements.push({ k, v }) | ||
| } | ||
| else if (isAssociation && !v.virtual && v.type !== 'cds.LargeBinary' && !(v.on && !v.keys)) { | ||
| // Check if this element is a foreign key (FK elements don't need join) | ||
| const isFK = $leafLink.definition.keys?.some(key => key.ref[0] === k) | ||
| if (!isFK) { | ||
| // Create a fake column with ref [<inlined assoc>, <element name>] and proper $refLinks | ||
| const fakeCol = { | ||
| ref: [...col.ref, k], | ||
| } | ||
| // Copy $refLinks and add new link for the target element with proper alias | ||
| const fakeRefLinks = [ | ||
| ...$refLinks, | ||
| { definition: v, target: targetDef, alias: k } | ||
| ] | ||
| defineProperty(fakeCol, '$refLinks', fakeRefLinks) | ||
| defineProperty(fakeCol, 'isJoinRelevant', true) | ||
| // Merge into join tree | ||
| inferred.joinTree.mergeColumn(fakeCol, originalQuery.outerQueries) | ||
| } | ||
| } | ||
| } | ||
| }) | ||
| // link calculated elements after association joins are registered in the join tree | ||
| for (const { k, v } of deferredCalcElements) { | ||
| linkCalculatedElement( | ||
| { ref: [k], $refLinks: [{ definition: v, target: targetDef }] }, | ||
| $leafLink, | ||
| ) | ||
| } | ||
| elements = { ...elements, ...wildCardElements } | ||
@@ -739,3 +788,3 @@ } else { | ||
| if (inlineCol.as) nameParts.push(inlineCol.as) | ||
| else nameParts.push(...inlineCol.ref.map(idOnly)) | ||
| else if (inlineCol.ref) nameParts.push(...inlineCol.ref.map(idOnly)) | ||
| const name = nameParts.join('_') | ||
@@ -752,2 +801,4 @@ if (inlineCol.inline) { | ||
| elements[name] = {} | ||
| } else if (inlineCol.xpr) { | ||
| elements[name] = {} | ||
| } else { | ||
@@ -780,2 +831,12 @@ elements[name] = inlineCol.$refLinks[inlineCol.$refLinks.length - 1].definition | ||
| } | ||
| // Check for duplicate wildcards before creating the subquery | ||
| let seenWildcard = false | ||
| for (const e of expand) { | ||
| if (e === '*') { | ||
| if (seenWildcard) { | ||
| throw new Error(`Duplicate wildcard "*" in expand of "${col.as || col.ref.map(idOnly).join('_')}"`) | ||
| } | ||
| seenWildcard = true | ||
| } | ||
| } | ||
| const target = getDefinition($leafLink.definition.target) | ||
@@ -782,0 +843,0 @@ if (target) { |
@@ -136,16 +136,18 @@ 'use strict' | ||
| * @param {unknown[]} outerQueries - An array of outer queries. | ||
| * @param {string} key - The key to be used for storing the alias in the map. If not provided, the upper-case version of the alias will be used as the key. | ||
| * @returns {string} - The next unambiguous table alias. | ||
| */ | ||
| addNextAvailableTableAlias(alias, outerQueries) { | ||
| addNextAvailableTableAlias(alias, outerQueries, key) { | ||
| const upperAlias = alias.toUpperCase() | ||
| if (this._queryAliases.get(upperAlias) || outerQueries?.some(outer => outerHasAlias(outer))) { | ||
| if (this._queryAliases.get(upperAlias) || outerQueries?.some(outer => outerHasAlias(outer, key))) { | ||
| let j = 2 | ||
| while (this._queryAliases.get(upperAlias + j) || outerQueries?.some(outer => outerHasAlias(outer, j))) j += 1 | ||
| while (this._queryAliases.get(upperAlias + j) || outerQueries?.some(outer => outerHasAlias(outer, key, j))) j += 1 | ||
| alias += j | ||
| } | ||
| this._queryAliases.set(alias.toUpperCase(), alias) | ||
| this._queryAliases.set(key || alias.toUpperCase(), alias) | ||
| return alias | ||
| function outerHasAlias(outer, number) { | ||
| return outer.joinTree._queryAliases.get(number ? upperAlias + number : upperAlias) | ||
| function outerHasAlias(outer, searchInValues = false, number) { | ||
| const currAlias = number ? upperAlias + number : upperAlias | ||
| return searchInValues ? Array.from(outer.joinTree._queryAliases.values()).includes(currAlias) : outer.joinTree._queryAliases.get(currAlias) | ||
| } | ||
@@ -152,0 +154,0 @@ } |
@@ -5,5 +5,5 @@ const cds = require('@sap/cds'), | ||
| const { pipeline } = require('stream/promises') | ||
| const { resolveView, getDBTable, getTransition } = require('@sap/cds/libx/_runtime/common/utils/resolveView') | ||
| const DatabaseService = require('./common/DatabaseService') | ||
| const cqn4sql = require('./cqn4sql') | ||
| const { resolveTable } = require('./utils') | ||
@@ -172,3 +172,3 @@ const BINARY_TYPES = { | ||
| return iterator !== false && isOne ? rows[0] : rows | ||
| return !iterator && isOne ? rows[0] : rows | ||
| } catch (err) { | ||
@@ -235,3 +235,4 @@ // Ensure that iterators receive pre stream errors | ||
| async function deep_delete(/** @type {Request} */ req) { | ||
| const transitions = getTransition(req.target, this, false, req.query.cmd || 'DELETE') | ||
| const resolve = this.resolve | ||
| const transitions = resolve.transitions(req.query) | ||
| if (transitions.target !== transitions.queryTarget) { | ||
@@ -259,3 +260,3 @@ const keys = [] | ||
| } | ||
| const table = getDBTable(req.target) | ||
| const table = resolveTable(req.target) | ||
| const { compositions } = table | ||
@@ -411,6 +412,2 @@ if (compositions) { | ||
| let q = this.cqn4sql(query) | ||
| let kind = q.kind || Object.keys(q)[0] | ||
| if (kind in { INSERT: 1, DELETE: 1, UPSERT: 1, UPDATE: 1 }) { | ||
| q = resolveView(q, this.model, this) // REVISIT: before resolveView was called on flat cqn obtained from cqn4sql -> is it correct to call on original q instead? | ||
| } | ||
| let cqn2sql = new this.class.CQN2SQL(this) | ||
@@ -417,0 +414,0 @@ return cqn2sql.render(q, values) |
+29
-0
| 'use strict' | ||
| const cds = require('@sap/cds') | ||
| /** | ||
@@ -30,2 +32,21 @@ * Formats a ref array into a string representation. | ||
| function isRuntimeView(definition) { | ||
| if (!definition || !cds.env.features.runtime_views) return false | ||
| if (definition['_isRuntimeView']) return true | ||
| if (!definition['@cds.persistence.skip']) { | ||
| Object.defineProperty(definition, '_isRuntimeView', { | ||
| value: true, | ||
| writable: false, | ||
| configurable: true, | ||
| enumerable: false | ||
| }) | ||
| return true | ||
| } | ||
| // views with "as select from" variant are also runtime views, even if they are annotated with persistence skip | ||
| if (definition.query && !definition.query._target) return true | ||
| if (definition.query) return isRuntimeView(definition.query._target) | ||
| return false | ||
| } | ||
| /** | ||
@@ -140,2 +161,8 @@ * Determines if a definition is calculated on read. | ||
| function resolveTable(target) { | ||
| if (target.query?._target && !Object.prototype.hasOwnProperty.call(target, '@cds.persistence.table')) | ||
| return resolveTable(target.query._target) | ||
| return target | ||
| } | ||
| // export the function to be used in other modules | ||
@@ -150,2 +177,4 @@ module.exports = { | ||
| hasOwnSkip, | ||
| isRuntimeView, | ||
| resolveTable | ||
| } |
+2
-2
| { | ||
| "name": "@cap-js/db-service", | ||
| "version": "2.8.2", | ||
| "version": "2.9.0", | ||
| "description": "CDS base database service", | ||
@@ -30,5 +30,5 @@ "homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service", | ||
| "peerDependencies": { | ||
| "@sap/cds": ">=9.4.5" | ||
| "@sap/cds": ">=9.8" | ||
| }, | ||
| "license": "Apache-2.0" | ||
| } |
Sorry, the diff of this file is too big to display
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
388454
5.31%7667
5.56%75
4.17%