diff --git a/CMakeLists.txt b/CMakeLists.txt index 0ee24804e..350cff96b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) @@ -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() diff --git a/ODBC_HANDOFF.md b/ODBC_HANDOFF.md new file mode 100644 index 000000000..b2f04024a --- /dev/null +++ b/ODBC_HANDOFF.md @@ -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 + 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`; 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. diff --git a/README.md b/README.md index 848355970..d3281bf70 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/cmake/configs/Sqlpp23ODBCConfig.cmake b/cmake/configs/Sqlpp23ODBCConfig.cmake new file mode 100644 index 000000000..94ca5188a --- /dev/null +++ b/cmake/configs/Sqlpp23ODBCConfig.cmake @@ -0,0 +1,2 @@ +include(CMakeFindDependencyMacro) +find_dependency(ODBC) diff --git a/docs/connectors.md b/docs/connectors.md index 819a133d3..4daea3da1 100644 --- a/docs/connectors.md +++ b/docs/connectors.md @@ -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) diff --git a/docs/connectors/odbc.md b/docs/connectors/odbc.md new file mode 100644 index 000000000..ba34dd014 --- /dev/null +++ b/docs/connectors/odbc.md @@ -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(); + +// 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(); +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) diff --git a/include/sqlpp23/odbc/cursor_result.h b/include/sqlpp23/odbc/cursor_result.h new file mode 100644 index 000000000..a0cfbd4c4 --- /dev/null +++ b/include/sqlpp23/odbc/cursor_result.h @@ -0,0 +1,679 @@ +#pragma once + +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace sqlpp::odbc { +// Reads the rows of an executed statement through an ODBC cursor. +// +// If all selected columns can be bound to fixed-size buffers (numeric, +// date/time, and text/blob columns whose declared maximum size fits into +// connection_config::max_bound_column_size), the columns are bound with +// SQLBindCol and connection_config::row_array_size rows are fetched per +// driver round trip (a "block cursor" in ODBC terms). +// +// Otherwise rows are fetched one at a time and each value is streamed with +// SQLGetData into a buffer that grows as needed. +class cursor_result_t { + public: + enum class column_type : unsigned char { + boolean, + int64, + uint64, + floating_point, + text, + blob, + date, + time_of_day, + timestamp, + }; + + private: + struct column_t { + column_type type; + size_t buffer_size{0}; // bytes per row (only used in block mode) + size_t value_size{0}; // bytes of the current value (streaming mode) + bool fetched{false}; // current row's value was streamed already + std::vector buffer; + std::vector indicators; + }; + + std::shared_ptr _statement; // SQLHSTMT + const connection_config* _config{nullptr}; + std::vector _columns; + // Registered with the driver (SQL_ATTR_ROWS_FETCHED_PTR), heap-allocated so + // that the address stays stable when the result is moved. + std::unique_ptr _fetched_row_count; + void* _result_row_address{nullptr}; + bool _fetch_prepared{false}; + bool _block_mode{false}; + size_t _rows_in_buffer{0}; + size_t _current_row{0}; + + public: + cursor_result_t() = default; + cursor_result_t(std::shared_ptr statement, + const connection_config* config) + : _statement{std::move(statement)}, _config{config} { + if constexpr (debug_enabled) { + _config->debug.log(log_category::result, + "ODBC: constructing cursor result, using handle at {}", + std::hash{}(_statement.get())); + } + } + + cursor_result_t(const cursor_result_t&) = delete; + cursor_result_t(cursor_result_t&& rhs) = default; + cursor_result_t& operator=(const cursor_result_t&) = delete; + cursor_result_t& operator=(cursor_result_t&& rhs) { + if (this != &rhs) { + release_cursor(); + _statement = std::move(rhs._statement); + _config = rhs._config; + _columns = std::move(rhs._columns); + _fetched_row_count = std::move(rhs._fetched_row_count); + _result_row_address = rhs._result_row_address; + _fetch_prepared = rhs._fetch_prepared; + _block_mode = rhs._block_mode; + _rows_in_buffer = rhs._rows_in_buffer; + _current_row = rhs._current_row; + } + return *this; + } + + ~cursor_result_t() { release_cursor(); } + + bool operator==(const cursor_result_t& rhs) const { + return _statement == rhs._statement; + } + + template + void next(ResultRow& result_row) { + if (not _statement) { + sqlpp::detail::result_row_bridge{}.invalidate(result_row); + return; + } + + if (&result_row != _result_row_address) { + // Records the column plan via the bind_field functions below. + sqlpp::detail::result_row_bridge{}.bind_fields(result_row, *this); + if (not _fetch_prepared) { + prepare_fetch(); + _fetch_prepared = true; + } + _result_row_address = &result_row; + } + + if (next_row()) { + if (not result_row) { + sqlpp::detail::result_row_bridge{}.validate(result_row); + } + sqlpp::detail::result_row_bridge{}.read_fields(result_row, *this); + } else { + if (result_row) { + sqlpp::detail::result_row_bridge{}.invalidate(result_row); + } + } + } + + const debug_logger& debug() const { return _config->debug; } + + void plan_column(size_t field_index, column_type type) { + if (_columns.size() <= field_index) { + _columns.resize(field_index + 1); + } + auto& column = _columns[field_index]; + column.type = type; + column.buffer_size = fixed_buffer_size(type); // 0 for text and blob + } + + bool get_is_null(size_t field_index) { + auto& column = _columns[field_index]; + if (_block_mode) { + return column.indicators[_current_row] == SQL_NULL_DATA; + } + stream_value_if_required(field_index); + return column.indicators[0] == SQL_NULL_DATA; + } + + bool get_bool(size_t field_index) { + return get_fixed(field_index) != 0; + } + + int64_t get_int64(size_t field_index) { + return get_fixed(field_index); + } + + uint64_t get_uint64(size_t field_index) { + return get_fixed(field_index); + } + + double get_double(size_t field_index) { + return get_fixed(field_index); + } + + SQL_DATE_STRUCT get_date(size_t field_index) { + return get_fixed(field_index); + } + + SQL_TIME_STRUCT get_time(size_t field_index) { + return get_fixed(field_index); + } + + SQL_TIMESTAMP_STRUCT get_timestamp(size_t field_index) { + return get_fixed(field_index); + } + + std::string_view get_text(size_t field_index) { + const auto value = get_variable(field_index); + return {value.data(), value.size()}; + } + + std::span get_blob(size_t field_index) { + const auto value = get_variable(field_index); + return {reinterpret_cast(value.data()), value.size()}; + } + + private: + SQLHSTMT native_handle() const { return _statement.get(); } + + static size_t fixed_buffer_size(column_type type) { + switch (type) { + case column_type::boolean: + return 1; + case column_type::int64: + case column_type::uint64: + case column_type::floating_point: + return 8; + case column_type::date: + return sizeof(SQL_DATE_STRUCT); + case column_type::time_of_day: + return sizeof(SQL_TIME_STRUCT); + case column_type::timestamp: + return sizeof(SQL_TIMESTAMP_STRUCT); + case column_type::text: + case column_type::blob: + return 0; + } + return 0; + } + + static SQLSMALLINT c_type(column_type type) { + switch (type) { + case column_type::boolean: + return SQL_C_BIT; + case column_type::int64: + return SQL_C_SBIGINT; + case column_type::uint64: + return SQL_C_UBIGINT; + case column_type::floating_point: + return SQL_C_DOUBLE; + case column_type::date: + return SQL_C_TYPE_DATE; + case column_type::time_of_day: + return SQL_C_TYPE_TIME; + case column_type::timestamp: + return SQL_C_TYPE_TIMESTAMP; + case column_type::text: + return SQL_C_CHAR; + case column_type::blob: + return SQL_C_BINARY; + } + return SQL_C_DEFAULT; + } + + // Decides between block fetch and row-by-row streaming and sets up the + // column buffers accordingly. + void prepare_fetch() { + bool all_columns_bindable = true; + for (size_t index = 0; index < _columns.size(); ++index) { + auto& column = _columns[index]; + if (column.buffer_size != 0) { + continue; // fixed-size column + } + if (column.type == column_type::blob) { + // Blob values are typically large and drivers do not reliably report + // sizes for binary columns (e.g. the SQLite driver claims 255 bytes + // for any blob), so blobs are always streamed. + all_columns_bindable = false; + continue; + } + SQLULEN declared_size{0}; + SQLSMALLINT sql_type{}, decimal_digits{}, nullable{}, name_length{}; + SQLCHAR name[256]; + detail::throw_on_error( + SQLDescribeCol(native_handle(), static_cast(index + 1), + name, sizeof(name), &name_length, &sql_type, + &declared_size, &decimal_digits, &nullable), + "ODBC: could not describe result column", SQL_HANDLE_STMT, + native_handle()); + // Values of the long types can exceed the reported column size, e.g. + // unbounded TEXT columns. + const bool unbounded_type = sql_type == SQL_LONGVARCHAR or + sql_type == SQL_WLONGVARCHAR or + sql_type == SQL_LONGVARBINARY; + // Text sizes are reported in characters which can take up to 4 bytes + // each (UTF-8), plus the terminating NUL. + const size_t required_bytes = declared_size * 4 + 1; + if (unbounded_type or declared_size == 0 or + required_bytes > _config->max_bound_column_size) { + all_columns_bindable = false; + } else { + column.buffer_size = required_bytes; + } + } + + _block_mode = all_columns_bindable; + if (not _block_mode) { + if constexpr (debug_enabled) { + _config->debug.log(log_category::result, + "ODBC: streaming rows one at a time (result " + "contains long variable-size columns)"); + } + for (auto& column : _columns) { + column.indicators.resize(1); + // Text/blob buffers grow on demand while streaming, starting with + // the declared size if it is known. + column.buffer.resize(column.buffer_size); + } + return; + } + + size_t rowset_size = + _config->row_array_size > 0 ? _config->row_array_size : 1; + if (rowset_size > 1) { + const auto rc = SQLSetStmtAttr( + native_handle(), SQL_ATTR_ROW_ARRAY_SIZE, + reinterpret_cast(static_cast(rowset_size)), 0); + if (rc != SQL_SUCCESS and rc != SQL_SUCCESS_WITH_INFO) { + // The driver does not support block cursors. + rowset_size = 1; + } else { + // The driver may have lowered the value (SQLSTATE 01S02). + SQLULEN actual_size{1}; + if (SQLGetStmtAttr(native_handle(), SQL_ATTR_ROW_ARRAY_SIZE, + &actual_size, SQL_IS_UINTEGER, + nullptr) == SQL_SUCCESS) { + rowset_size = actual_size; + } + } + } + if constexpr (debug_enabled) { + _config->debug.log(log_category::result, + "ODBC: fetching rows in blocks of {}", rowset_size); + } + + _fetched_row_count = std::make_unique(0); + detail::throw_on_error( + SQLSetStmtAttr(native_handle(), SQL_ATTR_ROWS_FETCHED_PTR, + _fetched_row_count.get(), 0), + "ODBC: could not register the fetched-row counter", SQL_HANDLE_STMT, + native_handle()); + + for (size_t index = 0; index < _columns.size(); ++index) { + auto& column = _columns[index]; + column.buffer.resize(column.buffer_size * rowset_size); + column.indicators.resize(rowset_size); + detail::throw_on_error( + SQLBindCol(native_handle(), static_cast(index + 1), + c_type(column.type), column.buffer.data(), + static_cast(column.buffer_size), + column.indicators.data()), + "ODBC: could not bind result column", SQL_HANDLE_STMT, + native_handle()); + } + } + + bool next_row() { + if (_current_row + 1 < _rows_in_buffer) { + ++_current_row; + return true; + } + + if constexpr (debug_enabled) { + _config->debug.log(log_category::result, + "ODBC: fetching next {} from the driver", + _block_mode ? "rowset" : "row"); + } + const auto rc = SQLFetch(native_handle()); + if (rc == SQL_NO_DATA) { + _rows_in_buffer = 0; + _current_row = 0; + return false; + } + detail::throw_on_error(rc, "ODBC: could not fetch rows", SQL_HANDLE_STMT, + native_handle()); + _rows_in_buffer = _block_mode ? *_fetched_row_count : 1; + _current_row = 0; + if (not _block_mode) { + for (auto& column : _columns) { + column.fetched = false; + } + } + return _rows_in_buffer > 0; + } + + template + T get_fixed(size_t field_index) { + auto& column = _columns[field_index]; + T value{}; + if (_block_mode) { + std::memcpy(&value, + column.buffer.data() + _current_row * column.buffer_size, + sizeof(T)); + } else { + stream_value_if_required(field_index); + std::memcpy(&value, column.buffer.data(), sizeof(T)); + } + return value; + } + + std::string_view get_variable(size_t field_index) { + auto& column = _columns[field_index]; + if (_block_mode) { + const auto indicator = column.indicators[_current_row]; + if (indicator == SQL_NULL_DATA) { + return {}; + } + // The buffer reserves one byte for the terminating NUL. Only text + // columns are bound (blobs are always streamed). + if (indicator == SQL_NO_TOTAL or + static_cast(indicator) >= column.buffer_size) { + throw exception{ + "ODBC: a text value was longer than the column's declared maximum " + "size suggests; fix the column's declared size or set " + "connection_config::max_bound_column_size to 0 to always stream " + "variable-size columns", + "01004"}; + } + return {column.buffer.data() + _current_row * column.buffer_size, + static_cast(indicator)}; + } + stream_value_if_required(field_index); + return {column.buffer.data(), column.value_size}; + } + + // Streaming mode: retrieves the value of the given column of the current + // row with SQLGetData. Columns are requested in increasing index order + // (fields are read in select order), which every ODBC driver supports. + void stream_value_if_required(size_t field_index) { + auto& column = _columns[field_index]; + if (column.fetched) { + return; + } + column.fetched = true; + + if (column.type != column_type::text and + column.type != column_type::blob) { // fixed-size column + const auto rc = SQLGetData( + native_handle(), static_cast(field_index + 1), + c_type(column.type), column.buffer.data(), + static_cast(column.buffer.size()), column.indicators.data()); + detail::throw_on_error(rc, "ODBC: could not get column data", + SQL_HANDLE_STMT, native_handle()); + return; + } + + // Variable-size column: read in parts, growing the buffer as needed. + const auto column_c_type = c_type(column.type); + // SQLGetData NUL-terminates character data. + const size_t terminator = column.type == column_type::text ? 1 : 0; + if (column.buffer.size() < 256) { + column.buffer.resize(256); + } + size_t received = 0; + for (;;) { + SQLLEN indicator{}; + const auto rc = SQLGetData( + native_handle(), static_cast(field_index + 1), + column_c_type, column.buffer.data() + received, + static_cast(column.buffer.size() - received), &indicator); + if (rc == SQL_NO_DATA) { + break; // the previous part already contained the complete value + } + detail::throw_on_error(rc, "ODBC: could not get column data", + SQL_HANDLE_STMT, native_handle()); + if (indicator == SQL_NULL_DATA) { + column.indicators[0] = SQL_NULL_DATA; + column.value_size = 0; + return; + } + if (rc == SQL_SUCCESS) { + // The final part; the indicator holds its size. + received += static_cast(indicator); + break; + } + // SQL_SUCCESS_WITH_INFO: the value was truncated to the buffer. The + // indicator holds the remaining size before this call (or SQL_NO_TOTAL + // if the driver does not know it). + const size_t part = column.buffer.size() - received - terminator; + received += part; + if (indicator == SQL_NO_TOTAL) { + column.buffer.resize(column.buffer.size() * 2); + } else { + column.buffer.resize( + received + (static_cast(indicator) - part) + terminator); + } + } + column.indicators[0] = static_cast(received); + column.value_size = received; + } + + // Leaves the statement ready for re-execution: closes the cursor and + // removes all references to buffers owned by this result. + void release_cursor() noexcept { + if (not _statement) { + return; + } + SQLFreeStmt(native_handle(), SQL_CLOSE); + SQLFreeStmt(native_handle(), SQL_UNBIND); + SQLSetStmtAttr(native_handle(), SQL_ATTR_ROWS_FETCHED_PTR, nullptr, 0); + SQLSetStmtAttr(native_handle(), SQL_ATTR_ROW_ARRAY_SIZE, + reinterpret_cast(SQLULEN{1}), 0); + } +}; + +inline void bind_field(cursor_result_t& result, + size_t field_index, + bool& /*value*/) { + result.plan_column(field_index, cursor_result_t::column_type::boolean); +} + +inline void bind_field(cursor_result_t& result, + size_t field_index, + int64_t& /*value*/) { + result.plan_column(field_index, cursor_result_t::column_type::int64); +} + +inline void bind_field(cursor_result_t& result, + size_t field_index, + uint64_t& /*value*/) { + result.plan_column(field_index, cursor_result_t::column_type::uint64); +} + +inline void bind_field(cursor_result_t& result, + size_t field_index, + double& /*value*/) { + result.plan_column(field_index, cursor_result_t::column_type::floating_point); +} + +inline void bind_field(cursor_result_t& result, + size_t field_index, + std::string_view& /*value*/) { + result.plan_column(field_index, cursor_result_t::column_type::text); +} + +inline void bind_field(cursor_result_t& result, + size_t field_index, + std::span& /*value*/) { + result.plan_column(field_index, cursor_result_t::column_type::blob); +} + +inline void bind_field(cursor_result_t& result, + size_t field_index, + std::chrono::sys_days& /*value*/) { + result.plan_column(field_index, cursor_result_t::column_type::date); +} + +inline void bind_field(cursor_result_t& result, + size_t field_index, + std::chrono::microseconds& /*value*/) { + result.plan_column(field_index, cursor_result_t::column_type::time_of_day); +} + +inline void bind_field(cursor_result_t& result, + size_t field_index, + ::sqlpp::chrono::sys_microseconds& /*value*/) { + result.plan_column(field_index, cursor_result_t::column_type::timestamp); +} + +inline void read_field(cursor_result_t& result, + size_t field_index, + bool& value) { + if constexpr (debug_enabled) { + result.debug().log(log_category::result, + "ODBC: reading boolean result at index {}", field_index); + } + value = result.get_bool(field_index); +} + +inline void read_field(cursor_result_t& result, + size_t field_index, + int64_t& value) { + if constexpr (debug_enabled) { + result.debug().log(log_category::result, + "ODBC: reading integral result at index {}", + field_index); + } + value = result.get_int64(field_index); +} + +inline void read_field(cursor_result_t& result, + size_t field_index, + uint64_t& value) { + if constexpr (debug_enabled) { + result.debug().log(log_category::result, + "ODBC: reading unsigned integral result at index {}", + field_index); + } + value = result.get_uint64(field_index); +} + +inline void read_field(cursor_result_t& result, + size_t field_index, + double& value) { + if constexpr (debug_enabled) { + result.debug().log(log_category::result, + "ODBC: reading floating_point result at index {}", + field_index); + } + value = result.get_double(field_index); +} + +inline void read_field(cursor_result_t& result, + size_t field_index, + std::string_view& value) { + if constexpr (debug_enabled) { + result.debug().log(log_category::result, + "ODBC: reading text result at index {}", field_index); + } + value = result.get_text(field_index); +} + +inline void read_field(cursor_result_t& result, + size_t field_index, + std::span& value) { + if constexpr (debug_enabled) { + result.debug().log(log_category::result, + "ODBC: reading blob result at index {}", field_index); + } + value = result.get_blob(field_index); +} + +inline void read_field(cursor_result_t& result, + size_t field_index, + std::chrono::sys_days& value) { + if constexpr (debug_enabled) { + result.debug().log(log_category::result, + "ODBC: reading date result at index {}", field_index); + } + const auto date = result.get_date(field_index); + value = std::chrono::year{date.year} / std::chrono::month{date.month} / + std::chrono::day{date.day}; +} + +inline void read_field(cursor_result_t& result, + size_t field_index, + std::chrono::microseconds& value) { + if constexpr (debug_enabled) { + result.debug().log(log_category::result, + "ODBC: reading time of day result at index {}", + field_index); + } + const auto time = result.get_time(field_index); + value = std::chrono::hours{time.hour} + std::chrono::minutes{time.minute} + + std::chrono::seconds{time.second}; +} + +inline void read_field(cursor_result_t& result, + size_t field_index, + ::sqlpp::chrono::sys_microseconds& value) { + if constexpr (debug_enabled) { + result.debug().log(log_category::result, + "ODBC: reading timestamp result at index {}", + field_index); + } + const auto ts = result.get_timestamp(field_index); + value = std::chrono::sys_days{std::chrono::year{ts.year} / + std::chrono::month{ts.month} / + std::chrono::day{ts.day}} + + std::chrono::hours{ts.hour} + std::chrono::minutes{ts.minute} + + std::chrono::seconds{ts.second} + + // The fraction is in billionths of a second. + std::chrono::duration_cast( + std::chrono::nanoseconds{ts.fraction}); +} + +} // namespace sqlpp::odbc diff --git a/include/sqlpp23/odbc/database/connection.h b/include/sqlpp23/odbc/database/connection.h new file mode 100644 index 000000000..7def2fe11 --- /dev/null +++ b/include/sqlpp23/odbc/database/connection.h @@ -0,0 +1,422 @@ +#pragma once + +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sqlpp::odbc { + +struct command_result { + uint64_t affected_rows; +}; + +// Base connection class +class connection_base : public sqlpp::connection { + public: + using _connection_base_t = connection_base; + using _config_t = connection_config; + using _config_ptr_t = std::shared_ptr; + using _handle_t = detail::connection_handle; + + using _prepared_statement_t = prepared_statement_t; + + private: + friend sqlpp::statement_handler_t; + + bool _transaction_active{false}; + + void validate_connection_handle() const { + if (not _handle.native_handle()) { + throw std::logic_error{"connection handle used, but not initialized"}; + } + } + + std::shared_ptr allocate_statement() { + validate_connection_handle(); + SQLHANDLE statement{SQL_NULL_HANDLE}; + const auto rc = + SQLAllocHandle(SQL_HANDLE_STMT, native_handle(), &statement); + if (rc != SQL_SUCCESS and rc != SQL_SUCCESS_WITH_INFO) { + throw detail::make_exception("ODBC: could not allocate statement handle", + SQL_HANDLE_DBC, native_handle()); + } + return {statement, detail::handle_deleter{SQL_HANDLE_STMT}}; + } + + static uint64_t affected_row_count(SQLHSTMT statement) { + SQLLEN row_count{0}; + detail::throw_on_error(SQLRowCount(statement, &row_count), + "ODBC: could not determine affected row count", + SQL_HANDLE_STMT, statement); + // Drivers report -1 when the count is not applicable. + return row_count > 0 ? static_cast(row_count) : 0; + } + + // direct execution + command_result execute_direct(std::string_view statement) { + if constexpr (debug_enabled) { + _handle.debug().log(log_category::statement, "ODBC: executing: '{}'", + statement); + } + auto handle = allocate_statement(); + const auto rc = SQLExecDirect( + handle.get(), + reinterpret_cast(const_cast(statement.data())), + static_cast(statement.size())); + if (rc == SQL_NO_DATA) { + // Valid outcome of e.g. a searched update affecting no rows. + return {.affected_rows = 0}; + } + detail::throw_on_error(rc, "ODBC: could not execute statement", + SQL_HANDLE_STMT, handle.get()); + return {.affected_rows = affected_row_count(handle.get())}; + } + + // prepared execution + void execute_prepared(prepared_statement_t& prepared) { + if constexpr (debug_enabled) { + _handle.debug().log(log_category::statement, + "ODBC: executing prepared statement"); + } + const auto rc = SQLExecute(prepared.native_handle()); + if (rc == SQL_NO_DATA) { + return; + } + detail::throw_on_error(rc, "ODBC: could not execute prepared statement", + SQL_HANDLE_STMT, prepared.native_handle()); + } + + template + prepared_statement_t prepare_statement(const Statement& statement) { + context_t context{this}; + const auto query = to_sql_string(context, statement); + return prepared_statement_t{allocate_statement(), query, context._count, + _handle.config.get()}; + } + + template + prepared_statement_t& bind_prepared_parameters(PreparedStatement& statement) { + auto& prepared = + sqlpp::statement_handler_t{}.get_prepared_statement(statement); + prepared._reset(); + sqlpp::statement_handler_t{}.bind_parameters(statement); + return prepared; + } + + template + command_result run_prepared_command(PreparedCommand& command) { + auto& prepared = bind_prepared_parameters(command); + execute_prepared(prepared); + return {.affected_rows = affected_row_count(prepared.native_handle())}; + } + + //! select returns a result (which can be iterated row by row) + template + cursor_result_t _select(const Select& s) { + context_t context{this}; + const auto query = to_sql_string(context, s); + if constexpr (debug_enabled) { + _handle.debug().log(log_category::statement, "ODBC: selecting: '{}'", + query); + } + auto statement = allocate_statement(); + const auto rc = SQLExecDirect( + statement.get(), + reinterpret_cast(const_cast(query.data())), + static_cast(query.size())); + if (rc != SQL_NO_DATA) { + detail::throw_on_error(rc, "ODBC: could not execute select", + SQL_HANDLE_STMT, statement.get()); + } + return {std::move(statement), _handle.config.get()}; + } + + template + prepared_statement_t _prepare_select(const Select& s) { + return prepare_statement(s); + } + + template + cursor_result_t _run_prepared_select(PreparedSelect& s) { + auto& prepared = bind_prepared_parameters(s); + execute_prepared(prepared); + return {prepared._statement, _handle.config.get()}; + } + + //! insert returns the number of affected rows + //! Note: ODBC has no portable way to report the last inserted id. Use a + //! select with a database-specific function if you need it. + template + command_result _insert(const Insert& i) { + context_t context{this}; + return execute_direct(to_sql_string(context, i)); + } + + template + prepared_statement_t _prepare_insert(const Insert& i) { + return prepare_statement(i); + } + + template + command_result _run_prepared_insert(PreparedInsert& i) { + return run_prepared_command(i); + } + + //! update returns the number of affected rows + template + command_result _update(const Update& u) { + context_t context{this}; + return execute_direct(to_sql_string(context, u)); + } + + template + prepared_statement_t _prepare_update(const Update& u) { + return prepare_statement(u); + } + + template + command_result _run_prepared_update(PreparedUpdate& u) { + return run_prepared_command(u); + } + + //! delete_from returns the number of deleted rows + template + command_result _delete_from(const Delete& r) { + context_t context{this}; + return execute_direct(to_sql_string(context, r)); + } + + template + prepared_statement_t _prepare_delete_from(const Delete& r) { + return prepare_statement(r); + } + + template + command_result _run_prepared_delete_from(PreparedDelete& r) { + return run_prepared_command(r); + } + + //! Execute a single arbitrary statement (e.g. create a table) + template + command_result _execute(const Execute& x) { + context_t context{this}; + return execute_direct(to_sql_string(context, x)); + } + + template + prepared_statement_t _prepare_execute(const Execute& x) { + return prepare_statement(x); + } + + template + command_result _run_prepared_execute(PreparedExecute& x) { + return run_prepared_command(x); + } + + void set_autocommit(bool enabled) { + detail::throw_on_error( + SQLSetConnectAttr( + native_handle(), SQL_ATTR_AUTOCOMMIT, + reinterpret_cast(static_cast( + enabled ? SQL_AUTOCOMMIT_ON : SQL_AUTOCOMMIT_OFF)), + SQL_IS_UINTEGER), + "ODBC: could not change autocommit mode", SQL_HANDLE_DBC, + native_handle()); + } + + void end_transaction(SQLSMALLINT completion_type) { + detail::throw_on_error( + SQLEndTran(SQL_HANDLE_DBC, native_handle(), completion_type), + completion_type == SQL_COMMIT ? "ODBC: could not commit transaction" + : "ODBC: could not roll back transaction", + SQL_HANDLE_DBC, native_handle()); + set_autocommit(true); + _transaction_active = false; + } + + public: + template + requires(sqlpp::is_statement_v) + auto operator()(const T& t) { + sqlpp::check_run_consistency(t).verify(); + sqlpp::check_compatibility(t).verify(); + return sqlpp::statement_handler_t{}.run(t, *this); + } + + template + requires(sqlpp::is_prepared_statement_v>) + auto operator()(T&& t) { + return sqlpp::statement_handler_t{}.run(std::forward(t), *this); + } + + command_result operator()(std::string_view t) { return execute_direct(t); } + + template + requires(sqlpp::is_statement_v) + auto prepare(const T& t) { + sqlpp::check_prepare_consistency(t).verify(); + sqlpp::check_compatibility(t).verify(); + return sqlpp::statement_handler_t{}.prepare(t, *this); + } + + //! set the transaction isolation level for this connection + void set_default_isolation_level(isolation_level level) { + SQLULEN mask{}; + switch (level) { + case isolation_level::read_uncommitted: + mask = SQL_TXN_READ_UNCOMMITTED; + break; + case isolation_level::read_committed: + mask = SQL_TXN_READ_COMMITTED; + break; + case isolation_level::repeatable_read: + mask = SQL_TXN_REPEATABLE_READ; + break; + case isolation_level::serializable: + mask = SQL_TXN_SERIALIZABLE; + break; + case isolation_level::undefined: + throw sqlpp::exception{"ODBC: invalid isolation level"}; + } + detail::throw_on_error( + SQLSetConnectAttr(native_handle(), SQL_ATTR_TXN_ISOLATION, + reinterpret_cast(mask), SQL_IS_UINTEGER), + "ODBC: could not set the transaction isolation level", SQL_HANDLE_DBC, + native_handle()); + } + + //! get the currently active transaction isolation level + sqlpp::isolation_level get_default_isolation_level() { + // The attribute is documented as a 32-bit mask, the wider buffer guards + // against drivers that write SQLULEN. + SQLULEN mask{0}; + detail::throw_on_error( + SQLGetConnectAttr(native_handle(), SQL_ATTR_TXN_ISOLATION, &mask, + SQL_IS_UINTEGER, nullptr), + "ODBC: could not get the transaction isolation level", SQL_HANDLE_DBC, + native_handle()); + switch (mask) { + case SQL_TXN_READ_UNCOMMITTED: + return isolation_level::read_uncommitted; + case SQL_TXN_READ_COMMITTED: + return isolation_level::read_committed; + case SQL_TXN_REPEATABLE_READ: + return isolation_level::repeatable_read; + case SQL_TXN_SERIALIZABLE: + return isolation_level::serializable; + default: + return isolation_level::undefined; + } + } + + //! start a transaction by turning off autocommit; ODBC has no explicit + //! BEGIN. If an isolation level is given, it is set on the connection and + //! stays in effect for subsequent transactions, too. + void start_transaction(isolation_level level = isolation_level::undefined) { + if (level != isolation_level::undefined) { + set_default_isolation_level(level); + } + set_autocommit(false); + _transaction_active = true; + } + + //! commit transaction + void commit_transaction() { end_transaction(SQL_COMMIT); } + + //! rollback transaction + void rollback_transaction() { + if constexpr (debug_enabled) { + _handle.debug().log(log_category::connection, + "ODBC: rolling back unfinished transaction"); + } + end_transaction(SQL_ROLLBACK); + } + + //! report a rollback failure (will be called by transactions in case of a + //! rollback failure in the destructor) + void report_rollback_failure(const std::string& message) noexcept { + if constexpr (debug_enabled) { + _handle.debug().log(log_category::connection, "rollback failure: {}", + message); + } + } + + //! check if transaction is active + bool is_transaction_active() { return _transaction_active; } + + SQLHDBC native_handle() const { return _handle.native_handle(); } + + // Standard SQL string escaping (doubling single quotes). This works + // without asking the driver, but be aware that some databases have + // non-standard quirks (e.g. backslash escapes in MySQL's default mode). + // Prefer prepared statement parameters over string literals. + std::string escape(const std::string_view& s) const { + auto result = std::string{}; + result.reserve(s.size() * 2); + for (const auto c : s) { + if (c == '\'') + result.push_back(c); // Escaping + result.push_back(c); + } + return result; + } + + protected: + _handle_t _handle; + + // Constructors + connection_base() = default; + connection_base(_handle_t handle) : _handle{std::move(handle)} {} +}; + +inline auto context_t::escape(std::string_view t) -> std::string { + return _db->escape(t); +} + +using connection = sqlpp::normal_connection; +using pooled_connection = sqlpp::pooled_connection; +} // namespace sqlpp::odbc diff --git a/include/sqlpp23/odbc/database/connection_config.h b/include/sqlpp23/odbc/database/connection_config.h new file mode 100644 index 000000000..b17764e78 --- /dev/null +++ b/include/sqlpp23/odbc/database/connection_config.h @@ -0,0 +1,79 @@ +#pragma once + +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include + +#include + +namespace sqlpp::odbc { +// Connect to a data source that is configured in the ODBC driver manager +// (e.g. in odbc.ini), using SQLConnect. +struct data_source { + std::string name; + std::string username; + std::string password; + + bool operator==(const data_source&) const = default; +}; + +// Connect using a complete ODBC connection string, e.g. +// "Driver=PostgreSQL Unicode;Server=localhost;Database=test;Uid=me;Pwd=pw;" +// using SQLDriverConnect (without prompting). +struct connection_string { + std::string value; + + bool operator==(const connection_string&) const = default; +}; + +struct connection_config { + // There is exactly one way to connect per configuration: either through a + // configured data source or through a connection string. + std::variant source; + + // Number of rows fetched per driver round trip when a select result can be + // bound to fixed-size buffers (see max_bound_column_size). 1 disables + // multi-row fetch. + std::size_t row_array_size{64}; + + // Per-row buffer limit (in bytes) for binding text and blob result columns. + // Columns whose maximum size is unknown or would exceed this limit make the + // result fall back to fetching one row at a time (streaming each value via + // SQLGetData), which has no size limit. + std::size_t max_bound_column_size{4096}; + + debug_logger debug; // not compared + + bool operator==(const connection_config& other) const { + return source == other.source and row_array_size == other.row_array_size and + max_bound_column_size == other.max_bound_column_size; + } +}; +} // namespace sqlpp::odbc diff --git a/include/sqlpp23/odbc/database/connection_handle.h b/include/sqlpp23/odbc/database/connection_handle.h new file mode 100644 index 000000000..897540bf8 --- /dev/null +++ b/include/sqlpp23/odbc/database/connection_handle.h @@ -0,0 +1,174 @@ +#pragma once + +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace sqlpp::odbc::detail { +struct handle_deleter { + SQLSMALLINT handle_type; + void operator()(SQLHANDLE handle) const noexcept { + SQLFreeHandle(handle_type, handle); + } +}; + +// SQLHANDLE is void*, so this holds any ODBC handle. +using unique_handle = + std::unique_ptr, handle_deleter>; + +inline unique_handle allocate_handle(SQLSMALLINT handle_type, + SQLHANDLE parent) { + SQLHANDLE handle{SQL_NULL_HANDLE}; + const auto rc = SQLAllocHandle(handle_type, parent, &handle); + if (rc != SQL_SUCCESS and rc != SQL_SUCCESS_WITH_INFO) { + if (parent != SQL_NULL_HANDLE) { + throw make_exception( + "ODBC: could not allocate handle", + handle_type == SQL_HANDLE_DBC ? SQL_HANDLE_ENV : SQL_HANDLE_DBC, + parent); + } + throw exception{"ODBC: could not allocate environment handle"}; + } + return unique_handle{handle, handle_deleter{handle_type}}; +} + +struct connection_handle { + std::shared_ptr config; + // Declaration order matters: the connection is freed before the environment. + unique_handle environment; + unique_handle connection; + + connection_handle() = default; + + connection_handle(const std::shared_ptr& conf) + : config{conf}, + environment{allocate_handle(SQL_HANDLE_ENV, SQL_NULL_HANDLE)} { + throw_on_error( + SQLSetEnvAttr(environment.get(), SQL_ATTR_ODBC_VERSION, + reinterpret_cast(SQL_OV_ODBC3_80), 0), + "ODBC: could not request ODBC 3.8", SQL_HANDLE_ENV, environment.get()); + connection = allocate_handle(SQL_HANDLE_DBC, environment.get()); + + std::visit([this](const auto& source) { connect(source); }, conf->source); + } + + connection_handle(const connection_handle&) = delete; + connection_handle(connection_handle&&) = default; + connection_handle& operator=(const connection_handle&) = delete; + connection_handle& operator=(connection_handle&& rhs) { + if (this != &rhs) { + disconnect(); + config = std::move(rhs.config); + connection = std::move(rhs.connection); + environment = std::move(rhs.environment); + } + return *this; + } + + ~connection_handle() { disconnect(); } + + SQLHDBC native_handle() const { return connection.get(); } + + bool is_connected() const { + if (not connection) { + return false; + } + // SQL_ATTR_CONNECTION_DEAD reflects the last known state of the + // connection without a server round trip. The attribute is documented as + // 32 bit, the wider buffer guards against drivers that write SQLULEN. + SQLULEN dead{SQL_CD_FALSE}; + const auto rc = + SQLGetConnectAttr(connection.get(), SQL_ATTR_CONNECTION_DEAD, &dead, + SQL_IS_UINTEGER, nullptr); + if (rc != SQL_SUCCESS and rc != SQL_SUCCESS_WITH_INFO) { + // The driver cannot tell, assume the connection is still alive. + return true; + } + return dead != SQL_CD_TRUE; + } + + bool ping_server() const { + // ODBC has no portable ping. Drivers that implement + // SQL_ATTR_CONNECTION_DEAD actively are covered by this, for others this + // is a passive check only. + return is_connected(); + } + + const debug_logger& debug() const { return config->debug; } + + private: + void connect(const data_source& source) { + if constexpr (debug_enabled) { + debug().log(log_category::connection, "ODBC: connecting to DSN '{}'", + source.name); + } + throw_on_error( + SQLConnect( + connection.get(), to_sqlchar_pointer(source.name), + static_cast(source.name.size()), + source.username.empty() ? nullptr + : to_sqlchar_pointer(source.username), + static_cast(source.username.size()), + source.password.empty() ? nullptr + : to_sqlchar_pointer(source.password), + static_cast(source.password.size())), + "ODBC: could not connect to data source '" + source.name + "'", + SQL_HANDLE_DBC, connection.get()); + } + + void connect(const connection_string& source) { + if constexpr (debug_enabled) { + debug().log(log_category::connection, + "ODBC: connecting using a connection string"); + } + throw_on_error( + SQLDriverConnect(connection.get(), nullptr, + to_sqlchar_pointer(source.value), + static_cast(source.value.size()), nullptr, + 0, nullptr, SQL_DRIVER_NOPROMPT), + "ODBC: could not connect using the connection string", SQL_HANDLE_DBC, + connection.get()); + } + + void disconnect() noexcept { + if (connection) { + SQLDisconnect(connection.get()); + } + } +}; +} // namespace sqlpp::odbc::detail diff --git a/include/sqlpp23/odbc/database/connection_pool.h b/include/sqlpp23/odbc/database/connection_pool.h new file mode 100644 index 000000000..ffb4ee5f0 --- /dev/null +++ b/include/sqlpp23/odbc/database/connection_pool.h @@ -0,0 +1,35 @@ +#pragma once + +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +namespace sqlpp::odbc { +using connection_pool = sqlpp::connection_pool; +} // namespace sqlpp::odbc diff --git a/include/sqlpp23/odbc/database/exception.h b/include/sqlpp23/odbc/database/exception.h new file mode 100644 index 000000000..c6f4f255d --- /dev/null +++ b/include/sqlpp23/odbc/database/exception.h @@ -0,0 +1,56 @@ +#pragma once + +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include + +#include + +namespace sqlpp::odbc { +class exception : public sqlpp::exception { + std::string _sql_state; + SQLINTEGER _native_error_code; + + public: + exception(const std::string& what_arg, + std::string sql_state = {}, + SQLINTEGER native_error_code = 0) + : sqlpp::exception{what_arg}, + _sql_state{std::move(sql_state)}, + _native_error_code{native_error_code} {} + + // Five-character SQLSTATE of the first diagnostic record, e.g. "42S02". + // Empty if no diagnostic record was available. + const std::string& sql_state() const { return _sql_state; } + + // Driver-specific error code of the first diagnostic record. + SQLINTEGER native_error_code() const { return _native_error_code; } +}; +} // namespace sqlpp::odbc diff --git a/include/sqlpp23/odbc/database/serializer_context.h b/include/sqlpp23/odbc/database/serializer_context.h new file mode 100644 index 000000000..cc2570540 --- /dev/null +++ b/include/sqlpp23/odbc/database/serializer_context.h @@ -0,0 +1,53 @@ +#pragma once + +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include + +namespace sqlpp::odbc { + +class connection_base; + +// Context for serialization +struct context_t { + explicit context_t(connection_base* db) : _db(db) {} + context_t(const context_t&) = delete; + context_t(context_t&&) = delete; + context_t& operator=(const context_t&) = delete; + context_t& operator=(context_t&&) = delete; + + // The implementation is in connection.h + auto escape(std::string_view t) -> std::string; + + size_t _count = 0; + connection_base* _db; +}; + +} // namespace sqlpp::odbc diff --git a/include/sqlpp23/odbc/detail/diagnostics.h b/include/sqlpp23/odbc/detail/diagnostics.h new file mode 100644 index 000000000..42fd01408 --- /dev/null +++ b/include/sqlpp23/odbc/detail/diagnostics.h @@ -0,0 +1,93 @@ +#pragma once + +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +#include +#include + +#include + +namespace sqlpp::odbc::detail { +// Builds an exception from all diagnostic records of the given handle. +// SQLSTATE and native error code are taken from the first record. +inline exception make_exception(std::string_view context, + SQLSMALLINT handle_type, + SQLHANDLE handle) { + std::string message{context}; + std::string sql_state; + SQLINTEGER native_error_code{0}; + + for (SQLSMALLINT record_number = 1;; ++record_number) { + SQLCHAR state[SQL_SQLSTATE_SIZE + 1] = {}; + SQLCHAR text[SQL_MAX_MESSAGE_LENGTH] = {}; + SQLINTEGER native_error{0}; + SQLSMALLINT text_length{0}; + const auto rc = + SQLGetDiagRec(handle_type, handle, record_number, state, &native_error, + text, sizeof(text), &text_length); + if (rc != SQL_SUCCESS and rc != SQL_SUCCESS_WITH_INFO) { + break; + } + if (record_number == 1) { + sql_state = reinterpret_cast(state); + native_error_code = native_error; + } + message += ": ["; + message += reinterpret_cast(state); + message += "] "; + message += reinterpret_cast(text); + } + + if (sql_state.empty()) { + message += ": no diagnostic information available"; + } + + return exception{message, std::move(sql_state), native_error_code}; +} + +// Throws if the return code indicates failure. SQL_SUCCESS_WITH_INFO is +// treated as success. Returns the return code so that callers can +// distinguish e.g. SQL_NO_DATA where it is a valid outcome. +inline SQLRETURN throw_on_error(SQLRETURN rc, + std::string_view context, + SQLSMALLINT handle_type, + SQLHANDLE handle) { + if (rc == SQL_SUCCESS or rc == SQL_SUCCESS_WITH_INFO or rc == SQL_NO_DATA) { + return rc; + } + throw make_exception(context, handle_type, handle); +} + +// ODBC functions take non-const SQLCHAR* for input strings. +inline SQLCHAR* to_sqlchar_pointer(const std::string& s) { + return reinterpret_cast(const_cast(s.data())); +} +} // namespace sqlpp::odbc::detail diff --git a/include/sqlpp23/odbc/odbc.h b/include/sqlpp23/odbc/odbc.h new file mode 100644 index 000000000..052830df9 --- /dev/null +++ b/include/sqlpp23/odbc/odbc.h @@ -0,0 +1,31 @@ +#pragma once + +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include diff --git a/include/sqlpp23/odbc/prepared_statement.h b/include/sqlpp23/odbc/prepared_statement.h new file mode 100644 index 000000000..288a7983f --- /dev/null +++ b/include/sqlpp23/odbc/prepared_statement.h @@ -0,0 +1,352 @@ +#pragma once + +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace sqlpp::odbc { +// Forward declaration +class connection_base; + +class prepared_statement_t { + friend class ::sqlpp::odbc::connection_base; + + // Owned storage for parameter values that have to be converted to ODBC + // structs and for the length/NULL indicators. The driver reads from these + // addresses when the statement is executed, so they have to remain stable: + // the vector is sized once in the constructor and never resized. + struct parameter_data { + union { + unsigned char bool_; + int64_t int64_; + uint64_t uint64_; + double double_; + SQL_DATE_STRUCT date_; + SQL_TIME_STRUCT time_; + SQL_TIMESTAMP_STRUCT timestamp_; + }; + SQLLEN length_or_indicator; + }; + + std::shared_ptr _statement; // SQLHSTMT + const connection_config* _config; + std::vector _parameters; + + public: + prepared_statement_t() = delete; + prepared_statement_t(std::shared_ptr statement, + std::string_view statement_text, + size_t parameter_count, + const connection_config* config) + : _statement{std::move(statement)}, + _config{config}, + _parameters(parameter_count) { + if constexpr (debug_enabled) { + _config->debug.log(log_category::statement, "ODBC: preparing: '{}'", + statement_text); + } + detail::throw_on_error( + SQLPrepare(native_handle(), + reinterpret_cast( + const_cast(statement_text.data())), + static_cast(statement_text.size())), + "ODBC: could not prepare statement", SQL_HANDLE_STMT, native_handle()); + } + + prepared_statement_t(const prepared_statement_t&) = delete; + prepared_statement_t(prepared_statement_t&& rhs) = default; + prepared_statement_t& operator=(const prepared_statement_t&) = delete; + prepared_statement_t& operator=(prepared_statement_t&&) = default; + ~prepared_statement_t() = default; + + bool operator==(const prepared_statement_t& rhs) const { + return _statement == rhs._statement; + } + + SQLHSTMT native_handle() const { return _statement.get(); } + const debug_logger& debug() const { return _config->debug; } + + void _reset() { + if constexpr (debug_enabled) { + _config->debug.log(log_category::statement, + "ODBC: closing cursor of prepared statement"); + } + // Closes the cursor of a previous execution, if any. SQL_CLOSE is + // explicitly harmless when no cursor is open. + SQLFreeStmt(native_handle(), SQL_CLOSE); + } + + void bind_parameter(size_t parameter_index, const bool& value) { + auto& data = _parameters[parameter_index]; + data.bool_ = value; + data.length_or_indicator = 0; + bind(parameter_index, SQL_C_BIT, SQL_BIT, 1, 0, &data.bool_, 0); + } + + void bind_parameter(size_t parameter_index, const int64_t& value) { + auto& data = _parameters[parameter_index]; + data.int64_ = value; + data.length_or_indicator = 0; + bind(parameter_index, SQL_C_SBIGINT, SQL_BIGINT, 0, 0, &data.int64_, 0); + } + + void bind_parameter(size_t parameter_index, const uint64_t& value) { + auto& data = _parameters[parameter_index]; + data.uint64_ = value; + data.length_or_indicator = 0; + bind(parameter_index, SQL_C_UBIGINT, SQL_BIGINT, 0, 0, &data.uint64_, 0); + } + + void bind_parameter(size_t parameter_index, const double& value) { + auto& data = _parameters[parameter_index]; + data.double_ = value; + data.length_or_indicator = 0; + bind(parameter_index, SQL_C_DOUBLE, SQL_DOUBLE, 0, 0, &data.double_, 0); + } + + // Note: The text is not copied. The caller (the prepared statement of the + // core library) owns it and keeps it alive until execution. + void bind_parameter(size_t parameter_index, const std::string_view& value) { + auto& data = _parameters[parameter_index]; + data.length_or_indicator = static_cast(value.size()); + bind(parameter_index, SQL_C_CHAR, + value.size() > 8000 ? SQL_LONGVARCHAR : SQL_VARCHAR, + value.empty() ? 1 : value.size(), 0, const_cast(value.data()), + static_cast(value.size())); + } + + // Note: The blob is not copied, see the note for text above. + void bind_parameter(size_t parameter_index, + const std::vector& value) { + auto& data = _parameters[parameter_index]; + data.length_or_indicator = static_cast(value.size()); + bind(parameter_index, SQL_C_BINARY, + value.size() > 8000 ? SQL_LONGVARBINARY : SQL_VARBINARY, + value.empty() ? 1 : value.size(), 0, + const_cast(value.data()), static_cast(value.size())); + } + + void bind_parameter(size_t parameter_index, + const std::chrono::sys_days& value) { + const std::chrono::year_month_day ymd{value}; + auto& data = _parameters[parameter_index]; + data.date_ = { + .year = static_cast(static_cast(ymd.year())), + .month = static_cast(static_cast(ymd.month())), + .day = static_cast(static_cast(ymd.day()))}; + data.length_or_indicator = 0; + bind(parameter_index, SQL_C_TYPE_DATE, SQL_TYPE_DATE, SQL_DATE_LEN, 0, + &data.date_, 0); + } + + // Note: ODBC's SQL_TIME_STRUCT has no sub-second precision, fractional + // seconds are dropped. + void bind_parameter(size_t parameter_index, + const std::chrono::microseconds& value) { + const auto hours = std::chrono::floor(value); + const auto minutes = + std::chrono::floor(value - hours); + const auto seconds = + std::chrono::floor(value - hours - minutes); + auto& data = _parameters[parameter_index]; + data.time_ = {.hour = static_cast(hours.count()), + .minute = static_cast(minutes.count()), + .second = static_cast(seconds.count())}; + data.length_or_indicator = 0; + bind(parameter_index, SQL_C_TYPE_TIME, SQL_TYPE_TIME, SQL_TIME_LEN, 0, + &data.time_, 0); + } + + void bind_parameter(size_t parameter_index, + const ::sqlpp::chrono::sys_microseconds& value) { + const auto days = std::chrono::floor(value); + const std::chrono::year_month_day ymd{days}; + const std::chrono::hh_mm_ss time{value - days}; + auto& data = _parameters[parameter_index]; + data.timestamp_ = { + .year = static_cast(static_cast(ymd.year())), + .month = static_cast(static_cast(ymd.month())), + .day = static_cast(static_cast(ymd.day())), + .hour = static_cast(time.hours().count()), + .minute = static_cast(time.minutes().count()), + .second = static_cast(time.seconds().count()), + // Billionths of a second. + .fraction = static_cast( + std::chrono::duration_cast( + time.subseconds()) + .count())}; + data.length_or_indicator = 0; + // Column size 26 and 6 decimal digits: "YYYY-MM-DD hh:mm:ss.ffffff" + bind(parameter_index, SQL_C_TYPE_TIMESTAMP, SQL_TYPE_TIMESTAMP, 26, 6, + &data.timestamp_, 0); + } + + void bind_null(size_t parameter_index) { + if constexpr (debug_enabled) { + _config->debug.log(log_category::parameter, + "ODBC: binding NULL parameter at index {}", + parameter_index); + } + auto& data = _parameters[parameter_index]; + data.length_or_indicator = SQL_NULL_DATA; + bind(parameter_index, SQL_C_DEFAULT, SQL_VARCHAR, 1, 0, nullptr, 0); + } + + private: + void bind(size_t parameter_index, + SQLSMALLINT c_type, + SQLSMALLINT sql_type, + SQLULEN column_size, + SQLSMALLINT decimal_digits, + void* value, + SQLLEN buffer_length) { + detail::throw_on_error( + SQLBindParameter(native_handle(), + static_cast(parameter_index + 1), + SQL_PARAM_INPUT, c_type, sql_type, column_size, + decimal_digits, value, buffer_length, + &_parameters[parameter_index].length_or_indicator), + "ODBC: could not bind parameter", SQL_HANDLE_STMT, native_handle()); + } +}; + +inline void bind_parameter(prepared_statement_t& statement, + size_t parameter_index, + const bool& value) { + if constexpr (debug_enabled) { + statement.debug().log(log_category::parameter, + "ODBC: binding boolean parameter {} at index {}", + value, parameter_index); + } + statement.bind_parameter(parameter_index, value); +} + +inline void bind_parameter(prepared_statement_t& statement, + size_t parameter_index, + const int64_t& value) { + if constexpr (debug_enabled) { + statement.debug().log(log_category::parameter, + "ODBC: binding integral parameter {} at index {}", + value, parameter_index); + } + statement.bind_parameter(parameter_index, value); +} + +inline void bind_parameter(prepared_statement_t& statement, + size_t parameter_index, + const uint64_t& value) { + if constexpr (debug_enabled) { + statement.debug().log( + log_category::parameter, + "ODBC: binding unsigned integral parameter {} at index {}", value, + parameter_index); + } + statement.bind_parameter(parameter_index, value); +} + +inline void bind_parameter(prepared_statement_t& statement, + size_t parameter_index, + const double& value) { + if constexpr (debug_enabled) { + statement.debug().log( + log_category::parameter, + "ODBC: binding floating_point parameter {} at index {}", value, + parameter_index); + } + statement.bind_parameter(parameter_index, value); +} + +inline void bind_parameter(prepared_statement_t& statement, + size_t parameter_index, + const std::string_view& value) { + if constexpr (debug_enabled) { + statement.debug().log(log_category::parameter, + "ODBC: binding text parameter '{}' at index {}", + value, parameter_index); + } + statement.bind_parameter(parameter_index, value); +} + +inline void bind_parameter(prepared_statement_t& statement, + size_t parameter_index, + const std::vector& value) { + if constexpr (debug_enabled) { + statement.debug().log( + log_category::parameter, + "ODBC: binding blob parameter with {} bytes at index {}", value.size(), + parameter_index); + } + statement.bind_parameter(parameter_index, value); +} + +inline void bind_parameter(prepared_statement_t& statement, + size_t parameter_index, + const std::chrono::sys_days& value) { + if constexpr (debug_enabled) { + statement.debug().log(log_category::parameter, + "ODBC: binding date parameter {} at index {}", value, + parameter_index); + } + statement.bind_parameter(parameter_index, value); +} + +inline void bind_parameter(prepared_statement_t& statement, + size_t parameter_index, + const std::chrono::microseconds& value) { + if constexpr (debug_enabled) { + statement.debug().log(log_category::parameter, + "ODBC: binding time of day parameter {} at index {}", + value, parameter_index); + } + statement.bind_parameter(parameter_index, value); +} + +inline void bind_parameter(prepared_statement_t& statement, + size_t parameter_index, + const ::sqlpp::chrono::sys_microseconds& value) { + if constexpr (debug_enabled) { + statement.debug().log(log_category::parameter, + "ODBC: binding timestamp parameter {} at index {}", + value, parameter_index); + } + statement.bind_parameter(parameter_index, value); +} + +} // namespace sqlpp::odbc diff --git a/include/sqlpp23/odbc/to_sql_string.h b/include/sqlpp23/odbc/to_sql_string.h new file mode 100644 index 000000000..69fecd559 --- /dev/null +++ b/include/sqlpp23/odbc/to_sql_string.h @@ -0,0 +1,71 @@ +#pragma once + +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include + +#include +#include +#include +#include + +namespace sqlpp::odbc { +// ODBC uses unnumbered '?' parameter markers. +template +auto to_sql_string(context_t& context, const parameter_t&) + -> std::string { + ++context._count; + return "?"; +} + +// Date and time literals use the ODBC escape sequences which every driver +// translates into the syntax of its database. +inline auto to_sql_string(context_t&, const std::chrono::sys_days& t) + -> std::string { + return std::format("{{d '{0:%Y-%m-%d}'}}", t); +} + +// Note: The ODBC time escape sequence has no sub-second precision, +// fractional seconds are dropped. +inline auto to_sql_string(context_t&, const std::chrono::microseconds& t) + -> std::string { + return std::format("{{t '{0:%H:%M:%S}'}}", + std::chrono::floor(t)); +} + +template +auto to_sql_string( + context_t&, + const std::chrono::time_point& t) + -> std::string { + return std::format("{{ts '{0:%Y-%m-%d %H:%M:%S}'}}", t); +} + +} // namespace sqlpp::odbc diff --git a/modules/sqlpp23.odbc.cppm b/modules/sqlpp23.odbc.cppm new file mode 100644 index 000000000..77ca1d988 --- /dev/null +++ b/modules/sqlpp23.odbc.cppm @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +module; + +#include + +export module sqlpp23.odbc; + +export namespace sqlpp::odbc { +using ::sqlpp::odbc::bind_field; +using ::sqlpp::odbc::bind_parameter; +using ::sqlpp::odbc::read_field; + +using ::sqlpp::odbc::connection; +using ::sqlpp::odbc::connection_config; +using ::sqlpp::odbc::connection_pool; +using ::sqlpp::odbc::connection_string; +using ::sqlpp::odbc::context_t; +using ::sqlpp::odbc::data_source; +using ::sqlpp::odbc::pooled_connection; + +using ::sqlpp::odbc::command_result; +using ::sqlpp::odbc::cursor_result_t; +using ::sqlpp::odbc::exception; +using ::sqlpp::odbc::prepared_statement_t; + +using ::sqlpp::odbc::to_sql_string; +} // namespace sqlpp::odbc diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 958dcb48d..d1d6cd70c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -32,6 +32,10 @@ if(BUILD_MYSQL_CONNECTOR OR BUILD_MARIADB_CONNECTOR) add_subdirectory(mysql) endif() +if(BUILD_ODBC_CONNECTOR) + add_subdirectory(odbc) +endif() + if(BUILD_POSTGRESQL_CONNECTOR) add_subdirectory(postgresql) endif() diff --git a/tests/core/types/detail/get_last_if.cpp b/tests/core/types/detail/get_last_if.cpp index 027ca56dc..d5dd17536 100644 --- a/tests/core/types/detail/get_last_if.cpp +++ b/tests/core/types/detail/get_last_if.cpp @@ -54,12 +54,7 @@ void test_get_last_if() { float, int64_t, short, size_t>, short>::value, ""); - static_assert( - std::is_same< - sqlpp::detail::get_last_if_t, - size_t>::value, - ""); + // Ending on a non-matching type static_assert( diff --git a/tests/include/sqlpp23/tests/odbc/all.h b/tests/include/sqlpp23/tests/odbc/all.h new file mode 100644 index 000000000..34e2b6a22 --- /dev/null +++ b/tests/include/sqlpp23/tests/odbc/all.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +// Note: To work around a GCC bug, make sure that for any standard header that +// is both included and imported from a module, the #include directive comes +// before the import declaration. For details see +// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=114795#c3 + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#ifdef BUILD_WITH_MODULES +import sqlpp23.core; +import sqlpp23.odbc; +import sqlpp23.test.odbc.tables; +#else +#include +#include +#include +#include +#endif diff --git a/tests/include/sqlpp23/tests/odbc/make_test_connection.h b/tests/include/sqlpp23/tests/odbc/make_test_connection.h new file mode 100644 index 000000000..e87950c1c --- /dev/null +++ b/tests/include/sqlpp23/tests/odbc/make_test_connection.h @@ -0,0 +1,76 @@ +#pragma once + +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include + +#ifdef BUILD_WITH_MODULES +import sqlpp23.core; +import sqlpp23.odbc; +#else +#include +#include +#endif + +namespace sqlpp::odbc { +inline debug_logger get_debug_logger( + const std::vector& categories = {log_category::all}) { + return debug_logger(categories, + [](sqlpp::log_category, const std::string& message) { + std::clog << message << '\n'; + }); +} + +// Get configuration for test connection. The connection string is taken from +// the environment (SQLPP_ODBC_CONNECTION_STRING) since ODBC tests require an +// externally configured driver and database. +inline std::shared_ptr make_test_config( + const std::vector& categories = {log_category::all}) { + auto config = std::make_shared(); + + if (const char* value = std::getenv("SQLPP_ODBC_CONNECTION_STRING")) { + config->source = sqlpp::odbc::connection_string{value}; + } + config->debug = get_debug_logger(categories); + + return config; +} + +inline ::sqlpp::odbc::connection make_test_connection( + const std::vector& categories = {log_category::all}) { + namespace sql = sqlpp::odbc; + + auto config = make_test_config(categories); + + sql::connection db; + db.connect_using(config); + return db; +} +} // namespace sqlpp::odbc diff --git a/tests/include/sqlpp23/tests/odbc/serialize_helpers.h b/tests/include/sqlpp23/tests/odbc/serialize_helpers.h new file mode 100644 index 000000000..5d53839f3 --- /dev/null +++ b/tests/include/sqlpp23/tests/odbc/serialize_helpers.h @@ -0,0 +1,49 @@ +#pragma once + +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include + +// Serialization can be tested without a live connection: escaping is +// implemented in the connector, not in the driver. +#define SQLPP_COMPARE(expr, expected_string) \ + { \ + static sqlpp::odbc::connection compare_db; \ + sqlpp::odbc::context_t compare_context{&compare_db}; \ + \ + using sqlpp::to_sql_string; \ + const auto result = to_sql_string(compare_context, expr); \ + \ + if (result != expected_string) { \ + std::cerr << __FILE__ << " " << __LINE__ << '\n' \ + << "Expected: -->|" << expected_string << "|<--\n" \ + << "Received: -->|" << result << "|<--\n"; \ + return -1; \ + } \ + } diff --git a/tests/include/sqlpp23/tests/odbc/tables.h b/tests/include/sqlpp23/tests/odbc/tables.h new file mode 100644 index 000000000..ca2b79c7b --- /dev/null +++ b/tests/include/sqlpp23/tests/odbc/tables.h @@ -0,0 +1,209 @@ +#pragma once + +// clang-format off +// generated by ./scripts/sqlpp23-ddl2cpp --path-to-ddl tests/include/sqlpp23/tests/odbc/tables.sql --path-to-header tests/include/sqlpp23/tests/odbc/tables.h --namespace test --assume-auto-id --generate-table-creation-helper + +#include + +#include +#include +#include +#include + +namespace test { + template + void createTabFoo(Db& db) { + db(R"+++(DROP TABLE IF EXISTS tab_foo)+++"); + db(R"+++(CREATE TABLE tab_foo +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + text_nn_d varchar(255) NOT NULL DEFAULT '', + int_n bigint, + double_n double precision, + u_int_n bigint UNSIGNED, + bool_n bool, + blob_n blob +))+++"); + } + + struct TabFoo_ { + struct Id { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(id, id); + using data_type = ::sqlpp::integral; + using has_default = std::true_type; + }; + struct TextNnD { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(text_nn_d, textNnD); + using data_type = ::sqlpp::text; + using has_default = std::true_type; + }; + struct IntN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(int_n, intN); + using data_type = std::optional<::sqlpp::integral>; + using has_default = std::true_type; + }; + struct DoubleN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(double_n, doubleN); + using data_type = std::optional<::sqlpp::floating_point>; + using has_default = std::true_type; + }; + struct UIntN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(u_int_n, uIntN); + using data_type = std::optional<::sqlpp::unsigned_integral>; + using has_default = std::true_type; + }; + struct BoolN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(bool_n, boolN); + using data_type = std::optional<::sqlpp::boolean>; + using has_default = std::true_type; + }; + struct BlobN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(blob_n, blobN); + using data_type = std::optional<::sqlpp::blob>; + using has_default = std::true_type; + }; + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(tab_foo, tabFoo); + template + using _table_columns = sqlpp::table_columns; + using _required_insert_columns = sqlpp::detail::type_set<>; + }; + using TabFoo = ::sqlpp::table_t; + + template + void createTabBar(Db& db) { + db(R"+++(DROP TABLE IF EXISTS tab_bar)+++"); + db(R"+++(CREATE TABLE tab_bar +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + text_n varchar(255) NULL, + bool_nn bool NOT NULL DEFAULT false, + int_n int +))+++"); + } + + struct TabBar_ { + struct Id { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(id, id); + using data_type = ::sqlpp::integral; + using has_default = std::true_type; + }; + struct TextN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(text_n, textN); + using data_type = std::optional<::sqlpp::text>; + using has_default = std::true_type; + }; + struct BoolNn { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(bool_nn, boolNn); + using data_type = ::sqlpp::boolean; + using has_default = std::true_type; + }; + struct IntN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(int_n, intN); + using data_type = std::optional<::sqlpp::integral>; + using has_default = std::true_type; + }; + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(tab_bar, tabBar); + template + using _table_columns = sqlpp::table_columns; + using _required_insert_columns = sqlpp::detail::type_set<>; + }; + using TabBar = ::sqlpp::table_t; + + template + void createTabDateTime(Db& db) { + db(R"+++(DROP TABLE IF EXISTS tab_date_time)+++"); + db(R"+++(CREATE TABLE tab_date_time ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date_n date, + timestamp_n datetime(3), + date_timestamp_n_d datetime DEFAULT CURRENT_TIMESTAMP, + time_n time(3) +))+++"); + } + + struct TabDateTime_ { + struct Id { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(id, id); + using data_type = ::sqlpp::integral; + using has_default = std::true_type; + }; + struct DateN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(date_n, dateN); + using data_type = std::optional<::sqlpp::date>; + using has_default = std::true_type; + }; + struct TimestampN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(timestamp_n, timestampN); + using data_type = std::optional<::sqlpp::timestamp>; + using has_default = std::true_type; + }; + struct DateTimestampND { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(date_timestamp_n_d, dateTimestampND); + using data_type = std::optional<::sqlpp::timestamp>; + using has_default = std::true_type; + }; + struct TimeN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(time_n, timeN); + using data_type = std::optional<::sqlpp::time>; + using has_default = std::true_type; + }; + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(tab_date_time, tabDateTime); + template + using _table_columns = sqlpp::table_columns; + using _required_insert_columns = sqlpp::detail::type_set<>; + }; + using TabDateTime = ::sqlpp::table_t; + + template + void createTabDepartment(Db& db) { + db(R"+++(DROP TABLE IF EXISTS tab_department)+++"); + db(R"+++(CREATE TABLE tab_department ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name CHAR(100), + division VARCHAR(255) NOT NULL DEFAULT 'engineering' +))+++"); + } + + struct TabDepartment_ { + struct Id { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(id, id); + using data_type = ::sqlpp::integral; + using has_default = std::true_type; + }; + struct Name { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(name, name); + using data_type = std::optional<::sqlpp::text>; + using has_default = std::true_type; + }; + struct Division { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(division, division); + using data_type = ::sqlpp::text; + using has_default = std::true_type; + }; + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(tab_department, tabDepartment); + template + using _table_columns = sqlpp::table_columns; + using _required_insert_columns = sqlpp::detail::type_set<>; + }; + using TabDepartment = ::sqlpp::table_t; + +} // namespace test diff --git a/tests/include/sqlpp23/tests/odbc/tables.sql b/tests/include/sqlpp23/tests/odbc/tables.sql new file mode 100644 index 000000000..d74335f53 --- /dev/null +++ b/tests/include/sqlpp23/tests/odbc/tables.sql @@ -0,0 +1,40 @@ +DROP TABLE IF EXISTS tab_foo; + +CREATE TABLE tab_foo +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + text_nn_d varchar(255) NOT NULL DEFAULT '', + int_n bigint, + double_n double precision, + u_int_n bigint UNSIGNED, + bool_n bool, + blob_n blob +); + +DROP TABLE IF EXISTS tab_bar; + +CREATE TABLE tab_bar +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + text_n varchar(255) NULL, + bool_nn bool NOT NULL DEFAULT false, + int_n int +); + +DROP TABLE IF EXISTS tab_date_time; + +CREATE TABLE tab_date_time ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date_n date, + timestamp_n datetime(3), + date_timestamp_n_d datetime DEFAULT CURRENT_TIMESTAMP, + time_n time(3) +); + +DROP TABLE IF EXISTS tab_department; + +CREATE TABLE tab_department ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name CHAR(100), + division VARCHAR(255) NOT NULL DEFAULT 'engineering' +); diff --git a/tests/odbc/CMakeLists.txt b/tests/odbc/CMakeLists.txt new file mode 100644 index 000000000..e3b623743 --- /dev/null +++ b/tests/odbc/CMakeLists.txt @@ -0,0 +1,28 @@ +# Copyright (c) 2026, Leander Schulten +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright notice, this +# list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +add_testing_target(NAME odbc MOD_DEPS sqlpp23::odbc_module) + +add_subdirectory(serialize) +add_subdirectory(usage) diff --git a/tests/odbc/modules/sqlpp23.test.odbc.tables.cppm b/tests/odbc/modules/sqlpp23.test.odbc.tables.cppm new file mode 100644 index 000000000..6fecdaaac --- /dev/null +++ b/tests/odbc/modules/sqlpp23.test.odbc.tables.cppm @@ -0,0 +1,210 @@ +module; + +// clang-format off +// generated by ./scripts/sqlpp23-ddl2cpp --path-to-ddl tests/include/sqlpp23/tests/odbc/tables.sql --path-to-module tests/odbc/modules/sqlpp23.test.odbc.tables.cppm --namespace test --assume-auto-id --module-name sqlpp23.test.odbc.tables --generate-table-creation-helper + +#include +#include + +export module sqlpp23.test.odbc.tables; + +import sqlpp23.core; + +namespace test { + export template + void createTabFoo(Db& db) { + db(R"+++(DROP TABLE IF EXISTS tab_foo)+++"); + db(R"+++(CREATE TABLE tab_foo +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + text_nn_d varchar(255) NOT NULL DEFAULT '', + int_n bigint, + double_n double precision, + u_int_n bigint UNSIGNED, + bool_n bool, + blob_n blob +))+++"); + } + + export struct TabFoo_ { + struct Id { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(id, id); + using data_type = ::sqlpp::integral; + using has_default = std::true_type; + }; + struct TextNnD { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(text_nn_d, textNnD); + using data_type = ::sqlpp::text; + using has_default = std::true_type; + }; + struct IntN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(int_n, intN); + using data_type = std::optional<::sqlpp::integral>; + using has_default = std::true_type; + }; + struct DoubleN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(double_n, doubleN); + using data_type = std::optional<::sqlpp::floating_point>; + using has_default = std::true_type; + }; + struct UIntN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(u_int_n, uIntN); + using data_type = std::optional<::sqlpp::unsigned_integral>; + using has_default = std::true_type; + }; + struct BoolN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(bool_n, boolN); + using data_type = std::optional<::sqlpp::boolean>; + using has_default = std::true_type; + }; + struct BlobN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(blob_n, blobN); + using data_type = std::optional<::sqlpp::blob>; + using has_default = std::true_type; + }; + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(tab_foo, tabFoo); + template + using _table_columns = sqlpp::table_columns; + using _required_insert_columns = sqlpp::detail::type_set<>; + }; + export using TabFoo = ::sqlpp::table_t; + + export template + void createTabBar(Db& db) { + db(R"+++(DROP TABLE IF EXISTS tab_bar)+++"); + db(R"+++(CREATE TABLE tab_bar +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + text_n varchar(255) NULL, + bool_nn bool NOT NULL DEFAULT false, + int_n int +))+++"); + } + + export struct TabBar_ { + struct Id { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(id, id); + using data_type = ::sqlpp::integral; + using has_default = std::true_type; + }; + struct TextN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(text_n, textN); + using data_type = std::optional<::sqlpp::text>; + using has_default = std::true_type; + }; + struct BoolNn { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(bool_nn, boolNn); + using data_type = ::sqlpp::boolean; + using has_default = std::true_type; + }; + struct IntN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(int_n, intN); + using data_type = std::optional<::sqlpp::integral>; + using has_default = std::true_type; + }; + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(tab_bar, tabBar); + template + using _table_columns = sqlpp::table_columns; + using _required_insert_columns = sqlpp::detail::type_set<>; + }; + export using TabBar = ::sqlpp::table_t; + + export template + void createTabDateTime(Db& db) { + db(R"+++(DROP TABLE IF EXISTS tab_date_time)+++"); + db(R"+++(CREATE TABLE tab_date_time ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date_n date, + timestamp_n datetime(3), + date_timestamp_n_d datetime DEFAULT CURRENT_TIMESTAMP, + time_n time(3) +))+++"); + } + + export struct TabDateTime_ { + struct Id { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(id, id); + using data_type = ::sqlpp::integral; + using has_default = std::true_type; + }; + struct DateN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(date_n, dateN); + using data_type = std::optional<::sqlpp::date>; + using has_default = std::true_type; + }; + struct TimestampN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(timestamp_n, timestampN); + using data_type = std::optional<::sqlpp::timestamp>; + using has_default = std::true_type; + }; + struct DateTimestampND { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(date_timestamp_n_d, dateTimestampND); + using data_type = std::optional<::sqlpp::timestamp>; + using has_default = std::true_type; + }; + struct TimeN { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(time_n, timeN); + using data_type = std::optional<::sqlpp::time>; + using has_default = std::true_type; + }; + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(tab_date_time, tabDateTime); + template + using _table_columns = sqlpp::table_columns; + using _required_insert_columns = sqlpp::detail::type_set<>; + }; + export using TabDateTime = ::sqlpp::table_t; + + export template + void createTabDepartment(Db& db) { + db(R"+++(DROP TABLE IF EXISTS tab_department)+++"); + db(R"+++(CREATE TABLE tab_department ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name CHAR(100), + division VARCHAR(255) NOT NULL DEFAULT 'engineering' +))+++"); + } + + export struct TabDepartment_ { + struct Id { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(id, id); + using data_type = ::sqlpp::integral; + using has_default = std::true_type; + }; + struct Name { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(name, name); + using data_type = std::optional<::sqlpp::text>; + using has_default = std::true_type; + }; + struct Division { + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(division, division); + using data_type = ::sqlpp::text; + using has_default = std::true_type; + }; + SQLPP_CREATE_NAME_TAG_FOR_SQL_AND_CPP(tab_department, tabDepartment); + template + using _table_columns = sqlpp::table_columns; + using _required_insert_columns = sqlpp::detail::type_set<>; + }; + export using TabDepartment = ::sqlpp::table_t; + + +} // namespace test diff --git a/tests/odbc/serialize/CMakeLists.txt b/tests/odbc/serialize/CMakeLists.txt new file mode 100644 index 000000000..95be64734 --- /dev/null +++ b/tests/odbc/serialize/CMakeLists.txt @@ -0,0 +1,26 @@ +# Copyright (c) 2026, Leander Schulten +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright notice, this +# list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +add_subdirectory(data_types) +add_subdirectory(statement) diff --git a/tests/odbc/serialize/data_types/CMakeLists.txt b/tests/odbc/serialize/data_types/CMakeLists.txt new file mode 100644 index 000000000..32781d190 --- /dev/null +++ b/tests/odbc/serialize/data_types/CMakeLists.txt @@ -0,0 +1,30 @@ +# Copyright (c) 2026, Leander Schulten +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright notice, this +# list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +create_tests_group( + date + text + time + timestamp +) diff --git a/tests/odbc/serialize/data_types/date.cpp b/tests/odbc/serialize/data_types/date.cpp new file mode 100644 index 000000000..1c3f3b291 --- /dev/null +++ b/tests/odbc/serialize/data_types/date.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +int main() { + std::chrono::sys_days tp = + static_cast(std::chrono::February / 8 / 2025); + + // Dates are serialized as ODBC date escape sequences. + SQLPP_COMPARE(tp, "{d '2025-02-08'}"); + + return 0; +} diff --git a/tests/odbc/serialize/data_types/text.cpp b/tests/odbc/serialize/data_types/text.cpp new file mode 100644 index 000000000..45c7661a1 --- /dev/null +++ b/tests/odbc/serialize/data_types/text.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +int main() { + // Single quotes are escaped by doubling them (standard SQL). + SQLPP_COMPARE(R"(a)", R"('a')"); + SQLPP_COMPARE(R"(')", R"('''')"); + SQLPP_COMPARE(R"(it's)", R"('it''s')"); + SQLPP_COMPARE(R"(\)", R"('\')"); + + return 0; +} diff --git a/tests/odbc/serialize/data_types/time.cpp b/tests/odbc/serialize/data_types/time.cpp new file mode 100644 index 000000000..af60cff61 --- /dev/null +++ b/tests/odbc/serialize/data_types/time.cpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +int main() { + using namespace std::chrono_literals; + + // Times of day are serialized as ODBC time escape sequences. + SQLPP_COMPARE(10h + 9min + 8s, "{t '10:09:08'}"); + + // The ODBC time escape sequence has no sub-second precision, fractional + // seconds are dropped. + SQLPP_COMPARE(10h + 9min + 8s + 123456us, "{t '10:09:08'}"); + + return 0; +} diff --git a/tests/odbc/serialize/data_types/timestamp.cpp b/tests/odbc/serialize/data_types/timestamp.cpp new file mode 100644 index 000000000..cbe29ce0b --- /dev/null +++ b/tests/odbc/serialize/data_types/timestamp.cpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +int main() { + using namespace std::chrono_literals; + + const auto tp = + static_cast(std::chrono::February / 8 / 2025) + + 10h + 9min + 8s + 123456us; + + // Timestamps are serialized as ODBC timestamp escape sequences. + SQLPP_COMPARE(tp, "{ts '2025-02-08 10:09:08.123456'}"); + + return 0; +} diff --git a/tests/odbc/serialize/statement/CMakeLists.txt b/tests/odbc/serialize/statement/CMakeLists.txt new file mode 100644 index 000000000..0ed3ff2a3 --- /dev/null +++ b/tests/odbc/serialize/statement/CMakeLists.txt @@ -0,0 +1,28 @@ +# Copyright (c) 2026, Leander Schulten +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright notice, this +# list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +create_tests_group( + parameter + select +) diff --git a/tests/odbc/serialize/statement/parameter.cpp b/tests/odbc/serialize/statement/parameter.cpp new file mode 100644 index 000000000..c653b4e9a --- /dev/null +++ b/tests/odbc/serialize/statement/parameter.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +int main() { + const auto foo = test::TabFoo{}; + const auto bar = test::TabBar{}; + + // ODBC uses unnumbered '?' parameter markers. + SQLPP_COMPARE(parameter(foo.doubleN), "?"); + SQLPP_COMPARE(bar.id > parameter(foo.doubleN), "tab_bar.id > ?"); + SQLPP_COMPARE(bar.id > parameter(foo.doubleN) and + bar.textN == parameter(bar.textN), + "(tab_bar.id > ?) AND (tab_bar.text_n = ?)"); + + return 0; +} diff --git a/tests/odbc/serialize/statement/select.cpp b/tests/odbc/serialize/statement/select.cpp new file mode 100644 index 000000000..53dc5f290 --- /dev/null +++ b/tests/odbc/serialize/statement/select.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +int main() { + const auto bar = test::TabBar{}; + + SQLPP_COMPARE(select(bar.id).from(bar).where(bar.textN == "it's"), + "SELECT tab_bar.id FROM tab_bar WHERE tab_bar.text_n = " + "'it''s'"); + + SQLPP_COMPARE( + select(bar.id).from(bar).where(bar.id > parameter(bar.id)), + "SELECT tab_bar.id FROM tab_bar WHERE tab_bar.id > ?"); + + return 0; +} diff --git a/tests/odbc/usage/CMakeLists.txt b/tests/odbc/usage/CMakeLists.txt new file mode 100644 index 000000000..766a84993 --- /dev/null +++ b/tests/odbc/usage/CMakeLists.txt @@ -0,0 +1,28 @@ +# Copyright (c) 2026, Leander Schulten +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright notice, this +# list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +create_tests_group( + ConnectionConfig + Statements +) diff --git a/tests/odbc/usage/ConnectionConfig.cpp b/tests/odbc/usage/ConnectionConfig.cpp new file mode 100644 index 000000000..6a7e23019 --- /dev/null +++ b/tests/odbc/usage/ConnectionConfig.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +int main() { + namespace sql = sqlpp::odbc; + + // The two ways of connecting are distinct alternatives of a variant. + sql::connection_config config; + assert(std::holds_alternative(config.source)); + + config.source = sql::data_source{.name = "test_dsn", + .username = "user", + .password = "secret"}; + assert(std::holds_alternative(config.source)); + assert(std::get(config.source).name == "test_dsn"); + + config.source = sql::connection_string{"Driver=Foo;Database=bar;"}; + assert(std::holds_alternative(config.source)); + + // Configurations compare by value (the debug logger is not compared). + sql::connection_config other; + assert(config != other); + other.source = sql::connection_string{"Driver=Foo;Database=bar;"}; + assert(config == other); + other.row_array_size = config.row_array_size + 1; + assert(config != other); + + // A default-constructed connection is not connected. + sql::connection db; + assert(not db.is_connected()); + + // Connecting to a data source that does not exist reports an ODBC error + // with SQLSTATE and diagnostic message. + try { + auto bad_config = std::make_shared(); + bad_config->source = + sql::data_source{.name = "sqlpp23_no_such_dsn", .username = {}, .password = {}}; + db.connect_using(bad_config); + std::cerr << "Expected connect to throw\n"; + return -1; + } catch (const sql::exception& e) { + // IM002: data source name not found (exact state depends on the driver + // manager, so only check that a state was reported). + assert(not e.sql_state().empty()); + } + + return 0; +} diff --git a/tests/odbc/usage/Statements.cpp b/tests/odbc/usage/Statements.cpp new file mode 100644 index 000000000..eb60d98f9 --- /dev/null +++ b/tests/odbc/usage/Statements.cpp @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2026, Leander Schulten + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include + +#include + +// Exercises the execution paths of the connector: direct and prepared +// select/insert/update/delete, all data types, NULL values, block fetch +// across rowset boundaries, the SQLGetData streaming fallback, and +// transactions. +// +// Without a configured data source this only verifies that everything +// compiles: set SQLPP_ODBC_CONNECTION_STRING to run it against a real +// database (the DDL in tables.h may need adjustment for your database). +namespace { + +#define expect(condition) \ + if (not(condition)) { \ + throw std::runtime_error{std::format("{}:{}: check failed: {}", __FILE__, \ + __LINE__, #condition)}; \ + } + +template +size_t count_rows(Db& db) { + const auto foo = test::TabFoo{}; + size_t count = 0; + for (const auto& row : db(select(foo.id).from(foo))) { + (void)row; + ++count; + } + return count; +} + +void test_data_type_round_trips(sqlpp::odbc::connection& db) { + const auto foo = test::TabFoo{}; + test::createTabFoo(db); + + auto blob_value = std::vector(100 * 1000); + for (size_t index = 0; index < blob_value.size(); ++index) { + blob_value[index] = static_cast(index % 251); + } + + // Insert with direct execution (values become literals), read back. + db(insert_into(foo).set(foo.textNnD = "it's a text", foo.intN = -42, + foo.doubleN = 1.25, + foo.uIntN = uint64_t{1234567890123456789}, + foo.boolN = true, foo.blobN = blob_value)); + + size_t row_count = 0; + for (const auto& row : + db(select(all_of(foo)).from(foo).where(foo.textNnD == "it's a text"))) { + ++row_count; + expect(row.textNnD == "it's a text"); + expect(row.intN == -42); + expect(row.doubleN == 1.25); + expect(row.uIntN == uint64_t{1234567890123456789}); + expect(row.boolN == true); + expect(row.blobN.has_value()); + expect(std::ranges::equal(*row.blobN, blob_value)); + } + expect(row_count == 1); + + // Insert the same values as parameters of a prepared statement. + auto prepared_insert = db.prepare(insert_into(foo).set( + foo.textNnD = parameter(foo.textNnD), foo.intN = parameter(foo.intN), + foo.doubleN = parameter(foo.doubleN), foo.uIntN = parameter(foo.uIntN), + foo.boolN = parameter(foo.boolN), foo.blobN = parameter(foo.blobN))); + prepared_insert.parameters.textNnD = "prepared"; + prepared_insert.parameters.intN = -43; + prepared_insert.parameters.doubleN = 2.5; + prepared_insert.parameters.uIntN = uint64_t{987654321}; + prepared_insert.parameters.boolN = false; + prepared_insert.parameters.blobN = blob_value; + db(prepared_insert); + + auto prepared_select = + db.prepare(select(all_of(foo)) + .from(foo) + .where(foo.textNnD == parameter(foo.textNnD))); + prepared_select.parameters.textNnD = "prepared"; + row_count = 0; + for (const auto& row : db(prepared_select)) { + ++row_count; + expect(row.intN == -43); + expect(row.doubleN == 2.5); + expect(row.uIntN == uint64_t{987654321}); + expect(row.boolN == false); + expect(row.blobN.has_value()); + expect(std::ranges::equal(*row.blobN, blob_value)); + } + expect(row_count == 1); + + // NULL values, direct and as parameters. + db(update(foo) + .set(foo.intN = std::nullopt, foo.doubleN = std::nullopt, + foo.uIntN = std::nullopt, foo.boolN = std::nullopt, + foo.blobN = std::nullopt) + .where(foo.textNnD == "it's a text")); + prepared_insert.parameters.textNnD = "nulls"; + prepared_insert.parameters.intN = std::nullopt; + prepared_insert.parameters.doubleN = std::nullopt; + prepared_insert.parameters.uIntN = std::nullopt; + prepared_insert.parameters.boolN = std::nullopt; + prepared_insert.parameters.blobN = std::nullopt; + db(prepared_insert); + + for (const auto& row : + db(select(all_of(foo)) + .from(foo) + .where(foo.textNnD == "it's a text" or foo.textNnD == "nulls"))) { + expect(row.intN == std::nullopt); + expect(row.doubleN == std::nullopt); + expect(row.uIntN == std::nullopt); + expect(row.boolN == std::nullopt); + expect(row.blobN == std::nullopt); + } + + // Update and delete report affected rows. + expect(db(update(foo).set(foo.intN = 1).where(foo.textNnD == "nulls")) + .affected_rows == 1); + expect(db(delete_from(foo).where(foo.textNnD == "nulls")).affected_rows == 1); +} + +void test_date_time_round_trips(sqlpp::odbc::connection& db) { + using namespace std::chrono_literals; + const auto tab = test::TabDateTime{}; + test::createTabDateTime(db); + + const auto date_value = + static_cast(std::chrono::February / 8 / 2025); + // The connector preserves microseconds, but some drivers round timestamp + // parameters (e.g. the SQLite driver stores milliseconds), so use a + // millisecond-precision value to keep this test portable. + const auto timestamp_value = date_value + 10h + 9min + 8s + 123ms; + // The ODBC time type has no sub-second precision. + const auto time_value = 10h + 9min + 8s; + + // Insert as literals ({d ...}, {ts ...}, {t ...} escapes). + db(insert_into(tab).set(tab.dateN = date_value, + tab.timestampN = timestamp_value, + tab.timeN = std::chrono::microseconds{time_value})); + + // Insert the same values as parameters (SQL_C_TYPE_* structs). + auto prepared_insert = db.prepare( + insert_into(tab).set(tab.dateN = parameter(tab.dateN), + tab.timestampN = parameter(tab.timestampN), + tab.timeN = parameter(tab.timeN))); + prepared_insert.parameters.dateN = date_value; + prepared_insert.parameters.timestampN = timestamp_value; + prepared_insert.parameters.timeN = std::chrono::microseconds{time_value}; + db(prepared_insert); + + size_t row_count = 0; + for (const auto& row : + db(select(tab.dateN, tab.timestampN, tab.timeN).from(tab))) { + ++row_count; + expect(row.dateN == date_value); + expect(row.timestampN == timestamp_value); + expect(row.timeN == time_value); + } + expect(row_count == 2); +} + +// Fetching more rows than fit into one rowset must cross rowset boundaries +// correctly (db is configured with a small row_array_size). +void test_multiple_rowsets(sqlpp::odbc::connection& db) { + const auto foo = test::TabFoo{}; + test::createTabFoo(db); + + auto prepared_insert = db.prepare(insert_into(foo).set( + foo.textNnD = "row", foo.intN = parameter(foo.intN))); + constexpr int64_t row_count = 10; + for (int64_t i = 0; i < row_count; ++i) { + prepared_insert.parameters.intN = i; + db(prepared_insert); + } + + // varchar(255) is bindable, so this uses block fetch, crossing rowset + // boundaries and reading text from the bound column arrays. + int64_t expected = 0; + for (const auto& row : + db(select(foo.textNnD, foo.intN).from(foo).order_by(foo.intN.asc()))) { + expect(row.textNnD == "row"); + expect(row.intN == expected); + ++expected; + } + expect(expected == row_count); +} + +// With a tiny max_bound_column_size, text columns cannot be bound and the +// result falls back to streaming values via SQLGetData (db is configured +// accordingly). Values larger than the initial streaming buffer exercise +// the buffer growth loop. +void test_streamed_text(sqlpp::odbc::connection& db) { + const auto foo = test::TabFoo{}; + test::createTabFoo(db); + + const auto long_text = std::string(5000, 'x') + "end"; + auto prepared_insert = db.prepare( + insert_into(foo).set(foo.textNnD = parameter(foo.textNnD), foo.intN = 7)); + prepared_insert.parameters.textNnD = long_text; + db(prepared_insert); + db(insert_into(foo).set(foo.textNnD = "", foo.intN = std::nullopt)); + + size_t row_count = 0; + for (const auto& row : + db(select(foo.textNnD, foo.intN).from(foo).order_by(foo.id.asc()))) { + if (row_count == 0) { + expect(row.textNnD == long_text); + expect(row.intN == 7); + } else { + expect(row.textNnD == ""); + expect(row.intN == std::nullopt); + } + ++row_count; + } + expect(row_count == 2); +} + +void test_transactions(sqlpp::odbc::connection& db) { + const auto foo = test::TabFoo{}; + test::createTabFoo(db); + + { + auto tx = start_transaction(db); + db(insert_into(foo).set(foo.textNnD = "committed")); + tx.commit(); + } + expect(count_rows(db) == 1); + expect(not db.is_transaction_active()); + + { + auto tx = start_transaction(db); + db(insert_into(foo).set(foo.textNnD = "rolled back")); + expect(db.is_transaction_active()); + tx.rollback(); + } + expect(count_rows(db) == 1); + expect(not db.is_transaction_active()); +} +} // namespace + +int main() { + if (std::getenv("SQLPP_ODBC_CONNECTION_STRING") == nullptr) { + std::println( + "ODBC: skipping statement execution, SQLPP_ODBC_CONNECTION_STRING is " + "not set"); + return 0; + } + + try { + { + auto db = sqlpp::odbc::make_test_connection(); + expect(db.is_connected()); + test_data_type_round_trips(db); + test_date_time_round_trips(db); + test_transactions(db); + } + { + // Small rowsets to test fetching across rowset boundaries. + auto config = sqlpp::odbc::make_test_config(); + config->row_array_size = 3; + sqlpp::odbc::connection db; + db.connect_using(config); + test_multiple_rowsets(db); + } + { + // Tiny column-buffer limit to force the SQLGetData streaming fallback. + auto config = sqlpp::odbc::make_test_config(); + config->max_bound_column_size = 8; + sqlpp::odbc::connection db; + db.connect_using(config); + test_streamed_text(db); + } + } catch (const std::exception& e) { + std::println(stderr, "Exception: {}", e.what()); + return -1; + } + + std::println("ODBC: all statement tests passed"); + return 0; +}