From f667321a6994eaee078e0dc3b8c13d77da4f7c14 Mon Sep 17 00:00:00 2001 From: David Li Date: Tue, 4 Aug 2026 10:54:04 +0900 Subject: [PATCH 1/6] feat(c/driver/sqlite): ensure batch rows option can be retrieved The getter existed, but the framework needed to be extended to also allow fetching it as a string, and tests were added. Closes #3456. Assisted-by: GPT-5.6 Sol --- c/driver/framework/base_driver.h | 26 +++++-- c/driver/sqlite/sqlite_test.cc | 71 +++++++++++++++++++ .../adbc_driver_sqlite/tests/test_lowlevel.py | 7 ++ .../tests/testthat/test-options.R | 32 +++++---- 4 files changed, 115 insertions(+), 21 deletions(-) diff --git a/c/driver/framework/base_driver.h b/c/driver/framework/base_driver.h index bebdffea6b..36150c8853 100644 --- a/c/driver/framework/base_driver.h +++ b/c/driver/framework/base_driver.h @@ -54,8 +54,7 @@ enum class LifecycleState { kInitialized, }; -/// \brief A typed option value wrapper. It currently does not attempt -/// conversion (i.e., getting a double option as a string). +/// \brief A typed option value wrapper. Attempts some conversions between types. class Option { public: /// \brief The option is unset. @@ -169,11 +168,26 @@ class Option { return std::visit( [&](auto&& value) -> AdbcStatusCode { using T = std::decay_t; - if constexpr (std::is_same_v) { - size_t value_size_with_terminator = value.size() + 1; + if constexpr (std::is_same_v || std::is_same_v || + std::is_same_v) { + char formatted[24]; // Enough room for double/int64_t + std::string_view string_value; + if constexpr (std::is_same_v) { + string_value = value; + } else { + auto result = + std::to_chars(formatted, formatted + sizeof(formatted), value); + if (result.ec != std::errc()) { + return status::Internal("Could not format numeric option value") + .ToAdbc(error); + } + string_value = std::string_view( + formatted, static_cast(result.ptr - formatted)); + } + size_t value_size_with_terminator = string_value.size() + 1; if (*length >= value_size_with_terminator) { - std::memcpy(out, value.data(), value.size()); - out[value.size()] = 0; + std::memcpy(out, string_value.data(), string_value.size()); + out[string_value.size()] = 0; } *length = value_size_with_terminator; return ADBC_STATUS_OK; diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc index 243449eb07..77ad60e857 100644 --- a/c/driver/sqlite/sqlite_test.cc +++ b/c/driver/sqlite/sqlite_test.cc @@ -454,6 +454,77 @@ TEST(SqliteUriWrapper, SqliteUriFilename) { adbc_validation::IsOkStatus(&error)); } +TEST(SqliteOptions, BatchRowsGetOption) { + struct AdbcError error = ADBC_ERROR_INIT; + adbc_validation::Handle database; + adbc_validation::Handle connection; + adbc_validation::Handle statement; + constexpr const char* kBatchRows = "adbc.sqlite.query.batch_rows"; + + ASSERT_THAT(AdbcDatabaseNew(&database.value, &error), + adbc_validation::IsOkStatus(&error)); + + int64_t int_value = 0; + ASSERT_THAT(AdbcDatabaseGetOptionInt(&database.value, kBatchRows, &int_value, &error), + adbc_validation::IsOkStatus(&error)); + EXPECT_EQ(1024, int_value); + + char too_small[2] = {'x', 'x'}; + size_t length = sizeof(too_small); + ASSERT_THAT( + AdbcDatabaseGetOption(&database.value, kBatchRows, too_small, &length, &error), + adbc_validation::IsOkStatus(&error)); + EXPECT_EQ(5, length); + EXPECT_THAT(too_small, ::testing::ElementsAre('x', 'x')); + + ASSERT_THAT(AdbcDatabaseSetOption(&database.value, kBatchRows, "41", &error), + adbc_validation::IsOkStatus(&error)); + char string_value[3] = {}; + length = sizeof(string_value); + ASSERT_THAT( + AdbcDatabaseGetOption(&database.value, kBatchRows, string_value, &length, &error), + adbc_validation::IsOkStatus(&error)); + EXPECT_EQ(3, length); + EXPECT_STREQ("41", string_value); + + ASSERT_THAT(AdbcDatabaseInit(&database.value, &error), + adbc_validation::IsOkStatus(&error)); + ASSERT_THAT(AdbcConnectionNew(&connection.value, &error), + adbc_validation::IsOkStatus(&error)); + ASSERT_THAT(AdbcConnectionInit(&connection.value, &database.value, &error), + adbc_validation::IsOkStatus(&error)); + ASSERT_THAT( + AdbcConnectionGetOptionInt(&connection.value, kBatchRows, &int_value, &error), + adbc_validation::IsOkStatus(&error)); + EXPECT_EQ(41, int_value); + + ASSERT_THAT(AdbcStatementNew(&connection.value, &statement.value, &error), + adbc_validation::IsOkStatus(&error)); + ASSERT_THAT(AdbcStatementGetOptionInt(&statement.value, kBatchRows, &int_value, &error), + adbc_validation::IsOkStatus(&error)); + EXPECT_EQ(41, int_value); + + ASSERT_THAT(AdbcStatementSetOptionInt(&statement.value, kBatchRows, 42, &error), + adbc_validation::IsOkStatus(&error)); + char statement_value[3] = {}; + length = sizeof(statement_value); + ASSERT_THAT(AdbcStatementGetOption(&statement.value, kBatchRows, statement_value, + &length, &error), + adbc_validation::IsOkStatus(&error)); + EXPECT_EQ(3, length); + EXPECT_STREQ("42", statement_value); + + ASSERT_THAT(AdbcStatementSetOption(&statement.value, "adbc.statement.bind_by_name", + ADBC_OPTION_VALUE_ENABLED, &error), + adbc_validation::IsOkStatus(&error)); + char bind_by_name[sizeof(ADBC_OPTION_VALUE_ENABLED)] = {}; + length = sizeof(bind_by_name); + ASSERT_THAT(AdbcStatementGetOption(&statement.value, "adbc.statement.bind_by_name", + bind_by_name, &length, &error), + adbc_validation::IsOkStatus(&error)); + EXPECT_STREQ(ADBC_OPTION_VALUE_ENABLED, bind_by_name); +} + class SqliteStatementTest : public ::testing::Test, public adbc_validation::StatementTest { public: diff --git a/python/adbc_driver_sqlite/tests/test_lowlevel.py b/python/adbc_driver_sqlite/tests/test_lowlevel.py index 9c8afcac3b..f1f2174a6f 100644 --- a/python/adbc_driver_sqlite/tests/test_lowlevel.py +++ b/python/adbc_driver_sqlite/tests/test_lowlevel.py @@ -49,6 +49,13 @@ def test_options(sqlite): adbc_driver_sqlite.StatementOptions.BATCH_ROWS.value: "1", } ) + assert ( + stmt.get_option(adbc_driver_sqlite.StatementOptions.BATCH_ROWS.value) == "1" + ) + assert ( + stmt.get_option_int(adbc_driver_sqlite.StatementOptions.BATCH_ROWS.value) + == 1 + ) stmt.set_sql_query("SELECT 1") stream, _ = stmt.execute_query() reader = pyarrow.RecordBatchReader._import_from_c(stream.address) diff --git a/r/adbcdrivermanager/tests/testthat/test-options.R b/r/adbcdrivermanager/tests/testthat/test-options.R index 8d978f838c..dc8c2f295d 100644 --- a/r/adbcdrivermanager/tests/testthat/test-options.R +++ b/r/adbcdrivermanager/tests/testthat/test-options.R @@ -251,43 +251,35 @@ test_that("void driver errors getting bytes option of incorrect type", { ) }) -test_that("void driver errors getting integer option of incorrect type", { +test_that("void driver errors getting integer option as bytes", { db <- adbc_database_init(adbc_driver_void()) adbc_database_set_options(db, list("some_key" = 123L)) - expect_error( - adbc_database_get_option(db, "some_key"), - class = "adbc_status_not_found" - ) - expect_error( adbc_database_get_option_bytes(db, "some_key"), class = "adbc_status_not_found" ) - - }) test_that("void driver can get integer option of compatible type", { db <- adbc_database_init(adbc_driver_void()) adbc_database_set_options(db, list("some_key" = 123L)) + expect_identical( + adbc_database_get_option(db, "some_key"), + "123" + ) + expect_identical( adbc_database_get_option_double(db, "some_key"), 123.0 ) }) - -test_that("void driver errors getting double option of incorrect type", { +test_that("void driver errors getting double option as incompatible type", { db <- adbc_database_init(adbc_driver_void()) adbc_database_set_options(db, list("some_key" = 123.4)) - expect_error( - adbc_database_get_option(db, "some_key"), - class = "adbc_status_not_found" - ) - expect_error( adbc_database_get_option_bytes(db, "some_key"), class = "adbc_status_not_found" @@ -299,6 +291,16 @@ test_that("void driver errors getting double option of incorrect type", { ) }) +test_that("void driver can get double option of compatible type", { + db <- adbc_database_init(adbc_driver_void()) + adbc_database_set_options(db, list("some_key" = 123.4)) + + expect_identical( + adbc_database_get_option(db, "some_key"), + "123.4" + ) +}) + test_that("key_value_options works", { expect_identical( key_value_options(NULL), From eb419cb0b62ae2b71136f8990c9ec52c1edd108d Mon Sep 17 00:00:00 2001 From: David Li Date: Mon, 24 Aug 2026 14:53:30 +0900 Subject: [PATCH 2/6] use fmtlib instead --- c/driver/framework/base_driver.h | 21 ++++++++++----------- c/driver/framework/status.h | 4 ---- c/driver/postgresql/result_helper.cc | 1 - c/driver/sqlite/sqlite.cc | 1 - 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/c/driver/framework/base_driver.h b/c/driver/framework/base_driver.h index 36150c8853..578bb3066a 100644 --- a/c/driver/framework/base_driver.h +++ b/c/driver/framework/base_driver.h @@ -30,6 +30,8 @@ #include +#include "fmt/core.h" + #include "driver/framework/status.h" /// \file base.h ADBC Driver Framework @@ -170,19 +172,16 @@ class Option { using T = std::decay_t; if constexpr (std::is_same_v || std::is_same_v || std::is_same_v) { - char formatted[24]; // Enough room for double/int64_t std::string_view string_value; - if constexpr (std::is_same_v) { - string_value = value; + std::string allocated_value; + if constexpr (std::is_same_v) { + allocated_value = fmt::format("{}", value); + string_value = allocated_value; + } else if constexpr (std::is_same_v) { + allocated_value = fmt::format("{}", value); + string_value = allocated_value; } else { - auto result = - std::to_chars(formatted, formatted + sizeof(formatted), value); - if (result.ec != std::errc()) { - return status::Internal("Could not format numeric option value") - .ToAdbc(error); - } - string_value = std::string_view( - formatted, static_cast(result.ptr - formatted)); + string_value = value; } size_t value_size_with_terminator = string_value.size() + 1; if (*length >= value_size_with_terminator) { diff --git a/c/driver/framework/status.h b/c/driver/framework/status.h index d42535e5ac..c73a138068 100644 --- a/c/driver/framework/status.h +++ b/c/driver/framework/status.h @@ -26,10 +26,8 @@ #include #include -#if defined(ADBC_FRAMEWORK_USE_FMT) #include #include -#endif #include @@ -326,7 +324,6 @@ STATUS_CTOR(Unknown, UNKNOWN) } // namespace adbc::driver::status -#if defined(ADBC_FRAMEWORK_USE_FMT) namespace adbc::driver::status::fmt { #define STATUS_CTOR(NAME, CODE) \ @@ -348,7 +345,6 @@ STATUS_CTOR(Unknown, UNKNOWN) #undef STATUS_CTOR } // namespace adbc::driver::status::fmt -#endif #define UNWRAP_ERRNO_IMPL(NAME, CODE, RHS) \ do { \ diff --git a/c/driver/postgresql/result_helper.cc b/c/driver/postgresql/result_helper.cc index 2557e8f944..1fcc42a9c3 100644 --- a/c/driver/postgresql/result_helper.cc +++ b/c/driver/postgresql/result_helper.cc @@ -27,7 +27,6 @@ #include #include -#define ADBC_FRAMEWORK_USE_FMT #include "driver/framework/status.h" #include "error.h" diff --git a/c/driver/sqlite/sqlite.cc b/c/driver/sqlite/sqlite.cc index 62dec5ab25..6aea5ee142 100644 --- a/c/driver/sqlite/sqlite.cc +++ b/c/driver/sqlite/sqlite.cc @@ -26,7 +26,6 @@ #include #include -#define ADBC_FRAMEWORK_USE_FMT #include "driver/framework/base_driver.h" #include "driver/framework/connection.h" #include "driver/framework/database.h" From 4782859377460822c21076d6b27b9b1fd5d7d9ff Mon Sep 17 00:00:00 2001 From: David Li Date: Mon, 24 Aug 2026 15:07:09 +0900 Subject: [PATCH 3/6] we always use {fmt} now --- r/adbcdrivermanager/src/Makevars | 2 +- r/adbcdrivermanager/src/Makevars.win | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/r/adbcdrivermanager/src/Makevars b/r/adbcdrivermanager/src/Makevars index 5476c6e35f..f17dd3681b 100644 --- a/r/adbcdrivermanager/src/Makevars +++ b/r/adbcdrivermanager/src/Makevars @@ -17,7 +17,7 @@ CXX_STD = CXX20 CONDA_BUILD ?= "0" -PKG_CPPFLAGS=-I../src/c/include -I../src/c -I../src/c/vendor -DADBC_EXPORT="" -DADBC_CONDA_BUILD=$(CONDA_BUILD) +PKG_CPPFLAGS=-I../src/c/include -I../src/c -I../src/c/vendor -I../src/c/vendor/fmt/include -DADBC_EXPORT="" -DADBC_CONDA_BUILD=$(CONDA_BUILD) -DFMT_HEADER_ONLY=1 OBJECTS = driver_test.o \ error.o \ diff --git a/r/adbcdrivermanager/src/Makevars.win b/r/adbcdrivermanager/src/Makevars.win index 6a086d3691..ac0c8cec9e 100644 --- a/r/adbcdrivermanager/src/Makevars.win +++ b/r/adbcdrivermanager/src/Makevars.win @@ -17,7 +17,7 @@ CXX_STD = CXX20 CONDA_BUILD ?= "0" -PKG_CPPFLAGS=-I../src/c/include -I../src/c -I../src/c/vendor -DADBC_EXPORT="" -DADBC_CONDA_BUILD=$(CONDA_BUILD) +PKG_CPPFLAGS=-I../src/c/include -I../src/c -I../src/c/vendor -I../src/c/vendor/fmt/include -DADBC_EXPORT="" -DADBC_CONDA_BUILD=$(CONDA_BUILD) -DFMT_HEADER_ONLY=1 PKG_LIBS=-lshell32 -ladvapi32 -luuid OBJECTS = driver_test.o \ From 54a7972fa3ba5b3ed138230b155afd5960f25e4f Mon Sep 17 00:00:00 2001 From: David Li Date: Mon, 24 Aug 2026 15:18:15 +0900 Subject: [PATCH 4/6] meson --- c/driver/postgresql/meson.build | 2 +- c/driver/sqlite/meson.build | 2 +- c/subprojects/fmt.wrap | 20 ++++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/c/driver/postgresql/meson.build b/c/driver/postgresql/meson.build index 7af8f849d9..fcb87c01a0 100644 --- a/c/driver/postgresql/meson.build +++ b/c/driver/postgresql/meson.build @@ -70,7 +70,7 @@ foreach name, conf : postgres_tests sources: conf['sources'], include_directories: [include_dir, driver_dir, c_dir, safe_math_dir], link_with: [adbc_common_lib, adbc_postgres_driver_lib], - dependencies: [libpq_dep, adbc_validation_dep], + dependencies: [libpq_dep, adbc_validation_dep, fmt_dep], cpp_args: cpp_args, ) test('adbc-' + name, exc) diff --git a/c/driver/sqlite/meson.build b/c/driver/sqlite/meson.build index 6ddc91d03e..c34b024864 100644 --- a/c/driver/sqlite/meson.build +++ b/c/driver/sqlite/meson.build @@ -49,6 +49,6 @@ exc = executable( sources: ['sqlite_test.cc'], include_directories: [include_dir, c_dir, driver_dir], link_with: [adbc_common_lib, adbc_sqlite3_driver_lib], - dependencies: [sqlite3_dep, adbc_validation_dep], + dependencies: [sqlite3_dep, adbc_validation_dep, fmt_dep], ) test('adbc-driver-sqlite', exc) diff --git a/c/subprojects/fmt.wrap b/c/subprojects/fmt.wrap index 4e96460106..0d68e134c7 100644 --- a/c/subprojects/fmt.wrap +++ b/c/subprojects/fmt.wrap @@ -1,13 +1,13 @@ [wrap-file] -directory = fmt-10.2.0 -source_url = https://github.com/fmtlib/fmt/archive/10.2.0.tar.gz -source_filename = fmt-10.2.0.tar.gz -source_hash = 3ca91733a7313a8ad41c0885929415f8ec0a2a31d4dc7e27e9331412f4ca26ac -patch_filename = fmt_10.2.0-2_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/fmt_10.2.0-2/get_patch -patch_hash = 2428c3a386a8390c76378f81ef804a297f4edc3b789499dd56629b7902b8ddb7 -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/fmt_10.2.0-2/fmt-10.2.0.tar.gz -wrapdb_version = 10.2.0-2 +directory = fmt-12.0.0 +source_url = https://github.com/fmtlib/fmt/archive/12.0.0.tar.gz +source_filename = fmt-12.0.0.tar.gz +source_hash = aa3e8fbb6a0066c03454434add1f1fc23299e85758ceec0d7d2d974431481e40 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/fmt_12.0.0-1/fmt-12.0.0.tar.gz +patch_filename = fmt_12.0.0-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/fmt_12.0.0-1/get_patch +patch_hash = 307f288ebf3850abf2f0c50ac1fb07de97df9538d39146d802f3c0d6cada8998 +wrapdb_version = 12.0.0-1 [provide] -fmt = fmt_dep +dependency_names = fmt \ No newline at end of file From 9d742688478acfdb45f5f2a3d42f50e0015db3aa Mon Sep 17 00:00:00 2001 From: David Li Date: Mon, 24 Aug 2026 16:44:14 +0900 Subject: [PATCH 5/6] recipe --- c/subprojects/fmt.wrap | 2 +- docs/source/cpp/recipe_driver/CMakeLists.txt | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/c/subprojects/fmt.wrap b/c/subprojects/fmt.wrap index 0d68e134c7..7e9c5c42b7 100644 --- a/c/subprojects/fmt.wrap +++ b/c/subprojects/fmt.wrap @@ -10,4 +10,4 @@ patch_hash = 307f288ebf3850abf2f0c50ac1fb07de97df9538d39146d802f3c0d6cada8998 wrapdb_version = 12.0.0-1 [provide] -dependency_names = fmt \ No newline at end of file +dependency_names = fmt diff --git a/docs/source/cpp/recipe_driver/CMakeLists.txt b/docs/source/cpp/recipe_driver/CMakeLists.txt index b4c6b06199..c04e186caa 100644 --- a/docs/source/cpp/recipe_driver/CMakeLists.txt +++ b/docs/source/cpp/recipe_driver/CMakeLists.txt @@ -29,6 +29,12 @@ set(CMAKE_CXX_STANDARD 17) set(NANOARROW_IPC ON) set(NANOARROW_NAMESPACE "DriverExamplePrivate") set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +fetchcontent_declare(fmt + GIT_REPOSITORY https://github.com/fmtlib/fmt + GIT_TAG 1be298e1bd68957e4cd352e1f676f00e07dcfb57) # 12.2.0 +fetchcontent_makeavailable(fmt) + fetchcontent_declare(nanoarrow GIT_REPOSITORY https://github.com/apache/arrow-nanoarrow.git GIT_TAG apache-arrow-nanoarrow-0.9.0 @@ -47,7 +53,7 @@ target_link_libraries(adbc_driver_framework PRIVATE nanoarrow::nanoarrow_static) add_library(driver_example SHARED driver_example.cc) target_include_directories(driver_example PRIVATE ../../../../c ../../../../c/include) -target_link_libraries(driver_example PRIVATE adbc_driver_framework +target_link_libraries(driver_example PRIVATE adbc_driver_framework fmt::fmt nanoarrow::nanoarrow_ipc) install(TARGETS driver_example) From de4ee5ebadea9e8124661de24baf81bb63e7f607 Mon Sep 17 00:00:00 2001 From: David Li Date: Mon, 24 Aug 2026 17:04:55 +0900 Subject: [PATCH 6/6] recipe --- docs/source/cpp/recipe_driver/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/cpp/recipe_driver/CMakeLists.txt b/docs/source/cpp/recipe_driver/CMakeLists.txt index c04e186caa..aae426f302 100644 --- a/docs/source/cpp/recipe_driver/CMakeLists.txt +++ b/docs/source/cpp/recipe_driver/CMakeLists.txt @@ -49,7 +49,7 @@ add_library(adbc_driver_framework ../../../../c/driver/framework/utility.cc ../../../../c/driver/framework/objects.cc) target_include_directories(adbc_driver_framework PRIVATE ../../../../c ../../../../c/include) -target_link_libraries(adbc_driver_framework PRIVATE nanoarrow::nanoarrow_static) +target_link_libraries(adbc_driver_framework PRIVATE fmt::fmt nanoarrow::nanoarrow_static) add_library(driver_example SHARED driver_example.cc) target_include_directories(driver_example PRIVATE ../../../../c ../../../../c/include)