Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Adjusting to consolidated cds.infer and query._target #1041

Merged
merged 4 commits into from
Mar 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion db-service/lib/InsertResults.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ module.exports = class InsertResult {
})
}

const { target } = this.query
const target = this.query._target
if (!target?.keys) return (super[iterator] = this.results[iterator])
const keys = Object.keys(target.keys),
[k1] = keys
Expand Down
10 changes: 5 additions & 5 deletions db-service/lib/SQLService.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class SQLService extends DatabaseService {
this.on(['INSERT', 'UPSERT', 'UPDATE'], require('./deep-queries').onDeep)
if (cds.env.features.db_strict) {
this.before(['INSERT', 'UPSERT', 'UPDATE'], ({ query }) => {
const elements = query.target?.elements
const elements = query._target?.elements
if (!elements) return
const kind = query.kind || Object.keys(query)[0]
const operation = query[kind]
Expand Down Expand Up @@ -120,10 +120,10 @@ class SQLService extends DatabaseService {
async onSELECT({ query, data }) {
// REVISIT: for custom joins, infer is called twice, which is bad
// --> make cds.infer properly work with custom joins and remove this
if (!query.target) {
if (!(query._target instanceof cds.entity)) {
try { this.infer(query) } catch { /**/ }
}
if (query.target && !query.target._unresolved) {
if (!query._target?._unresolved) { // REVISIT: use query._target instead
// Will return multiple rows with objects inside
query.SELECT.expand = 'root'
}
Expand Down Expand Up @@ -267,7 +267,7 @@ class SQLService extends DatabaseService {
)
// Prepare and run deep query, à la CQL`DELETE from Foo[pred]:comp1.comp2...`
const query = DELETE.from({ ref: [...from.ref, c.name] })
query.target = c._target
query._target = c._target
return this.onDELETE({ query, depth, visited: [...visited], target: c._target })
}),
)
Expand Down Expand Up @@ -382,7 +382,7 @@ class SQLService extends DatabaseService {
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 target = q[kind]._transitions?.[0].target
if (target) q.target = target // REVISIT: Why isn't that done in resolveView?
if (target) q._target = target // REVISIT: Why isn't that done in resolveView?
}
let cqn2sql = new this.class.CQN2SQL(this)
return cqn2sql.render(q, values)
Expand Down
32 changes: 16 additions & 16 deletions db-service/lib/cqn2sql.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class CQN2SQLRenderer {
if (cds.env.sql.names === 'quoted') {
this.class.prototype.name = (name, query) => {
const e = name.id || name
return (query?.target || this.model?.definitions[e])?.['@cds.persistence.name'] || e
return (query?._target || this.model?.definitions[e])?.['@cds.persistence.name'] || e
}
this.class.prototype.quote = (s) => `"${String(s).replace(/"/g, '""')}"`
}
Expand Down Expand Up @@ -105,7 +105,7 @@ class CQN2SQLRenderer {
* @returns {import('./infer/cqn').Query}
*/
infer(q) {
return q.target ? q : cds_infer(q)
return q._target instanceof cds.entity ? q : cds_infer(q)
}

cqn4sql(q) {
Expand Down Expand Up @@ -497,7 +497,7 @@ class CQN2SQLRenderer {
*/
INSERT_entries(q) {
const { INSERT } = q
const elements = q.elements || q.target?.elements
const elements = q.elements || q._target?.elements
if (!elements && !INSERT.entries?.length) {
return // REVISIT: mtx sends an insert statement without entries and no reference entity
}
Expand All @@ -509,7 +509,7 @@ class CQN2SQLRenderer {
this.columns = columns

const alias = INSERT.into.as
const entity = this.name(q.target?.name || INSERT.into.ref[0], q)
const entity = this.name(q._target?.name || INSERT.into.ref[0], q)
if (!elements) {
this.entries = INSERT.entries.map(e => columns.map(c => e[c]))
const param = this.param.bind(this, { ref: ['?'] })
Expand All @@ -535,7 +535,7 @@ class CQN2SQLRenderer {
}

async *INSERT_entries_stream(entries, binaryEncoding = 'base64') {
const elements = this.cqn.target?.elements || {}
const elements = this.cqn._target?.elements || {}
const bufferLimit = 65536 // 1 << 16
let buffer = '['

Expand Down Expand Up @@ -584,7 +584,7 @@ class CQN2SQLRenderer {
}

async *INSERT_rows_stream(entries, binaryEncoding = 'base64') {
const elements = this.cqn.target?.elements || {}
const elements = this.cqn._target?.elements || {}
const bufferLimit = 65536 // 1 << 16
let buffer = '['

Expand Down Expand Up @@ -637,9 +637,9 @@ class CQN2SQLRenderer {
*/
INSERT_rows(q) {
const { INSERT } = q
const entity = this.name(q.target?.name || INSERT.into.ref[0], q)
const entity = this.name(q._target?.name || INSERT.into.ref[0], q)
const alias = INSERT.into.as
const elements = q.elements || q.target?.elements
const elements = q.elements || q._target?.elements
const columns = this.columns = INSERT.columns || cds.error`Cannot insert rows without columns or elements`

if (!elements) {
Expand Down Expand Up @@ -683,9 +683,9 @@ class CQN2SQLRenderer {
*/
INSERT_select(q) {
const { INSERT } = q
const entity = this.name(q.target.name, q)
const entity = this.name(q._target.name, q)
const alias = INSERT.into.as
const elements = q.elements || q.target?.elements || {}
const elements = q.elements || q._target?.elements || {}
const columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
))
Expand Down Expand Up @@ -726,15 +726,15 @@ class CQN2SQLRenderer {
const { UPSERT } = q

let sql = this.INSERT({ __proto__: q, INSERT: UPSERT })
if (!q.target?.keys) return sql
if (!q._target?.keys) return sql
const keys = []
for (const k of ObjectKeys(q.target?.keys)) {
const element = q.target.keys[k]
for (const k of ObjectKeys(q._target?.keys)) {
const element = q._target.keys[k]
if (element.isAssociation || element.virtual) continue
keys.push(k)
}

const elements = q.target?.elements || {}
const elements = q._target?.elements || {}
// temporal data
for (const k of ObjectKeys(elements)) {
if (elements[k]['@cds.valid.from']) keys.push(k)
Expand All @@ -751,7 +751,7 @@ class CQN2SQLRenderer {
.filter(c => keys.includes(c.name))
.map(c => `${c.onInsert || c.sql} as ${this.quote(c.name)}`)

const entity = this.name(q.target?.name || UPSERT.into.ref[0], q)
const entity = this.name(q._target?.name || UPSERT.into.ref[0], q)
sql = `SELECT ${managed.map(c => c.upsert
.replace(/value->/g, '"$$$$value$$$$"->')
.replace(/json_type\(value,/g, 'json_type("$$$$value$$$$",'))
Expand Down Expand Up @@ -780,7 +780,7 @@ class CQN2SQLRenderer {
*/
UPDATE(q) {
const { entity, with: _with, data, where } = q.UPDATE
const elements = q.target?.elements
const elements = q._target?.elements
let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity, q))}`
if (entity.as) sql += ` AS ${this.quote(entity.as)}`

Expand Down
11 changes: 7 additions & 4 deletions db-service/lib/cqn4sql.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

const cds = require('@sap/cds')
cds.infer.target = q => q._target || q.target // instanceof cds.entity ? q._target : q.target

const infer = require('./infer')
const { computeColumnsToBeSearched } = require('./search')
Expand Down Expand Up @@ -224,7 +225,8 @@ function cqn4sql(originalQuery, model) {
*/
function transformQueryForInsertUpsert(kind) {
const { as } = transformedQuery[kind].into
transformedQuery[kind].into = { ref: [inferred.target.name] }
const target = cds.infer.target (inferred) // REVISIT: we should reliably use inferred._target instead
transformedQuery[kind].into = { ref: [target.name] }
if (as) transformedQuery[kind].into.as = as
return transformedQuery
}
Expand Down Expand Up @@ -800,7 +802,7 @@ function cqn4sql(originalQuery, model) {
} else {
outerAlias = transformedQuery.SELECT.from.as
subqueryFromRef = [
...(transformedQuery.SELECT.from.ref || /* subq in from */ [transformedQuery.SELECT.from.target.name]),
...(transformedQuery.SELECT.from.ref || /* subq in from */ transformedQuery.SELECT.from.SELECT.from.ref),
...ref,
]
}
Expand Down Expand Up @@ -1063,7 +1065,8 @@ function cqn4sql(originalQuery, model) {
outerQueries.push(inferred)
Object.defineProperty(q, 'outerQueries', { value: outerQueries })
}
if (isLocalized(inferred.target)) q.SELECT.localized = true
const target = cds.infer.target (inferred) // REVISIT: we should reliably use inferred._target instead
if (isLocalized(target)) q.SELECT.localized = true
if (q.SELECT.from.ref && !q.SELECT.from.as) assignUniqueSubqueryAlias()
return cqn4sql(q, model)

Expand Down Expand Up @@ -2243,7 +2246,7 @@ function cqn4sql(originalQuery, model) {
* - or null, if no searchable columns are found in neither in `@cds.search` nor in the target entity itself.
*/
function getSearchTerm(search, query) {
const entity = query.SELECT.from.SELECT ? query.SELECT.from : query.target
const entity = query.SELECT.from.SELECT ? query.SELECT.from : cds.infer.target(query) // REVISIT: we should reliably use inferred._target instead
const searchIn = computeColumnsToBeSearched(inferred, entity)
if (searchIn.length > 0) {
const xpr = search
Expand Down
8 changes: 4 additions & 4 deletions db-service/lib/deep-queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ async function onDeep(req, next) {
// const target = query.sources[Object.keys(query.sources)[0]]
if (!this.model?.definitions[_target_name4(req.query)]) return next()

const { target } = this.infer(query)
if (!hasDeep(query, target)) return next()
if (!hasDeep(query)) return next()

const target = this.infer(query)._target
const beforeData = query.INSERT ? [] : await this.run(getExpandForDeep(query, target, true))
if (query.UPDATE && !beforeData.length) return 0

Expand All @@ -64,10 +64,10 @@ async function onDeep(req, next) {
return rootResult ?? beforeData.length
}

const hasDeep = (q, target) => {
const hasDeep = (q) => {
const data = q.INSERT?.entries || (q.UPDATE?.data && [q.UPDATE.data]) || (q.UPDATE?.with && [q.UPDATE.with])
if (data)
for (const c in target.compositions) {
for (const c in q._target.compositions) {
for (const row of data) if (row[c] !== undefined) return true
}
}
Expand Down
2 changes: 1 addition & 1 deletion db-service/lib/fill-in-keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ module.exports = async function fill_in_keys(req, next) {
// REVISIT dummy handler until we have input processing
if (!req.target || !this.model || req.target._unresolved) return next()
// only for deep update
if (req.event === 'UPDATE' && hasDeep(req.query, req.target)) {
if (req.event === 'UPDATE' && hasDeep(req.query)) {
// REVISIT for deep update we need to inject the keys first
enrichDataWithKeysFromWhere(req.data, req, this)
}
Expand Down
13 changes: 4 additions & 9 deletions db-service/lib/infer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,16 @@ function infer(originalQuery, model) {
const sources = inferTarget(_.from || _.into || _.entity, {})
const joinTree = new JoinTree(sources)
const aliases = Object.keys(sources)
const target = aliases.length === 1 ? getDefinitionFromSources(sources, aliases[0]) : originalQuery
Object.defineProperties(inferred, {
// REVISIT: public, or for local reuse, or in cqn4sql only?
sources: { value: sources, writable: true },
target: {
value: aliases.length === 1 ? getDefinitionFromSources(sources, aliases[0]) : originalQuery,
writable: true,
}, // REVISIT: legacy?
_target: { value: target, writable: true, configurable: true }, // REVISIT: legacy?
})
// also enrich original query -> writable because it may be inferred again
Object.defineProperties(originalQuery, {
sources: { value: sources, writable: true },
target: {
value: aliases.length === 1 ? getDefinitionFromSources(sources, aliases[0]) : originalQuery,
writable: true,
},
_target: { value: target, writable: true, configurable: true },
})
if (originalQuery.SELECT || originalQuery.DELETE || originalQuery.UPDATE) {
$combinedElements = inferCombinedElements()
Expand Down Expand Up @@ -120,7 +115,7 @@ function infer(originalQuery, model) {
from.as ||
(ref.length === 1
? first.substring(first.lastIndexOf('.') + 1)
: (ref.at(-1).id || ref.at(-1)));
: (ref.at(-1).id || ref.at(-1)));
if (alias in querySources) throw new Error(`Duplicate alias "${alias}"`)
querySources[alias] = { definition: target, args }
const last = from.$refLinks.at(-1)
Expand Down
4 changes: 2 additions & 2 deletions db-service/test/cds-infer/elements.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ describe('infer elements', () => {
}`)
let { Books, Genres } = model.entities

expect(inferred.target)
expect(inferred._target)
.equals(Books.elements.genre._target.elements.foo._target)
.equals(Genres.elements.foo._target)

Expand All @@ -272,7 +272,7 @@ describe('infer elements', () => {
}`)
let { Books, Authors } = model.entities

expect(inferred.target).to.deep.equal(Books.elements.coAuthor._target).to.deep.equal(Authors)
expect(inferred._target).to.deep.equal(Books.elements.coAuthor._target).to.deep.equal(Authors)
expect(inferred.elements).to.deep.equal({
name: Authors.elements.name,
})
Expand Down
14 changes: 7 additions & 7 deletions db-service/test/cds-infer/source.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ describe('simple', () => {
let query = cds.ql`SELECT from bookshop.Books { ID, author }`
let inferred = _inferred(query)
expect(inferred).to.deep.equal(query)
expect(inferred).to.have.property('target')
expect(inferred).to.have.property('_target')
expect(inferred).to.have.property('elements')
let { Books } = model.entities
expect(inferred.target).to.deep.equal(Books)
expect(inferred._target).to.deep.equal(Books)
expect(inferred.elements).to.deep.equal({
ID: Books.elements.ID,
author: Books.elements.author,
Expand All @@ -35,9 +35,9 @@ describe('simple', () => {
let i = INSERT.into`bookshop.Books`.entries({ ID: 201 })
let d = DELETE.from('bookshop.Books').where({ stock: { '<': 1 } })
let { Authors, Books } = model.entities
expect(_inferred(u)).to.have.property('target').that.equals(Authors)
expect(_inferred(i)).to.have.property('target').that.equals(Books)
expect(_inferred(d)).to.have.property('target').that.equals(Books)
expect(_inferred(u)).to.have.property('_target').that.equals(Authors)
expect(_inferred(i)).to.have.property('_target').that.equals(Books)
expect(_inferred(d)).to.have.property('_target').that.equals(Books)
})
})
describe('scoped queries', () => {
Expand Down Expand Up @@ -116,7 +116,7 @@ describe('multiple sources', () => {
}`)
let { Books, Authors } = model.entities

expect(inferred.target).to.deep.equal(inferred)
expect(inferred._target).to.deep.equal(inferred)

expect(inferred.elements).to.deep.equal({
aID: Authors.elements.ID,
Expand Down Expand Up @@ -150,7 +150,7 @@ describe('multiple sources', () => {
let { Books } = model.entities

// same base entity, addressable via both aliases
expect(inferred.target).to.deep.equal(inferred)
expect(inferred._target).to.deep.equal(inferred)
expect(inferred.sources['firstBook'].definition).to.deep.equal(inferred.sources['secondBook'].definition).to.deep.equal(Books)

expect(inferred.elements).to.deep.equal({
Expand Down
18 changes: 9 additions & 9 deletions hana/lib/HANAService.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,10 @@ class HANAService extends SQLService {
async onSELECT(req) {
const { query, data } = req

if (!query.target || query.target._unresolved) {
if (!query._target || query._target._unresolved) {
try { this.infer(query) } catch { /**/ }
}
if (!query.target || query.target._unresolved) {
if (!query._target || query._target._unresolved) {
return super.onSELECT(req)
}

Expand Down Expand Up @@ -777,9 +777,9 @@ class HANAService extends SQLService {
this.values = undefined
const { INSERT } = q
// REVISIT: should @cds.persistence.name be considered ?
const entity = q.target?.['@cds.persistence.name'] || this.name(q.target?.name || INSERT.into.ref[0], q)
const entity = q._target?.['@cds.persistence.name'] || this.name(q._target?.name || INSERT.into.ref[0], q)

const elements = q.elements || q.target?.elements
const elements = q.elements || q._target?.elements
if (!elements) {
return super.INSERT_entries(q)
}
Expand Down Expand Up @@ -845,7 +845,7 @@ class HANAService extends SQLService {
// - Object JSON INSERT (1x)
// The problem with Simple INSERT is the type mismatch from csv files
// Recommendation is to always use entries
const elements = q.elements || q.target?.elements
const elements = q.elements || q._target?.elements
if (!elements) {
return super.INSERT_rows(q)
}
Expand Down Expand Up @@ -874,16 +874,16 @@ class HANAService extends SQLService {
UPSERT(q) {
const { UPSERT } = q
// REVISIT: should @cds.persistence.name be considered ?
const entity = q.target?.['@cds.persistence.name'] || this.name(q.target?.name || UPSERT.into.ref[0], q)
const elements = q.target?.elements || {}
const entity = q._target?.['@cds.persistence.name'] || this.name(q._target?.name || UPSERT.into.ref[0], q)
const elements = q._target?.elements || {}
const insert = this.INSERT({ __proto__: q, INSERT: UPSERT })

let keys = q.target?.keys
let keys = q._target?.keys
if (!keys) return insert
keys = Object.keys(keys).filter(k => !keys[k].isAssociation && !keys[k].virtual)

// temporal data
keys.push(...ObjectKeys(q.target.elements).filter(e => q.target.elements[e]['@cds.valid.from']))
keys.push(...ObjectKeys(q._target.elements).filter(e => q._target.elements[e]['@cds.valid.from']))

const managed = this.managed(
this.columns.map(c => ({ name: c })),
Expand Down
Loading
Loading