hinge/infrastructure/duckdb/executor.rs
1use duckdb::Connection;
2use std::sync::{Arc, Mutex};
3
4// ── Error ─────────────────────────────────────────────────────────────────────
5
6#[derive(Debug, thiserror::Error)]
7pub enum DuckDbConnectionError {
8 #[error("invalid DuckDB URL: {0}")]
9 InvalidUrl(String),
10 #[error("failed to open DuckDB database: {0}")]
11 Open(#[from] duckdb::Error),
12}
13
14// ── Executor ──────────────────────────────────────────────────────────────────
15
16/// Executes SQL assets against a DuckDB database.
17///
18/// DuckDB is an embedded, in-process analytical database — no server required.
19/// Ideal for local development, testing, and file-based analytics pipelines.
20///
21/// Asset execution is dispatched to a blocking thread via
22/// `tokio::task::spawn_blocking` since DuckDB's Rust binding is synchronous.
23///
24/// See [`DuckDbExecutor::from_url`] for URL format details.
25pub struct DuckDbExecutor {
26 conn: Arc<Mutex<Connection>>,
27}
28
29impl DuckDbExecutor {
30 /// Build an executor from a connection URL.
31 ///
32 /// Supported formats:
33 /// ```text
34 /// duckdb://:memory: in-memory database (discarded on drop)
35 /// duckdb:///absolute/path.db persistent database at an absolute path
36 /// duckdb://./relative.db persistent database relative to the CWD
37 /// ```
38 pub fn from_url(url: &str) -> Result<Self, DuckDbConnectionError> {
39 let path = url
40 .strip_prefix("duckdb://")
41 .ok_or_else(|| DuckDbConnectionError::InvalidUrl(
42 format!("expected duckdb:// prefix, got '{url}'"),
43 ))?;
44
45 let conn = match path {
46 "" | ":memory:" => Connection::open_in_memory()?,
47 p => Connection::open(p)?,
48 };
49
50 Ok(Self { conn: Arc::new(Mutex::new(conn)) })
51 }
52
53 pub(super) fn conn(&self) -> Arc<Mutex<Connection>> {
54 Arc::clone(&self.conn)
55 }
56}