Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ include(sqlpp23_build_targets)

option(BUILD_MYSQL_CONNECTOR "Build MySQL Connector" OFF)
option(BUILD_MARIADB_CONNECTOR "Build MariaDB Connector" OFF)
option(BUILD_ODBC_CONNECTOR "Build ODBC Connector" OFF)
option(BUILD_POSTGRESQL_CONNECTOR "Build PostgreSQL Connector" OFF)
option(BUILD_SQLITE3_CONNECTOR "Build SQLite3 Connector" OFF)
option(BUILD_SQLCIPHER_CONNECTOR "Build SQLite3 Connector with SQLCipher" OFF)
Expand Down Expand Up @@ -78,6 +79,9 @@ endif()
if(BUILD_MARIADB_CONNECTOR)
add_build_component(NAME MariaDB PACKAGE MariaDB DEPENDENCIES MariaDB::MariaDB HEADER_DIR mysql MODULE_INTERFACE sqlpp23.mysql.cppm)
endif()
if(BUILD_ODBC_CONNECTOR)
add_build_component(NAME ODBC PACKAGE ODBC DEPENDENCIES ODBC::ODBC HEADER_DIR odbc MODULE_INTERFACE sqlpp23.odbc.cppm)
endif()
if(BUILD_POSTGRESQL_CONNECTOR)
add_build_component(NAME PostgreSQL PACKAGE PostgreSQL DEPENDENCIES PostgreSQL::PostgreSQL HEADER_DIR postgresql MODULE_INTERFACE sqlpp23.postgresql.cppm)
endif()
Expand Down
131 changes: 131 additions & 0 deletions ODBC_HANDOFF.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# ODBC connector — session handoff

Context document for continuing work on the sqlpp23 ODBC connector on
another machine. Written after the initial implementation on macOS
(2026-07-14). Tell Claude "Read ODBC_HANDOFF.md" at the start of the next
session. This file can be deleted once the cross-platform testing is done.

## State

- Complete new ODBC connector in `include/sqlpp23/odbc/`, enabled with
`-DBUILD_ODBC_CONNECTOR=ON` (CMake's built-in `FindODBC`).
- All 136 tests pass on macOS (unixODBC 2.3.14, Homebrew); the 8 ODBC tests
also pass *live* against the SQLite ODBC driver (`brew install sqliteodbc`).
- The `BUILD_WITH_MODULES` variant of `modules/sqlpp23.odbc.cppm` is
**unvalidated**: no `clang-scan-deps` on the Mac (this equally affects the
existing core/sqlite3 modules). Worth trying on Windows with MSVC.
- Docs: `docs/connectors/odbc.md` (includes how to run the tests).

## File map

```
include/sqlpp23/odbc/
odbc.h umbrella header
cursor_result.h result type (block cursor + SQLGetData streaming)
prepared_statement.h SQLPrepare/SQLBindParameter, param buffers
to_sql_string.h '?' params, {d}/{t}/{ts} escape sequences
detail/diagnostics.h throw_on_error / make_exception (SQLGetDiagRec)
database/connection.h connection_base, transactions, isolation levels
database/connection_config.h variant<data_source, connection_string> + tuning
database/connection_handle.h env (ODBC 3.8) + dbc RAII, SQLConnect/SQLDriverConnect
database/exception.h exception with sql_state() + native_error_code()
database/connection_pool.h, database/serializer_context.h
modules/sqlpp23.odbc.cppm
tests/odbc/ serialize (offline), usage (ConnectionConfig, Statements)
tests/include/sqlpp23/tests/odbc/ tables.h (SQLite-flavored DDL!), helpers
```

## Key design decisions

- **One result class** (`cursor_result_t`), two fetch strategies decided at
first `next()` after the row's `bind_field` calls record the column plan:
- **Block cursor**: all columns bindable → `SQLBindCol` column-wise arrays,
`SQL_ATTR_ROW_ARRAY_SIZE = connection_config::row_array_size` (default 64),
`SQL_ATTR_ROWS_FETCHED_PTR` (heap-allocated so moves are safe). Handles
drivers that lower/reject the rowset size.
- **Streaming**: any blob column, any long-typed column (`SQL_LONGVARCHAR`,
`SQL_WLONGVARCHAR`, `SQL_LONGVARBINARY`), unknown size, or text wider than
`connection_config::max_bound_column_size` (default 4096; text budgeted
×4 for UTF-8 +1 NUL) → row-at-a-time, `SQLGetData` per column in
increasing index order (the only pattern *every* driver supports),
growing buffer with the standard truncation/`SQL_NO_TOTAL` loop.
- All-or-nothing: mixing bound arrays and SQLGetData needs `SQLSetPos` /
`SQL_GD_BLOCK`, which is driver-specific — deliberately avoided.
- Statement handles are `shared_ptr<void>`; a result from a prepared
statement shares ownership. Result dtor does `SQL_CLOSE` + `SQL_UNBIND` and
resets `ROWS_FETCHED_PTR`/`ROW_ARRAY_SIZE` so the prepared statement can be
re-executed.
- Transactions: no `BEGIN` in ODBC — autocommit off + `SQLEndTran`; isolation
via `SQL_ATTR_TXN_ISOLATION` (persists across transactions; documented).
- Parameters: scalars/chrono copied into per-index `parameter_data` (vector
sized once at prepare from the serializer's param count — never resized,
addresses are registered with the driver); text/blob bound by pointer into
the core prepared statement's storage (same lifetime guarantee MySQL
connector relies on). NULL = `SQL_C_DEFAULT`/`SQL_VARCHAR` + `SQL_NULL_DATA`.
- No `last_insert_id` (nothing portable in ODBC). No `constraints.h`
(generic ODBC can't know backend capabilities at compile time).
- `escape()` doubles single quotes without needing a live connection — that's
why serialize tests run offline (`SQLPP_COMPARE` uses an unconnected
`connection`).

## Lessons learned (from live testing with sqliteodbc)

- **Do not trust `SQLDescribeCol` sizes for binary columns**: sqliteodbc
reports every `blob` as `SQL_BINARY` size 255. Originally blobs were
block-bound by declared size; a 100 KB blob tripped the truncation guard.
Fix: blobs and long types always stream.
- sqliteodbc reports unbounded `TEXT` as `SQL_LONGVARCHAR` with fake size
65536 → the long-type check catches it.
- sqliteodbc rounds bound **timestamp parameters to milliseconds** (literals
and reads keep microseconds — reads return correct nanosecond `fraction`).
`Statements.cpp` therefore uses a millisecond-precision test value.
- sqliteodbc supports block cursors (rowset sizes 3 and 64 verified).
- unixODBC quirk: attribute buffers for "32-bit" attributes are read into
`SQLULEN`-sized variables defensively (some drivers write 64 bits).
- `assert`-style checks in `Statements.cpp` use a custom `expect` macro that
throws (works in Release builds too).

## How to run the tests

```sh
cmake -S . -B build -DBUILD_ODBC_CONNECTOR=ON
cmake --build build
ctest --test-dir build -R odbc # offline part
# Live part (any ODBC driver; example: SQLite driver on macOS):
SQLPP_ODBC_CONNECTION_STRING="Driver=/opt/homebrew/lib/libsqlite3odbc.dylib;Database=/tmp/sqlpp23_odbc_test.db;" \
ctest --test-dir build -R odbc
```

`tests/odbc/usage/Statements.cpp` is the integration test: it skips (returns
0) without `SQLPP_ODBC_CONNECTION_STRING`, otherwise asserts round trips of
all data types, NULLs, rowset boundaries, streaming, and transactions.

## TODO / next steps on Windows

- Windows has its own driver manager (`odbc32.lib`) — `find_package(ODBC)`
should just work; no unixODBC needed.
- Test against real drivers, e.g.:
- SQL Server: `Driver={ODBC Driver 18 for SQL Server};Server=...;Database=...;Uid=...;Pwd=...;TrustServerCertificate=yes;`
- MySQL: `Driver={MySQL ODBC 9.x Unicode Driver};Server=...;Database=...;User=...;Password=...;`
- PostgreSQL: `Driver={PostgreSQL Unicode};Server=...;Database=...;Uid=...;Pwd=...;`
- SQLite: `Driver=SQLite3 ODBC Driver;Database=c:\path\test.db;`
- **The test DDL is SQLite-flavored** (`INTEGER PRIMARY KEY AUTOINCREMENT` in
`tests/include/sqlpp23/tests/odbc/tables.h` / `tables.sql`) — needs
per-backend DDL to run `Statements.cpp` against other databases.
- Things to watch on Windows / other drivers:
- The connector uses the **narrow (ANSI) ODBC API**. On Windows the driver
manager converts to the W-API using the ANSI code page — non-ASCII text
may mangle unless the process code page is UTF-8. Possible follow-up:
optional wide-char (`SQLWCHAR`) support.
- MSVC: repo targets C++23; `not`/`and` alternative tokens are used
throughout (core does the same) — needs `/permissive-` (default with
recent MSVC standards flags).
- SQL Server: `time` columns are `SQL_SS_TIME2` (driver-specific type) —
check that binding `SQL_C_TYPE_TIME` still works; `datetimeoffset` is not
supported by the connector.
- Check `SQL_C_UBIGINT` support on each driver (used for unsigned bigint).
- Verify the truncation guard doesn't fire on drivers that report character
column sizes differently (we budget ×4 bytes/char for UTF-8).
- Consider CI for the ODBC build (e.g. Linux + unixODBC + sqliteodbc, and a
Windows job).
- `BUILD_WITH_MODULES` build of `sqlpp23.odbc.cppm` still unvalidated.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Connector libraries can inform you and your IDE of missing features at compile t
They also interpret expressions specifically where needed.
For example, the connector could use the `operator||` or the `concat` method for string concatenation without your being required to change the statement.

Connectors for MariaDB, MySQL, PostgreSQL, sqlite3, sqlcipher are included in this repository.
Connectors for MariaDB, MySQL, ODBC, PostgreSQL, sqlite3, sqlcipher are included in this repository.

Documentation is found in [docs](/docs/README.md).

Expand Down
2 changes: 2 additions & 0 deletions cmake/configs/Sqlpp23ODBCConfig.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include(CMakeFindDependencyMacro)
find_dependency(ODBC)
2 changes: 2 additions & 0 deletions docs/connectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ See the links below for details:

[MySQL & MariaDB](/docs/connectors/mysql.md)

[ODBC](/docs/connectors/odbc.md)

[PostgreSQL](/docs/connectors/postgresql.md)

[Sqlite3 and SQLCipher](/docs/connectors/sqlite3.md)
Expand Down
136 changes: 136 additions & 0 deletions docs/connectors/odbc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
[**\< Connectors**](/docs/connectors.md)

# ODBC connector

The ODBC connector uses ODBC 3.8 and works with any database that provides an
ODBC driver (through a driver manager like unixODBC, iODBC, or the Windows
ODBC driver manager).

Since ODBC only standardizes the API, not the SQL dialect, statements are
serialized in a portable fashion:

- parameters use unnumbered `?` markers,
- date/time literals use the ODBC escape sequences `{d '...'}`, `{t '...'}`,
and `{ts '...'}`, which the driver translates for the database.

Anything beyond that (e.g. vendor-specific functions used via
`sqlpp::verbatim`) is passed through unchanged, so it is your responsibility
to only use SQL that your database understands.

## Creating a connection

A connection configuration holds exactly one way of connecting — either a
data source configured in the driver manager (e.g. `odbc.ini`) or a complete
connection string:

```c++
auto config = std::make_shared<sqlpp::odbc::connection_config>();

// Either: connect to a configured data source (SQLConnect)
config->source = sqlpp::odbc::data_source{
.name = "my_dsn", .username = "user", .password = "password"};

// Or: connect using a connection string (SQLDriverConnect)
config->source = sqlpp::odbc::connection_string{
"Driver=PostgreSQL Unicode;Server=localhost;Database=test;Uid=user;Pwd=password;"};

// Create a connection
sqlpp::odbc::connection db;
db.connect_using(config); // This can throw an exception.
```

See also the [logging documentation](/docs/logging.md).

## Fetching multiple rows at once

Select results are read through an ODBC block cursor where possible: the
connector binds the result columns to fixed-size buffers and fetches
`connection_config::row_array_size` rows per driver round trip (default: 64).

This requires that the size of each row is known up front. Text columns
qualify if their declared maximum size fits into
`connection_config::max_bound_column_size` bytes per row (default: 4096,
budgeted with 4 bytes per character for UTF-8). If any column exceeds the
limit — or its size cannot be trusted, as with unbounded `TEXT` columns and
blobs — the result transparently falls back to fetching one row at a time
and streaming each value with `SQLGetData`, which has no size limit.

Setting `max_bound_column_size = 0` always streams results that contain
text or blob columns. This is useful for databases that do not enforce
declared column sizes (e.g. SQLite, where a `varchar(255)` column may hold
longer values).

```c++
auto config = std::make_shared<sqlpp::odbc::connection_config>();
config->row_array_size = 256; // fetch up to 256 rows per round trip
config->max_bound_column_size = 65536; // allow wider text/blob columns
```

Setting `row_array_size = 1` disables multi-row fetch.

## Transactions

ODBC has no explicit `BEGIN`: `start_transaction` turns off autocommit and
`commit_transaction`/`rollback_transaction` end the transaction via
`SQLEndTran` and turn autocommit back on.

If you pass an isolation level to `start_transaction`, it is set through
`SQL_ATTR_TXN_ISOLATION` and remains in effect for subsequent transactions on
the same connection as well.

## Time of day precision

ODBC's `SQL_TIME_STRUCT` and time escape sequence have no sub-second
precision. Fractional seconds of `sqlpp::time` values are dropped when
serializing or binding parameters. Timestamps (`sqlpp::timestamp`) keep their
microsecond precision.

## `last_insert_id`

ODBC has no portable way to report the id of the last inserted row, so the
connector does not offer `last_insert_id`. Use a select with the appropriate
function of your database (e.g. `last_insert_rowid()`, `lastval()`,
`SCOPE_IDENTITY()`) if you need it.

## Running the tests

The ODBC tests live in `tests/odbc` and are built with
`-DBUILD_ODBC_CONNECTOR=ON`. The serialization and configuration tests run
without any ODBC driver:

```sh
cmake -S . -B build -DBUILD_ODBC_CONNECTOR=ON
cmake --build build
ctest --test-dir build -R odbc
```

The statement test (`sqlpp23.odbc.usage.Statements`) only checks compilation
by default. When `SQLPP_ODBC_CONNECTION_STRING` is set, it additionally runs
all statement types against the configured database and verifies the results,
e.g. with the serverless [SQLite ODBC driver](http://www.ch-werner.de/sqliteodbc/)
(`brew install sqliteodbc`):

```sh
SQLPP_ODBC_CONNECTION_STRING="Driver=/opt/homebrew/lib/libsqlite3odbc.dylib;Database=/tmp/sqlpp23_odbc_test.db;" \
ctest --test-dir build -R odbc
```

The DDL in `tests/include/sqlpp23/tests/odbc/tables.h` is written for SQLite;
it may need adjustment for other databases.

## Exceptions

In exceptional situations the connector throws `sqlpp::odbc::exception` which
carries the message, SQLSTATE, and native error code of the diagnostic
records:

```c++
try {
db(select(sqlpp::verbatim("nonsense").as(sqlpp::alias::a)));
} catch (const sqlpp::odbc::exception& e) {
std::println("Exception: {}, SQLSTATE: {}, native error: {}", e.what(),
e.sql_state(), e.native_error_code());
}
```

[**\< Connectors**](/docs/connectors.md)
Loading