1use std::marker::PhantomData;
7
8use crate::condition::{Condition, JoinOp, OrderDir, SqlValue};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum Dialect {
18 #[default]
19 Postgres,
20 Sqlite,
21}
22
23#[derive(Debug, Clone)]
27pub enum Join {
28 Inner(String, String),
30 Left(String, String),
32 Right(String, String),
34 Raw(String),
36}
37
38#[derive(Debug, Clone)]
63pub struct QueryBuilder<T> {
64 table: String,
65 select_cols: Option<Vec<String>>,
66 select_raw: Option<String>,
67 distinct: bool,
68 distinct_on_cols: Vec<String>,
69 joins: Vec<Join>,
70 conditions: Vec<(JoinOp, Condition)>,
71 group_by: Vec<String>,
72 having: Option<String>,
73 order: Vec<(String, OrderDir)>,
74 order_raw: Option<String>,
75 limit_val: Option<usize>,
76 offset_val: Option<usize>,
77 pub use_replica: bool,
80 extra_select_exprs: Vec<String>,
83 _marker: PhantomData<T>,
84}
85
86impl<T> QueryBuilder<T> {
87 pub fn new(table: impl Into<String>) -> Self {
88 Self {
89 table: table.into(),
90 select_cols: None,
91 select_raw: None,
92 distinct: false,
93 distinct_on_cols: Vec::new(),
94 joins: Vec::new(),
95 conditions: Vec::new(),
96 group_by: Vec::new(),
97 having: None,
98 order: Vec::new(),
99 order_raw: None,
100 limit_val: None,
101 offset_val: None,
102 use_replica: true,
103 extra_select_exprs: Vec::new(),
104 _marker: PhantomData,
105 }
106 }
107
108 pub fn select(mut self, cols: &[&str]) -> Self {
111 self.select_cols = Some(cols.iter().map(|s| s.to_string()).collect());
112 self
113 }
114
115 pub fn select_raw(mut self, expr: &str) -> Self {
117 self.select_raw = Some(expr.to_string());
118 self
119 }
120
121 pub fn add_select_expr(mut self, expr: impl Into<String>) -> Self {
124 self.extra_select_exprs.push(expr.into());
125 self
126 }
127
128 pub fn distinct(mut self) -> Self {
130 self.distinct = true;
131 self
132 }
133
134 pub fn distinct_on(mut self, cols: &[&str]) -> Self {
136 self.distinct_on_cols = cols.iter().map(|s| s.to_string()).collect();
137 self
138 }
139
140 pub fn inner_join(mut self, table: &str, on: &str) -> Self {
155 self.joins
156 .push(Join::Inner(table.to_string(), on.to_string()));
157 self
158 }
159
160 pub fn left_join(mut self, table: &str, on: &str) -> Self {
162 self.joins
163 .push(Join::Left(table.to_string(), on.to_string()));
164 self
165 }
166
167 pub fn right_join(mut self, table: &str, on: &str) -> Self {
169 self.joins
170 .push(Join::Right(table.to_string(), on.to_string()));
171 self
172 }
173
174 pub fn join_raw(mut self, raw: &str) -> Self {
176 self.joins.push(Join::Raw(raw.to_string()));
177 self
178 }
179
180 pub fn group_by(mut self, cols: &[&str]) -> Self {
197 self.group_by = cols.iter().map(|s| s.to_string()).collect();
198 self
199 }
200
201 pub fn having(mut self, expr: &str) -> Self {
203 self.having = Some(expr.to_string());
204 self
205 }
206
207 pub fn where_eq(self, col: &str, val: impl Into<SqlValue>) -> Self {
210 self.push(JoinOp::And, Condition::Eq(col.into(), val.into()))
211 }
212
213 pub fn where_ne(self, col: &str, val: impl Into<SqlValue>) -> Self {
214 self.push(JoinOp::And, Condition::Ne(col.into(), val.into()))
215 }
216
217 pub fn where_gt(self, col: &str, val: impl Into<SqlValue>) -> Self {
218 self.push(JoinOp::And, Condition::Gt(col.into(), val.into()))
219 }
220
221 pub fn where_gte(self, col: &str, val: impl Into<SqlValue>) -> Self {
222 self.push(JoinOp::And, Condition::Gte(col.into(), val.into()))
223 }
224
225 pub fn where_lt(self, col: &str, val: impl Into<SqlValue>) -> Self {
226 self.push(JoinOp::And, Condition::Lt(col.into(), val.into()))
227 }
228
229 pub fn where_lte(self, col: &str, val: impl Into<SqlValue>) -> Self {
230 self.push(JoinOp::And, Condition::Lte(col.into(), val.into()))
231 }
232
233 pub fn where_like(self, col: &str, pattern: &str) -> Self {
234 self.push(JoinOp::And, Condition::Like(col.into(), pattern.into()))
235 }
236
237 pub fn where_not_like(self, col: &str, pattern: &str) -> Self {
238 self.push(JoinOp::And, Condition::NotLike(col.into(), pattern.into()))
239 }
240
241 pub fn where_null(self, col: &str) -> Self {
242 self.push(JoinOp::And, Condition::IsNull(col.into()))
243 }
244
245 pub fn where_not_null(self, col: &str) -> Self {
246 self.push(JoinOp::And, Condition::IsNotNull(col.into()))
247 }
248
249 pub fn where_in(self, col: &str, vals: Vec<impl Into<SqlValue>>) -> Self {
250 self.push(
251 JoinOp::And,
252 Condition::In(col.into(), vals.into_iter().map(Into::into).collect()),
253 )
254 }
255
256 pub fn where_not_in(self, col: &str, vals: Vec<impl Into<SqlValue>>) -> Self {
257 self.push(
258 JoinOp::And,
259 Condition::NotIn(col.into(), vals.into_iter().map(Into::into).collect()),
260 )
261 }
262
263 pub fn where_between(
264 self,
265 col: &str,
266 lo: impl Into<SqlValue>,
267 hi: impl Into<SqlValue>,
268 ) -> Self {
269 self.push(
270 JoinOp::And,
271 Condition::Between(col.into(), lo.into(), hi.into()),
272 )
273 }
274
275 pub fn where_not_between(
276 self,
277 col: &str,
278 lo: impl Into<SqlValue>,
279 hi: impl Into<SqlValue>,
280 ) -> Self {
281 self.push(
282 JoinOp::And,
283 Condition::NotBetween(col.into(), lo.into(), hi.into()),
284 )
285 }
286
287 pub fn where_raw(self, sql: &str) -> Self {
288 self.push(JoinOp::And, Condition::Raw(sql.into()))
289 }
290
291 pub fn where_ilike(self, col: &str, pattern: &str) -> Self {
293 self.push(JoinOp::And, Condition::ILike(col.into(), pattern.into()))
294 }
295
296 pub fn or_where_ilike(self, col: &str, pattern: &str) -> Self {
298 self.push(JoinOp::Or, Condition::ILike(col.into(), pattern.into()))
299 }
300
301 pub fn where_json(self, col: &str, key: &str, val: impl Into<SqlValue>) -> Self {
303 self.push(
304 JoinOp::And,
305 Condition::JsonGet(col.into(), key.into(), val.into()),
306 )
307 }
308
309 pub fn or_where_json(self, col: &str, key: &str, val: impl Into<SqlValue>) -> Self {
311 self.push(
312 JoinOp::Or,
313 Condition::JsonGet(col.into(), key.into(), val.into()),
314 )
315 }
316
317 pub fn where_json_contains(self, col: &str, json_val: &str) -> Self {
319 self.push(
320 JoinOp::And,
321 Condition::Raw(format!("{col} @> '{json_val}'::jsonb")),
322 )
323 }
324
325 pub fn where_column(self, col: &str, op: &str, val: impl Into<SqlValue>) -> Self {
333 self.where_op(col, op, val)
334 }
335
336 pub fn where_op(self, col: &str, op: &str, val: impl Into<SqlValue>) -> Self {
338 let cond = match op {
339 "=" | "==" => Condition::Eq(col.into(), val.into()),
340 "!=" | "<>" => Condition::Ne(col.into(), val.into()),
341 ">" => Condition::Gt(col.into(), val.into()),
342 ">=" => Condition::Gte(col.into(), val.into()),
343 "<" => Condition::Lt(col.into(), val.into()),
344 "<=" => Condition::Lte(col.into(), val.into()),
345 other => Condition::Raw(format!("{col} {other} {}", val.into())),
346 };
347 self.push(JoinOp::And, cond)
348 }
349
350 pub fn where_group<F>(self, f: F) -> Self
355 where
356 F: FnOnce(QueryBuilder<T>) -> QueryBuilder<T>,
357 {
358 let inner_builder = f(QueryBuilder::new(""));
359 if inner_builder.conditions.is_empty() {
360 return self;
361 }
362 self.push(JoinOp::And, Condition::Group(inner_builder.conditions))
363 }
364
365 pub fn or_where_group<F>(self, f: F) -> Self
367 where
368 F: FnOnce(QueryBuilder<T>) -> QueryBuilder<T>,
369 {
370 let inner_builder = f(QueryBuilder::new(""));
371 if inner_builder.conditions.is_empty() {
372 return self;
373 }
374 self.push(JoinOp::Or, Condition::Group(inner_builder.conditions))
375 }
376
377 pub fn or_where(self, col: &str, val: impl Into<SqlValue>) -> Self {
381 self.or_where_eq(col, val)
382 }
383
384 pub fn or_where_eq(self, col: &str, val: impl Into<SqlValue>) -> Self {
385 self.push(JoinOp::Or, Condition::Eq(col.into(), val.into()))
386 }
387
388 pub fn or_where_ne(self, col: &str, val: impl Into<SqlValue>) -> Self {
389 self.push(JoinOp::Or, Condition::Ne(col.into(), val.into()))
390 }
391
392 pub fn or_where_gt(self, col: &str, val: impl Into<SqlValue>) -> Self {
393 self.push(JoinOp::Or, Condition::Gt(col.into(), val.into()))
394 }
395
396 pub fn or_where_gte(self, col: &str, val: impl Into<SqlValue>) -> Self {
397 self.push(JoinOp::Or, Condition::Gte(col.into(), val.into()))
398 }
399
400 pub fn or_where_lt(self, col: &str, val: impl Into<SqlValue>) -> Self {
401 self.push(JoinOp::Or, Condition::Lt(col.into(), val.into()))
402 }
403
404 pub fn or_where_lte(self, col: &str, val: impl Into<SqlValue>) -> Self {
405 self.push(JoinOp::Or, Condition::Lte(col.into(), val.into()))
406 }
407
408 pub fn or_where_like(self, col: &str, pattern: &str) -> Self {
409 self.push(JoinOp::Or, Condition::Like(col.into(), pattern.into()))
410 }
411
412 pub fn or_where_null(self, col: &str) -> Self {
413 self.push(JoinOp::Or, Condition::IsNull(col.into()))
414 }
415
416 pub fn or_where_not_null(self, col: &str) -> Self {
417 self.push(JoinOp::Or, Condition::IsNotNull(col.into()))
418 }
419
420 pub fn or_where_in(self, col: &str, vals: Vec<impl Into<SqlValue>>) -> Self {
421 self.push(
422 JoinOp::Or,
423 Condition::In(col.into(), vals.into_iter().map(Into::into).collect()),
424 )
425 }
426
427 pub fn or_where_between(
428 self,
429 col: &str,
430 lo: impl Into<SqlValue>,
431 hi: impl Into<SqlValue>,
432 ) -> Self {
433 self.push(
434 JoinOp::Or,
435 Condition::Between(col.into(), lo.into(), hi.into()),
436 )
437 }
438
439 pub fn or_where_raw(self, sql: &str) -> Self {
440 self.push(JoinOp::Or, Condition::Raw(sql.into()))
441 }
442
443 pub fn order_by(mut self, col: &str) -> Self {
446 self.order.push((col.into(), OrderDir::Asc));
447 self
448 }
449
450 pub fn order_by_desc(mut self, col: &str) -> Self {
451 self.order.push((col.into(), OrderDir::Desc));
452 self
453 }
454
455 pub fn order_by_raw(mut self, expr: &str) -> Self {
458 self.order_raw = Some(expr.to_string());
459 self
460 }
461
462 pub fn order_by_many(mut self, cols: &[(&str, OrderDir)]) -> Self {
464 for (col, dir) in cols {
465 self.order.push((col.to_string(), *dir));
466 }
467 self
468 }
469
470 pub fn reorder(mut self, col: &str) -> Self {
472 self.order.clear();
473 self.order_raw = None;
474 self.order.push((col.into(), OrderDir::Asc));
475 self
476 }
477
478 pub fn reorder_desc(mut self, col: &str) -> Self {
480 self.order.clear();
481 self.order_raw = None;
482 self.order.push((col.into(), OrderDir::Desc));
483 self
484 }
485
486 pub fn limit(mut self, n: usize) -> Self {
489 self.limit_val = Some(n);
490 self
491 }
492
493 pub fn offset(mut self, n: usize) -> Self {
494 self.offset_val = Some(n);
495 self
496 }
497
498 pub fn to_sql(&self) -> (String, Vec<SqlValue>) {
506 self.to_sql_with_dialect(Dialect::Postgres)
507 }
508
509 pub fn to_sql_with_dialect(&self, dialect: Dialect) -> (String, Vec<SqlValue>) {
514 let base_cols = if let Some(raw) = &self.select_raw {
515 raw.clone()
516 } else {
517 self.select_cols
518 .as_ref()
519 .map(|c| c.join(", "))
520 .unwrap_or_else(|| "*".into())
521 };
522 let cols = if self.extra_select_exprs.is_empty() {
523 base_cols
524 } else {
525 format!("{}, {}", base_cols, self.extra_select_exprs.join(", "))
526 };
527
528 let distinct_kw = if !self.distinct_on_cols.is_empty() {
529 format!("DISTINCT ON ({}) ", self.distinct_on_cols.join(", "))
530 } else if self.distinct {
531 "DISTINCT ".to_string()
532 } else {
533 String::new()
534 };
535 let mut sql = format!("SELECT {distinct_kw}{cols} FROM {}", self.table);
536 let mut params: Vec<SqlValue> = Vec::new();
537
538 sql.push_str(&self.build_joins());
539 sql.push_str(&self.build_where_dialect(dialect, &mut params));
540 sql.push_str(&self.build_group_by());
541 sql.push_str(&self.build_order());
542
543 if let Some(n) = self.limit_val {
544 sql.push_str(&format!(" LIMIT {n}"));
545 }
546 if let Some(n) = self.offset_val {
547 sql.push_str(&format!(" OFFSET {n}"));
548 }
549
550 (sql, params)
551 }
552
553 pub fn to_count_sql(&self) -> (String, Vec<SqlValue>) {
555 self.to_count_sql_with_dialect(Dialect::Postgres)
556 }
557
558 pub fn to_count_sql_with_dialect(&self, dialect: Dialect) -> (String, Vec<SqlValue>) {
560 let mut params: Vec<SqlValue> = Vec::new();
561 let joins = self.build_joins();
562 let where_clause = self.build_where_dialect(dialect, &mut params);
563 (
564 format!(
565 "SELECT COUNT(*) FROM {}{}{}",
566 self.table, joins, where_clause
567 ),
568 params,
569 )
570 }
571
572 pub fn to_delete_sql(&self) -> (String, Vec<SqlValue>) {
574 self.to_delete_sql_with_dialect(Dialect::Postgres)
575 }
576
577 pub fn to_delete_sql_with_dialect(&self, dialect: Dialect) -> (String, Vec<SqlValue>) {
579 let mut params: Vec<SqlValue> = Vec::new();
580 let where_clause = self.build_where_dialect(dialect, &mut params);
581 (
582 format!("DELETE FROM {}{}", self.table, where_clause),
583 params,
584 )
585 }
586
587 pub fn to_update_sql(&self, data: &[(&str, SqlValue)]) -> (String, Vec<SqlValue>) {
589 self.to_update_sql_with_dialect(Dialect::Postgres, data)
590 }
591
592 pub fn to_update_sql_with_dialect(
594 &self,
595 dialect: Dialect,
596 data: &[(&str, SqlValue)],
597 ) -> (String, Vec<SqlValue>) {
598 let mut params: Vec<SqlValue> = Vec::new();
599 let set_clauses: Vec<String> = data
600 .iter()
601 .enumerate()
602 .map(|(i, (col, val))| {
603 params.push(val.clone());
604 match dialect {
605 Dialect::Postgres => format!("{col} = ${}", i + 1),
606 Dialect::Sqlite => format!("{col} = ?"),
607 }
608 })
609 .collect();
610
611 let mut sql = format!("UPDATE {} SET {}", self.table, set_clauses.join(", "));
612 sql.push_str(&self.build_where_dialect(dialect, &mut params));
613 (sql, params)
614 }
615
616 pub fn insert_sql(table: &str, data: &[(&str, SqlValue)]) -> (String, Vec<SqlValue>) {
620 Self::insert_sql_with_dialect(Dialect::Postgres, table, data)
621 }
622
623 pub fn insert_sql_with_dialect(
625 dialect: Dialect,
626 table: &str,
627 data: &[(&str, SqlValue)],
628 ) -> (String, Vec<SqlValue>) {
629 let cols: Vec<&str> = data.iter().map(|(c, _)| *c).collect();
630 let placeholders: Vec<String> = match dialect {
631 Dialect::Postgres => (1..=data.len()).map(|i| format!("${i}")).collect(),
632 Dialect::Sqlite => (0..data.len()).map(|_| "?".to_string()).collect(),
633 };
634 let params: Vec<SqlValue> = data.iter().map(|(_, v)| v.clone()).collect();
635 (
636 format!(
637 "INSERT INTO {table} ({}) VALUES ({})",
638 cols.join(", "),
639 placeholders.join(", ")
640 ),
641 params,
642 )
643 }
644
645 pub fn bulk_insert_sql(table: &str, rows: &[Vec<(&str, SqlValue)>]) -> (String, Vec<SqlValue>) {
661 assert!(
662 !rows.is_empty(),
663 "bulk_insert_sql requires at least one row"
664 );
665 let cols: Vec<&str> = rows[0].iter().map(|(c, _)| *c).collect();
666 let mut params: Vec<SqlValue> = Vec::new();
667 let mut value_groups: Vec<String> = Vec::new();
668 let mut offset = 1usize;
669
670 for row in rows {
671 let placeholders: Vec<String> = (offset..offset + row.len())
672 .map(|i| format!("${i}"))
673 .collect();
674 value_groups.push(format!("({})", placeholders.join(", ")));
675 for (_, v) in row.iter() {
676 params.push(v.clone());
677 }
678 offset += row.len();
679 }
680
681 (
682 format!(
683 "INSERT INTO {table} ({}) VALUES {}",
684 cols.join(", "),
685 value_groups.join(", ")
686 ),
687 params,
688 )
689 }
690
691 pub fn update_sql(
695 table: &str,
696 data: &[(&str, SqlValue)],
697 conditions: &[(JoinOp, Condition)],
698 ) -> (String, Vec<SqlValue>) {
699 let mut params: Vec<SqlValue> = Vec::new();
700 let set_clauses: Vec<String> = data
701 .iter()
702 .enumerate()
703 .map(|(i, (col, val))| {
704 params.push(val.clone());
705 format!("{col} = ${}", i + 1)
706 })
707 .collect();
708
709 let mut sql = format!("UPDATE {table} SET {}", set_clauses.join(", "));
710
711 if !conditions.is_empty() {
712 let where_frag = build_where_from(conditions, &mut params);
713 sql.push_str(&where_frag);
714 }
715
716 (sql, params)
717 }
718
719 pub fn when<F>(self, condition: bool, f: F) -> Self
733 where
734 F: FnOnce(Self) -> Self,
735 {
736 if condition {
737 f(self)
738 } else {
739 self
740 }
741 }
742
743 pub fn when_some<V, F>(self, opt: Option<V>, f: F) -> Self
755 where
756 F: FnOnce(Self, V) -> Self,
757 {
758 match opt {
759 Some(v) => f(self, v),
760 None => self,
761 }
762 }
763
764 pub fn push_condition(self, op: JoinOp, cond: Condition) -> Self {
768 self.push(op, cond)
769 }
770
771 pub fn on_write_db(mut self) -> Self {
775 self.use_replica = false;
776 self
777 }
778
779 pub fn nearest_to(self, col: &str, embedding: &[f32], k: usize) -> Self {
783 let vec_lit = format_vector(embedding);
784 self.order_by_raw(&format!("{col} <-> '{vec_lit}'::vector"))
785 .limit(k)
786 }
787
788 pub fn where_cosine_distance(
790 self,
791 col: &str,
792 embedding: &[f32],
793 op: &str,
794 threshold: f64,
795 ) -> Self {
796 let vec_lit = format_vector(embedding);
797 self.where_raw(&format!("{col} <=> '{vec_lit}'::vector {op} {threshold}"))
798 }
799
800 pub fn where_vector_distance(
802 self,
803 col: &str,
804 embedding: &[f32],
805 op: &str,
806 threshold: f64,
807 ) -> Self {
808 let vec_lit = format_vector(embedding);
809 self.where_raw(&format!("{col} <-> '{vec_lit}'::vector {op} {threshold}"))
810 }
811
812 pub fn where_inner_product(
814 self,
815 col: &str,
816 embedding: &[f32],
817 op: &str,
818 threshold: f64,
819 ) -> Self {
820 let vec_lit = format_vector(embedding);
821 self.where_raw(&format!("{col} <#> '{vec_lit}'::vector {op} {threshold}"))
822 }
823
824 fn push(mut self, op: JoinOp, cond: Condition) -> Self {
827 self.conditions.push((op, cond));
828 self
829 }
830
831 fn build_joins(&self) -> String {
832 let mut out = String::new();
833 for join in &self.joins {
834 match join {
835 Join::Inner(t, on) => out.push_str(&format!(" INNER JOIN {t} ON {on}")),
836 Join::Left(t, on) => out.push_str(&format!(" LEFT JOIN {t} ON {on}")),
837 Join::Right(t, on) => out.push_str(&format!(" RIGHT JOIN {t} ON {on}")),
838 Join::Raw(raw) => {
839 out.push(' ');
840 out.push_str(raw);
841 }
842 }
843 }
844 out
845 }
846
847 fn build_where_dialect(&self, dialect: Dialect, params: &mut Vec<SqlValue>) -> String {
848 build_where_from_dialect(dialect, &self.conditions, params)
849 }
850
851 fn build_group_by(&self) -> String {
852 let mut out = String::new();
853 if !self.group_by.is_empty() {
854 out.push_str(&format!(" GROUP BY {}", self.group_by.join(", ")));
855 }
856 if let Some(ref h) = self.having {
857 out.push_str(&format!(" HAVING {h}"));
858 }
859 out
860 }
861
862 fn build_order(&self) -> String {
863 let mut parts: Vec<String> = self
864 .order
865 .iter()
866 .map(|(col, dir)| format!("{col} {dir}"))
867 .collect();
868 if let Some(raw) = &self.order_raw {
869 parts.push(raw.clone());
870 }
871 if parts.is_empty() {
872 return String::new();
873 }
874 format!(" ORDER BY {}", parts.join(", "))
875 }
876
877 pub fn conditions(&self) -> &[(JoinOp, Condition)] {
879 &self.conditions
880 }
881
882 pub fn to_where_clause(&self) -> (String, Vec<SqlValue>) {
889 let mut params = Vec::new();
890 let clause = self.build_where_dialect(Dialect::Postgres, &mut params);
891 (clause, params)
892 }
893
894 pub fn to_aggregate_sql(&self, agg_expr: &str) -> (String, Vec<SqlValue>) {
898 let mut params: Vec<SqlValue> = Vec::new();
899 let joins = self.build_joins();
900 let where_clause = self.build_where_dialect(Dialect::Postgres, &mut params);
901 (
902 format!(
903 "SELECT {agg_expr} FROM {}{}{}",
904 self.table, joins, where_clause
905 ),
906 params,
907 )
908 }
909
910 pub fn upsert_sql(
917 table: &str,
918 data: &[(&str, SqlValue)],
919 conflict_cols: &[&str],
920 ) -> (String, Vec<SqlValue>) {
921 let cols: Vec<&str> = data.iter().map(|(c, _)| *c).collect();
922 let placeholders: Vec<String> = (1..=data.len()).map(|i| format!("${i}")).collect();
923 let params: Vec<SqlValue> = data.iter().map(|(_, v)| v.clone()).collect();
924
925 let conflict_target = conflict_cols.join(", ");
926 let update_set: Vec<String> = cols
927 .iter()
928 .filter(|c| !conflict_cols.contains(c))
929 .map(|c| format!("{c} = EXCLUDED.{c}"))
930 .collect();
931
932 let sql = if update_set.is_empty() {
933 format!(
934 "INSERT INTO {table} ({}) VALUES ({}) ON CONFLICT ({conflict_target}) DO NOTHING",
935 cols.join(", "),
936 placeholders.join(", "),
937 )
938 } else {
939 format!(
940 "INSERT INTO {table} ({}) VALUES ({}) ON CONFLICT ({conflict_target}) DO UPDATE SET {}",
941 cols.join(", "),
942 placeholders.join(", "),
943 update_set.join(", "),
944 )
945 };
946
947 (sql, params)
948 }
949}
950
951fn format_vector(v: &[f32]) -> String {
953 let elems: Vec<String> = v.iter().map(|x| x.to_string()).collect();
954 format!("[{}]", elems.join(","))
955}
956
957fn build_where_from(conditions: &[(JoinOp, Condition)], params: &mut Vec<SqlValue>) -> String {
958 build_where_from_dialect(Dialect::Postgres, conditions, params)
959}
960
961fn build_where_from_dialect(
962 dialect: Dialect,
963 conditions: &[(JoinOp, Condition)],
964 params: &mut Vec<SqlValue>,
965) -> String {
966 if conditions.is_empty() {
967 return String::new();
968 }
969 let mut out = " WHERE ".to_string();
970 for (idx, (op, cond)) in conditions.iter().enumerate() {
971 let (frag, ps) = match dialect {
972 Dialect::Postgres => cond.to_param_sql(params.len() + 1),
973 Dialect::Sqlite => cond.to_param_sql_sqlite(),
974 };
975 params.extend(ps);
976 if idx > 0 {
977 out.push(' ');
978 out.push_str(&op.to_string());
979 out.push(' ');
980 }
981 out.push_str(&frag);
982 }
983 out
984}
985
986#[cfg(test)]
989mod tests {
990 use super::*;
991
992 #[test]
993 fn simple_select() {
994 let (sql, params) = QueryBuilder::<()>::new("users").to_sql();
995 assert_eq!(sql, "SELECT * FROM users");
996 assert!(params.is_empty());
997 }
998
999 #[test]
1000 fn distinct_select() {
1001 let (sql, _) = QueryBuilder::<()>::new("users").distinct().to_sql();
1002 assert!(sql.starts_with("SELECT DISTINCT * FROM users"));
1003 }
1004
1005 #[test]
1006 fn where_eq_generates_param() {
1007 let (sql, params) = QueryBuilder::<()>::new("users")
1008 .where_eq("id", 42i64)
1009 .to_sql();
1010 assert!(sql.contains("WHERE id = $1"));
1011 assert_eq!(params.len(), 1);
1012 assert_eq!(params[0], SqlValue::Integer(42));
1013 }
1014
1015 #[test]
1016 fn multiple_conditions() {
1017 let (sql, params) = QueryBuilder::<()>::new("posts")
1018 .where_eq("active", true)
1019 .where_like("title", "%rust%")
1020 .to_sql();
1021 assert!(sql.contains("WHERE active = $1 AND title LIKE $2"));
1022 assert_eq!(params.len(), 2);
1023 }
1024
1025 #[test]
1026 fn or_conditions() {
1027 let (sql, params) = QueryBuilder::<()>::new("users")
1028 .where_eq("role", "admin")
1029 .or_where_eq("role", "moderator")
1030 .to_sql();
1031 assert!(sql.contains("WHERE role = $1 OR role = $2"));
1032 assert_eq!(params.len(), 2);
1033 }
1034
1035 #[test]
1036 fn where_between() {
1037 let (sql, params) = QueryBuilder::<()>::new("orders")
1038 .where_between("amount", 10i64, 100i64)
1039 .to_sql();
1040 assert!(sql.contains("amount BETWEEN $1 AND $2"));
1041 assert_eq!(params.len(), 2);
1042 }
1043
1044 #[test]
1045 fn where_not_in() {
1046 let (sql, params) = QueryBuilder::<()>::new("users")
1047 .where_not_in("status", vec!["banned", "deleted"])
1048 .to_sql();
1049 assert!(sql.contains("status NOT IN ($1, $2)"));
1050 assert_eq!(params.len(), 2);
1051 }
1052
1053 #[test]
1054 fn where_not_like() {
1055 let (sql, _) = QueryBuilder::<()>::new("users")
1056 .where_not_like("email", "%@spam.com")
1057 .to_sql();
1058 assert!(sql.contains("email NOT LIKE $1"));
1059 }
1060
1061 #[test]
1062 fn to_update_sql() {
1063 let (sql, params) = QueryBuilder::<()>::new("users")
1064 .where_eq("id", 1i64)
1065 .to_update_sql(&[("name", "Bob".into()), ("active", true.into())]);
1066 assert!(sql.starts_with("UPDATE users SET name = $1, active = $2"));
1067 assert!(sql.contains("WHERE id = $3"));
1068 assert_eq!(params.len(), 3);
1069 }
1070
1071 #[test]
1072 fn order_limit_offset() {
1073 let (sql, _) = QueryBuilder::<()>::new("users")
1074 .order_by_desc("created_at")
1075 .order_by("name")
1076 .limit(10)
1077 .offset(20)
1078 .to_sql();
1079 assert!(sql.contains("ORDER BY created_at DESC, name ASC"));
1080 assert!(sql.contains("LIMIT 10"));
1081 assert!(sql.contains("OFFSET 20"));
1082 }
1083
1084 #[test]
1085 fn count_sql() {
1086 let (sql, _) = QueryBuilder::<()>::new("users")
1087 .where_eq("active", true)
1088 .to_count_sql();
1089 assert!(sql.starts_with("SELECT COUNT(*) FROM users"));
1090 }
1091
1092 #[test]
1093 fn delete_sql() {
1094 let (sql, params) = QueryBuilder::<()>::new("sessions")
1095 .where_eq("user_id", 5i64)
1096 .to_delete_sql();
1097 assert!(sql.contains("DELETE FROM sessions WHERE user_id = $1"));
1098 assert_eq!(params.len(), 1);
1099 }
1100
1101 #[test]
1102 fn insert_sql() {
1103 let (sql, params) = QueryBuilder::<()>::insert_sql(
1104 "users",
1105 &[("name", "Alice".into()), ("email", "a@a.com".into())],
1106 );
1107 assert!(sql.contains("INSERT INTO users (name, email) VALUES ($1, $2)"));
1108 assert_eq!(params.len(), 2);
1109 }
1110
1111 #[test]
1112 fn where_in() {
1113 let (sql, params) = QueryBuilder::<()>::new("users")
1114 .where_in("id", vec![1i64, 2, 3])
1115 .to_sql();
1116 assert!(sql.contains("id IN ($1, $2, $3)"));
1117 assert_eq!(params.len(), 3);
1118 }
1119
1120 #[test]
1121 fn select_specific_columns() {
1122 let (sql, _) = QueryBuilder::<()>::new("users")
1123 .select(&["id", "email"])
1124 .to_sql();
1125 assert!(sql.starts_with("SELECT id, email FROM users"));
1126 }
1127
1128 #[test]
1129 fn option_value_null() {
1130 let val: SqlValue = Option::<i64>::None.into();
1131 assert_eq!(val, SqlValue::Null);
1132 }
1133
1134 #[test]
1135 fn option_value_some() {
1136 let val: SqlValue = Some(42i64).into();
1137 assert_eq!(val, SqlValue::Integer(42));
1138 }
1139
1140 #[test]
1141 fn inner_join() {
1142 let (sql, _) = QueryBuilder::<()>::new("orders")
1143 .inner_join("users", "users.id = orders.user_id")
1144 .to_sql();
1145 assert!(sql.contains("INNER JOIN users ON users.id = orders.user_id"));
1146 }
1147
1148 #[test]
1149 fn left_join_with_where() {
1150 let (sql, params) = QueryBuilder::<()>::new("orders")
1151 .left_join("users", "users.id = orders.user_id")
1152 .where_eq("orders.status", "paid")
1153 .to_sql();
1154 assert!(sql.contains("LEFT JOIN users ON users.id = orders.user_id"));
1155 assert!(sql.contains("WHERE orders.status = $1"));
1156 assert_eq!(params.len(), 1);
1157 }
1158
1159 #[test]
1160 fn right_join() {
1161 let (sql, _) = QueryBuilder::<()>::new("orders")
1162 .right_join("products", "products.id = orders.product_id")
1163 .to_sql();
1164 assert!(sql.contains("RIGHT JOIN products ON products.id = orders.product_id"));
1165 }
1166
1167 #[test]
1168 fn group_by_and_having() {
1169 let (sql, _) = QueryBuilder::<()>::new("orders")
1170 .select(&["user_id", "COUNT(*) as total"])
1171 .group_by(&["user_id"])
1172 .having("COUNT(*) > 5")
1173 .to_sql();
1174 assert!(sql.contains("GROUP BY user_id"));
1175 assert!(sql.contains("HAVING COUNT(*) > 5"));
1176 let gpos = sql.find("GROUP BY").unwrap();
1178 let hpos = sql.find("HAVING").unwrap();
1179 assert!(gpos < hpos);
1180 }
1181
1182 #[test]
1183 fn count_sql_with_join() {
1184 let (sql, _) = QueryBuilder::<()>::new("orders")
1185 .inner_join("users", "users.id = orders.user_id")
1186 .where_eq("users.active", true)
1187 .to_count_sql();
1188 assert!(sql.contains("INNER JOIN users ON users.id = orders.user_id"));
1189 assert!(sql.contains("SELECT COUNT(*) FROM orders"));
1190 }
1191
1192 #[test]
1193 fn bulk_insert_sql_two_rows() {
1194 let rows: Vec<Vec<(&str, SqlValue)>> = vec![
1195 vec![("name", "Alice".into()), ("email", "a@a.com".into())],
1196 vec![("name", "Bob".into()), ("email", "b@b.com".into())],
1197 ];
1198 let (sql, params) = QueryBuilder::<()>::bulk_insert_sql("users", &rows);
1199 assert!(sql.starts_with("INSERT INTO users (name, email) VALUES"));
1200 assert!(sql.contains("($1, $2), ($3, $4)"));
1201 assert_eq!(params.len(), 4);
1202 }
1203
1204 #[test]
1205 fn bulk_insert_sql_single_row() {
1206 let rows = vec![vec![("x", SqlValue::Integer(1))]];
1207 let (sql, params) = QueryBuilder::<()>::bulk_insert_sql("t", &rows);
1208 assert!(sql.contains("($1)"));
1209 assert_eq!(params.len(), 1);
1210 }
1211
1212 #[test]
1213 fn where_ilike() {
1214 let (sql, params) = QueryBuilder::<()>::new("users")
1215 .where_ilike("name", "alice%")
1216 .to_sql();
1217 assert!(sql.contains("name ILIKE $1"));
1218 assert_eq!(params.len(), 1);
1219 }
1220
1221 #[test]
1222 fn where_op_gt() {
1223 let (sql, params) = QueryBuilder::<()>::new("users")
1224 .where_op("age", ">", 18i64)
1225 .to_sql();
1226 assert!(sql.contains("age > $1"));
1227 assert_eq!(params.len(), 1);
1228 }
1229
1230 #[test]
1231 fn where_group_subquery() {
1232 let (sql, params) = QueryBuilder::<()>::new("users")
1233 .where_eq("active", true)
1234 .where_group(|q| q.where_eq("role", "admin").or_where_eq("role", "mod"))
1235 .to_sql();
1236 assert!(sql.contains("active = $1"));
1237 assert!(sql.contains("AND (role = $2 OR role = $3)"));
1238 assert_eq!(params.len(), 3);
1239 }
1240
1241 #[test]
1242 fn select_raw() {
1243 let (sql, _) = QueryBuilder::<()>::new("users")
1244 .select_raw("id, LOWER(email) as email_lower")
1245 .to_sql();
1246 assert!(sql.starts_with("SELECT id, LOWER(email) as email_lower FROM users"));
1247 }
1248
1249 #[test]
1250 fn distinct_on() {
1251 let (sql, _) = QueryBuilder::<()>::new("users")
1252 .distinct_on(&["email"])
1253 .to_sql();
1254 assert!(sql.starts_with("SELECT DISTINCT ON (email) * FROM users"));
1255 }
1256
1257 #[test]
1258 fn join_raw() {
1259 let (sql, _) = QueryBuilder::<()>::new("users")
1260 .join_raw("INNER JOIN subscriptions s ON s.user_id = users.id AND s.active = true")
1261 .to_sql();
1262 assert!(sql.contains("INNER JOIN subscriptions s ON s.user_id = users.id"));
1263 }
1264
1265 #[test]
1266 fn order_by_raw() {
1267 let (sql, _) = QueryBuilder::<()>::new("users")
1268 .order_by_raw("NULLS LAST, score DESC")
1269 .to_sql();
1270 assert!(sql.contains("ORDER BY NULLS LAST, score DESC"));
1271 }
1272
1273 #[test]
1274 fn order_by_many() {
1275 let (sql, _) = QueryBuilder::<()>::new("users")
1276 .order_by_many(&[("role", OrderDir::Asc), ("created_at", OrderDir::Desc)])
1277 .to_sql();
1278 assert!(sql.contains("ORDER BY role ASC, created_at DESC"));
1279 }
1280
1281 #[test]
1282 fn reorder_clears_previous() {
1283 let (sql, _) = QueryBuilder::<()>::new("users")
1284 .order_by("name")
1285 .reorder_desc("created_at")
1286 .to_sql();
1287 assert!(sql.contains("ORDER BY created_at DESC"));
1288 assert!(!sql.contains("name"));
1289 }
1290
1291 #[test]
1292 fn upsert_sql_basic() {
1293 let (sql, params) = QueryBuilder::<()>::upsert_sql(
1294 "users",
1295 &[("email", "x@y.com".into()), ("name", "Bob".into())],
1296 &["email"],
1297 );
1298 assert!(sql.contains("ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name"));
1299 assert_eq!(params.len(), 2);
1300 }
1301
1302 #[test]
1303 fn where_json_extraction() {
1304 let (sql, params) = QueryBuilder::<()>::new("users")
1305 .where_json("settings", "theme", "dark")
1306 .to_sql();
1307 assert!(sql.contains("settings->>'theme' = $1"), "sql={sql}");
1308 assert_eq!(params.len(), 1);
1309 assert_eq!(params[0], SqlValue::Text("dark".into()));
1310 }
1311
1312 #[test]
1313 fn where_json_contains_raw() {
1314 let (sql, _) = QueryBuilder::<()>::new("posts")
1315 .where_json_contains("tags", r#"["rust"]"#)
1316 .to_sql();
1317 assert!(sql.contains(r#"tags @> '["rust"]'::jsonb"#), "sql={sql}");
1318 }
1319
1320 #[test]
1321 fn to_aggregate_sql() {
1322 let (sql, params) = QueryBuilder::<()>::new("orders")
1323 .where_eq("user_id", 1i64)
1324 .to_aggregate_sql("MAX(total)");
1325 assert!(sql.starts_with("SELECT MAX(total) FROM orders"));
1326 assert!(sql.contains("WHERE user_id = $1"));
1327 assert_eq!(params.len(), 1);
1328 }
1329
1330 #[test]
1333 fn when_applies_closure_when_true() {
1334 let (sql, params) = QueryBuilder::<()>::new("users")
1335 .when(true, |q| q.where_eq("active", true))
1336 .to_sql();
1337 assert!(sql.contains("WHERE active = $1"));
1338 assert_eq!(params.len(), 1);
1339 }
1340
1341 #[test]
1342 fn when_noop_when_false() {
1343 let (sql, params) = QueryBuilder::<()>::new("users")
1344 .when(false, |q| q.where_eq("active", true))
1345 .to_sql();
1346 assert!(!sql.contains("WHERE"));
1347 assert!(params.is_empty());
1348 }
1349
1350 #[test]
1351 fn when_some_applies_with_value() {
1352 let role: Option<&str> = Some("admin");
1353 let (sql, params) = QueryBuilder::<()>::new("users")
1354 .when_some(role, |q, r| q.where_eq("role", r))
1355 .to_sql();
1356 assert!(sql.contains("role = $1"));
1357 assert_eq!(params.len(), 1);
1358 }
1359
1360 #[test]
1361 fn when_some_noop_when_none() {
1362 let role: Option<&str> = None;
1363 let (sql, params) = QueryBuilder::<()>::new("users")
1364 .when_some(role, |q, r| q.where_eq("role", r))
1365 .to_sql();
1366 assert!(!sql.contains("WHERE"));
1367 assert!(params.is_empty());
1368 }
1369
1370 #[test]
1371 fn chained_when_calls() {
1372 let (sql, params) = QueryBuilder::<()>::new("users")
1373 .when(true, |q| q.where_eq("active", true))
1374 .when(true, |q| q.where_eq("role", "admin"))
1375 .when(false, |q| q.where_eq("deleted", true))
1376 .to_sql();
1377 assert!(sql.contains("active = $1"));
1378 assert!(sql.contains("role = $2"));
1379 assert!(!sql.contains("deleted"));
1380 assert_eq!(params.len(), 2);
1381 }
1382
1383 #[test]
1384 fn add_select_expr_appends_to_star() {
1385 let (sql, _) = QueryBuilder::<()>::new("users")
1386 .add_select_expr(
1387 "(SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id) AS posts_count",
1388 )
1389 .to_sql();
1390 assert!(sql.starts_with("SELECT *, (SELECT COUNT(*)"));
1391 }
1392
1393 #[test]
1394 fn add_select_expr_appends_to_cols() {
1395 let (sql, _) = QueryBuilder::<()>::new("users")
1396 .select(&["id", "email"])
1397 .add_select_expr("42 AS answer")
1398 .to_sql();
1399 assert!(sql.starts_with("SELECT id, email, 42 AS answer FROM users"));
1400 }
1401
1402 #[test]
1403 fn multiple_add_select_exprs() {
1404 let (sql, _) = QueryBuilder::<()>::new("users")
1405 .add_select_expr("(SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id) AS posts_count")
1406 .add_select_expr("(SELECT COUNT(*) FROM comments WHERE comments.user_id = users.id) AS comments_count")
1407 .to_sql();
1408 assert!(sql.contains("posts_count"));
1409 assert!(sql.contains("comments_count"));
1410 }
1411
1412 #[test]
1413 fn where_column_alias() {
1414 let (sql, params) = QueryBuilder::<()>::new("users")
1415 .where_column("age", ">", 18i64)
1416 .to_sql();
1417 assert!(sql.contains("age > $1"));
1418 assert_eq!(params.len(), 1);
1419 }
1420
1421 #[test]
1422 fn or_where_alias() {
1423 let (sql, params) = QueryBuilder::<()>::new("users")
1424 .where_eq("role", "admin")
1425 .or_where("role", "moderator")
1426 .to_sql();
1427 assert!(sql.contains("role = $1 OR role = $2"));
1428 assert_eq!(params.len(), 2);
1429 }
1430
1431 #[test]
1432 fn subquery_exists_no_inner() {
1433 use crate::condition::Condition;
1434 use crate::condition::JoinOp;
1435 let (sql, params) = QueryBuilder::<()>::new("users")
1436 .push_condition(
1437 JoinOp::And,
1438 Condition::Subquery {
1439 exists: true,
1440 table: "posts".to_string(),
1441 fk_expr: "posts.user_id = users.id".to_string(),
1442 inner: vec![],
1443 },
1444 )
1445 .to_sql();
1446 assert!(sql.contains("EXISTS (SELECT 1 FROM posts WHERE posts.user_id = users.id)"));
1447 assert!(params.is_empty());
1448 }
1449
1450 #[test]
1451 fn subquery_not_exists_with_inner() {
1452 let inner = vec![(
1453 JoinOp::And,
1454 Condition::Eq("published".to_string(), SqlValue::Bool(true)),
1455 )];
1456 let (sql, params) = QueryBuilder::<()>::new("users")
1457 .push_condition(
1458 JoinOp::And,
1459 Condition::Subquery {
1460 exists: false,
1461 table: "posts".to_string(),
1462 fk_expr: "posts.user_id = users.id".to_string(),
1463 inner,
1464 },
1465 )
1466 .to_sql();
1467 assert!(sql.contains(
1468 "NOT EXISTS (SELECT 1 FROM posts WHERE posts.user_id = users.id AND published = $1)"
1469 ));
1470 assert_eq!(params.len(), 1);
1471 assert_eq!(params[0], SqlValue::Bool(true));
1472 }
1473
1474 #[test]
1475 fn subquery_outer_params_plus_inner_params() {
1476 let inner = vec![(
1477 JoinOp::And,
1478 Condition::Eq("published".to_string(), SqlValue::Bool(true)),
1479 )];
1480 let (sql, params) = QueryBuilder::<()>::new("users")
1481 .where_eq("active", true) .push_condition(
1483 JoinOp::And,
1484 Condition::Subquery {
1485 exists: true,
1486 table: "posts".to_string(),
1487 fk_expr: "posts.user_id = users.id".to_string(),
1488 inner,
1489 },
1490 )
1491 .to_sql();
1492 assert!(sql.contains("active = $1"));
1493 assert!(sql.contains("published = $2"), "sql={sql}");
1494 assert_eq!(params.len(), 2);
1495 }
1496
1497 #[test]
1500 fn use_replica_default_true() {
1501 let q = QueryBuilder::<()>::new("users");
1502 assert!(q.use_replica);
1503 }
1504
1505 #[test]
1506 fn on_write_db_clears_replica() {
1507 let q = QueryBuilder::<()>::new("users").on_write_db();
1508 assert!(!q.use_replica);
1509 }
1510
1511 #[test]
1514 fn nearest_to_generates_order_and_limit() {
1515 let embedding = vec![1.0f32, 2.0, 3.0];
1516 let (sql, params) = QueryBuilder::<()>::new("documents")
1517 .nearest_to("embedding", &embedding, 10)
1518 .to_sql();
1519 assert!(sql.contains("embedding <-> '[1,2,3]'::vector"), "sql={sql}");
1520 assert!(sql.contains("LIMIT 10"), "sql={sql}");
1521 assert!(params.is_empty());
1522 }
1523
1524 #[test]
1525 fn where_cosine_distance_generates_filter() {
1526 let embedding = vec![0.5f32, 0.5];
1527 let (sql, params) = QueryBuilder::<()>::new("docs")
1528 .where_cosine_distance("embedding", &embedding, "<", 0.3)
1529 .to_sql();
1530 assert!(
1531 sql.contains("embedding <=> '[0.5,0.5]'::vector < 0.3"),
1532 "sql={sql}"
1533 );
1534 assert!(params.is_empty());
1535 }
1536
1537 #[test]
1538 fn where_vector_distance_generates_filter() {
1539 let embedding = vec![1.0f32];
1540 let (sql, _) = QueryBuilder::<()>::new("docs")
1541 .where_vector_distance("vec", &embedding, "<", 1.5)
1542 .to_sql();
1543 assert!(sql.contains("vec <-> '[1]'::vector < 1.5"), "sql={sql}");
1544 }
1545
1546 #[test]
1547 fn nearest_to_with_additional_filter() {
1548 let embedding = vec![1.0f32, 0.0];
1549 let (sql, params) = QueryBuilder::<()>::new("docs")
1550 .where_eq("active", true)
1551 .nearest_to("emb", &embedding, 5)
1552 .to_sql();
1553 assert!(sql.contains("WHERE active = $1"), "sql={sql}");
1554 assert!(sql.contains("emb <-> '[1,0]'::vector"), "sql={sql}");
1555 assert!(sql.contains("LIMIT 5"), "sql={sql}");
1556 assert_eq!(params.len(), 1);
1557 }
1558}