1use crate::value::{FromValue, Value};
8use crate::dialect::{Dialect, ReturningStyle, quote_qualified, validate_identifier};
9use crate::{Database, Row};
10use rustlavel_core::{Error, Json, Result};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Direction {
14 Asc,
15 Desc,
16}
17
18impl Direction {
19 fn as_sql(self) -> &'static str {
20 match self {
21 Direction::Asc => "asc",
22 Direction::Desc => "desc",
23 }
24 }
25}
26
27#[derive(Debug, Clone)]
29enum Condition {
30 Comparison { column: String, operator: String, value: Value, or: bool },
31 In { column: String, values: Vec<Value>, negated: bool, or: bool },
32 Null { column: String, negated: bool, or: bool },
33 Between { column: String, low: Value, high: Value, or: bool },
34 Group { conditions: Vec<Condition>, or: bool },
36}
37
38impl Condition {
39 fn is_or(&self) -> bool {
40 match self {
41 Condition::Comparison { or, .. }
42 | Condition::In { or, .. }
43 | Condition::Null { or, .. }
44 | Condition::Between { or, .. }
45 | Condition::Group { or, .. } => *or,
46 }
47 }
48}
49
50#[derive(Debug, Clone)]
51struct Join {
52 kind: &'static str,
53 table: String,
54 left: String,
55 operator: String,
56 right: String,
57}
58
59#[derive(Debug, Clone)]
61pub struct QueryBuilder {
62 table: String,
63 columns: Vec<String>,
64 conditions: Vec<Condition>,
65 joins: Vec<Join>,
66 order: Vec<(String, Direction)>,
67 group: Vec<String>,
68 limit: Option<i64>,
69 offset: Option<i64>,
70 distinct: bool,
71}
72
73impl QueryBuilder {
74 pub fn new(table: impl Into<String>) -> Self {
75 QueryBuilder {
76 table: table.into(),
77 columns: Vec::new(),
78 conditions: Vec::new(),
79 joins: Vec::new(),
80 order: Vec::new(),
81 group: Vec::new(),
82 limit: None,
83 offset: None,
84 distinct: false,
85 }
86 }
87
88 pub fn select(mut self, columns: &[&str]) -> Self {
90 self.columns = columns.iter().map(|c| (*c).to_string()).collect();
91 self
92 }
93
94 pub fn distinct(mut self) -> Self {
95 self.distinct = true;
96 self
97 }
98
99 pub fn filter(self, column: &str, value: impl Into<Value>) -> Self {
101 self.filter_op(column, "=", value)
102 }
103
104 pub fn filter_op(mut self, column: &str, operator: &str, value: impl Into<Value>) -> Self {
107 self.conditions.push(Condition::Comparison {
108 column: column.to_string(),
109 operator: operator.to_string(),
110 value: value.into(),
111 or: false,
112 });
113 self
114 }
115
116 pub fn or_filter(mut self, column: &str, value: impl Into<Value>) -> Self {
117 self.conditions.push(Condition::Comparison {
118 column: column.to_string(),
119 operator: "=".to_string(),
120 value: value.into(),
121 or: true,
122 });
123 self
124 }
125
126 pub fn filter_in(mut self, column: &str, values: Vec<Value>) -> Self {
127 self.conditions.push(Condition::In {
128 column: column.to_string(),
129 values,
130 negated: false,
131 or: false,
132 });
133 self
134 }
135
136 pub fn filter_not_in(mut self, column: &str, values: Vec<Value>) -> Self {
137 self.conditions.push(Condition::In {
138 column: column.to_string(),
139 values,
140 negated: true,
141 or: false,
142 });
143 self
144 }
145
146 pub fn filter_null(mut self, column: &str) -> Self {
147 self.conditions.push(Condition::Null { column: column.to_string(), negated: false, or: false });
148 self
149 }
150
151 pub fn filter_not_null(mut self, column: &str) -> Self {
152 self.conditions.push(Condition::Null { column: column.to_string(), negated: true, or: false });
153 self
154 }
155
156 pub fn filter_between(mut self, column: &str, low: impl Into<Value>, high: impl Into<Value>) -> Self {
157 self.conditions.push(Condition::Between {
158 column: column.to_string(),
159 low: low.into(),
160 high: high.into(),
161 or: false,
162 });
163 self
164 }
165
166 pub fn filter_like(self, column: &str, pattern: impl Into<Value>) -> Self {
168 self.filter_op(column, "like", pattern)
169 }
170
171 pub fn group_filter(mut self, build: impl FnOnce(QueryBuilder) -> QueryBuilder) -> Self {
173 let nested = build(QueryBuilder::new(self.table.clone()));
174 if !nested.conditions.is_empty() {
175 self.conditions.push(Condition::Group { conditions: nested.conditions, or: false });
176 }
177 self
178 }
179
180 pub fn or_group_filter(mut self, build: impl FnOnce(QueryBuilder) -> QueryBuilder) -> Self {
181 let nested = build(QueryBuilder::new(self.table.clone()));
182 if !nested.conditions.is_empty() {
183 self.conditions.push(Condition::Group { conditions: nested.conditions, or: true });
184 }
185 self
186 }
187
188 pub fn join(self, table: &str, left: &str, operator: &str, right: &str) -> Self {
189 self.add_join("inner join", table, left, operator, right)
190 }
191
192 pub fn left_join(self, table: &str, left: &str, operator: &str, right: &str) -> Self {
193 self.add_join("left join", table, left, operator, right)
194 }
195
196 fn add_join(
197 mut self,
198 kind: &'static str,
199 table: &str,
200 left: &str,
201 operator: &str,
202 right: &str,
203 ) -> Self {
204 self.joins.push(Join {
205 kind,
206 table: table.to_string(),
207 left: left.to_string(),
208 operator: operator.to_string(),
209 right: right.to_string(),
210 });
211 self
212 }
213
214 pub fn order_by(mut self, column: &str, direction: Direction) -> Self {
215 self.order.push((column.to_string(), direction));
216 self
217 }
218
219 pub fn latest(self, column: &str) -> Self {
220 self.order_by(column, Direction::Desc)
221 }
222
223 pub fn group_by(mut self, columns: &[&str]) -> Self {
224 self.group = columns.iter().map(|c| (*c).to_string()).collect();
225 self
226 }
227
228 pub fn limit(mut self, limit: i64) -> Self {
229 self.limit = Some(limit);
230 self
231 }
232
233 pub fn offset(mut self, offset: i64) -> Self {
234 self.offset = Some(offset);
235 self
236 }
237
238 pub fn page(self, page: i64, per_page: i64) -> Self {
240 let page = page.max(1);
241 self.limit(per_page).offset((page - 1) * per_page)
242 }
243
244 pub fn to_sql(&self, dialect: &dyn Dialect) -> Result<(String, Vec<Value>)> {
252 let mut params = Vec::new();
253 let mut sql = String::from("select ");
254
255 if self.distinct {
256 sql.push_str("distinct ");
257 }
258
259 if self.columns.is_empty() {
260 sql.push('*');
261 } else {
262 let rendered: Result<Vec<String>> =
263 self.columns.iter().map(|c| column_ref(dialect, c)).collect();
264 sql.push_str(&rendered?.join(", "));
265 }
266
267 sql.push_str(" from ");
268 sql.push_str("e_qualified(dialect, &self.table)?);
269
270 for join in &self.joins {
271 check_operator(&join.operator)?;
272 sql.push_str(&format!(
273 " {} {} on {} {} {}",
274 join.kind,
275 quote_qualified(dialect, &join.table)?,
276 column_ref(dialect, &join.left)?,
277 join.operator,
278 column_ref(dialect, &join.right)?
279 ));
280 }
281
282 if !self.conditions.is_empty() {
283 sql.push_str(" where ");
284 sql.push_str(&render_conditions(dialect, &self.conditions, &mut params)?);
285 }
286
287 if !self.group.is_empty() {
288 let rendered: Result<Vec<String>> =
289 self.group.iter().map(|c| column_ref(dialect, c)).collect();
290 sql.push_str(&format!(" group by {}", rendered?.join(", ")));
291 }
292
293 if !self.order.is_empty() {
294 let rendered: Result<Vec<String>> = self
295 .order
296 .iter()
297 .map(|(column, direction)| {
298 Ok(format!("{} {}", column_ref(dialect, column)?, direction.as_sql()))
299 })
300 .collect();
301 sql.push_str(&format!(" order by {}", rendered?.join(", ")));
302 }
303
304 sql.push_str(&dialect.limit_offset(self.limit, self.offset, !self.order.is_empty()));
308
309 Ok((sql, params))
310 }
311
312 pub fn to_count_sql(&self, dialect: &dyn Dialect) -> Result<(String, Vec<Value>)> {
314 let counting = QueryBuilder {
315 columns: vec!["count(*) as aggregate".to_string()],
316 order: Vec::new(),
317 limit: None,
318 offset: None,
319 ..self.clone()
320 };
321 counting.to_sql(dialect)
322 }
323
324 fn to_insert_sql(
325 &self,
326 dialect: &dyn Dialect,
327 rows: &[Vec<(String, Value)>],
328 returning: Option<&str>,
329 ) -> Result<(String, Vec<Value>)> {
330 let first = rows.first().ok_or_else(|| Error::msg("insert needs at least one row"))?;
331 let columns: Vec<String> = first.iter().map(|(name, _)| name.clone()).collect();
332
333 let rendered: Result<Vec<String>> = columns
334 .iter()
335 .map(|c| {
336 validate_identifier(c, dialect.max_identifier_length()).map(|_| dialect.quote(c))
337 })
338 .collect();
339
340 let mut params = Vec::new();
341 let mut placeholders = Vec::new();
342
343 for row in rows {
344 if row.len() != columns.len() {
345 return Err(Error::msg(
346 "every row in a bulk insert must have the same columns".to_string(),
347 ));
348 }
349 let mut slots = Vec::with_capacity(row.len());
350 for (name, value) in row {
351 if !columns.contains(name) {
352 return Err(Error::msg(format!(
353 "column `{name}` is missing from the first row of this insert"
354 )));
355 }
356 params.push(value.clone());
357 slots.push(dialect.placeholder(params.len()));
358 }
359 placeholders.push(format!("({})", slots.join(", ")));
360 }
361
362 let table = quote_qualified(dialect, &self.table)?;
363 let columns_sql = rendered?.join(", ");
364 let values_sql = placeholders.join(", ");
365
366 let sql = match (returning, dialect.returning()) {
370 (None, _) | (Some(_), ReturningStyle::SeparateQuery(_)) => {
371 format!("insert into {table} ({columns_sql}) values {values_sql}")
372 }
373 (Some(column), ReturningStyle::Suffix) => {
374 validate_identifier(column, dialect.max_identifier_length())?;
375 format!(
376 "insert into {table} ({columns_sql}) values {values_sql} returning {}",
377 dialect.quote(column)
378 )
379 }
380 (Some(column), ReturningStyle::OutputClause) => {
381 validate_identifier(column, dialect.max_identifier_length())?;
382 format!(
386 "insert into {table} ({columns_sql}) output inserted.{} values {values_sql}",
387 dialect.quote(column)
388 )
389 }
390 };
391
392 Ok((sql, params))
393 }
394
395 fn to_update_sql(
396 &self,
397 dialect: &dyn Dialect,
398 values: &[(String, Value)],
399 ) -> Result<(String, Vec<Value>)> {
400 if values.is_empty() {
401 return Err(Error::msg("update needs at least one column"));
402 }
403
404 let mut params = Vec::new();
405 let mut assignments = Vec::with_capacity(values.len());
406
407 for (column, value) in values {
408 validate_identifier(column, dialect.max_identifier_length())?;
409 params.push(value.clone());
410 assignments.push(format!(
411 "{} = {}",
412 dialect.quote(column),
413 dialect.placeholder(params.len())
414 ));
415 }
416
417 let mut sql = format!(
418 "update {} set {}",
419 quote_qualified(dialect, &self.table)?,
420 assignments.join(", ")
421 );
422
423 if self.conditions.is_empty() {
424 return Err(Error::msg(
427 "refusing to update every row: add a filter, or call `update_all` if that is really intended"
428 .to_string(),
429 ));
430 }
431
432 sql.push_str(" where ");
433 sql.push_str(&render_conditions(dialect, &self.conditions, &mut params)?);
434 Ok((sql, params))
435 }
436
437 fn to_delete_sql(&self, dialect: &dyn Dialect, allow_all: bool) -> Result<(String, Vec<Value>)> {
438 let mut params = Vec::new();
439 let mut sql = format!("delete from {}", quote_qualified(dialect, &self.table)?);
440
441 if self.conditions.is_empty() {
442 if !allow_all {
443 return Err(Error::msg(
444 "refusing to delete every row: add a filter, or call `delete_all` if that is really intended"
445 .to_string(),
446 ));
447 }
448 } else {
449 sql.push_str(" where ");
450 sql.push_str(&render_conditions(dialect, &self.conditions, &mut params)?);
451 }
452
453 Ok((sql, params))
454 }
455
456 pub async fn get(&self, db: &Database) -> Result<Vec<Row>> {
460 let (sql, params) = self.to_sql(db.dialect())?;
461 db.select(&sql, ¶ms).await
462 }
463
464 pub async fn first(&self, db: &Database) -> Result<Option<Row>> {
466 let (sql, params) = self.clone().limit(1).to_sql(db.dialect())?;
467 db.select_one(&sql, ¶ms).await
468 }
469
470 pub async fn get_json(&self, db: &Database) -> Result<Json> {
472 Ok(crate::rows_to_json(&self.get(db).await?))
473 }
474
475 pub async fn count(&self, db: &Database) -> Result<i64> {
476 let (sql, params) = self.to_count_sql(db.dialect())?;
477 Ok(db.scalar::<i64>(&sql, ¶ms).await?.unwrap_or(0))
478 }
479
480 pub async fn exists(&self, db: &Database) -> Result<bool> {
481 Ok(self.count(db).await? > 0)
482 }
483
484 pub async fn insert(&self, db: &Database, values: &[(&str, Value)]) -> Result<i64> {
486 i64::from_value(&self.insert_returning(db, values, "id").await?)
489 }
490
491 pub async fn insert_returning(
494 &self,
495 db: &Database,
496 values: &[(&str, Value)],
497 column: &str,
498 ) -> Result<Value> {
499 let row: Vec<(String, Value)> =
500 values.iter().map(|(name, value)| ((*name).to_string(), value.clone())).collect();
501 let (sql, params) =
502 self.to_insert_sql(db.dialect(), std::slice::from_ref(&row), Some(column))?;
503
504 db.insert_returning_key(&sql, ¶ms, column).await?.ok_or_else(|| {
505 Error::msg(format!(
506 "the insert did not report a `{column}`. The column has to be generated by the \
507 database — an auto-incrementing key or a default — for it to have one to report."
508 ))
509 })
510 }
511
512 pub async fn insert_without_id(&self, db: &Database, values: &[(&str, Value)]) -> Result<u64> {
514 let row: Vec<(String, Value)> =
515 values.iter().map(|(name, value)| ((*name).to_string(), value.clone())).collect();
516 let (sql, params) = self.to_insert_sql(db.dialect(), std::slice::from_ref(&row), None)?;
517 db.execute(&sql, ¶ms).await
518 }
519
520 pub async fn insert_many(&self, db: &Database, rows: &[Vec<(String, Value)>]) -> Result<u64> {
522 if rows.is_empty() {
523 return Ok(0);
524 }
525 let (sql, params) = self.to_insert_sql(db.dialect(), rows, None)?;
526 db.execute(&sql, ¶ms).await
527 }
528
529 pub async fn update(&self, db: &Database, values: &[(&str, Value)]) -> Result<u64> {
531 let owned: Vec<(String, Value)> =
532 values.iter().map(|(name, value)| ((*name).to_string(), value.clone())).collect();
533 let (sql, params) = self.to_update_sql(db.dialect(), &owned)?;
534 db.execute(&sql, ¶ms).await
535 }
536
537 pub async fn delete(&self, db: &Database) -> Result<u64> {
539 let (sql, params) = self.to_delete_sql(db.dialect(), false)?;
540 db.execute(&sql, ¶ms).await
541 }
542
543 pub async fn delete_all(&self, db: &Database) -> Result<u64> {
545 let (sql, params) = self.to_delete_sql(db.dialect(), true)?;
546 db.execute(&sql, ¶ms).await
547 }
548}
549
550fn render_conditions(
552 dialect: &dyn Dialect,
553 conditions: &[Condition],
554 params: &mut Vec<Value>,
555) -> Result<String> {
556 let mut out = String::new();
557
558 for (index, condition) in conditions.iter().enumerate() {
559 if index > 0 {
560 out.push_str(if condition.is_or() { " or " } else { " and " });
561 }
562
563 match condition {
564 Condition::Comparison { column, operator, value, .. } => {
565 check_operator(operator)?;
566 params.push(value.clone());
567 out.push_str(&format!(
568 "{} {operator} {}",
569 column_ref(dialect, column)?,
570 dialect.placeholder(params.len())
571 ));
572 }
573 Condition::In { column, values, negated, .. } => {
574 if values.is_empty() {
575 out.push_str(if *negated { "true" } else { "false" });
577 continue;
578 }
579 let mut slots = Vec::with_capacity(values.len());
580 for value in values {
581 params.push(value.clone());
582 slots.push(dialect.placeholder(params.len()));
583 }
584 out.push_str(&format!(
585 "{} {}in ({})",
586 column_ref(dialect, column)?,
587 if *negated { "not " } else { "" },
588 slots.join(", ")
589 ));
590 }
591 Condition::Null { column, negated, .. } => {
592 out.push_str(&format!(
593 "{} is {}null",
594 column_ref(dialect, column)?,
595 if *negated { "not " } else { "" }
596 ));
597 }
598 Condition::Between { column, low, high, .. } => {
599 params.push(low.clone());
600 let low_slot = dialect.placeholder(params.len());
601 params.push(high.clone());
602 out.push_str(&format!(
603 "{} between {low_slot} and {}",
604 column_ref(dialect, column)?,
605 dialect.placeholder(params.len())
606 ));
607 }
608 Condition::Group { conditions, .. } => {
609 out.push_str(&format!("({})", render_conditions(dialect, conditions, params)?));
610 }
611 }
612 }
613
614 Ok(out)
615}
616
617fn check_operator(operator: &str) -> Result<()> {
622 const ALLOWED: &[&str] = &[
623 "=", "!=", "<>", "<", "<=", ">", ">=", "like", "not like", "ilike", "not ilike", "@>", "<@",
624 "?", "is distinct from",
625 ];
626
627 if ALLOWED.contains(&operator.to_ascii_lowercase().as_str()) {
628 Ok(())
629 } else {
630 Err(Error::msg(format!(
631 "`{operator}` is not an allowed comparison operator. Allowed: {}",
632 ALLOWED.join(", ")
633 )))
634 }
635}
636
637fn column_ref(dialect: &dyn Dialect, column: &str) -> Result<String> {
640 if column == "*" {
641 return Ok("*".to_string());
642 }
643
644 if let Some(rest) = column.strip_prefix("count(*) as ") {
647 validate_identifier(rest, dialect.max_identifier_length())?;
648 return Ok(format!("count(*) as {}", dialect.quote(rest)));
649 }
650
651 for prefix in ["count", "sum", "avg", "min", "max"] {
652 if let Some(inner) = column
653 .strip_prefix(&format!("{prefix}("))
654 .and_then(|rest| rest.strip_suffix(')'))
655 {
656 let rendered =
657 if inner == "*" { "*".to_string() } else { quote_qualified(dialect, inner)? };
658 return Ok(format!("{prefix}({rendered})"));
659 }
660 }
661
662 quote_qualified(dialect, column)
663}
664
665#[cfg(test)]
666mod tests {
667 use super::*;
668 use crate::dialect::{MySql, Postgres, SqlServer};
669
670 fn sql_of(builder: QueryBuilder) -> (String, Vec<Value>) {
674 builder.to_sql(&Postgres).unwrap()
675 }
676
677 #[test]
678 fn builds_a_plain_select() {
679 let (sql, params) = sql_of(QueryBuilder::new("users"));
680
681 assert_eq!(sql, r#"select * from "users""#);
682 assert!(params.is_empty());
683 }
684
685 #[test]
686 fn values_become_numbered_parameters() {
687 let (sql, params) = sql_of(
688 QueryBuilder::new("users")
689 .select(&["id", "name"])
690 .filter("email", "ada@example.com")
691 .filter_op("age", ">", 18),
692 );
693
694 assert_eq!(
695 sql,
696 r#"select "id", "name" from "users" where "email" = $1 and "age" > $2"#
697 );
698 assert_eq!(params, vec![Value::Text("ada@example.com".into()), Value::Int(18)]);
699 }
700
701 #[test]
702 fn a_value_can_never_change_the_statement() {
703 let (sql, params) = sql_of(QueryBuilder::new("users").filter("name", "'; drop table users; --"));
704
705 assert_eq!(sql, r#"select * from "users" where "name" = $1"#);
706 assert_eq!(params[0], Value::Text("'; drop table users; --".into()));
707 }
708
709 #[test]
710 fn an_injected_identifier_is_rejected() {
711 let error = QueryBuilder::new("users").filter("name; drop table users", 1).to_sql(&Postgres).unwrap_err();
712 assert!(error.to_string().contains("not a valid SQL identifier"));
713
714 assert!(QueryBuilder::new("users; drop table users").to_sql(&Postgres).is_err());
715 }
716
717 #[test]
718 fn an_unknown_operator_is_rejected() {
719 let error = QueryBuilder::new("users").filter_op("id", "; drop", 1).to_sql(&Postgres).unwrap_err();
720 assert!(error.to_string().contains("not an allowed comparison operator"));
721 }
722
723 #[test]
724 fn renders_in_null_and_between() {
725 let (sql, params) = sql_of(
726 QueryBuilder::new("posts")
727 .filter_in("status", vec![Value::from("draft"), Value::from("live")])
728 .filter_not_null("published_at")
729 .filter_between("views", 10, 100),
730 );
731
732 assert_eq!(
733 sql,
734 r#"select * from "posts" where "status" in ($1, $2) and "published_at" is not null and "views" between $3 and $4"#
735 );
736 assert_eq!(params.len(), 4);
737 }
738
739 #[test]
740 fn an_empty_in_list_matches_nothing_instead_of_breaking() {
741 let (sql, _) = sql_of(QueryBuilder::new("posts").filter_in("id", vec![]));
742 assert_eq!(sql, r#"select * from "posts" where false"#);
743 }
744
745 #[test]
746 fn groups_keep_or_conditions_together() {
747 let (sql, params) = sql_of(
748 QueryBuilder::new("users")
749 .filter("active", true)
750 .group_filter(|q| q.filter("role", "admin").or_filter("role", "owner")),
751 );
752
753 assert_eq!(
754 sql,
755 r#"select * from "users" where "active" = $1 and ("role" = $2 or "role" = $3)"#
756 );
757 assert_eq!(params.len(), 3);
758 }
759
760 #[test]
761 fn joins_order_and_paging() {
762 let (sql, _) = sql_of(
763 QueryBuilder::new("posts")
764 .select(&["posts.title", "users.name"])
765 .join("users", "posts.user_id", "=", "users.id")
766 .latest("posts.created_at")
767 .page(3, 20),
768 );
769
770 assert_eq!(
771 sql,
772 r#"select "posts"."title", "users"."name" from "posts" inner join "users" on "posts"."user_id" = "users"."id" order by "posts"."created_at" desc limit 20 offset 40"#
773 );
774 }
775
776 #[test]
777 fn counting_drops_order_and_paging() {
778 let (sql, _) = QueryBuilder::new("users")
779 .filter("active", true)
780 .latest("created_at")
781 .page(2, 10)
782 .to_count_sql(&Postgres)
783 .unwrap();
784
785 assert_eq!(
786 sql,
787 r#"select count(*) as "aggregate" from "users" where "active" = $1"#
788 );
789 }
790
791 #[test]
792 fn builds_an_insert_with_a_returning_clause() {
793 let row = vec![
794 ("name".to_string(), Value::from("Ada")),
795 ("email".to_string(), Value::from("ada@example.com")),
796 ];
797 let (sql, params) =
798 QueryBuilder::new("users").to_insert_sql(&Postgres, std::slice::from_ref(&row), Some("id")).unwrap();
799
800 assert_eq!(
801 sql,
802 r#"insert into "users" ("name", "email") values ($1, $2) returning "id""#
803 );
804 assert_eq!(params.len(), 2);
805 }
806
807 #[test]
808 fn a_bulk_insert_numbers_every_row() {
809 let rows = vec![
810 vec![("name".to_string(), Value::from("a"))],
811 vec![("name".to_string(), Value::from("b"))],
812 ];
813 let (sql, params) = QueryBuilder::new("users").to_insert_sql(&Postgres, &rows, None).unwrap();
814
815 assert_eq!(sql, r#"insert into "users" ("name") values ($1), ($2)"#);
816 assert_eq!(params.len(), 2);
817 }
818
819 #[test]
820 fn update_and_delete_refuse_to_touch_every_row_by_accident() {
821 let values = vec![("name".to_string(), Value::from("x"))];
822
823 let error = QueryBuilder::new("users").to_update_sql(&Postgres, &values).unwrap_err();
824 assert!(error.to_string().contains("refusing to update every row"));
825
826 let error = QueryBuilder::new("users").to_delete_sql(&Postgres, false).unwrap_err();
827 assert!(error.to_string().contains("refusing to delete every row"));
828
829 assert!(QueryBuilder::new("users").to_delete_sql(&Postgres, true).is_ok());
831 assert!(
832 QueryBuilder::new("users")
833 .filter("id", 1)
834 .to_update_sql(&Postgres, &values)
835 .is_ok()
836 );
837 }
838
839 #[test]
840 fn one_chain_produces_correct_sql_for_every_database() {
841 let query = QueryBuilder::new("users")
842 .select(&["id", "name"])
843 .filter("active", true)
844 .latest("created_at")
845 .page(2, 10);
846
847 let (postgres, _) = query.to_sql(&Postgres).unwrap();
848 assert_eq!(
849 postgres,
850 r#"select "id", "name" from "users" where "active" = $1 order by "created_at" desc limit 10 offset 10"#
851 );
852
853 let (mysql, _) = query.to_sql(&MySql).unwrap();
854 assert_eq!(
855 mysql,
856 "select `id`, `name` from `users` where `active` = ? order by `created_at` desc limit 10 offset 10"
857 );
858
859 let (sqlserver, _) = query.to_sql(&SqlServer).unwrap();
860 assert_eq!(
861 sqlserver,
862 "select [id], [name] from [users] where [active] = @P1 order by [created_at] desc offset 10 rows fetch next 10 rows only"
863 );
864 }
865
866 #[test]
867 fn parameters_are_numbered_or_not_according_to_the_database() {
868 let query = QueryBuilder::new("posts").filter("a", 1).filter("b", 2).filter("c", 3);
869
870 let (postgres, params) = query.to_sql(&Postgres).unwrap();
871 assert!(postgres.ends_with("$1 and \"b\" = $2 and \"c\" = $3"), "{postgres}");
872 assert_eq!(params.len(), 3);
873
874 let (mysql, params) = query.to_sql(&MySql).unwrap();
877 assert!(mysql.ends_with("? and `b` = ? and `c` = ?"), "{mysql}");
878 assert_eq!(params.len(), 3);
879 }
880
881 #[test]
882 fn a_generated_key_is_asked_for_in_each_databases_own_way() {
883 let row = vec![("name".to_string(), Value::from("Ada"))];
884 let rows = std::slice::from_ref(&row);
885
886 let (postgres, _) =
887 QueryBuilder::new("users").to_insert_sql(&Postgres, rows, Some("id")).unwrap();
888 assert!(postgres.ends_with(r#"returning "id""#), "{postgres}");
889
890 let (sqlserver, _) =
892 QueryBuilder::new("users").to_insert_sql(&SqlServer, rows, Some("id")).unwrap();
893 assert_eq!(
894 sqlserver,
895 "insert into [users] ([name]) output inserted.[id] values (@P1)"
896 );
897
898 let (mysql, _) = QueryBuilder::new("users").to_insert_sql(&MySql, rows, Some("id")).unwrap();
900 assert_eq!(mysql, "insert into `users` (`name`) values (?)");
901 }
902
903 #[test]
904 fn sql_server_gets_an_ordering_it_can_page_with() {
905 let (sql, _) = QueryBuilder::new("users").page(3, 20).to_sql(&SqlServer).unwrap();
908
909 assert!(sql.contains("order by (select null)"), "{sql}");
910 assert!(sql.ends_with("offset 40 rows fetch next 20 rows only"), "{sql}");
911 }
912
913 #[test]
914 fn an_injected_identifier_is_rejected_whatever_the_database() {
915 for dialect in [&Postgres as &dyn Dialect, &MySql, &SqlServer] {
916 let error = QueryBuilder::new("users")
917 .filter("name; drop table users", 1)
918 .to_sql(dialect)
919 .unwrap_err();
920
921 assert!(
922 error.to_string().contains("not a valid SQL identifier"),
923 "{} accepted it",
924 dialect.name()
925 );
926 }
927 }
928
929 #[test]
930 fn an_update_numbers_values_before_conditions() {
931 let values = vec![("name".to_string(), Value::from("Ada"))];
932 let (sql, params) =
933 QueryBuilder::new("users").filter("id", 7).to_update_sql(&Postgres, &values).unwrap();
934
935 assert_eq!(sql, r#"update "users" set "name" = $1 where "id" = $2"#);
936 assert_eq!(params, vec![Value::Text("Ada".into()), Value::Int(7)]);
937 }
938}