oxide_sql_core/builder/
insert.rs

1//! Dynamic INSERT statement builder using the typestate pattern.
2//!
3//! This module provides string-based query building. For compile-time
4//! validated queries using schema traits, use `Insert` from `builder::typed`.
5
6use std::marker::PhantomData;
7
8use super::value::{SqlValue, ToSqlValue};
9
10// Typestate markers
11
12/// Marker: No table specified yet.
13pub struct NoTable;
14/// Marker: Table has been specified.
15pub struct HasTable;
16/// Marker: No values specified yet.
17pub struct NoValues;
18/// Marker: Values have been specified.
19pub struct HasValues;
20
21/// A dynamic INSERT statement builder using string-based column names.
22///
23/// For compile-time validated queries, use `Insert` from `builder::typed`.
24pub struct InsertDyn<Table, Values> {
25    table: Option<String>,
26    columns: Vec<String>,
27    values: Vec<Vec<SqlValue>>,
28    _state: PhantomData<(Table, Values)>,
29}
30
31impl InsertDyn<NoTable, NoValues> {
32    /// Creates a new INSERT builder.
33    #[must_use]
34    pub fn new() -> Self {
35        Self {
36            table: None,
37            columns: vec![],
38            values: vec![],
39            _state: PhantomData,
40        }
41    }
42}
43
44impl Default for InsertDyn<NoTable, NoValues> {
45    fn default() -> Self {
46        Self::new()
47    }
48}
49
50// Transition: NoTable -> HasTable
51impl<Values> InsertDyn<NoTable, Values> {
52    /// Specifies the table to insert into.
53    #[must_use]
54    pub fn into_table(self, table: &str) -> InsertDyn<HasTable, Values> {
55        InsertDyn {
56            table: Some(String::from(table)),
57            columns: self.columns,
58            values: self.values,
59            _state: PhantomData,
60        }
61    }
62}
63
64// Methods available after specifying table
65impl<Values> InsertDyn<HasTable, Values> {
66    /// Specifies the columns to insert into.
67    #[must_use]
68    pub fn columns(mut self, cols: &[&str]) -> Self {
69        self.columns = cols.iter().map(|s| String::from(*s)).collect();
70        self
71    }
72}
73
74// Transition: NoValues -> HasValues
75impl InsertDyn<HasTable, NoValues> {
76    /// Adds a row of values to insert.
77    #[must_use]
78    pub fn values<T: ToSqlValue>(self, vals: Vec<T>) -> InsertDyn<HasTable, HasValues> {
79        let sql_values: Vec<SqlValue> = vals.into_iter().map(ToSqlValue::to_sql_value).collect();
80        InsertDyn {
81            table: self.table,
82            columns: self.columns,
83            values: vec![sql_values],
84            _state: PhantomData,
85        }
86    }
87
88    /// Adds multiple rows of values to insert.
89    #[must_use]
90    pub fn values_many<T: ToSqlValue>(self, rows: Vec<Vec<T>>) -> InsertDyn<HasTable, HasValues> {
91        let sql_rows: Vec<Vec<SqlValue>> = rows
92            .into_iter()
93            .map(|row| row.into_iter().map(ToSqlValue::to_sql_value).collect())
94            .collect();
95        InsertDyn {
96            table: self.table,
97            columns: self.columns,
98            values: sql_rows,
99            _state: PhantomData,
100        }
101    }
102}
103
104// Methods available after adding values
105impl InsertDyn<HasTable, HasValues> {
106    /// Adds another row of values.
107    #[must_use]
108    pub fn and_values<T: ToSqlValue>(mut self, vals: Vec<T>) -> Self {
109        let sql_values: Vec<SqlValue> = vals.into_iter().map(ToSqlValue::to_sql_value).collect();
110        self.values.push(sql_values);
111        self
112    }
113
114    /// Builds the INSERT statement and returns SQL with parameters.
115    #[must_use]
116    pub fn build(self) -> (String, Vec<SqlValue>) {
117        let mut sql = String::from("INSERT INTO ");
118        let mut params = vec![];
119
120        if let Some(ref table) = self.table {
121            sql.push_str(table);
122        }
123
124        if !self.columns.is_empty() {
125            sql.push_str(" (");
126            sql.push_str(&self.columns.join(", "));
127            sql.push(')');
128        }
129
130        sql.push_str(" VALUES ");
131
132        let row_strs: Vec<String> = self
133            .values
134            .iter()
135            .map(|row| {
136                let placeholders: Vec<&str> = row.iter().map(|_| "?").collect();
137                format!("({})", placeholders.join(", "))
138            })
139            .collect();
140
141        sql.push_str(&row_strs.join(", "));
142
143        for row in self.values {
144            params.extend(row);
145        }
146
147        (sql, params)
148    }
149
150    /// Builds the INSERT statement and returns only the SQL string.
151    ///
152    /// **Warning**: Parameters are NOT inlined. Use `build()` to get parameters.
153    #[must_use]
154    pub fn build_sql(self) -> String {
155        let (sql, _) = self.build();
156        sql
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_simple_insert() {
166        let (sql, params) = InsertDyn::new()
167            .into_table("users")
168            .columns(&["name", "email"])
169            .values(vec!["Alice", "alice@example.com"])
170            .build();
171
172        assert_eq!(sql, "INSERT INTO users (name, email) VALUES (?, ?)");
173        assert_eq!(params.len(), 2);
174    }
175
176    #[test]
177    fn test_insert_multiple_rows() {
178        let (sql, params) = InsertDyn::new()
179            .into_table("users")
180            .columns(&["name"])
181            .values(vec!["Alice"])
182            .and_values(vec!["Bob"])
183            .and_values(vec!["Charlie"])
184            .build();
185
186        assert_eq!(sql, "INSERT INTO users (name) VALUES (?), (?), (?)");
187        assert_eq!(params.len(), 3);
188    }
189
190    #[test]
191    fn test_insert_without_columns() {
192        let (sql, params) = InsertDyn::new()
193            .into_table("users")
194            .values(vec!["Alice", "alice@example.com"])
195            .build();
196
197        assert_eq!(sql, "INSERT INTO users VALUES (?, ?)");
198        assert_eq!(params.len(), 2);
199    }
200
201    #[test]
202    fn test_insert_with_integers() {
203        let (sql, params) = InsertDyn::new()
204            .into_table("orders")
205            .columns(&["user_id", "amount"])
206            .values(vec![1_i64.to_sql_value(), 100_i64.to_sql_value()])
207            .build();
208
209        assert_eq!(sql, "INSERT INTO orders (user_id, amount) VALUES (?, ?)");
210        assert_eq!(params.len(), 2);
211    }
212
213    #[test]
214    fn test_insert_sql_injection_prevention() {
215        let malicious = "'; DROP TABLE users; --";
216        let (sql, params) = InsertDyn::new()
217            .into_table("users")
218            .columns(&["name"])
219            .values(vec![malicious])
220            .build();
221
222        // SQL uses parameterized placeholder
223        assert_eq!(sql, "INSERT INTO users (name) VALUES (?)");
224        // Malicious input is safely stored as parameter
225        assert!(matches!(&params[0], SqlValue::Text(s) if s == malicious));
226    }
227}