From c5652ef35ac09112d3b0e69207cea731a2c14514 Mon Sep 17 00:00:00 2001 From: Bob den Os <bob.den.os@sap.com> Date: Mon, 9 Dec 2024 10:02:28 +0100 Subject: [PATCH] Add support for odbc hana driver --- hana/lib/HANAService.js | 6 +- hana/lib/drivers/bin/extract.sh | 4 + hana/lib/drivers/index.js | 1 + hana/lib/drivers/odbc.js | 162 ++++++++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 3 deletions(-) create mode 100755 hana/lib/drivers/bin/extract.sh create mode 100644 hana/lib/drivers/odbc.js diff --git a/hana/lib/HANAService.js b/hana/lib/HANAService.js index 3a8ba7444..ab28f2009 100644 --- a/hana/lib/HANAService.js +++ b/hana/lib/HANAService.js @@ -333,7 +333,7 @@ class HANAService extends SQLService { } let { limit, one, orderBy, expand, columns = ['*'], localized, count, parent } = q.SELECT - + // When one of these is defined wrap the query in a sub query if (expand || (parent && (limit || one || orderBy))) { @@ -1236,7 +1236,7 @@ SELECT ${mixing} FROM JSON_TABLE(SRC.JSON, '$' COLUMNS(${extraction})) AS NEW LE const stmt = await this.dbc.prepare(createContainerDatabase) const res = this.ensureDBC() && await stmt.run([creds.user, creds.password, creds.containerGroup, !clean]) - res && DEBUG?.(res.changes.map(r => r.MESSAGE).join('\n')) + res && DEBUG?.(res.changes?.map?.(r => r.MESSAGE).join('\n')) } finally { if (this.dbc) { // Release table lock @@ -1282,7 +1282,7 @@ SELECT ${mixing} FROM JSON_TABLE(SRC.JSON, '$' COLUMNS(${extraction})) AS NEW LE try { const stmt = await this.dbc.prepare(createContainerTenant.replaceAll('{{{GROUP}}}', creds.containerGroup)) const res = this.ensureDBC() && await stmt.run([creds.user, creds.password, creds.schema, !clean]) - res && DEBUG?.(res.changes.map?.(r => r.MESSAGE).join('\n')) + res && DEBUG?.(res.changes?.map?.(r => r.MESSAGE).join('\n')) break } catch (e) { err = e diff --git a/hana/lib/drivers/bin/extract.sh b/hana/lib/drivers/bin/extract.sh new file mode 100755 index 000000000..0a2d5b3d5 --- /dev/null +++ b/hana/lib/drivers/bin/extract.sh @@ -0,0 +1,4 @@ +## TODO: use following find command to identify the source of the ABAP odbc linux driver and download it out of the HANA binaries +## docker exec hce-hana-1 find /hana/shared/ -name "libodbcHDB.so" +docker exec hce-hana-1 cat /hana/shared/H00/exe/linuxx86_64/HDB_4.00.000.00.1728375763_870b57d282c6ae9aedc90ae2f41f20321f3e060a/libsapcrypto.so > libsapcrypto.so +docker exec hce-hana-1 cat /hana/shared/H00/exe/linuxx86_64/HDB_4.00.000.00.1728375763_870b57d282c6ae9aedc90ae2f41f20321f3e060a/libodbcHDB.so > libodbcHDB.so diff --git a/hana/lib/drivers/index.js b/hana/lib/drivers/index.js index c3b1a0cd3..0348daf6c 100644 --- a/hana/lib/drivers/index.js +++ b/hana/lib/drivers/index.js @@ -3,6 +3,7 @@ const cds = require('@sap/cds') Object.defineProperties(module.exports, { hdb: { get: () => require('./hdb') }, 'hana-client': { get: () => require('./hana-client') }, + 'odbc': { get: () => require('./odbc') }, default: { get() { try { diff --git a/hana/lib/drivers/odbc.js b/hana/lib/drivers/odbc.js new file mode 100644 index 000000000..c9530ccd9 --- /dev/null +++ b/hana/lib/drivers/odbc.js @@ -0,0 +1,162 @@ +const path = require('path') +const { Readable, Stream } = require('stream') +const { text } = require('stream/consumers') + +const odbc = require('odbc') + +const { driver, prom, handleLevel } = require('./base') + +const credentialMappings = [ + { old: 'certificate', new: 'ca' }, + { old: 'encrypt', new: 'useTLS' }, + { old: 'sslValidateCertificate', new: 'rejectUnauthorized' }, + { old: 'validate_certificate', new: 'rejectUnauthorized' }, +] + +class HDBDriver extends driver { + /** + * Instantiates the HDBDriver class + * @param {import('./base').Credentials} creds The credentials for the HDBDriver instance + */ + constructor(creds) { + creds = { + fetchSize: 1 << 16, // V8 default memory page size + ...creds, + } + + // Retain hana credential mappings to hdb / node credential mapping + for (const m of credentialMappings) { + if (m.old in creds && !(m.new in creds)) creds[m.new] = creds[m.old] + } + + creds.connectionString = [ + // src: https://community.sap.com/t5/technology-blogs-by-sap/using-the-odbc-driver-for-abap-on-linux/ba-p/13513705 + `driver=${path.resolve(__dirname, 'bin/libodbcHDB.so')}`, + 'client=100', // TODO: see what this means + 'trustall=true', // supersecure + `cryptolibrary=${path.resolve(__dirname, 'bin/libsapcrypto.so')}`, + `encrypt=${creds.encrypt}`, + `sslValidateCertificate=${creds.sslValidateCertificate}`, + `disableCloudRedirect=true`, + + `servernode=${creds.host}:${creds.port}`, + `database=${creds.schema}`, + `uid=${creds.user}`, + `pwd=${creds.password}`, + + 'uidtype=alias', + 'typemap=semantic', // semantic or native or ... + ].join(';') + + super(creds) + this.connected = odbc.connect(creds) + this.connected.then(dbc => this._native = dbc) + this.connected.catch(() => this.destroy?.()) + } + + set(variables) { + // TODO: + } + + async validate() { + // TODO: + return true + } + + /** + * Connects the driver using the provided credentials + * @returns {Promise<any>} + */ + async connect() { + return this.connected.then(async () => { + const [version] = await Promise.all([ + this._native.query('SELECT VERSION FROM "SYS"."M_DATABASE"'), + this._creds.schema && this._native.query(`SET SCHEMA ${this._creds.schema}`), + ]) + const split = version[0].VERSION.split('.') + this.server = { + major: split[0], + minor: split[2], + patch: split[3], + } + }) + } + + async disconnect() { + return this._native.close() + } + + // TODO: find out how to do this with odbc driver + async begin() { + return this._native.beginTransaction() + } + async commit() { + return this._native.commit() + } + async rollback() { + return this._native.rollback() + } + + async exec(sql) { + await this.connected + return this._native.query(sql) + } + + async prepare(sql, hasBlobs) { + try { + const stmt = await this._native.createStatement() + await stmt.prepare(sql) + const run = async (args) => { + try { + await stmt.bind(await this._extractStreams(args).values) + return await stmt.execute() + } catch (e) { + throw e.odbcErrors[0] + } + } + return { + run, + get: (..._) => run(..._).then(r => r[0]), + all: run, + proc: async (data, outParameters) => { + // this._native.callProcedure(null,null,) + const rows = await run(data) + return rows // TODO: see what this driver returns for procedures + }, + + stream: (..._) => stmt.bind(..._).then(async () => Readable.from(this._iterator(await stmt.execute({ cursor: true })))), + } + } catch (e) { + e.message += ' in:\n' + (e.query = sql) + throw e + } + } + + _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) { + return text(v) + } + return v + }) + return { + values: Promise.all(values), + streams, + } + } + + // TODO: implement proper raw stream + async *_iterator(cursor) { + while (!cursor.noData) { + for (const row of await cursor.fetch()) { + yield row + } + } + await cursor.close() + } +} + +module.exports.driver = HDBDriver