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