From ab3f287bd53720b713ffd8f447647f78bba678a3 Mon Sep 17 00:00:00 2001
From: David Li
Date: Tue, 19 May 2026 08:38:30 +0900
Subject: [PATCH] feat: implement current catalog/schema
---
AGENTS.md | 11 ++----
README.md | 53 +++++++++++++++++++++++++
ci/docker/quack-server/init.sql | 1 +
src/adbc_driver_quack.cc | 70 ++++++++++++++++++++++++++++-----
tests/exported_symbols_test.cc | 27 +++++--------
validation/tests/quack.py | 6 ++-
6 files changed, 132 insertions(+), 36 deletions(-)
create mode 100644 README.md
diff --git a/AGENTS.md b/AGENTS.md
index 2b880ca..ffc48c9 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -57,13 +57,10 @@ For C++ tests after that build:
The build script requires a vcpkg toolchain via `CMAKE_TOOLCHAIN_FILE`,
`VCPKG_ROOT`, or `VCPKG_INSTALLATION_ROOT`.
-For direct CMake iteration, use a build directory under `build/`, for example:
-
-```bash
-cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
-cmake --build build
-ctest --test-dir build --output-on-failure
-```
+Prefer `./ci/scripts/build.sh test linux amd64` over direct CMake invocation.
+That script configures the vcpkg toolchain, uses the expected CI build
+directory, builds the tests, and copies the driver library to
+`build/libadbc_driver_quack.so` for validation.
Validation is configured as a Pixi task and expects the driver library at
`build/libadbc_driver_quack.so` on Linux:
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ad5fba3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,53 @@
+
+
+# ADBC Driver for DuckDB Quack
+
+This repository contains a C++ ADBC driver for DuckDB's Quack remote protocol.
+
+## Build
+
+Use the repository root as the working directory. The supported local Linux
+amd64 build path is:
+
+```bash
+./ci/scripts/build.sh test linux amd64
+```
+
+The build script requires a vcpkg toolchain via `CMAKE_TOOLCHAIN_FILE`,
+`VCPKG_ROOT`, or `VCPKG_INSTALLATION_ROOT`. It configures the CI build under
+`build/ci-test-linux-amd64`, builds the driver and C++ tests, and copies the
+driver library to `build/libadbc_driver_quack.so`.
+
+## Test
+
+Run C++ tests after building:
+
+```bash
+./ci/scripts/test.sh linux amd64
+```
+
+Validation tests use the copied driver library and the Quack server defined in
+`compose.yaml`:
+
+```bash
+pixi run validate --collect-only
+pixi run validate -k connection
+pixi run validate
+```
+
+Rebuild with `./ci/scripts/build.sh test linux amd64` before running
+validation.
diff --git a/ci/docker/quack-server/init.sql b/ci/docker/quack-server/init.sql
index 176b74e..429b238 100644
--- a/ci/docker/quack-server/init.sql
+++ b/ci/docker/quack-server/init.sql
@@ -21,3 +21,4 @@ CALL quack_serve(
allow_other_hostname => true
);
CREATE TABLE IF NOT EXISTS quack_validation_ready AS SELECT 1 AS ok;
+CREATE SCHEMA IF NOT EXISTS quack_validation_secondary;
diff --git a/src/adbc_driver_quack.cc b/src/adbc_driver_quack.cc
index 08fc087..d27a9ed 100644
--- a/src/adbc_driver_quack.cc
+++ b/src/adbc_driver_quack.cc
@@ -60,6 +60,9 @@ AdbcStatusCode DriverConnectionNew(AdbcConnection* connection,
AdbcError* error);
AdbcStatusCode DriverConnectionRelease(AdbcConnection* connection,
AdbcError* error);
+AdbcStatusCode DriverConnectionSetOption(AdbcConnection* connection,
+ char const* key, char const* value,
+ AdbcError* error);
AdbcStatusCode DriverStatementBind(AdbcStatement* statement, ArrowArray* values,
ArrowSchema* schema, AdbcError* error);
AdbcStatusCode DriverStatementBindStream(AdbcStatement* statement,
@@ -633,6 +636,25 @@ AdbcStatusCode RunDuckDbQuery(ConnectionState* state, std::string const& sql,
return Ok(error);
}
+std::string Lowercase(std::string_view value) {
+ std::string lowered(value);
+ for (char& c : lowered) {
+ c = static_cast(std::tolower(static_cast(c)));
+ }
+ return lowered;
+}
+
+bool IsNotFoundDuckDbError(duckdb_error_type error_type,
+ std::string_view message) {
+ std::string const lower_message = Lowercase(message);
+ return (error_type == DUCKDB_ERROR_CATALOG &&
+ lower_message.find("not found") != std::string::npos) ||
+ lower_message.find("does not exist") != std::string::npos ||
+ lower_message.find("not found") != std::string::npos ||
+ lower_message.find("no such table") != std::string::npos ||
+ lower_message.find("no catalog + schema named") != std::string::npos;
+}
+
AdbcStatusCode RunDuckDbQueryAllowNotFound(ConnectionState* state,
std::string const& sql,
AdbcError* error) {
@@ -645,17 +667,9 @@ AdbcStatusCode RunDuckDbQueryAllowNotFound(ConnectionState* state,
result_error != nullptr ? result_error : "DuckDB query failed";
auto const error_type =
static_cast(duckdb_result_error_type(&result));
+ auto const result_error_type = duckdb_result_error_type(&result);
duckdb_destroy_result(&result);
- std::string const lower_message = [&message] {
- std::string lowered = message;
- for (char& c : lowered) {
- c = static_cast(std::tolower(static_cast(c)));
- }
- return lowered;
- }();
- if (lower_message.find("does not exist") != std::string::npos ||
- lower_message.find("not found") != std::string::npos ||
- lower_message.find("no such table") != std::string::npos) {
+ if (IsNotFoundDuckDbError(result_error_type, message)) {
return NotFound(error, std::move(message), error_type);
}
return IoError(error, std::move(message), error_type);
@@ -670,6 +684,13 @@ AdbcStatusCode RunRemoteQuery(ConnectionState* state, std::string const& sql,
error);
}
+AdbcStatusCode RunRemoteQueryAllowNotFound(ConnectionState* state,
+ std::string const& sql,
+ AdbcError* error) {
+ return RunDuckDbQueryAllowNotFound(
+ state, adbc_driver_quack::BuildRemoteQuerySql(sql), error);
+}
+
AdbcStatusCode GetColumnDefinitions(ConnectionState* state,
std::string const& table_name,
std::vector* definitions,
@@ -985,6 +1006,7 @@ AdbcStatusCode InitDriver(int version, void* raw_driver, AdbcError* error) {
driver->ConnectionGetObjects = DriverConnectionGetObjects;
driver->ConnectionNew = DriverConnectionNew;
driver->ConnectionRelease = DriverConnectionRelease;
+ driver->ConnectionSetOption = DriverConnectionSetOption;
driver->StatementBind = DriverStatementBind;
driver->StatementBindStream = DriverStatementBindStream;
@@ -1209,6 +1231,27 @@ AdbcStatusCode DriverConnectionGetObjects(
return Ok(error);
}
+AdbcStatusCode DriverConnectionSetOption(AdbcConnection* connection,
+ char const* key, char const* value,
+ AdbcError* error) {
+ ConnectionState* state = GetConnection(connection);
+ if (state == nullptr || !state->initialized) {
+ return InvalidState(error, "connection is not initialized");
+ }
+ if (key == nullptr || value == nullptr) {
+ return InvalidArgument(error,
+ "connection option key and value must not be null");
+ }
+
+ if (std::strcmp(key, ADBC_CONNECTION_OPTION_CURRENT_CATALOG) == 0 ||
+ std::strcmp(key, ADBC_CONNECTION_OPTION_CURRENT_DB_SCHEMA) == 0) {
+ std::string const sql = "USE " + QuoteIdentifier(value);
+ return RunRemoteQueryAllowNotFound(state, sql, error);
+ }
+
+ return NotImplemented(error, "unsupported connection option");
+}
+
AdbcStatusCode DriverStatementNew(AdbcConnection* connection,
AdbcStatement* statement, AdbcError* error) {
if (statement == nullptr) {
@@ -1394,6 +1437,13 @@ ADBC_EXPORT AdbcStatusCode AdbcConnectionRelease(AdbcConnection* connection,
return DriverConnectionRelease(connection, error);
}
+ADBC_EXPORT AdbcStatusCode AdbcConnectionSetOption(AdbcConnection* connection,
+ char const* key,
+ char const* value,
+ AdbcError* error) {
+ return DriverConnectionSetOption(connection, key, value, error);
+}
+
ADBC_EXPORT AdbcStatusCode AdbcConnectionGetInfo(AdbcConnection* connection,
uint32_t const* info_codes,
size_t info_codes_length,
diff --git a/tests/exported_symbols_test.cc b/tests/exported_symbols_test.cc
index 4e74fff..ac65e0f 100644
--- a/tests/exported_symbols_test.cc
+++ b/tests/exported_symbols_test.cc
@@ -62,23 +62,15 @@ TEST(ExportedSymbolsTest, ExportsRequiredAdbcEntryPoints) {
ASSERT_NE(library, nullptr);
char const* symbols[] = {
- "AdbcDatabaseNew",
- "AdbcDatabaseSetOption",
- "AdbcDatabaseInit",
- "AdbcDatabaseRelease",
- "AdbcConnectionNew",
- "AdbcConnectionInit",
- "AdbcConnectionGetInfo",
- "AdbcConnectionRelease",
- "AdbcStatementNew",
- "AdbcStatementSetSqlQuery",
- "AdbcStatementExecuteQuery",
- "AdbcStatementPrepare",
- "AdbcStatementSetOption",
- "AdbcStatementBind",
- "AdbcStatementBindStream",
- "AdbcStatementRelease",
- "AdbcDriverInit",
+ "AdbcDatabaseNew", "AdbcDatabaseSetOption",
+ "AdbcDatabaseInit", "AdbcDatabaseRelease",
+ "AdbcConnectionNew", "AdbcConnectionInit",
+ "AdbcConnectionGetInfo", "AdbcConnectionSetOption",
+ "AdbcConnectionRelease", "AdbcStatementNew",
+ "AdbcStatementSetSqlQuery", "AdbcStatementExecuteQuery",
+ "AdbcStatementPrepare", "AdbcStatementSetOption",
+ "AdbcStatementBind", "AdbcStatementBindStream",
+ "AdbcStatementRelease", "AdbcDriverInit",
"AdbcDriverQuackInit",
};
@@ -114,6 +106,7 @@ TEST(ExportedSymbolsTest, DriverInitFunctionsPopulateDriverTable) {
EXPECT_NE(driver.ConnectionNew, nullptr) << init_symbol;
EXPECT_NE(driver.ConnectionInit, nullptr) << init_symbol;
EXPECT_NE(driver.ConnectionGetInfo, nullptr) << init_symbol;
+ EXPECT_NE(driver.ConnectionSetOption, nullptr) << init_symbol;
EXPECT_NE(driver.ConnectionRelease, nullptr) << init_symbol;
EXPECT_NE(driver.StatementNew, nullptr) << init_symbol;
EXPECT_NE(driver.StatementSetSqlQuery, nullptr) << init_symbol;
diff --git a/validation/tests/quack.py b/validation/tests/quack.py
index ea0de88..7268df6 100644
--- a/validation/tests/quack.py
+++ b/validation/tests/quack.py
@@ -28,8 +28,8 @@ class QuackQuirks(model.DriverQuirks):
short_version = "1.5"
features = model.DriverFeatures(
connection_get_table_schema=False,
- connection_set_current_catalog=False,
- connection_set_current_schema=False,
+ connection_set_current_catalog=True,
+ connection_set_current_schema=True,
connection_transactions=False,
current_catalog="quack-validation",
current_schema="main",
@@ -41,6 +41,8 @@ class QuackQuirks(model.DriverQuirks):
statement_prepare=False,
statement_rows_affected=False,
statement_rows_affected_ddl=False,
+ secondary_catalog="quack-validation",
+ secondary_schema="quack_validation_secondary",
supported_xdbc_fields=[],
)
setup = model.DriverSetup(