Skip to content

Commit

Permalink
feat: Compress HANA expand queries by reducing duplicated statements (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
BobdenOs authored Dec 20, 2023
1 parent 3eadfea commit 3d29351
Showing 1 changed file with 28 additions and 35 deletions.
63 changes: 28 additions & 35 deletions hana/lib/HANAService.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ class HANAService extends SQLService {
// REVISIT: disable this for queries like (SELECT 1)
// Will return multiple rows with objects inside
query.SELECT.expand = 'root'
const { cqn, temporary, blobs, values } = this.cqn2sql(query, data)
const { cqn, temporary, blobs, withclause, values } = this.cqn2sql(query, data)
// REVISIT: add prepare options when param:true is used
const sqlScript = this.wrapTemporary(temporary, blobs)
const sqlScript = this.wrapTemporary(temporary, withclause, blobs)
let rows = values?.length ? await (await this.prepare(sqlScript)).all(values) : await this.exec(sqlScript)
if (rows.length) {
rows = this.parseRows(rows)
Expand All @@ -131,7 +131,7 @@ class HANAService extends SQLService {
}

async onSTREAM(req) {
let { cqn, sql, values, temporary, blobs } = this.cqn2sql(req.query)
let { cqn, sql, values, temporary, withclause, blobs } = this.cqn2sql(req.query)
// writing stream
if (req.query.STREAM.into) {
const ps = await this.prepare(sql)
Expand All @@ -140,7 +140,7 @@ class HANAService extends SQLService {
// reading stream
if (temporary?.length) {
// Full SELECT CQN support streaming
sql = this.wrapTemporary(temporary, blobs)
sql = this.wrapTemporary(temporary, withclause, blobs)
}
const ps = await this.prepare(sql)
const stream = await ps.stream(values, cqn.SELECT?.one)
Expand All @@ -149,20 +149,17 @@ class HANAService extends SQLService {
}

// Allow for running complex expand queries in a single statement
wrapTemporary(temporary, blobs) {
wrapTemporary(temporary, withclauses, blobs) {
const blobColumn = b => `"${b.replace(/"/g, '""')}"`

const values = temporary
.map(t => {
if (blobs.length) {
const localBlobs = t.blobs
const blobColumns = blobs.filter(b => !(b in localBlobs)).map(b => `NULL AS ${blobColumn(b)}`)
if (blobColumns.length) return `SELECT ${blobColumns},${t.select}`
}
return `SELECT ${t.select}`
const blobColumns = blobs.map(b => (b in t.blobs) ? blobColumn(b) : `NULL AS ${blobColumn(b)}`)
return `SELECT "_path_","_blobs_","_expands_","_json_"${blobColumns.length ? ',' : ''}${blobColumns} FROM (${t.select})`
})

const ret = values.length === 1 ? values[0] : 'SELECT * FROM ' + values.map(v => `(${v})`).join(' UNION ALL ') + ' ORDER BY "_path_" ASC'
const withclause = withclauses.length ? `WITH ${withclauses} ` : ''
const ret = withclause + (values.length === 1 ? values[0] : 'SELECT * FROM ' + values.map(v => `(${v})`).join(' UNION ALL ') + ' ORDER BY "_path_" ASC')
DEBUG?.(ret)
return ret
}
Expand Down Expand Up @@ -265,9 +262,17 @@ class HANAService extends SQLService {
SELECT(q) {
// Collect all queries and blob columns of all queries
this.blobs = this.blobs || []
this.withclause = this.withclause || []
this.temporary = this.temporary || []
this.temporaryValues = this.temporaryValues || []

const walkAlias = q => {
if (q.args) return q.as || walkAlias(q.args[0])
if (q.SELECT?.from) return walkAlias(q.SELECT?.from)
return q.as
}
const alias = walkAlias(q)
q.as = alias
const src = q

const { limit, one, orderBy, expand, columns, localized, count, from, parent } = q.SELECT

Check warning on line 278 in hana/lib/HANAService.js

View workflow job for this annotation

GitHub Actions / Node.js 18

'from' is assigned a value but never used

Check warning on line 278 in hana/lib/HANAService.js

View workflow job for this annotation

GitHub Actions / HANA Node.js 18

'from' is assigned a value but never used
Expand All @@ -289,7 +294,7 @@ class HANAService extends SQLService {
if (parent) {
// Track parent _path_ for later concatination
if (!columns.find(c => this.column_name(c) === '_path_'))
columns.push({ ref: [parent.as, '_path_'], as: '_path_' })
columns.push({ ref: [parent.as, '_path_'], as: '_parent_path_' })
}

if (orderBy) {
Expand All @@ -309,7 +314,7 @@ class HANAService extends SQLService {
// Insert row number column for reducing or sorting the final result
const over = { xpr: [] }
// TODO: replace with full path partitioning
if (parent) over.xpr.push(`PARTITION BY ${this.ref({ ref: ['_path_'] })}`)
if (parent) over.xpr.push(`PARTITION BY ${this.ref({ ref: ['_parent_path_'] })}`)
if (orderBy) over.xpr.push(` ORDER BY ${this.orderBy(orderBy, localized)}`)
const rn = { xpr: [{ func: 'ROW_NUMBER', args: [] }, 'OVER', over], as: '$$RN$$' }
q.as = q.SELECT.from.as
Expand All @@ -318,6 +323,7 @@ class HANAService extends SQLService {
q.as = q.SELECT.from.as

q = cds.ql.SELECT(outputColumns.map(c => (c.elements ? c : { __proto__: c, ref: [this.column_name(c)] }))).from(q)
q.as = q.SELECT.from.as
Object.defineProperty(q, 'elements', { value: elements })
Object.defineProperty(q, 'element', { value: element })

Expand All @@ -330,7 +336,7 @@ class HANAService extends SQLService {
? [
{
func: 'concat',
args: [{ ref: ['_path_'] }, { val: `].${q.element.name}[`, param: false }],
args: [{ ref: ['_parent_path_'] }, { val: `].${q.element.name}[`, param: false }],
},
{ func: 'lpad', args: [{ ref: ['$$RN$$'] }, { val: 6, param: false }, { val: '0', param: false }] },
]
Expand Down Expand Up @@ -375,7 +381,8 @@ class HANAService extends SQLService {

if (expand === 'root') {
this.cqn = q
this.temporary.unshift({ blobs: this._blobs, select: this.sql.substring(7) })
this.withclause.unshift(`${this.quote(alias)} as (${this.sql})`)
this.temporary.unshift({ blobs: this._blobs, select: `SELECT ${this._outputColumns} FROM ${this.quote(alias)}` })
if (this.values) {
this.temporaryValues.unshift(this.values)
this.values = this.temporaryValues.flat()
Expand All @@ -401,9 +408,7 @@ class HANAService extends SQLService {
if (x.elements) {
expands[this.column_name(x)] = x.SELECT.one ? null : []

const parent = cds.ql.clone(src)
parent.as = parent.SELECT.from.as || parent.SELECT.from.args[0].as
parent.SELECT.expand = true
const parent = src
x.element._foreignKeys.forEach(k => {
if (!parent.SELECT.columns.find(c => this.column_name(c) === k.parentElement.name)) {
parent.SELECT.columns.push({ ref: [parent.as, k.parentElement.name] })
Expand All @@ -412,7 +417,7 @@ class HANAService extends SQLService {

x.SELECT.from = {
join: 'inner',
args: [parent, x.SELECT.from],
args: [{ ref: [parent.as], as: parent.as }, x.SELECT.from],
on: x.SELECT.where,
as: x.SELECT.from.as,
}
Expand Down Expand Up @@ -485,28 +490,16 @@ class HANAService extends SQLService {

// Calculate final output columns once
let outputColumns = ''
outputColumns = `${path} as "_path_",${blobs} as "_blobs_",${expands} as "_expands_",${jsonColumn}`
outputColumns = `_path_ as "_path_",${blobs} as "_blobs_",${expands} as "_expands_",${jsonColumn}`
if (blobColumns.length)
outputColumns = `${outputColumns},${blobColumns.map(b => `${this.quote(b)} as "${b.replace(/"/g, '""')}"`)}`
if (this.foreignKeys?.length) {
outputColumns += ',' + this.foreignKeys.map(c => this.column_expr({ ref: c.ref.slice(-1) }))
}

if (structures.length && sql.length) {
this._outputColumns = outputColumns
// Select all columns to be able to use the _outputColumns in the outer select
sql = `*,${rawJsonColumn}`
} else {
sql = outputColumns
}
this._outputColumns = outputColumns
sql = `*,${path} as _path_,${rawJsonColumn}`
}
return sql
}

SELECT_expand(_, sql) {
if (this._outputColumns) {
return `SELECT ${this._outputColumns} FROM (${sql})`
}
return sql
}

Expand Down

0 comments on commit 3d29351

Please sign in to comment.