drizzle_core/
prepared.rs

1pub mod owned;
2
3use crate::prelude::*;
4use crate::{
5    Param, ParamBind, SQL, SQLChunk, ToSQL, prepared::owned::OwnedPreparedStatement,
6    traits::SQLParam,
7};
8use compact_str::CompactString;
9use core::fmt;
10use smallvec::SmallVec;
11
12/// A pre-rendered SQL statement with parameter placeholders
13/// Structure: [text, param, text, param, text] where text segments
14/// are pre-rendered and params are placeholders to be bound later
15#[derive(Debug, Clone)]
16pub struct PreparedStatement<'a, V: SQLParam> {
17    /// Pre-rendered text segments
18    pub text_segments: Box<[CompactString]>,
19    /// Parameter placeholders (in order)  
20    pub params: Box<[Param<'a, V>]>,
21}
22
23impl<'a, V: SQLParam> From<OwnedPreparedStatement<V>> for PreparedStatement<'a, V> {
24    fn from(value: OwnedPreparedStatement<V>) -> Self {
25        Self {
26            text_segments: value.text_segments,
27            params: value.params.iter().map(|v| v.clone().into()).collect(),
28        }
29    }
30}
31
32impl<'a, V: SQLParam + core::fmt::Display> core::fmt::Display for PreparedStatement<'a, V> {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        write!(f, "{}", self.to_sql())
35    }
36}
37
38/// Internal helper for binding parameters with optimizations
39/// Returns SQL string and an iterator over bound parameter values
40pub(crate) fn bind_parameters_internal<'a, V, T, P>(
41    text_segments: &[CompactString],
42    params: &[P],
43    param_binds: impl IntoIterator<Item = ParamBind<'a, T>>,
44    param_name_fn: impl Fn(&P) -> Option<&str>,
45    param_value_fn: impl Fn(&P) -> Option<&V>,
46    placeholder_fn: impl Fn(&P) -> String,
47) -> (String, impl Iterator<Item = V>)
48where
49    V: SQLParam + Clone,
50    T: SQLParam + Into<V>,
51{
52    // Collect param binds into HashMap for efficient lookup
53    let param_map: HashMap<&str, V> = param_binds
54        .into_iter()
55        .map(|p| (p.name, p.value.into()))
56        .collect();
57
58    // Pre-allocate string capacity based on text segments total length
59    let estimated_capacity: usize =
60        text_segments.iter().map(|s| s.len()).sum::<usize>() + params.len() * 8;
61    let mut sql = String::with_capacity(estimated_capacity);
62    let mut bound_params = SmallVec::<[V; 8]>::new_const();
63
64    // Use iterator to avoid bounds checking
65    let mut param_iter = params.iter();
66
67    for text_segment in text_segments {
68        sql.push_str(text_segment);
69
70        if let Some(param) = param_iter.next() {
71            // Always add the placeholder
72            sql.push_str(&placeholder_fn(param));
73
74            // For parameters, prioritize internal values first, then external bindings
75            if let Some(value) = param_value_fn(param) {
76                // Use internal parameter value (from prepared statement)
77                bound_params.push(value.clone());
78            } else if let Some(name) = param_name_fn(param) {
79                // If no internal value, try external binding for named parameters
80                if let Some(value) = param_map.get(name) {
81                    bound_params.push(value.clone());
82                }
83            }
84        }
85    }
86
87    // Note: We don't add unmatched external parameters as they should only be used
88    // for parameters that have corresponding placeholders in the SQL
89
90    (sql, bound_params.into_iter())
91}
92
93impl<'a, V: SQLParam> PreparedStatement<'a, V> {
94    /// Bind parameters and render final SQL string
95    pub fn bind<T: SQLParam + Into<V>>(
96        &self,
97        param_binds: impl IntoIterator<Item = ParamBind<'a, T>>,
98    ) -> (String, impl Iterator<Item = V>) {
99        bind_parameters_internal(
100            &self.text_segments,
101            &self.params,
102            param_binds,
103            |p| p.placeholder.name,
104            |p| p.value.as_ref().map(|v| v.as_ref()),
105            |p| p.placeholder.to_string(),
106        )
107    }
108}
109
110impl<'a, V: SQLParam> ToSQL<'a, V> for PreparedStatement<'a, V> {
111    fn to_sql(&self) -> SQL<'a, V> {
112        // Calculate exact capacity needed: text_segments.len() + params.len()
113        let capacity = self.text_segments.len() + self.params.len();
114        let mut chunks = SmallVec::with_capacity(capacity);
115
116        // Interleave text segments and params: text[0], param[0], text[1], param[1], ..., text[n]
117        // Use iterators to avoid bounds checking and minimize allocations
118        let mut param_iter = self.params.iter();
119
120        for text_segment in &self.text_segments {
121            chunks.push(SQLChunk::Raw(Cow::Owned(text_segment.to_string())));
122
123            // Add corresponding param if available
124            if let Some(param) = param_iter.next() {
125                chunks.push(SQLChunk::Param(param.clone()));
126            }
127        }
128
129        SQL { chunks }
130    }
131}
132/// Pre-render SQL by processing chunks and separating text from parameters
133pub fn prepare_render<'a, V: SQLParam>(sql: SQL<'a, V>) -> PreparedStatement<'a, V> {
134    let mut text_segments = Vec::new();
135    let mut params = Vec::new();
136    let mut current_text = String::new();
137
138    for (i, chunk) in sql.chunks.iter().enumerate() {
139        match chunk {
140            SQLChunk::Param(param) => {
141                text_segments.push(CompactString::new(&current_text));
142                current_text.clear();
143                params.push(param.clone());
144            }
145            _ => {
146                sql.write_chunk_to(&mut current_text, chunk, i);
147            }
148        }
149
150        // Add space if needed between chunks (matching chunk_needs_space logic)
151        if let Some(next) = sql.chunks.get(i + 1) {
152            // Check if we need spacing between these chunks
153            let needs_space = chunk_needs_space_for_prepare(chunk, next, &current_text);
154            if needs_space {
155                current_text.push(' ');
156            }
157        }
158    }
159
160    text_segments.push(CompactString::new(&current_text));
161
162    PreparedStatement {
163        text_segments: text_segments.into_boxed_slice(),
164        params: params.into_boxed_slice(),
165    }
166}
167
168/// Check if space is needed between chunks during prepare_render
169fn chunk_needs_space_for_prepare<V: SQLParam>(
170    current: &SQLChunk<'_, V>,
171    next: &SQLChunk<'_, V>,
172    current_text: &str,
173) -> bool {
174    // No space if current text already ends with space
175    if current_text.ends_with(' ') {
176        return false;
177    }
178
179    // No space if next raw text starts with space
180    if let SQLChunk::Raw(text) = next
181        && text.starts_with(' ')
182    {
183        return false;
184    }
185
186    // Space between word-like chunks
187    current.is_word_like() && next.is_word_like()
188}