@cap-js/hana
Version:
CDS database service for SAP HANA
428 lines (383 loc) • 14.4 kB
JavaScript
const { Readable, Stream } = require('stream')
const { pipeline } = require('stream/promises')
const cds = require('@sap/cds')
const hdb = require('@sap/hana-client')
const { driver, prom, handleLevel } = require('./base')
const { resultSetStream } = require('./stream')
const { wrap_client } = require('./dynatrace')
const LOG = cds.log('@sap/hana-client')
if (process.env.NODE_ENV === 'production' && !process.env.HDB_NODEJS_THREADPOOL_SIZE && !process.env.UV_THREADPOOL_SIZE) LOG.warn("When using @sap/hana-client, it's strongly recommended to adjust its thread pool size with environment variable `HDB_NODEJS_THREADPOOL_SIZE`, otherwise it might lead to performance issues.\nLearn more: https://help.sap.com/docs/SAP_HANA_CLIENT/f1b440ded6144a54ada97ff95dac7adf/31a8c93a574b4f8fb6a8366d2c758f21.html")
const streamUnsafe = false
const credentialMappings = [
{ old: 'schema', new: 'currentSchema' },
{ old: 'hostname_in_certificate', new: 'sslHostNameInCertificate' },
{ old: 'validate_certificate', new: 'sslValidateCertificate' },
]
class HANAClientDriver extends driver {
/**
* Instantiates the HANAClientDriver class
* @param {import('./base').Credentials} creds The credentials for the HANAClientDriver instance
*/
constructor(creds) {
// Enable native @sap/hana-client connection pooling
creds = Object.assign({
// REVISIT: add pooling related credentials when switching to native pools
// Enables the @sap/hana-client native connection pool implementation
// pooling: true,
// poolingCheck: true,
// maxPoolSize: 100, // TODO: align to options.pool configurations
// If communicationTimeout is not set queries will hang for over 10 minutes
communicationTimeout: 60000,
// connectTimeout: 1000,
// compress: true, // TODO: test whether having compression enabled makes things faster
// statement caches come with a side effect when the database structure changes which does not apply to CAP
// statementCacheSize: 100, // TODO: test whether statementCaches make things faster
}, creds)
// Retain node-hdb credential mappings to @sap/hana-client credential mapping
for (const m of credentialMappings) {
if (m.old in creds && !(m.new in creds)) creds[m.new] = creds[m.old]
}
super(creds)
this._native = hdb.createConnection(creds)
this._native = wrap_client(this._native, creds, creds.tenant)
this._native.set = function (variables) {
for (const key in variables) {
this.setClientInfo(key, variables[key])
}
}
this._native.setAutoCommit(false)
}
async prepare(sql, hasBlobs) {
const ret = await super.prepare(sql)
// hana-client ResultSet API does not allow for deferred streaming of blobs
// With the current design of the hana-client ResultSet it is only
// possible to read all LOBs into memory to do deferred streaming
// Main reason is that the ResultSet only allowes using getData() on the current row
// with the current next() implemenation it is only possible to go foward in the ResultSet
// It would be required to allow using getDate() on previous rows
if (hasBlobs) {
ret.all = async (values) => {
const stmt = await ret._prep
// Create result set
const reset = async function () {
if (this) await prom(this, 'close')()
const rs = await prom(stmt, 'executeQuery')(values)
rs.reset = reset
return rs
}
const rs = await reset()
const rsStreamsProm = {}
const rsStreams = new Promise((resolve, reject) => {
rsStreamsProm.resolve = resolve
rsStreamsProm.reject = reject
})
rsStreams.catch(() => { })
rs._rowPosition = -1
const _next = prom(rs, 'next')
const next = () => {
rs._rowPosition++
return _next()
}
const getValue = prom(rs, 'getValue')
const result = []
// Fetch the next row
while (await next()) {
const cols = stmt.getColumnInfo()
// column 0-3 are metadata columns
const values = await Promise.all([getValue(0), getValue(1), getValue(2), getValue(3)])
const row = {}
for (let i = 0; i < cols.length; i++) {
const col = cols[i]
// column >3 are all blob columns
row[col.columnName] = i > 3 ?
rs.isNull(i)
? null
: col.nativeType === 12 || col.nativeType === 13 // return binary type as simple buffer
? await getValue(i)
: Readable.from(streamBlob(rsStreams, rs._rowPosition, i), { objectMode: false })
: values[i]
}
result.push(row)
}
rs.reset().then(rsStreamsProm.resolve, rsStreamsProm.reject)
return result
}
}
ret.run = async params => {
const { values, streams } = this._extractStreams(params)
const stmt = await ret._prep
let changes = await prom(stmt, 'exec')(values)
await this._sendStreams(stmt, streams)
// REVISIT: hana-client does not return any changes when doing an update with streams
// This causes the best assumption to be that the changes are one
// To get the correct information it is required to send a count with the update where clause
if (streams.length && changes === 0) {
changes = 1
}
return { changes }
}
ret.proc = async (data, outParameters) => {
const stmt = await ret._prep
const rows = await prom(stmt, 'execQuery')(data)
return this._getResultForProcedure(rows, outParameters, stmt)
}
ret.stream = async (values, one, objectMode) => {
const stmt = await ret._prep
values = Array.isArray(values) ? values : []
// Uses the native exec method instead of executeQuery to initialize a full stream
// As executeQuery does not request the whole result set at once
// It is required to request each value at once and each row at once
// When this is done with sync functions it is blocking the main thread
// When this is done with async functions it is extremely slow
// While with node-hdb it is possible to get the raw stream from the query
// Allowing for an efficient inline modification of the stream
// This is not possible with the current implementation of hana-client
// Which creates an inherent limitation to the maximum size of a result set (~0xfffffffb)
if (streamUnsafe && sql.startsWith('DO')) {
const rows = await prom(stmt, 'exec')(values, { rowsAsArray: true })
return Readable.from(rowsIterator(rows, stmt.getColumnInfo()), { objectMode: false })
}
const rs = await prom(stmt, 'executeQuery')(values)
const cols = rs.getColumnInfo()
// If the query only returns a single row with a single blob it is the final stream
if (cols.length === 1 && cols[0].type === 1) {
if (rs.getRowCount() === 0) return null
await prom(rs, 'next')()
if (rs.isNull(0)) return null
return Readable.from(streamBlob(rs, undefined, 0), { objectMode: false })
}
return rsIterator(rs, one, objectMode)
}
return ret
}
async validate() {
return this._native.state() === 'connected'
}
_getResultForProcedure(rows, outParameters, stmt) {
const result = {}
// build result from scalar params
const paramInfo = stmt.getParameterInfo()
for (let i = 0; i < paramInfo.length; i++) {
if (paramInfo[i].direction > 1) {
result[paramInfo[i].name] = stmt.getParameterValue(i)
}
}
const resultSet = Array.isArray(rows) ? rows[0] : rows
// merge table output params into scalar params
const params = Array.isArray(outParameters) && outParameters.filter(md => !(md.PARAMETER_NAME in result))
if (params && params.length) {
for (let i = 0; i < params.length; i++) {
const parameterName = params[i].PARAMETER_NAME
result[parameterName] = []
while (resultSet.next()) {
result[parameterName].push(resultSet.getValues())
}
resultSet.nextResult()
}
}
return result
}
_extractStreams(values) {
// Removes all streams from the values and replaces them with a placeholder
if (!Array.isArray(values)) return { values: [], streams: [] }
const streams = []
values = values.map((v, i) => {
if (v instanceof Stream) {
streams[i] = v
return { sendParameterData: true }
}
return v
})
return {
values,
streams,
}
}
async _sendStreams(stmt, streams) {
// Sends all streams to the database
const sendParameterData = prom(stmt, 'sendParameterData')
for (let i = 0; i < streams.length; i++) {
const curStream = streams[i]
if (!curStream) continue
for await (const chunk of curStream) {
curStream.pause()
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
if (buffer.length) await sendParameterData(i, buffer)
curStream.resume()
}
await sendParameterData(i, null)
}
}
}
HANAClientDriver.pool = true
async function rsIterator(rs, one, objectMode) {
rs._rowPosition = -1
rs.nextAsync = prom(rs, 'next')
rs.getValueAsync = prom(rs, 'getValue')
rs.getDataAsync = prom(rs, 'getData')
const blobs = rs.getColumnInfo().slice(4).map(b => b.columnName)
const levels = [
{
index: 0,
suffix: one ? '' : ']',
path: '$[',
expands: {},
},
]
const state = {
rs,
levels,
blobs,
columnIndex: 0,
binaryBuffer: new Buffer.alloc(1 << 16),
done() {
this.columnIndex = 0
this.rs._rowPosition++
return this.rs.nextCanBlock()
? this.rs.nextAsync().then(a => !a)
: !this.rs.next()
},
inject(str) {
if (str == null) return
this.stream.push(str)
},
read() {
this.columnIndex++
},
readString() {
const index = this.columnIndex++
if (index === 3) {
const _inject = str => {
this.inject(str.slice(0, -1))
return str.length
}
return this.rs.getValuesCanBlock()
? this.rs.getValueAsync(index).then(_inject)
: _inject(this.rs.getValue(index))
}
return this.rs.getValuesCanBlock()
? this.rs.getValueAsync(index)
: this.rs.getValue(index)
},
readBlob() {
const index = this.columnIndex++
const stream = Readable.from(streamBlob(this.rs, undefined, index, this.binaryBuffer), { objectMode: false })
stream.setEncoding('base64')
this.stream.push('"')
return pipeline(stream, this.stream, { end: false }).then(() => { this.stream.push('"') })
}
}
if (objectMode) {
state.inject = function inject() { }
state.readString = function readString() {
const index = this.columnIndex++
return this.rs.getValuesCanBlock()
? this.rs.getValueAsync(index)
: this.rs.getValue(index)
}
state.readBlob = function readBlob() {
const index = this.columnIndex++
const col = this.rs.getColumnInfo()[index]
return this.rs.isNull(index)
? null
: col.nativeType === 12 || col.nativeType === 13 // return binary type as simple buffer
? this.rs.getValue(index)
: Readable.from(streamBlob(this.rs, rs._rowPosition, index), { objectMode: false })
}
}
return resultSetStream(state, one, objectMode)
}
async function* streamBlob(rs, rowIndex = -1, columnIndex, binaryBuffer) {
const promChain = {
resolve: () => { },
reject: () => { }
}
try {
// Check if the resultset is a promise
if (rs.then) {
// Copy the current Promise
const prom = new Promise((resolve, reject) => rs.then(resolve, reject))
// Enqueue all following then calls till after the current call
const next = new Promise((resolve, reject) => {
promChain.resolve = resolve
promChain.reject = reject
})
rs.then = (resolve, reject) => next.then(resolve, reject)
rs = await prom
}
// Check if the provided resultset is on the correct row
if (rowIndex >= 0) {
rs._rowPosition ??= -1
if (rowIndex - rs._rowPosition < 0) {
rs = await rs.reset()
rs._rowPosition ??= -1
}
const _next = prom(rs, 'next')
const next = () => {
rs._rowPosition++
return _next()
}
// Move result set to the correct row
while (rowIndex - rs._rowPosition > 0) {
await next()
}
}
let blobPosition = 0
const getData = prom(rs, 'getData')
while (true) {
const buffer = binaryBuffer || Buffer.allocUnsafe(1 << 16)
const read = await getData(columnIndex, blobPosition, buffer, 0, buffer.byteLength)
blobPosition += read
if (read < buffer.byteLength) {
yield buffer.subarray(0, read)
break
}
yield buffer
}
} catch (e) {
promChain.reject(e)
} finally {
promChain.resolve(rs)
}
}
async function* rowsIterator(rows, cols) {
cols = cols.map(b => b.columnName)
const levels = [
{
index: 0,
suffix: ']',
path: '$[',
expands: {},
},
]
yield '['
for (const row of rows) {
// Made all functions possible promises giving a 5x speed up
const path = row[0]
const blobs = JSON.parse(row[1])
const expands = JSON.parse(row[2])
yield handleLevel(levels, path, expands)
// Read and write JSON blob data
const json = row[3]
let hasProperties = json.length > 2
yield json.slice(0, -1)
for (let i = 4; i < cols.length; i++) {
const blobColumn = cols[i]
// Skip all blobs that are not part of this row
if (!(blobColumn in blobs)) {
continue
}
yield `${hasProperties ? ',' : ''}${JSON.stringify(blobColumn)}:`
hasProperties = true
yield row[i] ? `"${row[i].toString('base64')}"` : 'null'
}
const level = levels[levels.length - 1]
level.hasProperties = hasProperties
}
yield levels
.reverse()
.map(l => l.suffix)
.join('')
}
module.exports.driver = HANAClientDriver
module.exports.driver._driver = hdb