Notes · 2026-08-29

Five things OpenLink Virtuoso's ODBC driver does silently and wrongly

virtodbc.so from libvirtodbc0 7.2.12 stores UTF-16 text as garbage, aborts on any Charset= but the literal UTF-8, stores every SQL_C_SBIGINT as 0, and strides date parameter arrays by ColumnSize.

Virtuoso is ODBC-native: port 1111 carries its own binary wire protocol and virtodbc.so speaks it directly, so there is no separate client library between you and these defects. Three of the five below return SQL_SUCCESS with no diagnostic, which is what makes them worth writing down: nothing tells the application that the value it stored is not the value it sent.

1. SQL_C_WCHAR is a four-byte wchar_t, not the driver manager's SQLWCHAR

virtodbc.so is a pure ANSI driver — it exports no …W entry points at all — and implements SQL_C_WCHAR as four-byte wchar_t rather than the two-byte SQLWCHAR unixODBC's headers define, on both the parameter and the fetch side. A UTF-16 buffer is therefore consumed four bytes at a time. hello with indicator 10 stores as ??; a one-character string stores as the empty string; héllo 🚀 stores as the single character 0. Every one of them SQL_SUCCESS, no diagnostic. On the read side a two-byte SQLWCHAR fetch widens each UTF-8 byte to a four-byte unit (68 00 00 00 c3 00 00 00 …). Hand the same driver a wchar_t array and the text round-trips exactly, emoji included — which is the proof of the width.

The narrow path is exact for UTF-8, so staying on it is the workaround. Reported on 2026-08-29 as openlink/virtuoso-opensource#1470, open. Its macOS counterpart was filed four days earlier as openlink/virtuoso-opensource#1469: the Homebrew 7.2.17 build is iODBC-width and nothing says so.

Related: The process dies with SIGABRT on the first SQL error, and it is the manager

A second defect sits next to this one: SQL_C_CHAR bound against SQL_WCHAR/SQL_WVARCHAR with SQL_NTS stores the correct text followed by 100–150 characters of adjacent process memory, different on every run, again with no diagnostic. An explicit byte length in place of SQL_NTS stores the value correctly.

2. Any Charset= but the exact literal UTF-8 aborts the process

GPF: Dkbox.c:638 Double free

Client-side, inside SQLDriverConnect. UTF8, lowercase utf-8, UTF-16, and every charset Virtuoso itself lists in DB.DBA.SYS_CHARSETSISO-8859-1, WINDOWS-1252, KOI8-R — all abort. The one value that connects is not safe either: over a Charset=UTF-8 connection wide NVARCHAR columns read back doubly encoded, each stored byte recoded as a single-byte character, so héllo 🚀 comes back as héllo ð… while SQLGetData returns SQL_SUCCESS and reports the original byte length in StrLen_or_Ind — an application that honours the indicator gets the mojibake cut mid-sequence. The workaround is to put no Charset= in the connection string at all. openlink/virtuoso-opensource#1471, open.

3. SQL_C_SBIGINT parameters store 0

Whatever the value — 42 as much as 1234567890123 — and whether it is declared SQL_BIGINT, SQL_NUMERIC or SQL_DECIMAL. The driver's conversion table has no SQL_C_SBIGINT. The read side matches: SQLGetData and SQLBindCol with SQL_C_SBIGINT return SQL_SUCCESS with an indicator of 0 and leave the buffer untouched, so a column declared BIGINT reads back as zeros.

Sending the digits as SQL_C_CHAR text declared SQL_NUMERIC or SQL_DECIMAL is exact, INT64_MIN and INT64_MAX included. SQL_NUMERIC is what makes it exact, not the text: the same digits declared SQL_BIGINT, SQL_VARCHAR or SQL_CHAR also store 0. On the read side SQL_C_CHAR and SQL_C_DOUBLE deliver the value; SQL_C_SLONG truncates silently (9223372036854775807 arrives as -1). A related one: a SQL_C_SLONG parameter is read as eight bytes — a C long on LP64 — whatever BufferLength says, so an int32 bound out of a packed array picks up the word after it (SR346: Integer out of range for column i). openlink/virtuoso-opensource#1472, open.

4. Date parameter arrays are strided by ColumnSize

virtodbc.so accepts SQL_ATTR_PARAMSET_SIZE, executes every set, reports the right affected-row count and marks every element SQL_PARAM_SUCCESS — but for the datetime C structs it steps a column-wise array by SQLBindParameter's ColumnSize argument rather than by sizeof(SQL_DATE_STRUCT), which the specification says is what governs. ColumnSize 0 gives a stride of zero, so row 0's date is inserted into every row. Other values read at the wrong offset and past the end of the buffer: four dates bound with ColumnSize 10 over a packed array come back as 2024-01-01, 0197-08-05 and two more that change from run to run, and a timestamp array bound with ColumnSize 23 over 16-byte elements ends in -17837-65534-65535. SQL_C_CHAR, SQL_C_DOUBLE and SQL_C_SLONG arrays walk correctly, so the damage is invisible unless a date, time or timestamp column is in the batch. Laying the elements out ColumnSize bytes apart, or binding row-wise, is correct in every case; turning parameter arrays off is the blunter fix. openlink/virtuoso-opensource#1473, open.

5. On Windows, SQLSetPos and a four-byte indicator stride

virtodbc.dll 7.2.17 advertises the SQL_GD_* extensions, but SQLSetPos(SQL_POSITION) fails on a block cursor, and the bound-column indicator array is written at a four-byte stride there. Recorded with its conditions, not yet filed upstream.

Related: A NULL column that reads back as the previous row's value

Dates

The macOS width was found on 2026-08-25 and the four Linux defects on 2026-08-29. The Windows pair is recorded in bench/BENCHMARKS-windows.md.

Where this comes from: the adbcBridge compatibility matrix runs one workload through every database's ODBC driver on Linux, macOS and Windows and records each failure with its first error; the row for this database is at https://adbcbridge.org/matrix/#virtuoso, and the finding is on file with its reproduction in docs/UPSTREAM.md of the repository.