Skip to main content

sql_composer_duckdb/
lib.rs

1//! DuckDB driver for sql-composer.
2//!
3//! Provides [`DuckDbConnection`], a thin wrapper around [`duckdb::Connection`]
4//! that implements [`ComposerConnection`] for composing SQL templates with bind
5//! values against DuckDB databases.
6//!
7//! # Example
8//!
9//! ```no_run
10//! use sql_composer::composer::Composer;
11//! use sql_composer::driver::ComposerConnection;
12//! use sql_composer::parser::parse_template;
13//! use sql_composer::types::{Dialect, TemplateSource};
14//! use sql_composer::bind_values;
15//! use sql_composer_duckdb::DuckDbConnection;
16//!
17//! let conn = DuckDbConnection::open_in_memory().unwrap();
18//! conn.execute("CREATE TABLE users (id INTEGER, name TEXT)", []).unwrap();
19//!
20//! let input = "SELECT * FROM users WHERE id = :bind(user_id)";
21//! let template = parse_template(input, TemplateSource::Literal("example".into())).unwrap();
22//! let composer = Composer::new(Dialect::Postgres);
23//!
24//! let values = bind_values!("user_id" => [Box::new(1i32) as Box<dyn duckdb::ToSql>]);
25//! let (sql, params) = conn.compose(&composer, &template, values).unwrap();
26//!
27//! let refs: Vec<&dyn duckdb::ToSql> = params.iter().map(|v| v.as_ref()).collect();
28//! let mut stmt = conn.prepare(&sql).unwrap();
29//! let _rows = stmt.query(refs.as_slice()).unwrap();
30//! ```
31
32pub use duckdb;
33
34use std::collections::BTreeMap;
35use std::ops::{Deref, DerefMut};
36
37use sql_composer::composer::Composer;
38use sql_composer::driver::{self, ComposerConnection};
39use sql_composer::types::Template;
40
41/// Error type for sql-composer-duckdb operations.
42#[derive(Debug, thiserror::Error)]
43pub enum Error {
44    /// An error from the sql-composer core.
45    #[error(transparent)]
46    Composer(#[from] sql_composer::Error),
47
48    /// An error from duckdb.
49    #[error(transparent)]
50    DuckDb(#[from] duckdb::Error),
51}
52
53/// A wrapper around [`duckdb::Connection`] that implements [`ComposerConnection`].
54///
55/// Dereferences to the inner `duckdb::Connection`, so all native methods
56/// are available directly.
57pub struct DuckDbConnection(pub duckdb::Connection);
58
59impl DuckDbConnection {
60    /// Open an in-memory DuckDB database.
61    pub fn open_in_memory() -> Result<Self, duckdb::Error> {
62        duckdb::Connection::open_in_memory().map(Self)
63    }
64
65    /// Open a DuckDB database at the given path.
66    pub fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Self, duckdb::Error> {
67        duckdb::Connection::open(path).map(Self)
68    }
69
70    /// Wrap an existing `duckdb::Connection`.
71    pub fn from_connection(conn: duckdb::Connection) -> Self {
72        Self(conn)
73    }
74}
75
76impl Deref for DuckDbConnection {
77    type Target = duckdb::Connection;
78
79    fn deref(&self) -> &Self::Target {
80        &self.0
81    }
82}
83
84impl DerefMut for DuckDbConnection {
85    fn deref_mut(&mut self) -> &mut Self::Target {
86        &mut self.0
87    }
88}
89
90impl ComposerConnection for DuckDbConnection {
91    type Value = Box<dyn duckdb::ToSql>;
92    type Statement = String;
93    type Error = Error;
94
95    fn compose(
96        &self,
97        composer: &Composer,
98        template: &Template,
99        mut values: BTreeMap<String, Vec<Self::Value>>,
100    ) -> Result<(String, Vec<Self::Value>), Error> {
101        let composed = composer.compose_with_values(template, &values)?;
102        let ordered = driver::resolve_values(&composed, &mut values)?;
103        Ok((composed.sql, ordered))
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use sql_composer::bind_values;
111    use sql_composer::parser::parse_template;
112    use sql_composer::types::{Dialect, TemplateSource};
113
114    fn boxed(v: impl duckdb::ToSql + 'static) -> Box<dyn duckdb::ToSql> {
115        Box::new(v)
116    }
117
118    #[test]
119    fn test_compose_and_query() {
120        let conn = DuckDbConnection::open_in_memory().unwrap();
121        conn.execute(
122            "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
123            [],
124        )
125        .unwrap();
126        conn.execute("INSERT INTO users (id, name) VALUES (1, 'Alice')", [])
127            .unwrap();
128        conn.execute("INSERT INTO users (id, name) VALUES (2, 'Bob')", [])
129            .unwrap();
130
131        // DuckDB uses Postgres-style $N placeholders
132        let input = "SELECT id, name FROM users WHERE id = :bind(user_id)";
133        let template = parse_template(input, TemplateSource::Literal("test".into())).unwrap();
134        let composer = Composer::new(Dialect::Postgres);
135
136        let values = bind_values!("user_id" => [boxed(1i32)]);
137        let (sql, params) = conn.compose(&composer, &template, values).unwrap();
138
139        assert_eq!(sql, "SELECT id, name FROM users WHERE id = $1");
140
141        let refs: Vec<&dyn duckdb::ToSql> = params.iter().map(|v| v.as_ref()).collect();
142        let mut stmt = conn.prepare(&sql).unwrap();
143        let rows: Vec<(i32, String)> = stmt
144            .query_map(refs.as_slice(), |row| Ok((row.get(0)?, row.get(1)?)))
145            .unwrap()
146            .collect::<Result<Vec<_>, _>>()
147            .unwrap();
148
149        assert_eq!(rows, vec![(1, "Alice".to_string())]);
150    }
151
152    #[test]
153    fn test_compose_multi_value_in_clause() {
154        let conn = DuckDbConnection::open_in_memory().unwrap();
155        conn.execute(
156            "CREATE TABLE items (id INTEGER PRIMARY KEY, label TEXT NOT NULL)",
157            [],
158        )
159        .unwrap();
160        conn.execute("INSERT INTO items (id, label) VALUES (1, 'a')", [])
161            .unwrap();
162        conn.execute("INSERT INTO items (id, label) VALUES (2, 'b')", [])
163            .unwrap();
164        conn.execute("INSERT INTO items (id, label) VALUES (3, 'c')", [])
165            .unwrap();
166
167        let input = "SELECT id, label FROM items WHERE id IN (:bind(ids)) ORDER BY id";
168        let template = parse_template(input, TemplateSource::Literal("test".into())).unwrap();
169        let composer = Composer::new(Dialect::Postgres);
170
171        let values = bind_values!("ids" => [boxed(1i32), boxed(3i32)]);
172        let (sql, params) = conn.compose(&composer, &template, values).unwrap();
173
174        assert_eq!(
175            sql,
176            "SELECT id, label FROM items WHERE id IN ($1, $2) ORDER BY id"
177        );
178
179        let refs: Vec<&dyn duckdb::ToSql> = params.iter().map(|v| v.as_ref()).collect();
180        let mut stmt = conn.prepare(&sql).unwrap();
181        let rows: Vec<(i32, String)> = stmt
182            .query_map(refs.as_slice(), |row| Ok((row.get(0)?, row.get(1)?)))
183            .unwrap()
184            .collect::<Result<Vec<_>, _>>()
185            .unwrap();
186
187        assert_eq!(rows, vec![(1, "a".to_string()), (3, "c".to_string())]);
188    }
189
190    #[test]
191    fn test_compose_returns_correct_sql() {
192        let conn = DuckDbConnection::open_in_memory().unwrap();
193
194        let input = "SELECT :bind(a) AS col_1, :bind(b) AS col_2";
195        let template = parse_template(input, TemplateSource::Literal("test".into())).unwrap();
196        let composer = Composer::new(Dialect::Postgres);
197
198        let values = bind_values!(
199            "a" => [boxed("hello")],
200            "b" => [boxed("world")]
201        );
202        let (sql, params) = conn.compose(&composer, &template, values).unwrap();
203
204        assert_eq!(sql, "SELECT $1 AS col_1, $2 AS col_2");
205        assert_eq!(params.len(), 2);
206    }
207}