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(