Skip to main content

sql_composer/
driver.rs

1//! Trait interface for database drivers and helpers for bind value resolution.
2//!
3//! Driver crates implement [`ComposerConnection`] (sync) or
4//! [`ComposerConnectionAsync`] (async) for their connection types.
5//! This module contains no database dependencies — only the interface.
6
7use std::collections::BTreeMap;
8
9use crate::composer::{ComposedSql, Composer};
10use crate::error::{Error, Result};
11use crate::types::Template;
12
13/// Trait for synchronous database drivers that can compose and prepare SQL.
14///
15/// Each driver crate implements this for its connection type, providing the
16/// bridge between sql-composer's template system and the database's API.
17///
18/// # Example
19///
20/// ```ignore
21/// let (sql, values) = conn.compose(&composer, &template, bind_values!("id" => [1]))?;
22/// let mut stmt = conn.prepare(&sql)?;
23/// let rows = stmt.query_map(params_from_iter(values.iter().map(|v| v.as_ref())), |row| { ... })?;
24/// ```
25pub trait ComposerConnection {
26    /// The database-specific value type for bind parameters.
27    ///
28    /// e.g. `Box<dyn rusqlite::types::ToSql>` or `Box<dyn duckdb::ToSql>`
29    type Value;
30
31    /// The composed SQL string (callers use this to prepare statements).
32    type Statement;
33
34    /// The error type for this driver.
35    type Error: From<Error>;
36
37    /// Compose a template with bind values, returning prepared SQL and ordered values.
38    ///
39    /// Takes the composer, a parsed template, and a map of named bind values.
40    /// Resolves bind parameter order and returns the SQL string with ordered
41    /// values ready for execution.
42    #[allow(clippy::type_complexity)]
43    fn compose(
44        &self,
45        composer: &Composer,
46        template: &Template,
47        values: BTreeMap<String, Vec<Self::Value>>,
48    ) -> std::result::Result<(Self::Statement, Vec<Self::Value>), Self::Error>;
49}
50
51/// Async version of [`ComposerConnection`] for async database drivers
52/// (e.g. tokio-postgres, mysql_async).
53pub trait ComposerConnectionAsync {
54    /// The database-specific value type for bind parameters.
55    type Value;
56
57    /// The composed SQL string.
58    type Statement;
59
60    /// The error type for this driver.
61    type Error: From<Error>;
62
63    /// Compose a template with bind values asynchronously.
64    #[allow(clippy::type_complexity)]
65    fn compose(
66        &self,
67        composer: &Composer,
68        template: &Template,
69        values: BTreeMap<String, Vec<Self::Value>>,
70    ) -> impl std::future::Future<
71        Output = std::result::Result<(Self::Statement, Vec<Self::Value>), Self::Error>,
72    > + Send;
73}
74
75/// Given a [`ComposedSql`] with ordered bind param names and a map of named values,
76/// produce the ordered value vector matching placeholder order.
77///
78/// For single-value bindings, each name maps to one value.
79/// For multi-value bindings (e.g. IN clauses), the composed SQL already has
80/// the correct number of placeholders per binding name, so we flatten all
81/// values for each occurrence.
82pub fn resolve_values<V>(
83    composed: &ComposedSql,
84    values: &mut BTreeMap<String, Vec<V>>,
85) -> Result<Vec<V>> {
86    let mut result = Vec::with_capacity(composed.bind_params.len());
87
88    for name in &composed.bind_params {
89        let vs = values
90            .get_mut(name)
91            .ok_or_else(|| Error::MissingBinding { name: name.clone() })?;
92
93        if vs.is_empty() {
94            return Err(Error::MissingBinding { name: name.clone() });
95        }
96
97        // Take the first value — each placeholder in bind_params corresponds
98        // to exactly one value. Multi-value bindings have been expanded by
99        // compose_with_values() so each placeholder gets its own entry.
100        result.push(vs.remove(0));
101    }
102
103    Ok(result)
104}
105
106/// Build a `BTreeMap<String, Vec<V>>` of named bind values.
107///
108/// # Example
109///
110/// ```
111/// use sql_composer::bind_values;
112///
113/// let values: std::collections::BTreeMap<String, Vec<i32>> = bind_values!(
114///     "user_id" => [42],
115///     "status" => [1, 2, 3],
116/// );
117/// assert_eq!(values["user_id"], vec![42]);
118/// assert_eq!(values["status"], vec![1, 2, 3]);
119/// ```
120#[macro_export]
121macro_rules! bind_values {
122    ($($key:literal => [$($value:expr),+ $(,)?]),+ $(,)?) => {{
123        let mut map = std::collections::BTreeMap::new();
124        $(
125            map.insert($key.to_string(), vec![$($value),+]);
126        )+
127        map
128    }};
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn test_resolve_values_basic() {
137        let composed = ComposedSql {
138            sql: "SELECT * FROM t WHERE a = $1 AND b = $2".into(),
139            bind_params: vec!["a".into(), "b".into()],
140        };
141        let mut values: BTreeMap<String, Vec<&str>> = BTreeMap::new();
142        values.insert("a".into(), vec!["hello"]);
143        values.insert("b".into(), vec!["world"]);
144
145        let result = resolve_values(&composed, &mut values).unwrap();
146        assert_eq!(result, vec!["hello", "world"]);
147    }
148
149    #[test]
150    fn test_resolve_values_missing_binding() {
151        let composed = ComposedSql {
152            sql: "SELECT * FROM t WHERE a = $1".into(),
153            bind_params: vec!["missing".into()],
154        };
155        let mut values: BTreeMap<String, Vec<&str>> = BTreeMap::new();
156
157        let err = resolve_values(&composed, &mut values).unwrap_err();
158        assert!(matches!(err, Error::MissingBinding { ref name } if name == "missing"));
159    }
160
161    #[test]
162    fn test_resolve_values_multi_value_expanded() {
163        // After compose_with_values, a multi-value binding like ids=[1,2,3]
164        // produces bind_params = ["ids", "ids", "ids"] with placeholders $1, $2, $3.
165        let composed = ComposedSql {
166            sql: "SELECT * FROM t WHERE id IN ($1, $2, $3)".into(),
167            bind_params: vec!["ids".into(), "ids".into(), "ids".into()],
168        };
169        let mut values: BTreeMap<String, Vec<i32>> = BTreeMap::new();
170        values.insert("ids".into(), vec![10, 20, 30]);
171
172        let result = resolve_values(&composed, &mut values).unwrap();
173        assert_eq!(result, vec![10, 20, 30]);
174    }
175
176    #[test]
177    fn test_bind_values_macro() {
178        let values: BTreeMap<String, Vec<i32>> = bind_values!(
179            "a" => [1, 2],
180            "b" => [3],
181        );
182        assert_eq!(values["a"], vec![1, 2]);
183        assert_eq!(values["b"], vec![3]);
184    }
185}