drizzle_core/prepared/
mod.rs

1mod owned;
2pub use owned::OwnedPreparedStatement;
3
4use crate::prelude::*;
5use crate::{
6    dialect::DialectExt,
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<'a, V: SQLParam> From<OwnedPreparedStatement<V>> for PreparedStatement<'a, 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<'a, V: SQLParam> core::fmt::Display for PreparedStatement<'a, 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) -> impl Iterator<Item = V>
52where
53    V: SQLParam + Clone,
54    T: SQLParam + Into<V>,
55{
56    // Collect param binds into name map and positional list for efficient lookup
57    let mut param_map: HashMap<&str, V> = HashMap::new();
58    let mut positional_params: SmallVec<[V; 8]> = SmallVec::new_const();
59    for bind in param_binds {
60        if bind.name.is_empty() {
61            positional_params.push(bind.value.into());
62        } else {
63            param_map.insert(bind.name, bind.value.into());
64        }
65    }
66    let mut positional_iter = positional_params.into_iter();
67
68    let mut bound_params = SmallVec::<[V; 8]>::new_const();
69
70    for param in params {
71        // For parameters, prioritize internal values first, then external bindings
72        if let Some(value) = param_value_fn(param) {
73            // Use internal parameter value (from prepared statement)
74            bound_params.push(value.clone());
75        } else if let Some(name) = param_name_fn(param) {
76            // If no internal value, try external binding for named parameters
77            if !name.is_empty() {
78                if let Some(value) = param_map.get(name) {
79                    bound_params.push(value.clone());
80                }
81            } else if let Some(value) = positional_iter.next() {
82                bound_params.push(value);
83            }
84        } else if let Some(value) = positional_iter.next() {
85            bound_params.push(value);
86        }
87    }
88
89    // Note: We don't add unmatched external parameters as they should only be used
90    // for parameters that have corresponding placeholders in the SQL
91
92    bound_params.into_iter()
93}
94
95fn render_sql_with_placeholders<P>(
96    text_segments: &[CompactString],
97    params: &[P],
98    placeholder_fn: impl Fn(&P, usize) -> Cow<'static, str>,
99) -> CompactString {
100    // Pre-allocate string capacity based on text segments total length
101    let estimated_capacity: usize =
102        text_segments.iter().map(|s| s.len()).sum::<usize>() + params.len() * 8;
103    let mut sql = String::with_capacity(estimated_capacity);
104
105    // Use iterator to avoid bounds checking
106    let mut param_iter = params.iter().enumerate();
107
108    for text_segment in text_segments {
109        sql.push_str(text_segment);
110
111        if let Some((idx, param)) = param_iter.next() {
112            // Always add the placeholder (idx + 1 for 1-based indexing)
113            sql.push_str(&placeholder_fn(param, idx + 1));
114        }
115    }
116
117    CompactString::new(sql)
118}
119
120impl<'a, V: SQLParam> PreparedStatement<'a, V> {
121    /// Bind parameters and return SQL with dialect-appropriate placeholders.
122    /// Uses `$1, $2, ...` for PostgreSQL, `:name` or `?` for SQLite, `?` for MySQL.
123    pub fn bind<T: SQLParam + Into<V>>(
124        &self,
125        param_binds: impl IntoIterator<Item = ParamBind<'a, T>>,
126    ) -> (&str, impl Iterator<Item = V>) {
127        let bound_params = bind_values_internal(
128            &self.params,
129            param_binds,
130            |p| p.placeholder.name,
131            |p| p.value.as_ref().map(|v| v.as_ref()),
132        );
133
134        (self.sql.as_str(), bound_params)
135    }
136
137    /// Returns the fully rendered SQL with placeholders.
138    pub fn sql(&self) -> &str {
139        self.sql.as_str()
140    }
141}
142
143impl<'a, V: SQLParam> ToSQL<'a, V> for PreparedStatement<'a, V> {
144    fn to_sql(&self) -> SQL<'a, V> {
145        // Calculate exact capacity needed: text_segments.len() + params.len()
146        let capacity = self.text_segments.len() + self.params.len();
147        let mut chunks = SmallVec::with_capacity(capacity);
148
149        // Interleave text segments and params: text[0], param[0], text[1], param[1], ..., text[n]
150        // Use iterators to avoid bounds checking and minimize allocations
151        let mut param_iter = self.params.iter();
152
153        for text_segment in &self.text_segments {
154            chunks.push(SQLChunk::Raw(Cow::Owned(text_segment.to_string())));
155
156            // Add corresponding param if available
157            if let Some(param) = param_iter.next() {
158                chunks.push(SQLChunk::Param(param.clone()));
159            }
160        }
161
162        SQL { chunks }
163    }
164}
165/// Pre-render SQL by processing chunks and separating text from parameters
166pub fn prepare_render<'a, V: SQLParam>(sql: SQL<'a, V>) -> PreparedStatement<'a, V> {
167    use crate::dialect::Dialect;
168
169    let mut text_segments = Vec::new();
170    let mut params = Vec::new();
171    let mut current_text = String::new();
172
173    for (i, chunk) in sql.chunks.iter().enumerate() {
174        match chunk {
175            SQLChunk::Param(param) => {
176                text_segments.push(CompactString::new(&current_text));
177                current_text.clear();
178                params.push(param.clone());
179            }
180            _ => {
181                sql.write_chunk_to(&mut current_text, chunk, i);
182            }
183        }
184
185        // Add space if needed between chunks (matching chunk_needs_space logic)
186        if let Some(next) = sql.chunks.get(i + 1) {
187            // Check if we need spacing between these chunks
188            let needs_space = chunk_needs_space_for_prepare(chunk, next, &current_text);
189            if needs_space {
190                current_text.push(' ');
191            }
192        }
193    }
194
195    text_segments.push(CompactString::new(&current_text));
196
197    let text_segments = text_segments.into_boxed_slice();
198    let params = params.into_boxed_slice();
199    let rendered_sql = render_sql_with_placeholders(&text_segments, &params, |p, idx| {
200        // Named placeholders use :name syntax only for SQLite
201        // PostgreSQL always uses $N, MySQL always uses ?
202        if let Some(name) = p.placeholder.name
203            && V::DIALECT == Dialect::SQLite
204        {
205            Cow::Owned(format!(":{}", name))
206        } else {
207            V::DIALECT.render_placeholder(idx)
208        }
209    });
210
211    PreparedStatement {
212        text_segments,
213        params,
214        sql: rendered_sql,
215    }
216}
217
218/// Check if space is needed between chunks during prepare_render
219fn chunk_needs_space_for_prepare<V: SQLParam>(
220    current: &SQLChunk<'_, V>,
221    next: &SQLChunk<'_, V>,
222    current_text: &str,
223) -> bool {
224    // No space if current text already ends with space
225    if current_text.ends_with(' ') {
226        return false;
227    }
228
229    // No space if next raw text starts with space
230    if let SQLChunk::Raw(text) = next
231        && text.starts_with(' ')
232    {
233        return false;
234    }
235
236    // Space between word-like chunks
237    current.is_word_like() && next.is_word_like()
238}