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)