Skip to main content

karbon_framework/db/
builder.rs

1use sqlx::Arguments;
2
3use super::{Db, DbArguments, placeholder};
4use crate::error::AppResult;
5
6// ─────────────────────────────────────────────
7// InsertBuilder
8// ─────────────────────────────────────────────
9
10/// Runtime INSERT query builder for custom inserts.
11///
12/// Accepts either a `&DbPool` or a `&mut Transaction` via `execute()`.
13pub struct InsertBuilder {
14    table: String,
15    columns: Vec<String>,
16    args: DbArguments,
17}
18
19impl InsertBuilder {
20    pub fn into(table: &str) -> Self {
21        Self {
22            table: table.to_string(),
23            columns: Vec::new(),
24            args: DbArguments::default(),
25        }
26    }
27
28    /// Add a column with a bound value
29    pub fn set<T>(mut self, column: &str, value: T) -> Self
30    where
31        T: sqlx::Type<Db> + sqlx::Encode<'static, Db> + Send + 'static,
32    {
33        self.columns.push(column.to_string());
34        self.args.add(value).unwrap();
35        self
36    }
37
38    /// Add a column with an Option value (binds NULL if None)
39    pub fn set_optional<T>(mut self, column: &str, value: Option<T>) -> Self
40    where
41        T: sqlx::Type<Db> + sqlx::Encode<'static, Db> + Send + 'static,
42    {
43        self.columns.push(column.to_string());
44        self.args.add(value).unwrap();
45        self
46    }
47
48    /// Execute the INSERT and return last_insert_id
49    pub async fn execute<'e, E>(self, executor: E) -> AppResult<u64>
50    where
51        E: sqlx::Executor<'e, Database = Db>,
52    {
53        let placeholders: Vec<String> = (1..=self.columns.len())
54            .map(placeholder)
55            .collect();
56
57        #[cfg(feature = "mysql")]
58        let sql = format!(
59            "INSERT INTO {} ({}) VALUES ({})",
60            self.table,
61            self.columns.join(", "),
62            placeholders.join(", ")
63        );
64
65        #[cfg(feature = "postgres")]
66        let sql = format!(
67            "INSERT INTO {} ({}) VALUES ({}) RETURNING id",
68            self.table,
69            self.columns.join(", "),
70            placeholders.join(", ")
71        );
72
73        #[cfg(feature = "mysql")]
74        {
75            let result = sqlx::query_with(&sql, self.args)
76                .execute(executor)
77                .await?;
78            Ok(result.last_insert_id())
79        }
80
81        #[cfg(feature = "postgres")]
82        {
83            use sqlx::Row;
84            let row = sqlx::query_with(&sql, self.args)
85                .fetch_one(executor)
86                .await?;
87            let id: i64 = row.get(0);
88            Ok(id as u64)
89        }
90    }
91}
92
93// ─────────────────────────────────────────────
94// UpdateBuilder
95// ─────────────────────────────────────────────
96
97/// Runtime UPDATE query builder for custom updates.
98///
99/// Accepts either a `&DbPool` or a `&mut Transaction` via `execute()`.
100pub struct UpdateBuilder {
101    table: String,
102    set_clauses: Vec<String>,
103    where_clauses: Vec<String>,
104    args: DbArguments,
105    param_count: usize,
106}
107
108impl UpdateBuilder {
109    pub fn table(table: &str) -> Self {
110        Self {
111            table: table.to_string(),
112            set_clauses: Vec::new(),
113            where_clauses: Vec::new(),
114            args: DbArguments::default(),
115            param_count: 0,
116        }
117    }
118
119    /// SET column = ? with a bound value
120    pub fn set<T>(mut self, column: &str, value: T) -> Self
121    where
122        T: sqlx::Type<Db> + sqlx::Encode<'static, Db> + Send + 'static,
123    {
124        self.param_count += 1;
125        self.set_clauses.push(format!("{} = {}", column, placeholder(self.param_count)));
126        self.args.add(value).unwrap();
127        self
128    }
129
130    /// SET column = <raw SQL expression> (e.g. NOW(), NULL, column + 1)
131    pub fn set_raw(mut self, column: &str, expr: &str) -> Self {
132        self.set_clauses.push(format!("{} = {}", column, expr));
133        self
134    }
135
136    /// SET column = ? only if value is Some, skip if None
137    pub fn set_if<T>(mut self, column: &str, value: Option<T>) -> Self
138    where
139        T: sqlx::Type<Db> + sqlx::Encode<'static, Db> + Send + 'static,
140    {
141        if let Some(v) = value {
142            self.param_count += 1;
143            self.set_clauses.push(format!("{} = {}", column, placeholder(self.param_count)));
144            self.args.add(v).unwrap();
145        }
146        self
147    }
148
149    /// WHERE column = ? with a bound value
150    pub fn where_eq<T>(mut self, column: &str, value: T) -> Self
151    where
152        T: sqlx::Type<Db> + sqlx::Encode<'static, Db> + Send + 'static,
153    {
154        self.param_count += 1;
155        self.where_clauses.push(format!("{} = {}", column, placeholder(self.param_count)));
156        self.args.add(value).unwrap();
157        self
158    }
159
160    /// WHERE <raw SQL> (e.g. "revoked = FALSE", "expires_at > NOW()")
161    pub fn where_raw(mut self, condition: &str) -> Self {
162        self.where_clauses.push(condition.to_string());
163        self
164    }
165
166    /// WHERE column != ? with a bound value
167    pub fn where_ne<T>(mut self, column: &str, value: T) -> Self
168    where
169        T: sqlx::Type<Db> + sqlx::Encode<'static, Db> + Send + 'static,
170    {
171        self.param_count += 1;
172        self.where_clauses.push(format!("{} != {}", column, placeholder(self.param_count)));
173        self.args.add(value).unwrap();
174        self
175    }
176
177    /// Execute the UPDATE and return rows_affected
178    pub async fn execute<'e, E>(self, executor: E) -> AppResult<u64>
179    where
180        E: sqlx::Executor<'e, Database = Db>,
181    {
182        if self.set_clauses.is_empty() {
183            return Ok(0);
184        }
185
186        let where_clause = if self.where_clauses.is_empty() {
187            String::new()
188        } else {
189            format!(" WHERE {}", self.where_clauses.join(" AND "))
190        };
191
192        let sql = format!(
193            "UPDATE {} SET {}{}",
194            self.table,
195            self.set_clauses.join(", "),
196            where_clause,
197        );
198
199        let result = sqlx::query_with(&sql, self.args)
200            .execute(executor)
201            .await?;
202
203        Ok(result.rows_affected())
204    }
205}
206
207// ─────────────────────────────────────────────
208// DeleteBuilder
209// ─────────────────────────────────────────────
210
211/// Runtime DELETE query builder.
212///
213/// Accepts either a `&DbPool` or a `&mut Transaction` via `execute()`.
214pub struct DeleteBuilder {
215    table: String,
216    where_clauses: Vec<String>,
217    args: DbArguments,
218    param_count: usize,
219}
220
221impl DeleteBuilder {
222    pub fn from(table: &str) -> Self {
223        Self {
224            table: table.to_string(),
225            where_clauses: Vec::new(),
226            args: DbArguments::default(),
227            param_count: 0,
228        }
229    }
230
231    /// WHERE column = ?
232    pub fn where_eq<T>(mut self, column: &str, value: T) -> Self
233    where
234        T: sqlx::Type<Db> + sqlx::Encode<'static, Db> + Send + 'static,
235    {
236        self.param_count += 1;
237        self.where_clauses.push(format!("{} = {}", column, placeholder(self.param_count)));
238        self.args.add(value).unwrap();
239        self
240    }
241
242    /// WHERE <raw SQL>
243    pub fn where_raw(mut self, condition: &str) -> Self {
244        self.where_clauses.push(condition.to_string());
245        self
246    }
247
248    /// Execute the DELETE and return rows_affected
249    pub async fn execute<'e, E>(self, executor: E) -> AppResult<u64>
250    where
251        E: sqlx::Executor<'e, Database = Db>,
252    {
253        let where_clause = if self.where_clauses.is_empty() {
254            String::new()
255        } else {
256            format!(" WHERE {}", self.where_clauses.join(" AND "))
257        };
258
259        let sql = format!("DELETE FROM {}{}", self.table, where_clause);
260
261        let result = sqlx::query_with(&sql, self.args)
262            .execute(executor)
263            .await?;
264
265        Ok(result.rows_affected())
266    }
267}
268
269// ─────────────────────────────────────────────
270// CountBuilder
271// ─────────────────────────────────────────────
272
273/// Runtime COUNT query builder.
274///
275/// Accepts either a `&DbPool` or a `&mut Transaction` via `execute()`.
276pub struct CountBuilder {
277    table: String,
278    where_clauses: Vec<String>,
279    args: DbArguments,
280    param_count: usize,
281}
282
283impl CountBuilder {
284    pub fn from(table: &str) -> Self {
285        Self {
286            table: table.to_string(),
287            where_clauses: Vec::new(),
288            args: DbArguments::default(),
289            param_count: 0,
290        }
291    }
292
293    /// WHERE column = ?
294    pub fn where_eq<T>(mut self, column: &str, value: T) -> Self
295    where
296        T: sqlx::Type<Db> + sqlx::Encode<'static, Db> + Send + 'static,
297    {
298        self.param_count += 1;
299        self.where_clauses.push(format!("{} = {}", column, placeholder(self.param_count)));
300        self.args.add(value).unwrap();
301        self
302    }
303
304    /// WHERE column != ?
305    pub fn where_ne<T>(mut self, column: &str, value: T) -> Self
306    where
307        T: sqlx::Type<Db> + sqlx::Encode<'static, Db> + Send + 'static,
308    {
309        self.param_count += 1;
310        self.where_clauses.push(format!("{} != {}", column, placeholder(self.param_count)));
311        self.args.add(value).unwrap();
312        self
313    }
314
315    /// WHERE <raw SQL>
316    pub fn where_raw(mut self, condition: &str) -> Self {
317        self.where_clauses.push(condition.to_string());
318        self
319    }
320
321    /// Execute the COUNT and return the result
322    pub async fn execute<'e, E>(self, executor: E) -> AppResult<i64>
323    where
324        E: sqlx::Executor<'e, Database = Db>,
325    {
326        let where_clause = if self.where_clauses.is_empty() {
327            String::new()
328        } else {
329            format!(" WHERE {}", self.where_clauses.join(" AND "))
330        };
331
332        let sql = format!("SELECT COUNT(*) FROM {}{}", self.table, where_clause);
333
334        let (count,): (i64,) = sqlx::query_as_with(&sql, self.args)
335            .fetch_one(executor)
336            .await?;
337
338        Ok(count)
339    }
340}