Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<!--
Copyright (c) 2026 ADBC Drivers Contributors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

# 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.
1 change: 1 addition & 0 deletions ci/docker/quack-server/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;
70 changes: 60 additions & 10 deletions src/adbc_driver_quack.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<char>(std::tolower(static_cast<unsigned char>(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) {
Expand All @@ -645,17 +667,9 @@ AdbcStatusCode RunDuckDbQueryAllowNotFound(ConnectionState* state,
result_error != nullptr ? result_error : "DuckDB query failed";
auto const error_type =
static_cast<int32_t>(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<char>(std::tolower(static_cast<unsigned char>(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);
Expand All @@ -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<std::string>* definitions,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
27 changes: 10 additions & 17 deletions tests/exported_symbols_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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",
};

Expand Down Expand Up @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions validation/tests/quack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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(
Expand Down
Loading