mod owned;
pub use owned::OwnedPreparedStatement;
use crate::prelude::*;
use crate::{
dialect::DialectExt,
param::{Param, ParamBind},
sql::{SQL, SQLChunk},
traits::{SQLParam, ToSQL},
};
use compact_str::CompactString;
use core::fmt;
use smallvec::SmallVec;
#[derive(Debug, Clone)]
pub struct PreparedStatement<'a, V: SQLParam> {
pub text_segments: Box<[CompactString]>,
pub params: Box<[Param<'a, V>]>,
pub sql: CompactString,
}
impl<'a, V: SQLParam> From<OwnedPreparedStatement<V>> for PreparedStatement<'a, V> {
fn from(value: OwnedPreparedStatement<V>) -> Self {
Self {
text_segments: value.text_segments,
params: value.params.iter().map(|v| v.clone().into()).collect(),
sql: value.sql,
}
}
}
impl<'a, V: SQLParam> core::fmt::Display for PreparedStatement<'a, V> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.sql())
}
}
pub(crate) fn bind_values_internal<'a, V, T, P>(
params: &[P],
param_binds: impl IntoIterator<Item = ParamBind<'a, T>>,
param_name_fn: impl Fn(&P) -> Option<&str>,
param_value_fn: impl Fn(&P) -> Option<&V>,
) -> impl Iterator<Item = V>
where
V: SQLParam + Clone,
T: SQLParam + Into<V>,
{
let mut param_map: HashMap<&str, V> = HashMap::new();
let mut positional_params: SmallVec<[V; 8]> = SmallVec::new_const();
for bind in param_binds {
if bind.name.is_empty() {
positional_params.push(bind.value.into());
} else {
param_map.insert(bind.name, bind.value.into());
}
}
let mut positional_iter = positional_params.into_iter();
let mut bound_params = SmallVec::<[V; 8]>::new_const();
for param in params {
if let Some(value) = param_value_fn(param) {
bound_params.push(value.clone());
} else if let Some(name) = param_name_fn(param) {
if !name.is_empty() {
if let Some(value) = param_map.get(name) {
bound_params.push(value.clone());
}
} else if let Some(value) = positional_iter.next() {
bound_params.push(value);
}
} else if let Some(value) = positional_iter.next() {
bound_params.push(value);
}
}
bound_params.into_iter()
}
fn render_sql_with_placeholders<P>(
text_segments: &[CompactString],
params: &[P],
placeholder_fn: impl Fn(&P, usize) -> Cow<'static, str>,
) -> CompactString {
let estimated_capacity: usize =
text_segments.iter().map(|s| s.len()).sum::<usize>() + params.len() * 8;
let mut sql = String::with_capacity(estimated_capacity);
let mut param_iter = params.iter().enumerate();
for text_segment in text_segments {
sql.push_str(text_segment);
if let Some((idx, param)) = param_iter.next() {
sql.push_str(&placeholder_fn(param, idx + 1));
}
}
CompactString::new(sql)
}
impl<'a, V: SQLParam> PreparedStatement<'a, V> {
pub fn bind<T: SQLParam + Into<V>>(
&self,
param_binds: impl IntoIterator<Item = ParamBind<'a, T>>,
) -> (&str, impl Iterator<Item = V>) {
let bound_params = bind_values_internal(
&self.params,
param_binds,
|p| p.placeholder.name,
|p| p.value.as_ref().map(|v| v.as_ref()),
);
(self.sql.as_str(), bound_params)
}
pub fn sql(&self) -> &str {
self.sql.as_str()
}
}
impl<'a, V: SQLParam> ToSQL<'a, V> for PreparedStatement<'a, V> {
fn to_sql(&self) -> SQL<'a, V> {
let capacity = self.text_segments.len() + self.params.len();
let mut chunks = SmallVec::with_capacity(capacity);
let mut param_iter = self.params.iter();
for text_segment in &self.text_segments {
chunks.push(SQLChunk::Raw(Cow::Owned(text_segment.to_string())));
if let Some(param) = param_iter.next() {
chunks.push(SQLChunk::Param(param.clone()));
}
}
SQL { chunks }
}
}
pub fn prepare_render<'a, V: SQLParam>(sql: SQL<'a, V>) -> PreparedStatement<'a, V> {
use crate::dialect::Dialect;
use crate::sql::chunk_needs_space;
let mut text_segments = Vec::new();
let mut params = Vec::new();
let mut current_text = String::new();
for (i, chunk) in sql.chunks.iter().enumerate() {
match chunk {
SQLChunk::Param(param) => {
text_segments.push(CompactString::new(¤t_text));
current_text.clear();
params.push(param.clone());
}
_ => {
sql.write_chunk_to(&mut current_text, chunk, i);
}
}
if let Some(next) = sql.chunks.get(i + 1)
&& !current_text.ends_with(' ')
&& chunk_needs_space(chunk, next)
{
current_text.push(' ');
}
}
text_segments.push(CompactString::new(¤t_text));
let text_segments = text_segments.into_boxed_slice();
let params = params.into_boxed_slice();
let rendered_sql = render_sql_with_placeholders(&text_segments, ¶ms, |p, idx| {
if let Some(name) = p.placeholder.name
&& V::DIALECT == Dialect::SQLite
{
Cow::Owned(format!(":{}", name))
} else {
V::DIALECT.render_placeholder(idx)
}
});
PreparedStatement {
text_segments,
params,
sql: rendered_sql,
}
}