sql_composer_duckdb/
lib.rs1pub 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#[derive(Debug, thiserror::Error)]
43pub enum Error {
44 #[error(transparent)]
46 Composer(#[from] sql_composer::Error),
47
48 #[error(transparent)]
50 DuckDb(#[from] duckdb::Error),
51}
52
53pub struct DuckDbConnection(pub duckdb::Connection);
58
59impl DuckDbConnection {
60 pub fn open_in_memory() -> Result<Self, duckdb::Error> {
62 duckdb::Connection::open_in_memory().map(Self)
63 }
64
65 pub fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Self, duckdb::Error> {
67 duckdb::Connection::open(path).map(Self)
68 }
69
70 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 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}