SAP HANA Express into Apache Arrow with ADBC
SAP HANA Express 2.00.088 (HANA 2.0 SPS08), read and written through adbcBridge — the ADBC driver that loads the database's ODBC driver — verified on Linux, macOS arm64 and Windows x64. Everything below is the compatibility matrix's record for this row.
Status
PASS
PASS (HDB (via ODBC) 02.00.0088, SAP HANA client 2.29.25 arm64 libodbcHDB.dylib, against the Linux-hosted server over one LAN hop, connected straight to the tenant SQL port — with DATABASENAME set, HANA redirects the client to the container's internal address, which only the Docker host can reach — so the numbers are network-bound: fetch 43.5k rows/s, ingest 20–26k rows/s with parameter arrays and 62 rows/s one row per execute, one commit round trip each; pyodbc within 10% of the bridge on fetch)
PASS (HDB (via ODBC) 02.00.0088, SAP HANA client 2.29.25 libodbcHDB.dll registered as HDBODBC — the name carries the libodbchdb key, so the HANA quirks apply on Windows unchanged; saplabs/hanaexpress under Docker Desktop, 12 GB, ~16 min to Startup finished!; connected straight to the tenant SQL port 39041 — published through a socat sidecar, since the compose service publishes 39013/39017 only, and with DATABASENAME set HANA redirects the client to the container's internal address, which a Windows host cannot reach; fetch 1.44–1.48M rows/s, ingest 364–398k rows/s with parameter arrays and 1.7k rows/s one row per execute)
Driver and connection string
- ODBC driver
- SAP HANA client ODBC 2.29.25 (
libodbcHDB.so, native wire) - Wire
- native ODBC
- adbcBridge
- 0.1.1 (release)
The connection string the matrix used, as the connection strings reference records it (Databases with their own native protocol and ODBC driver); {drv} is the driver library or its name from odbcinst.ini, and the host, port and credentials are the test server's.
Driver={drv};SERVERNODE=127.0.0.1:39017;UID=SYSTEM;PWD=AdbcBridge2026;DATABASENAME=HXE;
Python
import adbcbridge
# {drv}: the ODBC driver library (path), or its name from odbcinst.ini
conn = adbcbridge.connect(uri="Driver={drv};SERVERNODE=127.0.0.1:39017;UID=SYSTEM;PWD=AdbcBridge2026;DATABASENAME=HXE;")
with conn.cursor() as cur:
cur.execute("SELECT * FROM my_table")
table = cur.fetch_arrow_table() # pyarrow.Table
Polars and pandas
import polars as pl, pandas as pd
df = pl.read_database("SELECT * FROM my_table", connection=conn) # Polars, Arrow-native
pdf = pd.read_sql("SELECT * FROM my_table", conn) # pandas 2.2+, ADBC connection
pip install adbcbridge brings the driver library; the SAP HANA Express ODBC driver is installed the way its vendor documents, then named in Driver=. Rust, Go, C# and Java use the same connection string through their ADBC driver managers — see the docs.
What the matrix recorded
SAP's in-memory column store, first-party server (saplabs/hanaexpress, which pulled anonymously) driven by SAP's own first-party driver from the free HANA client download — the EULA that gates it is passed with the cookie its checkbox sets, and the ODBC member of the tarball needs no installer, no LD_LIBRARY_PATH and no .ini of its own.
What this stack needed
- Every workload type is a native HANA type and round-trips exactly, emoji and microseconds included (
iis int32; a bareCREATE TABLEis a column table,default_table_type = column, and aCREATE ROW TABLEpasses the same workload). - Two HANA spellings to know:
TIMESTAMPtakes no precision argument (TIMESTAMP(6)is42000,257 sql syntax error: incorrect syntax near "(") because it is always 7 fractional digits, and there is no multi-rowVALUESat all (INSERT INTO t VALUES (1,'a'),(2,'b')is42000,257 ... incorrect syntax near ",", with literals as with parameters). - Four driver quirks, all keyed on
libodbchdb. - The first is a correctness bug independent of this matrix: the driver decodes narrow statement text as Latin-1, not as the UTF-8 bytes unixODBC handed it, so the UTF-8 of
héllo 🚀(68 c3a9 6c6c6f 20 f09f9a80) is stored as the eleven Latin-1 characters those bytes spell (LENGTH(s)11, not 8) — the same corruption the Windows driver manager causes for every driver, here caused by one driver on every platform. - It hides because it is self-consistent within narrow statements, and shows the moment a literal must match a value sent as a bound parameter (which travels
SQL_C_WCHARand arrives correct). - No connection property (
CHAR_AS_UTF8,CHAR_SET,charset) and no locale (LC_ALL=C,en_US.UTF-8,C.UTF-8) changes it;SQLExecDirectWstores it exactly, sowide_sql(new) routes caller statement text through the W entry points on every platform — the mirror of the existingnarrow_sql, reusing the conversion the Windows build already had. - Second,
SQLGetTypeInfo(SQL_TYPE_TIMESTAMP)namesSECONDDATEfirst — HANA's whole-second timestamp (COLUMN_SIZE19,MAXIMUM_SCALE0, noCREATE_PARAMS) — with the 7-digitTIMESTAMPonly in rows two and three, so generated ingest DDL silently dropped every microsecond;ddl_timestamp_type_name(new) gives the name outright, sinceSECONDDATEhas noCREATE_PARAMSforfractional_time_type_formatto ask for a scale andTIMESTAMPtakes no precision argument either. - Third,
SQLGetTypeInfo(SQL_LONGVARCHAR)namesCLOB, which HANA bars fromORDER BY(HY000,264 invalid datatype: "V" LOB type in ORDER BY clause) andSELECT DISTINCT(HY000,264 ... LOB type in distinct select clause) — the SQL ServerTEXTtrap — soddl_string_as_max_varchar, the flag Db2 already sets, asks forVARCHAR(5000)instead, fully Unicode since HANA 2.0 mergedVARCHARintoNVARCHAR. - Fourth,
prefer_param_arrays(the third driver to set it, after maodbc and Vertica, and here the server's doing rather than the driver's): with no multi-rowVALUES,MultiRowSetup's probe is refused and ingest falls back to one execute per row, 7,675 rows/s against 1,142,092 with a bound array. - Server side, startup is 158 s on a cold volume (system database, then the
HXEtenant) and settles at ~5 GB under a 12 GB cap; 39013 (system DB) and 39017 (theHXEtenant) must be published unmapped, because the client resolves a tenant by asking for its port and reconnecting to exactly that number;--agree-to-sap-licenseand a world-writable mount holdingpasswords.json(the image'shxeadmis uid 12000) are required, and of SAP's four sysctls onlykernel.shmmaxis IPC-namespaced and settable per container — the entry was verified with the other three left at a stock Ubuntu's values.