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#[derive(Debug, Clone)]
16pub struct PreparedStatement<'a, V: SQLParam> {
17 pub text_segments: Box<[CompactString]>,
19 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
38pub(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 let param_map: HashMap<&str, V> = param_binds
54 .into_iter()
55 .map(|p| (p.name, p.value.into()))
56 .collect();
57
58 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 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 sql.push_str(&placeholder_fn(param));
73
74 if let Some(value) = param_value_fn(param) {
76 bound_params.push(value.clone());
78 } else if let Some(name) = param_name_fn(param) {
79 if let Some(value) = param_map.get(name) {
81 bound_params.push(value.clone());
82 }
83 }
84 }
85 }
86
87 (sql, bound_params.into_iter())
91}
92
93impl<'a, V: SQLParam> PreparedStatement<'a, V> {
94 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 let capacity = self.text_segments.len() + self.params.len();
114 let mut chunks = SmallVec::with_capacity(capacity);
115
116 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 if let Some(param) = param_iter.next() {
125 chunks.push(SQLChunk::Param(param.clone()));
126 }
127 }
128
129 SQL { chunks }
130 }
131}
132pub 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(¤t_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 if let Some(next) = sql.chunks.get(i + 1) {
152 let needs_space = chunk_needs_space_for_prepare(chunk, next, ¤t_text);
154 if needs_space {
155 current_text.push(' ');
156 }
157 }
158 }
159
160 text_segments.push(CompactString::new(¤t_text));
161
162 PreparedStatement {
163 text_segments: text_segments.into_boxed_slice(),
164 params: params.into_boxed_slice(),
165 }
166}
167
168fn chunk_needs_space_for_prepare<V: SQLParam>(
170 current: &SQLChunk<'_, V>,
171 next: &SQLChunk<'_, V>,
172 current_text: &str,
173) -> bool {
174 if current_text.ends_with(' ') {
176 return false;
177 }
178
179 if let SQLChunk::Raw(text) = next
181 && text.starts_with(' ')
182 {
183 return false;
184 }
185
186 current.is_word_like() && next.is_word_like()
188}