Skip to main content

drizzle_core/prepared/
mod.rs

1mod owned;
2pub use owned::OwnedPreparedStatement;
3
4use crate::prelude::*;
5use crate::{
6    error::DrizzleError,
7    param::{Param, ParamBind},
8    sql::{SQL, SQLChunk},
9    traits::{SQLParam, ToSQL},
10};
11use compact_str::CompactString;
12use core::fmt;
13use smallvec::SmallVec;
14
15/// A pre-rendered SQL statement with parameter placeholders
16/// Structure: [text, param, text, param, text] where text segments
17/// are pre-rendered and params are placeholders to be bound later
18#[derive(Debug, Clone)]
19pub struct PreparedStatement<'a, V: SQLParam> {
20    /// Pre-rendered text segments
21    pub text_segments: Box<[CompactString]>,
22    /// Parameter placeholders (in order)
23    pub params: Box<[Param<'a, V>]>,
24    /// Fully rendered SQL with placeholders for this dialect
25    pub sql: CompactString,
26}
27
28impl<V: SQLParam> From<OwnedPreparedStatement<V>> for PreparedStatement<'_, V> {
29    fn from(value: OwnedPreparedStatement<V>) -> Self {
30        Self {
31            text_segments: value.text_segments,
32            params: value.params.iter().map(|v| v.clone().into()).collect(),
33            sql: value.sql,
34        }
35    }
36}
37
38impl<V: SQLParam> core::fmt::Display for PreparedStatement<'_, V> {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        write!(f, "{}", self.sql())
41    }
42}
43
44/// Internal helper for binding parameters with optimizations
45/// Returns the bound parameter values in order.
46pub(crate) fn bind_values_internal<'a, V, T, P>(
47    params: &[P],
48    param_binds: impl IntoIterator<Item = ParamBind<'a, T>>,
49    param_name_fn: impl Fn(&P) -> Option<&str>,
50    param_value_fn: impl Fn(&P) -> Option<&V>,
51) -> crate::error::Result<SmallVec<[V; 8]>>
52where
53    V: SQLParam + Clone,
54    T: SQLParam + Into<V>,
55{
56    #[cfg(feature = "profiling")]
57    crate::drizzle_profile_scope!("prepared", "bind_values_internal");
58    let param_binds = param_binds.into_iter();
59    let (binds_lower, binds_upper) = param_binds.size_hint();
60
61    let mut expected_named = HashMap::<&str, usize>::new();
62    let mut expected_positional = 0usize;
63    for param in params {
64        if param_value_fn(param).is_some() {
65            continue;
66        }
67
68        match param_name_fn(param) {
69            Some(name) if !name.is_empty() => {
70                *expected_named.entry(name).or_insert(0) += 1;
71            }
72            _ => expected_positional += 1,
73        }
74    }
75
76    let mut param_map = HashMap::<&str, V>::with_capacity(expected_named.len().max(binds_lower));
77
78    let mut positional_params: SmallVec<[V; 8]> =
79        SmallVec::with_capacity(binds_upper.unwrap_or(binds_lower));
80
81    for bind in param_binds {
82        if bind.name.is_empty() {
83            positional_params.push(bind.value.into());
84        } else if param_map.insert(bind.name, bind.value.into()).is_some() {
85            return Err(DrizzleError::ParameterError(
86                format!("Duplicate parameter binding: '{}'", bind.name).into(),
87            ));
88        }
89    }
90
91    if positional_params.len() < expected_positional {
92        return Err(DrizzleError::ParameterError(
93            format!(
94                "Missing positional parameter(s): expected {}, got {}",
95                expected_positional,
96                positional_params.len()
97            )
98            .into(),
99        ));
100    }
101    if positional_params.len() > expected_positional {
102        return Err(DrizzleError::ParameterError(
103            format!(
104                "Unexpected positional parameter(s): expected {}, got {}",
105                expected_positional,
106                positional_params.len()
107            )
108            .into(),
109        ));
110    }
111
112    let mut missing_named: SmallVec<[&str; 8]> = expected_named
113        .keys()
114        .filter(|name| !param_map.contains_key(**name))
115        .copied()
116        .collect();
117    if !missing_named.is_empty() {
118        missing_named.sort_unstable();
119        return Err(DrizzleError::ParameterError(
120            format!("Missing named parameter(s): {}", missing_named.join(", ")).into(),
121        ));
122    }
123
124    let mut extra_named: SmallVec<[&str; 8]> = param_map
125        .keys()
126        .filter(|name| !expected_named.contains_key(**name))
127        .copied()
128        .collect();
129    if !extra_named.is_empty() {
130        extra_named.sort_unstable();
131        return Err(DrizzleError::ParameterError(
132            format!("Unexpected named parameter(s): {}", extra_named.join(", ")).into(),
133        ));
134    }
135
136    let mut positional_iter = positional_params.into_iter();
137
138    let mut bound_params = SmallVec::<[V; 8]>::with_capacity(params.len());
139
140    for param in params {
141        // For parameters, prioritize internal values first, then external bindings
142        if let Some(value) = param_value_fn(param) {
143            // Use internal parameter value (from prepared statement)
144            bound_params.push(value.clone());
145        } else if let Some(name) = param_name_fn(param) {
146            // If no internal value, try external binding for named parameters
147            if !name.is_empty() {
148                if let Some(value) = param_map.get(name) {
149                    bound_params.push(value.clone());
150                }
151            } else if let Some(value) = positional_iter.next() {
152                bound_params.push(value);
153            }
154        } else if let Some(value) = positional_iter.next() {
155            bound_params.push(value);
156        }
157    }
158
159    Ok(bound_params)
160}
161
162impl<'a, V: SQLParam> PreparedStatement<'a, V> {
163    /// Returns the number of external parameter bindings expected.
164    /// This counts params that need external binding (no pre-set value),
165    /// deduplicating named params since one binding satisfies all uses.
166    #[must_use]
167    pub fn external_param_count(&self) -> usize {
168        let mut named = HashSet::<&str>::new();
169        let mut positional = 0usize;
170        for param in &self.params {
171            if param.value.is_some() {
172                continue;
173            }
174            match param.placeholder.name {
175                Some(name) if !name.is_empty() => {
176                    named.insert(name);
177                }
178                _ => positional += 1,
179            }
180        }
181        named.len() + positional
182    }
183
184    /// Bind parameters and return SQL with dialect-appropriate placeholders.
185    /// Uses `$1, $2, ...` for `PostgreSQL`, `:name` or `?` for `SQLite`, `?` for `MySQL`.
186    ///
187    /// # Errors
188    ///
189    /// Returns an error if required parameters are missing or if a named
190    /// placeholder cannot be resolved from the supplied bindings.
191    pub fn bind<T: SQLParam + Into<V>>(
192        &self,
193        param_binds: impl IntoIterator<Item = ParamBind<'a, T>>,
194    ) -> crate::error::Result<(&str, impl Iterator<Item = V>)> {
195        let bound_params = bind_values_internal(
196            &self.params,
197            param_binds,
198            |p| p.placeholder.name,
199            |p| p.value.as_ref().map(core::convert::AsRef::as_ref),
200        )?;
201
202        Ok((self.sql.as_str(), bound_params.into_iter()))
203    }
204
205    /// Returns the fully rendered SQL with placeholders.
206    #[must_use]
207    pub fn sql(&self) -> &str {
208        self.sql.as_str()
209    }
210}
211
212impl<'a, V: SQLParam> ToSQL<'a, V> for PreparedStatement<'a, V> {
213    fn to_sql(&self) -> SQL<'a, V> {
214        // Calculate exact capacity needed: text_segments.len() + params.len()
215        let capacity = self.text_segments.len() + self.params.len();
216        let mut chunks = SmallVec::with_capacity(capacity);
217
218        // Interleave text segments and params: text[0], param[0], text[1], param[1], ..., text[n]
219        // Use iterators to avoid bounds checking and minimize allocations
220        let mut param_iter = self.params.iter();
221
222        for text_segment in &self.text_segments {
223            chunks.push(SQLChunk::Raw(Cow::Owned(text_segment.to_string())));
224
225            // Add corresponding param if available
226            if let Some(param) = param_iter.next() {
227                chunks.push(SQLChunk::Param(param.clone()));
228            }
229        }
230
231        SQL { chunks }
232    }
233}
234/// Pre-render SQL by processing chunks and separating text from parameters
235pub fn prepare_render<'a, V: SQLParam>(sql: &SQL<'a, V>) -> PreparedStatement<'a, V> {
236    use crate::dialect::{Dialect, write_placeholder};
237    use crate::sql::chunk_needs_space;
238
239    #[cfg(feature = "profiling")]
240    crate::drizzle_profile_scope!("prepared", "prepare_render");
241
242    if !sql
243        .chunks
244        .iter()
245        .any(|chunk| matches!(chunk, SQLChunk::Param(_)))
246    {
247        #[cfg(feature = "profiling")]
248        crate::drizzle_profile_scope!("prepared", "prepare_render.no_params");
249        let rendered_sql = CompactString::new(sql.sql());
250        return PreparedStatement {
251            text_segments: vec![rendered_sql.clone()].into_boxed_slice(),
252            params: Vec::new().into_boxed_slice(),
253            sql: rendered_sql,
254        };
255    }
256
257    #[cfg(feature = "profiling")]
258    crate::drizzle_profile_scope!("prepared", "prepare_render.scan");
259    let mut text_segments = Vec::new();
260    let mut params = Vec::new();
261    let mut current_text = String::new();
262    let mut rendered_sql = String::with_capacity(sql.chunks.len().saturating_mul(8).max(64));
263    let mut param_index = 1usize;
264
265    for (i, chunk) in sql.chunks.iter().enumerate() {
266        let current_text_ends_with_space = if let SQLChunk::Param(param) = chunk {
267            text_segments.push(CompactString::new(&current_text));
268            rendered_sql.push_str(&current_text);
269            current_text.clear();
270            params.push(param.clone());
271
272            if let Some(name) = param.placeholder.name
273                && V::DIALECT == Dialect::SQLite
274            {
275                rendered_sql.push(':');
276                rendered_sql.push_str(name);
277            } else {
278                write_placeholder(V::DIALECT, param_index, &mut rendered_sql);
279            }
280            param_index += 1;
281            false
282        } else {
283            sql.write_chunk_to(&mut current_text, chunk, i);
284            matches!(chunk, SQLChunk::Raw(text) if text.ends_with(' '))
285        };
286
287        // Use the canonical spacing logic, with an extra check for trailing spaces
288        // already in the accumulated text buffer
289        if let Some(next) = sql.chunks.get(i + 1)
290            && !current_text_ends_with_space
291            && chunk_needs_space(chunk, next)
292        {
293            current_text.push(' ');
294        }
295    }
296
297    text_segments.push(CompactString::new(&current_text));
298    rendered_sql.push_str(&current_text);
299
300    #[cfg(feature = "profiling")]
301    crate::drizzle_profile_scope!("prepared", "prepare_render.finalize");
302    let text_segments = text_segments.into_boxed_slice();
303    let params = params.into_boxed_slice();
304    let rendered_sql = CompactString::new(rendered_sql);
305
306    PreparedStatement {
307        text_segments,
308        params,
309        sql: rendered_sql,
310    }
311}