From 99eeb20f23cb891bf4307190410ce287a683e176 Mon Sep 17 00:00:00 2001 From: Chris Jones <190028784+LifeInTheWeeds@users.noreply.github.com> Date: Tue, 22 Sep 2026 09:38:04 -0400 Subject: [PATCH 1/3] fix(duckdb): add wildcard arm for non_exhaustive duckdb::types::Value duckdb 1.10505.0 marked `types::Value` as #[non_exhaustive]. Cargo.toml declares `duckdb = "1.10504.0"`, which caret-resolves forward, so a fresh `cargo build` now fails on the default feature set: error[E0004]: non-exhaustive patterns: `&_` not covered --> src/engine/duckdb/mod.rs:758:11 error: could not compile `plenum` (lib) due to 1 previous error The existing match is complete for every real variant; it only lacks the wildcard the attribute now requires. Renders the Debug form rather than the compiler-suggested `todo!()`. This function runs inside a live MCP server, where an unknown future variant should stay visible in the JSON rather than panicking the process or being silently nulled. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016pWGjWtzGf4UvtgiWqYQyR --- src/engine/duckdb/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/engine/duckdb/mod.rs b/src/engine/duckdb/mod.rs index 3ef9037..e2212ce 100644 --- a/src/engine/duckdb/mod.rs +++ b/src/engine/duckdb/mod.rs @@ -810,6 +810,11 @@ fn duckdb_value_to_json(value: &Value) -> serde_json::Value { Json::Object(obj) } Value::Union(inner) => duckdb_value_to_json(inner), + // `duckdb::types::Value` is #[non_exhaustive] as of duckdb 1.10505.0, so a + // wildcard is required. Rendering the Debug form keeps an unknown future + // variant VISIBLE in the JSON rather than silently nulling it out; the + // compiler's suggested `todo!()` would panic inside a live MCP server. + other => Json::String(format!("{other:?}")), } } From 3aea095acaec4d3e76003bc747c035e95e6f0986 Mon Sep 17 00:00:00 2001 From: Chris Jones <190028784+LifeInTheWeeds@users.noreply.github.com> Date: Tue, 22 Sep 2026 09:38:19 -0400 Subject: [PATCH 2/3] feat(cli): accept `duckdb` for --engine on connect/introspect/query The DuckDB engine was fully implemented but unreachable from the CLI. Three clap `value_parser` arrays hardcoded the engine list: src/main.rs:72 connect src/main.rs:156 introspect src/main.rs:283 query #[arg(long, value_parser = ["postgres", "mysql", "sqlite"])] clap rejects the value before `parse_engine` is ever called, so every downstream layer was dead code on this path: - parse_engine(): "duckdb" => Ok(DatabaseType::DuckDB) - connection builder: DatabaseType::DuckDB => ConnectionConfig::duckdb(file) (the "--file is required for duckdb" message could never fire) - validate_connection / introspect / execute: all dispatch DuckDbEngine - is_read_only_duckdb(): already implemented, REF-41 + REF-42 The interactive path did not have this gate -- `engine_choices` at main.rs:1032 already lists "duckdb" -- so the two entry points disagreed about which engines exist. Verified after the change, against a DuckDB file: connect --test -> {"ok":true,"engine":"duckdb","database_version":"v1.5.5"} --list-tables -> 7 tables returned Read-only enforcement re-checked at runtime; enabling the engine does not widen the write surface. SELECT allowed; DELETE, CTE-hidden DELETE ... RETURNING, SELECT ... INTO, COPY ... TO , INSTALL and ATTACH all rejected with CAPABILITY_VIOLATION. Note for follow-up: the engine list is declared in six places (three value_parser arrays, the interactive `engine_choices` vec, the `parse_engine` match, and `parse_engine`'s error string). Deriving clap::ValueEnum on DatabaseType would collapse all six and make this class of drift impossible. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016pWGjWtzGf4UvtgiWqYQyR --- src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index ec2d7c0..6748983 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,7 +69,7 @@ enum Commands { project_path: Option, /// Database engine - #[arg(long, value_parser = ["postgres", "mysql", "sqlite"])] + #[arg(long, value_parser = ["postgres", "mysql", "sqlite", "duckdb"])] engine: Option, /// Host (postgres/mysql) @@ -108,7 +108,7 @@ enum Commands { #[arg(long)] database: Option, - /// `SQLite` file path + /// `SQLite` / `DuckDB` file path #[arg(long)] file: Option, @@ -153,7 +153,7 @@ enum Commands { project_path: Option, /// Engine override - #[arg(long, value_parser = ["postgres", "mysql", "sqlite"])] + #[arg(long, value_parser = ["postgres", "mysql", "sqlite", "duckdb"])] engine: Option, /// Host override @@ -280,7 +280,7 @@ enum Commands { project_path: Option, /// Engine override - #[arg(long, value_parser = ["postgres", "mysql", "sqlite"])] + #[arg(long, value_parser = ["postgres", "mysql", "sqlite", "duckdb"])] engine: Option, /// Host override From 8407dbe223f9b7728430eee0548101cfaf0aae58 Mon Sep 17 00:00:00 2001 From: Chris Jones <190028784+LifeInTheWeeds@users.noreply.github.com> Date: Tue, 22 Sep 2026 11:44:12 -0400 Subject: [PATCH 3/3] fix(clippy): allow unused_async_trait_impl on the synchronous engine impls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI runs `cargo clippy --all-targets --all-features -- -D warnings` against `dtolnay/rust-toolchain@stable`, with nothing pinning the version. Clippy 0.1.98 (Rust 1.98, 2026-09-01) split `unused_async_trait_impl` out of `unused_async` as a SEPARATE lint, and it now fires 6 times: src/engine/sqlite/mod.rs -- validate_connection, introspect, execute src/engine/duckdb/mod.rs -- validate_connection, introspect, execute That is exactly the 3 `DatabaseEngine` methods in the 2 engines whose drivers are synchronous. `postgres` and `mysql` are unaffected because they genuinely `.await`. The `async` cannot be dropped: `DatabaseEngine` declares these methods as returning futures, so every impl must match that signature regardless of whether its body awaits. Satisfying the lint would mean hand-rolling `impl Future` bodies purely to appease it, which is strictly worse code. ⚠ The lint name matters, and the error text is the clue: `#[allow(clippy::unused_async)]` does NOT cover it, because since 0.1.98 the trait-impl case is its own lint. Both names are listed so the attribute holds either side of that split. This is drift, not regression: clippy passed on main in July against this same code. Nothing in the source changed; the toolchain moved underneath it. ⭐ The durable fix is to pin the clippy toolchain in `.github/workflows/ci.yml` rather than tracking `stable`. As written, any new upstream lint can turn main red with no repo change -- which is exactly what happened here. That is a maintainer call, so it is left out of this PR. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016pWGjWtzGf4UvtgiWqYQyR --- src/engine/duckdb/mod.rs | 8 ++++++++ src/engine/sqlite/mod.rs | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/engine/duckdb/mod.rs b/src/engine/duckdb/mod.rs index e2212ce..0bafbcc 100644 --- a/src/engine/duckdb/mod.rs +++ b/src/engine/duckdb/mod.rs @@ -39,7 +39,13 @@ pub struct DuckDbEngine; /// Default schema used when no `--schema` is provided. const DEFAULT_SCHEMA: &str = "main"; +// `DatabaseEngine` declares these methods as returning futures, so each impl must be +// `async` whether or not its body awaits. The DuckDB driver is synchronous, so all three +// methods here have no `.await` and trip `clippy::unused_async` -- a lint that cannot be +// satisfied without hand-rolling `impl Future` bodies purely to appease it. The +// `postgres` and `mysql` impls are unaffected because their drivers genuinely await. impl DatabaseEngine for DuckDbEngine { + #[allow(clippy::unused_async, clippy::unused_async_trait_impl)] async fn validate_connection(config: &ConnectionConfig) -> Result { let file_path = extract_file_path(config)?; let conn = open_connection(&file_path)?; @@ -60,6 +66,7 @@ impl DatabaseEngine for DuckDbEngine { }) } + #[allow(clippy::unused_async, clippy::unused_async_trait_impl)] async fn introspect( config: &ConnectionConfig, operation: &IntrospectOperation, @@ -97,6 +104,7 @@ impl DatabaseEngine for DuckDbEngine { Ok(result) } + #[allow(clippy::unused_async, clippy::unused_async_trait_impl)] async fn execute( config: &ConnectionConfig, query: &str, diff --git a/src/engine/sqlite/mod.rs b/src/engine/sqlite/mod.rs index 038b50b..94d6d4b 100644 --- a/src/engine/sqlite/mod.rs +++ b/src/engine/sqlite/mod.rs @@ -31,7 +31,13 @@ use crate::error::{PlenumError, Result}; /// `SQLite` database engine implementation pub struct SqliteEngine; +// `DatabaseEngine` declares these methods as returning futures, so each impl must be +// `async` whether or not its body awaits. The SQLite driver is synchronous, so all three +// methods here have no `.await` and trip `clippy::unused_async` -- a lint that cannot be +// satisfied without hand-rolling `impl Future` bodies purely to appease it. The +// `postgres` and `mysql` impls are unaffected because their drivers genuinely await. impl DatabaseEngine for SqliteEngine { + #[allow(clippy::unused_async, clippy::unused_async_trait_impl)] async fn validate_connection(config: &ConnectionConfig) -> Result { // Validate config is for SQLite if config.engine != DatabaseType::SQLite { @@ -74,6 +80,7 @@ impl DatabaseEngine for SqliteEngine { }) } + #[allow(clippy::unused_async, clippy::unused_async_trait_impl)] async fn introspect( config: &ConnectionConfig, operation: &IntrospectOperation, @@ -149,6 +156,7 @@ impl DatabaseEngine for SqliteEngine { Ok(result) } + #[allow(clippy::unused_async, clippy::unused_async_trait_impl)] async fn execute( config: &ConnectionConfig, query: &str,