oxide_sql_core/builder/
insert.rs1use std::marker::PhantomData;
7
8use super::value::{SqlValue, ToSqlValue};
9
10pub struct NoTable;
14pub struct HasTable;
16pub struct NoValues;
18pub struct HasValues;
20
21pub 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 #[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
50impl<Values> InsertDyn<NoTable, Values> {
52 #[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
64impl<Values> InsertDyn<HasTable, Values> {
66 #[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
74impl InsertDyn<HasTable, NoValues> {
76 #[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 #[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
104impl InsertDyn<HasTable, HasValues> {
106 #[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 #[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 #[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 assert_eq!(sql, "INSERT INTO users (name) VALUES (?)");
224 assert!(matches!(¶ms[0], SqlValue::Text(s) if s == malicious));
226 }
227}