use std::marker::PhantomData;
use super::value::{SqlValue, ToSqlValue};
pub struct NoTable;
pub struct HasTable;
pub struct NoValues;
pub struct HasValues;
pub struct InsertDyn<Table, Values> {
table: Option<String>,
columns: Vec<String>,
values: Vec<Vec<SqlValue>>,
_state: PhantomData<(Table, Values)>,
}
impl InsertDyn<NoTable, NoValues> {
#[must_use]
pub fn new() -> Self {
Self {
table: None,
columns: vec![],
values: vec![],
_state: PhantomData,
}
}
}
impl Default for InsertDyn<NoTable, NoValues> {
fn default() -> Self {
Self::new()
}
}
impl<Values> InsertDyn<NoTable, Values> {
#[must_use]
pub fn into_table(self, table: &str) -> InsertDyn<HasTable, Values> {
InsertDyn {
table: Some(String::from(table)),
columns: self.columns,
values: self.values,
_state: PhantomData,
}
}
}
impl<Values> InsertDyn<HasTable, Values> {
#[must_use]
pub fn columns(mut self, cols: &[&str]) -> Self {
self.columns = cols.iter().map(|s| String::from(*s)).collect();
self
}
}
impl InsertDyn<HasTable, NoValues> {
#[must_use]
pub fn values<T: ToSqlValue>(self, vals: Vec<T>) -> InsertDyn<HasTable, HasValues> {
let sql_values: Vec<SqlValue> = vals.into_iter().map(ToSqlValue::to_sql_value).collect();
InsertDyn {
table: self.table,
columns: self.columns,
values: vec![sql_values],
_state: PhantomData,
}
}
#[must_use]
pub fn values_many<T: ToSqlValue>(self, rows: Vec<Vec<T>>) -> InsertDyn<HasTable, HasValues> {
let sql_rows: Vec<Vec<SqlValue>> = rows
.into_iter()
.map(|row| row.into_iter().map(ToSqlValue::to_sql_value).collect())
.collect();
InsertDyn {
table: self.table,
columns: self.columns,
values: sql_rows,
_state: PhantomData,
}
}
}
impl InsertDyn<HasTable, HasValues> {
#[must_use]
pub fn and_values<T: ToSqlValue>(mut self, vals: Vec<T>) -> Self {
let sql_values: Vec<SqlValue> = vals.into_iter().map(ToSqlValue::to_sql_value).collect();
self.values.push(sql_values);
self
}
#[must_use]
pub fn build(self) -> (String, Vec<SqlValue>) {
let mut sql = String::from("INSERT INTO ");
let mut params = vec![];
if let Some(ref table) = self.table {
sql.push_str(table);
}
if !self.columns.is_empty() {
sql.push_str(" (");
sql.push_str(&self.columns.join(", "));
sql.push(')');
}
sql.push_str(" VALUES ");
let row_strs: Vec<String> = self
.values
.iter()
.map(|row| {
let placeholders: Vec<&str> = row.iter().map(|_| "?").collect();
format!("({})", placeholders.join(", "))
})
.collect();
sql.push_str(&row_strs.join(", "));
for row in self.values {
params.extend(row);
}
(sql, params)
}
#[must_use]
pub fn build_sql(self) -> String {
let (sql, _) = self.build();
sql
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_insert() {
let (sql, params) = InsertDyn::new()
.into_table("users")
.columns(&["name", "email"])
.values(vec!["Alice", "alice@example.com"])
.build();
assert_eq!(sql, "INSERT INTO users (name, email) VALUES (?, ?)");
assert_eq!(params.len(), 2);
}
#[test]
fn test_insert_multiple_rows() {
let (sql, params) = InsertDyn::new()
.into_table("users")
.columns(&["name"])
.values(vec!["Alice"])
.and_values(vec!["Bob"])
.and_values(vec!["Charlie"])
.build();
assert_eq!(sql, "INSERT INTO users (name) VALUES (?), (?), (?)");
assert_eq!(params.len(), 3);
}
#[test]
fn test_insert_without_columns() {
let (sql, params) = InsertDyn::new()
.into_table("users")
.values(vec!["Alice", "alice@example.com"])
.build();
assert_eq!(sql, "INSERT INTO users VALUES (?, ?)");
assert_eq!(params.len(), 2);
}
#[test]
fn test_insert_with_integers() {
let (sql, params) = InsertDyn::new()
.into_table("orders")
.columns(&["user_id", "amount"])
.values(vec![1_i64.to_sql_value(), 100_i64.to_sql_value()])
.build();
assert_eq!(sql, "INSERT INTO orders (user_id, amount) VALUES (?, ?)");
assert_eq!(params.len(), 2);
}
#[test]
fn test_insert_sql_injection_prevention() {
let malicious = "'; DROP TABLE users; --";
let (sql, params) = InsertDyn::new()
.into_table("users")
.columns(&["name"])
.values(vec![malicious])
.build();
assert_eq!(sql, "INSERT INTO users (name) VALUES (?)");
assert!(matches!(¶ms[0], SqlValue::Text(s) if s == malicious));
}
}