Skip to main content

drizzle_core/prepared/
owned.rs

1use crate::prelude::*;
2use crate::{
3    OwnedParam, ParamBind, SQL, SQLChunk, ToSQL,
4    prepared::{PreparedStatement, bind_values_internal},
5    traits::SQLParam,
6};
7use compact_str::CompactString;
8use core::fmt;
9use hashbrown::HashMap;
10use smallvec::SmallVec;
11
12/// An owned version of `PreparedStatement` with no lifetime dependencies
13#[derive(Debug, Clone)]
14pub struct OwnedPreparedStatement<V: SQLParam> {
15    /// Pre-rendered text segments
16    pub text_segments: Box<[CompactString]>,
17    /// Parameter placeholders (in order) - only placeholders, no values
18    pub params: Box<[OwnedParam<V>]>,
19    /// Fully rendered SQL with placeholders for this dialect
20    pub sql: CompactString,
21}
22impl<V: SQLParam> core::fmt::Display for OwnedPreparedStatement<V> {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        write!(f, "{}", self.sql())
25    }
26}
27
28impl<'a, V: SQLParam> From<PreparedStatement<'a, V>> for OwnedPreparedStatement<V> {
29    fn from(prepared: PreparedStatement<'a, V>) -> Self {
30        Self {
31            text_segments: prepared.text_segments,
32            params: prepared
33                .params
34                .into_iter()
35                .map(core::convert::Into::into)
36                .collect(),
37            sql: prepared.sql,
38        }
39    }
40}
41
42impl<V: SQLParam> OwnedPreparedStatement<V> {
43    /// Returns the number of external parameter bindings expected.
44    /// This counts params that need external binding (no pre-set value),
45    /// deduplicating named params since one binding satisfies all uses.
46    #[must_use]
47    pub fn external_param_count(&self) -> usize {
48        let mut named = HashMap::<&str, ()>::new();
49        let mut positional = 0usize;
50        for param in &self.params {
51            if param.value.is_some() {
52                continue;
53            }
54            match param.placeholder.name {
55                Some(name) if !name.is_empty() => {
56                    named.entry(name).or_insert(());
57                }
58                _ => positional += 1,
59            }
60        }
61        named.len() + positional
62    }
63
64    /// Bind parameters and return SQL with dialect-appropriate placeholders.
65    /// Uses `$1, $2, ...` for `PostgreSQL`, `?` for SQLite/MySQL.
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if required parameters are missing or if a named
70    /// placeholder cannot be resolved from the supplied bindings.
71    pub fn bind<'a, T: SQLParam + Into<V>>(
72        &self,
73        param_binds: impl IntoIterator<Item = ParamBind<'a, T>>,
74    ) -> crate::error::Result<(&str, impl Iterator<Item = V>)> {
75        let bound_params = bind_values_internal(
76            &self.params,
77            param_binds,
78            |p| p.placeholder.name,
79            |p| p.value.as_ref(), // OwnedParam can store values
80        )?;
81
82        Ok((self.sql.as_str(), bound_params.into_iter()))
83    }
84
85    /// Returns the fully rendered SQL with placeholders.
86    #[must_use]
87    pub fn sql(&self) -> &str {
88        self.sql.as_str()
89    }
90}
91
92impl<'a, V: SQLParam> ToSQL<'a, V> for OwnedPreparedStatement<V> {
93    fn to_sql(&self) -> SQL<'a, V> {
94        // Calculate exact capacity needed: text_segments.len() + params.len()
95        let capacity = self.text_segments.len() + self.params.len();
96        let mut chunks = SmallVec::with_capacity(capacity);
97
98        // Interleave text segments and params: text[0], param[0], text[1], param[1], ..., text[n]
99        // Use iterators to avoid bounds checking and minimize allocations
100        let mut param_iter = self.params.iter();
101
102        for text_segment in &self.text_segments {
103            chunks.push(SQLChunk::Raw(Cow::Owned(text_segment.to_string())));
104
105            // Add corresponding param if available
106            if let Some(param) = param_iter.next() {
107                chunks.push(SQLChunk::Param(param.clone().into()));
108            }
109        }
110
111        SQL { chunks }
112    }
113}