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, SQLiteNamedParams},
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    let mut sqlite_names = SQLiteNamedParams::default();
140
141    for param in params {
142        // SQLite renders a named parameter as `:name` and gives each distinct
143        // name one slot, so only its first occurrence binds a value.
144        if V::DIALECT == crate::dialect::Dialect::SQLite
145            && let Some(name) = param_name_fn(param)
146            && sqlite_names.is_repeat(name)
147        {
148            continue;
149        }
150
151        // For parameters, prioritize internal values first, then external bindings
152        if let Some(value) = param_value_fn(param) {
153            // Use internal parameter value (from prepared statement)
154            bound_params.push(value.clone());
155        } else if let Some(name) = param_name_fn(param) {
156            // If no internal value, try external binding for named parameters
157            if !name.is_empty() {
158                if let Some(value) = param_map.get(name) {
159                    bound_params.push(value.clone());
160                }
161            } else if let Some(value) = positional_iter.next() {
162                bound_params.push(value);
163            }
164        } else if let Some(value) = positional_iter.next() {
165            bound_params.push(value);
166        }
167    }
168
169    Ok(bound_params)
170}
171
172impl<'a, V: SQLParam> PreparedStatement<'a, V> {
173    /// Returns the number of external parameter bindings expected.
174    /// This counts params that need external binding (no pre-set value),
175    /// deduplicating named params since one binding satisfies all uses.
176    #[must_use]
177    pub fn external_param_count(&self) -> usize {
178        let mut named = HashSet::<&str>::new();
179        let mut positional = 0usize;
180        for param in &self.params {
181            if param.value.is_some() {
182                continue;
183            }
184            match param.placeholder.name {
185                Some(name) if !name.is_empty() => {
186                    named.insert(name);
187                }
188                _ => positional += 1,
189            }
190        }
191        named.len() + positional
192    }
193
194    /// Bind parameters and return SQL with dialect-appropriate placeholders.
195    /// Uses `$1, $2, ...` for `PostgreSQL`, `:name` or `?` for `SQLite`, `?` for `MySQL`.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if required parameters are missing or if a named
200    /// placeholder cannot be resolved from the supplied bindings.
201    pub fn bind<T: SQLParam + Into<V>>(
202        &self,
203        param_binds: impl IntoIterator<Item = ParamBind<'a, T>>,
204    ) -> crate::error::Result<(&str, impl Iterator<Item = V>)> {
205        let bound_params = bind_values_internal(
206            &self.params,
207            param_binds,
208            |p| p.placeholder.name,
209            |p| p.value.as_ref().map(core::convert::AsRef::as_ref),
210        )?;
211
212        Ok((self.sql.as_str(), bound_params.into_iter()))
213    }
214
215    /// Returns the fully rendered SQL with placeholders.
216    #[must_use]
217    pub fn sql(&self) -> &str {
218        self.sql.as_str()
219    }
220}
221
222impl<'a, V: SQLParam> ToSQL<'a, V> for PreparedStatement<'a, V> {
223    fn to_sql(&self) -> SQL<'a, V> {
224        // Calculate exact capacity needed: text_segments.len() + params.len()
225        let capacity = self.text_segments.len() + self.params.len();
226        let mut chunks = SmallVec::with_capacity(capacity);
227
228        // Interleave text segments and params: text[0], param[0], text[1], param[1], ..., text[n]
229        // Use iterators to avoid bounds checking and minimize allocations
230        let mut param_iter = self.params.iter();
231
232        for text_segment in &self.text_segments {
233            chunks.push(SQLChunk::Raw(Cow::Owned(text_segment.to_string())));
234
235            // Add corresponding param if available
236            if let Some(param) = param_iter.next() {
237                chunks.push(SQLChunk::Param(param.clone()));
238            }
239        }
240
241        SQL { chunks }
242    }
243}
244/// Pre-render SQL by processing chunks and separating text from parameters
245pub fn prepare_render<'a, V: SQLParam>(sql: &SQL<'a, V>) -> PreparedStatement<'a, V> {
246    use crate::dialect::{Dialect, write_placeholder};
247    use crate::sql::chunk_needs_space;
248
249    #[cfg(feature = "profiling")]
250    crate::drizzle_profile_scope!("prepared", "prepare_render");
251
252    if !sql
253        .chunks
254        .iter()
255        .any(|chunk| matches!(chunk, SQLChunk::Param(_)))
256    {
257        #[cfg(feature = "profiling")]
258        crate::drizzle_profile_scope!("prepared", "prepare_render.no_params");
259        let rendered_sql = CompactString::new(sql.sql());
260        return PreparedStatement {
261            text_segments: vec![rendered_sql.clone()].into_boxed_slice(),
262            params: Vec::new().into_boxed_slice(),
263            sql: rendered_sql,
264        };
265    }
266
267    #[cfg(feature = "profiling")]
268    crate::drizzle_profile_scope!("prepared", "prepare_render.scan");
269    let mut text_segments = Vec::new();
270    let mut params = Vec::new();
271    let mut current_text = String::new();
272    let mut rendered_sql = String::with_capacity(sql.chunks.len().saturating_mul(8).max(64));
273    let mut param_index = 1usize;
274
275    for (i, chunk) in sql.chunks.iter().enumerate() {
276        let current_text_ends_with_space = if let SQLChunk::Param(param) = chunk {
277            text_segments.push(CompactString::new(&current_text));
278            rendered_sql.push_str(&current_text);
279            current_text.clear();
280            params.push(param.clone());
281
282            if let Some(name) = param.placeholder.name
283                && V::DIALECT == Dialect::SQLite
284            {
285                rendered_sql.push(':');
286                rendered_sql.push_str(name);
287            } else {
288                write_placeholder(V::DIALECT, param_index, &mut rendered_sql);
289            }
290            param_index += 1;
291            false
292        } else {
293            sql.write_chunk_to(&mut current_text, chunk, i);
294            matches!(chunk, SQLChunk::Raw(text) if text.ends_with(' '))
295        };
296
297        // Use the canonical spacing logic, with an extra check for trailing spaces
298        // already in the accumulated text buffer
299        if let Some(next) = sql.chunks.get(i + 1)
300            && !current_text_ends_with_space
301            && chunk_needs_space(chunk, next)
302        {
303            current_text.push(' ');
304        }
305    }
306
307    text_segments.push(CompactString::new(&current_text));
308    rendered_sql.push_str(&current_text);
309
310    #[cfg(feature = "profiling")]
311    crate::drizzle_profile_scope!("prepared", "prepare_render.finalize");
312    let text_segments = text_segments.into_boxed_slice();
313    let params = params.into_boxed_slice();
314    let rendered_sql = CompactString::new(rendered_sql);
315
316    PreparedStatement {
317        text_segments,
318        params,
319        sql: rendered_sql,
320    }
321}