1use crate::built_query::BuiltQuery;
7use crate::dialect::Dialect;
8use crate::filter::{Filter, Op};
9use crate::value::Value;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Order {
14 Asc,
15 Desc,
16}
17
18pub struct QueryBuilder<'d> {
41 dialect: &'d dyn Dialect,
42 table: String,
43 kind: QueryKind,
44 columns: Vec<String>,
45 filters: Vec<Filter>,
46 order_by: Vec<(String, Order)>,
47 limit: Option<u64>,
48 offset: Option<u64>,
49 set_values: Vec<(String, Value)>,
51 returning: Vec<String>,
53}
54
55#[derive(Debug, Clone, Copy)]
56enum QueryKind {
57 Select,
58 Insert,
59 Update,
60 Delete,
61}
62
63impl<'d> QueryBuilder<'d> {
64 pub fn select(dialect: &'d dyn Dialect, table: &str) -> Self {
66 Self::new(dialect, table, QueryKind::Select)
67 }
68
69 pub fn insert(dialect: &'d dyn Dialect, table: &str) -> Self {
71 Self::new(dialect, table, QueryKind::Insert)
72 }
73
74 pub fn update(dialect: &'d dyn Dialect, table: &str) -> Self {
76 Self::new(dialect, table, QueryKind::Update)
77 }
78
79 pub fn delete(dialect: &'d dyn Dialect, table: &str) -> Self {
81 Self::new(dialect, table, QueryKind::Delete)
82 }
83
84 fn new(dialect: &'d dyn Dialect, table: &str, kind: QueryKind) -> Self {
85 Self {
86 dialect,
87 table: table.to_owned(),
88 kind,
89 columns: Vec::new(),
90 filters: Vec::new(),
91 order_by: Vec::new(),
92 limit: None,
93 offset: None,
94 set_values: Vec::new(),
95 returning: Vec::new(),
96 }
97 }
98
99 pub fn columns(mut self, cols: &[&str]) -> Self {
101 self.columns = cols.iter().map(|c| (*c).to_owned()).collect();
102 self
103 }
104
105 pub fn filter(mut self, f: Filter) -> Self {
107 self.filters.push(f);
108 self
109 }
110
111 pub fn order_by(mut self, column: &str, order: Order) -> Self {
113 self.order_by.push((column.to_owned(), order));
114 self
115 }
116
117 pub fn limit(mut self, n: u64) -> Self {
119 self.limit = Some(n);
120 self
121 }
122
123 pub fn offset(mut self, n: u64) -> Self {
125 self.offset = Some(n);
126 self
127 }
128
129 pub fn set(mut self, column: &str, value: impl Into<Value>) -> Self {
131 self.set_values.push((column.to_owned(), value.into()));
132 self
133 }
134
135 pub fn returning(mut self, cols: &[&str]) -> Self {
138 self.returning = cols.iter().map(|c| (*c).to_owned()).collect();
139 self
140 }
141
142 pub fn build(self) -> BuiltQuery {
144 match self.kind {
145 QueryKind::Select => self.build_select(),
146 QueryKind::Insert => self.build_insert(),
147 QueryKind::Update => self.build_update(),
148 QueryKind::Delete => self.build_delete(),
149 }
150 }
151
152 fn build_select(self) -> BuiltQuery {
153 let mut values = Vec::new();
154 let mut param_index = 1usize;
155
156 let cols = if self.columns.is_empty() {
157 "*".to_owned()
158 } else {
159 self.columns
160 .iter()
161 .map(|c| self.dialect.quote_identifier(c))
162 .collect::<Vec<_>>()
163 .join(", ")
164 };
165
166 let table = self.dialect.quote_identifier(&self.table);
167 let mut sql = format!("SELECT {cols} FROM {table}");
168
169 let where_clause = self.build_where(&mut values, &mut param_index);
170 if !where_clause.is_empty() {
171 sql.push_str(" WHERE ");
172 sql.push_str(&where_clause);
173 }
174
175 if !self.order_by.is_empty() {
176 sql.push_str(" ORDER BY ");
177 let orders: Vec<String> = self
178 .order_by
179 .iter()
180 .map(|(col, ord)| {
181 let dir = match ord {
182 Order::Asc => "ASC",
183 Order::Desc => "DESC",
184 };
185 format!("{} {}", self.dialect.quote_identifier(col), dir)
186 })
187 .collect();
188 sql.push_str(&orders.join(", "));
189 }
190
191 if let Some(limit) = self.limit {
192 use std::fmt::Write;
193 let _ = write!(sql, " LIMIT {limit}");
194 }
195
196 if let Some(offset) = self.offset {
197 use std::fmt::Write;
198 let _ = write!(sql, " OFFSET {offset}");
199 }
200
201 BuiltQuery { sql, values }
202 }
203
204 fn build_insert(self) -> BuiltQuery {
205 let mut values = Vec::new();
206
207 let table = self.dialect.quote_identifier(&self.table);
208
209 let mut col_names = Vec::new();
210 let mut placeholders = Vec::new();
211
212 for (param_index, (col, val)) in (1usize..).zip(self.set_values.iter()) {
213 col_names.push(self.dialect.quote_identifier(col));
214 placeholders.push(self.dialect.bind_parameter(param_index));
215 values.push(val.clone());
216 }
217
218 let mut sql = format!(
219 "INSERT INTO {} ({}) VALUES ({})",
220 table,
221 col_names.join(", "),
222 placeholders.join(", "),
223 );
224
225 if self.dialect.supports_returning() && !self.returning.is_empty() {
226 let ret_cols: Vec<String> = self
227 .returning
228 .iter()
229 .map(|c| self.dialect.quote_identifier(c))
230 .collect();
231 sql.push_str(" RETURNING ");
232 sql.push_str(&ret_cols.join(", "));
233 }
234
235 BuiltQuery { sql, values }
236 }
237
238 fn build_update(self) -> BuiltQuery {
239 let mut values = Vec::new();
240 let mut param_index = 1usize;
241
242 let table = self.dialect.quote_identifier(&self.table);
243
244 let mut assignments = Vec::new();
245 for (col, val) in &self.set_values {
246 let placeholder = self.dialect.bind_parameter(param_index);
247 assignments.push(format!(
248 "{} = {}",
249 self.dialect.quote_identifier(col),
250 placeholder
251 ));
252 values.push(val.clone());
253 param_index += 1;
254 }
255
256 let mut sql = format!("UPDATE {} SET {}", table, assignments.join(", "));
257
258 let where_clause = self.build_where(&mut values, &mut param_index);
259 if !where_clause.is_empty() {
260 sql.push_str(" WHERE ");
261 sql.push_str(&where_clause);
262 }
263
264 if self.dialect.supports_returning() && !self.returning.is_empty() {
265 let ret_cols: Vec<String> = self
266 .returning
267 .iter()
268 .map(|c| self.dialect.quote_identifier(c))
269 .collect();
270 sql.push_str(" RETURNING ");
271 sql.push_str(&ret_cols.join(", "));
272 }
273
274 BuiltQuery { sql, values }
275 }
276
277 fn build_delete(self) -> BuiltQuery {
278 let mut values = Vec::new();
279 let mut param_index = 1usize;
280
281 let table = self.dialect.quote_identifier(&self.table);
282 let mut sql = format!("DELETE FROM {table}");
283
284 let where_clause = self.build_where(&mut values, &mut param_index);
285 if !where_clause.is_empty() {
286 sql.push_str(" WHERE ");
287 sql.push_str(&where_clause);
288 }
289
290 if self.dialect.supports_returning() && !self.returning.is_empty() {
291 let ret_cols: Vec<String> = self
292 .returning
293 .iter()
294 .map(|c| self.dialect.quote_identifier(c))
295 .collect();
296 sql.push_str(" RETURNING ");
297 sql.push_str(&ret_cols.join(", "));
298 }
299
300 BuiltQuery { sql, values }
301 }
302
303 #[expect(
306 clippy::unreachable,
307 reason = "In/NotIn/IsNull/IsNotNull variants are handled by arms above; remaining ops are exhaustive"
308 )]
309 fn build_where(&self, values: &mut Vec<Value>, param_index: &mut usize) -> String {
310 if self.filters.is_empty() {
311 return String::new();
312 }
313
314 let mut parts = Vec::new();
315
316 for filter in &self.filters {
317 let quoted_col = self.dialect.quote_identifier(&filter.column);
318
319 let part = match filter.op {
320 Op::IsNull => format!("{quoted_col} IS NULL"),
321 Op::IsNotNull => format!("{quoted_col} IS NOT NULL"),
322 Op::In => {
323 let placeholders: Vec<String> = filter
324 .values
325 .iter()
326 .map(|v| {
327 let p = self.dialect.bind_parameter(*param_index);
328 values.push(v.clone());
329 *param_index += 1;
330 p
331 })
332 .collect();
333 format!("{} IN ({})", quoted_col, placeholders.join(", "))
334 }
335 op => {
336 let sql_op = match op {
337 Op::Eq => "=",
338 Op::Ne => "<>",
339 Op::Lt => "<",
340 Op::Le => "<=",
341 Op::Gt => ">",
342 Op::Ge => ">=",
343 Op::Like => "LIKE",
344 _ => unreachable!(),
345 };
346 let placeholder = self.dialect.bind_parameter(*param_index);
347 if let Some(v) = filter.values.first() {
348 values.push(v.clone());
349 }
350 *param_index += 1;
351 format!("{quoted_col} {sql_op} {placeholder}")
352 }
353 };
354
355 parts.push(part);
356 }
357
358 parts.join(" AND ")
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365 use crate::dialect::{MySqlDialect, PostgresDialect, SqliteDialect};
366
367 #[test]
370 fn select_all_columns() {
371 let d = SqliteDialect;
372 let q = QueryBuilder::select(&d, "users").build();
373 assert_eq!(q.sql, "SELECT * FROM \"users\"");
374 assert!(q.values.is_empty());
375 }
376
377 #[test]
378 fn select_specific_columns() {
379 let d = PostgresDialect;
380 let q = QueryBuilder::select(&d, "users")
381 .columns(&["id", "name"])
382 .build();
383 assert_eq!(q.sql, "SELECT \"id\", \"name\" FROM \"users\"");
384 }
385
386 #[test]
387 fn select_with_eq_filter_postgres() {
388 let d = PostgresDialect;
389 let q = QueryBuilder::select(&d, "users")
390 .columns(&["id", "name"])
391 .filter(Filter::new("name", Op::Eq, "Alice"))
392 .build();
393 assert_eq!(
394 q.sql,
395 "SELECT \"id\", \"name\" FROM \"users\" WHERE \"name\" = $1"
396 );
397 assert_eq!(q.values, vec![Value::Text("Alice".into())]);
398 }
399
400 #[test]
401 fn select_with_eq_filter_sqlite() {
402 let d = SqliteDialect;
403 let q = QueryBuilder::select(&d, "users")
404 .columns(&["id", "name"])
405 .filter(Filter::new("name", Op::Eq, "Alice"))
406 .build();
407 assert_eq!(
408 q.sql,
409 "SELECT \"id\", \"name\" FROM \"users\" WHERE \"name\" = ?"
410 );
411 assert_eq!(q.values, vec![Value::Text("Alice".into())]);
412 }
413
414 #[test]
415 fn select_with_eq_filter_mysql() {
416 let d = MySqlDialect;
417 let q = QueryBuilder::select(&d, "users")
418 .columns(&["id", "name"])
419 .filter(Filter::new("name", Op::Eq, "Alice"))
420 .build();
421 assert_eq!(q.sql, "SELECT `id`, `name` FROM `users` WHERE `name` = ?");
422 }
423
424 #[test]
425 fn select_with_multiple_filters() {
426 let d = PostgresDialect;
427 let q = QueryBuilder::select(&d, "users")
428 .filter(Filter::new("age", Op::Ge, 18))
429 .filter(Filter::new("active", Op::Eq, true))
430 .build();
431 assert_eq!(
432 q.sql,
433 "SELECT * FROM \"users\" WHERE \"age\" >= $1 AND \"active\" = $2"
434 );
435 assert_eq!(q.values, vec![Value::I32(18), Value::Bool(true)]);
436 }
437
438 #[test]
439 fn select_with_in_filter() {
440 let d = PostgresDialect;
441 let q = QueryBuilder::select(&d, "users")
442 .filter(Filter::in_list(
443 "id",
444 vec![Value::I64(1), Value::I64(2), Value::I64(3)],
445 ))
446 .build();
447 assert_eq!(
448 q.sql,
449 "SELECT * FROM \"users\" WHERE \"id\" IN ($1, $2, $3)"
450 );
451 assert_eq!(q.values, vec![Value::I64(1), Value::I64(2), Value::I64(3)]);
452 }
453
454 #[test]
455 fn select_with_is_null() {
456 let d = SqliteDialect;
457 let q = QueryBuilder::select(&d, "users")
458 .filter(Filter::is_null("deleted_at"))
459 .build();
460 assert_eq!(
461 q.sql,
462 "SELECT * FROM \"users\" WHERE \"deleted_at\" IS NULL"
463 );
464 assert!(q.values.is_empty());
465 }
466
467 #[test]
468 fn select_with_is_not_null() {
469 let d = PostgresDialect;
470 let q = QueryBuilder::select(&d, "users")
471 .filter(Filter::is_not_null("email"))
472 .build();
473 assert_eq!(q.sql, "SELECT * FROM \"users\" WHERE \"email\" IS NOT NULL");
474 }
475
476 #[test]
477 fn select_with_like() {
478 let d = SqliteDialect;
479 let q = QueryBuilder::select(&d, "users")
480 .filter(Filter::new("name", Op::Like, "%alice%"))
481 .build();
482 assert_eq!(q.sql, "SELECT * FROM \"users\" WHERE \"name\" LIKE ?");
483 assert_eq!(q.values, vec![Value::Text("%alice%".into())]);
484 }
485
486 #[test]
487 fn select_with_order_by() {
488 let d = PostgresDialect;
489 let q = QueryBuilder::select(&d, "users")
490 .order_by("name", Order::Asc)
491 .order_by("id", Order::Desc)
492 .build();
493 assert_eq!(
494 q.sql,
495 "SELECT * FROM \"users\" ORDER BY \"name\" ASC, \"id\" DESC"
496 );
497 }
498
499 #[test]
500 fn select_with_limit_offset() {
501 let d = SqliteDialect;
502 let q = QueryBuilder::select(&d, "posts")
503 .limit(10)
504 .offset(20)
505 .build();
506 assert_eq!(q.sql, "SELECT * FROM \"posts\" LIMIT 10 OFFSET 20");
507 }
508
509 #[test]
510 fn select_full_query_postgres() {
511 let d = PostgresDialect;
512 let q = QueryBuilder::select(&d, "users")
513 .columns(&["id", "name", "email"])
514 .filter(Filter::new("active", Op::Eq, true))
515 .filter(Filter::new("age", Op::Gt, 18))
516 .order_by("name", Order::Asc)
517 .limit(25)
518 .offset(50)
519 .build();
520 assert_eq!(
521 q.sql,
522 "SELECT \"id\", \"name\", \"email\" FROM \"users\" WHERE \"active\" = $1 AND \"age\" > $2 ORDER BY \"name\" ASC LIMIT 25 OFFSET 50"
523 );
524 assert_eq!(q.values, vec![Value::Bool(true), Value::I32(18)]);
525 }
526
527 #[test]
530 fn insert_sqlite() {
531 let d = SqliteDialect;
532 let q = QueryBuilder::insert(&d, "users")
533 .set("name", "Alice")
534 .set("email", "alice@example.com")
535 .returning(&["id"])
536 .build();
537 assert_eq!(
538 q.sql,
539 "INSERT INTO \"users\" (\"name\", \"email\") VALUES (?, ?) RETURNING \"id\""
540 );
541 assert_eq!(
542 q.values,
543 vec![
544 Value::Text("Alice".into()),
545 Value::Text("alice@example.com".into()),
546 ]
547 );
548 }
549
550 #[test]
551 fn insert_postgres() {
552 let d = PostgresDialect;
553 let q = QueryBuilder::insert(&d, "users")
554 .set("name", "Bob")
555 .set("age", 30)
556 .returning(&["id"])
557 .build();
558 assert_eq!(
559 q.sql,
560 "INSERT INTO \"users\" (\"name\", \"age\") VALUES ($1, $2) RETURNING \"id\""
561 );
562 assert_eq!(q.values, vec![Value::Text("Bob".into()), Value::I32(30)]);
563 }
564
565 #[test]
566 fn insert_mysql_no_returning() {
567 let d = MySqlDialect;
568 let q = QueryBuilder::insert(&d, "users")
569 .set("name", "Charlie")
570 .returning(&["id"])
571 .build();
572 assert_eq!(q.sql, "INSERT INTO `users` (`name`) VALUES (?)");
574 assert_eq!(q.values, vec![Value::Text("Charlie".into())]);
575 }
576
577 #[test]
580 fn update_postgres() {
581 let d = PostgresDialect;
582 let q = QueryBuilder::update(&d, "users")
583 .set("name", "Updated")
584 .set("age", 25)
585 .filter(Filter::new("id", Op::Eq, 1i64))
586 .build();
587 assert_eq!(
588 q.sql,
589 "UPDATE \"users\" SET \"name\" = $1, \"age\" = $2 WHERE \"id\" = $3"
590 );
591 assert_eq!(
592 q.values,
593 vec![Value::Text("Updated".into()), Value::I32(25), Value::I64(1),]
594 );
595 }
596
597 #[test]
598 fn update_sqlite() {
599 let d = SqliteDialect;
600 let q = QueryBuilder::update(&d, "users")
601 .set("name", "Updated")
602 .filter(Filter::new("id", Op::Eq, 1i64))
603 .returning(&["id", "name"])
604 .build();
605 assert_eq!(
606 q.sql,
607 "UPDATE \"users\" SET \"name\" = ? WHERE \"id\" = ? RETURNING \"id\", \"name\""
608 );
609 }
610
611 #[test]
612 fn update_mysql() {
613 let d = MySqlDialect;
614 let q = QueryBuilder::update(&d, "users")
615 .set("active", false)
616 .filter(Filter::new("id", Op::Eq, 42i64))
617 .build();
618 assert_eq!(q.sql, "UPDATE `users` SET `active` = ? WHERE `id` = ?");
619 assert_eq!(q.values, vec![Value::Bool(false), Value::I64(42)]);
620 }
621
622 #[test]
625 fn delete_postgres() {
626 let d = PostgresDialect;
627 let q = QueryBuilder::delete(&d, "users")
628 .filter(Filter::new("id", Op::Eq, 1i64))
629 .build();
630 assert_eq!(q.sql, "DELETE FROM \"users\" WHERE \"id\" = $1");
631 assert_eq!(q.values, vec![Value::I64(1)]);
632 }
633
634 #[test]
635 fn delete_sqlite() {
636 let d = SqliteDialect;
637 let q = QueryBuilder::delete(&d, "posts")
638 .filter(Filter::new("id", Op::Eq, 5i64))
639 .returning(&["id"])
640 .build();
641 assert_eq!(
642 q.sql,
643 "DELETE FROM \"posts\" WHERE \"id\" = ? RETURNING \"id\""
644 );
645 }
646
647 #[test]
648 fn delete_mysql_no_returning() {
649 let d = MySqlDialect;
650 let q = QueryBuilder::delete(&d, "posts")
651 .filter(Filter::new("id", Op::Eq, 5i64))
652 .returning(&["id"])
653 .build();
654 assert_eq!(q.sql, "DELETE FROM `posts` WHERE `id` = ?");
656 }
657
658 #[test]
659 fn delete_all_rows() {
660 let d = SqliteDialect;
661 let q = QueryBuilder::delete(&d, "logs").build();
662 assert_eq!(q.sql, "DELETE FROM \"logs\"");
663 assert!(q.values.is_empty());
664 }
665
666 #[test]
669 fn postgres_bind_params_numbered_correctly() {
670 let d = PostgresDialect;
671 let q = QueryBuilder::update(&d, "users")
672 .set("name", "A")
673 .set("email", "a@b.com")
674 .set("age", 30)
675 .filter(Filter::new("id", Op::Eq, 1i64))
676 .filter(Filter::new("active", Op::Eq, true))
677 .build();
678 assert_eq!(
679 q.sql,
680 "UPDATE \"users\" SET \"name\" = $1, \"email\" = $2, \"age\" = $3 WHERE \"id\" = $4 AND \"active\" = $5"
681 );
682 assert_eq!(q.values.len(), 5);
683 }
684
685 #[test]
688 fn ne_operator() {
689 let d = PostgresDialect;
690 let q = QueryBuilder::select(&d, "users")
691 .filter(Filter::new("status", Op::Ne, "banned"))
692 .build();
693 assert_eq!(q.sql, "SELECT * FROM \"users\" WHERE \"status\" <> $1");
694 }
695
696 #[test]
697 fn lt_le_gt_ge_operators() {
698 let d = PostgresDialect;
699 let q = QueryBuilder::select(&d, "products")
700 .filter(Filter::new("price", Op::Lt, 100i32))
701 .filter(Filter::new("stock", Op::Le, 50i32))
702 .filter(Filter::new("rating", Op::Gt, 3i32))
703 .filter(Filter::new("reviews", Op::Ge, 10i32))
704 .build();
705 assert_eq!(
706 q.sql,
707 "SELECT * FROM \"products\" WHERE \"price\" < $1 AND \"stock\" <= $2 AND \"rating\" > $3 AND \"reviews\" >= $4"
708 );
709 }
710}