Skip to main content

rustlavel_db/
builder.rs

1//! The query builder — `DB::table("users").where(...).get()` in Rust.
2//!
3//! Every value goes out as a bound parameter and every identifier is validated
4//! and quoted, so a builder chain cannot produce injectable SQL even when the
5//! column name came from user input.
6
7use 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/// One condition in a `where` clause.
28#[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    /// A nested group, so `where(a).where(|q| q.or(b).or(c))` keeps its meaning.
35    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/// A statement being assembled.
60#[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    /// Choose columns. Without this the query selects everything.
89    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    /// `where column = value`.
100    pub fn filter(self, column: &str, value: impl Into<Value>) -> Self {
101        self.filter_op(column, "=", value)
102    }
103
104    /// `where column <operator> value`, with the operator checked against a
105    /// list — an operator cannot be smuggled in from user input.
106    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    /// `where column like pattern`.
167    pub fn filter_like(self, column: &str, pattern: impl Into<Value>) -> Self {
168        self.filter_op(column, "like", pattern)
169    }
170
171    /// A parenthesised group of conditions.
172    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    /// Limit and offset for a 1-based page.
239    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    // --- SQL generation ---
245
246    /// Build the `select` statement and its parameters for one database.
247    ///
248    /// The dialect supplies the quoting, the placeholders and the paging
249    /// syntax, so the same builder chain is correct on PostgreSQL, MySQL and
250    /// SQL Server without the caller knowing which is underneath.
251    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(&quote_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        // Limit and offset are numbers the builder controls, never user text.
305        // The dialect decides how they are spelled, and whether an ordering has
306        // to be invented for them to be legal.
307        sql.push_str(&dialect.limit_offset(self.limit, self.offset, !self.order.is_empty()));
308
309        Ok((sql, params))
310    }
311
312    /// The `select count(*)` form of this query, ignoring order and paging.
313    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        // Where the "hand the new key back" clause goes is structural, not
367        // cosmetic: PostgreSQL appends it, SQL Server puts it before the column
368        // list, and MySQL has no clause at all.
369        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                // T-SQL puts OUTPUT between the column list and VALUES. Before
383                // the column list it parses as a second column list and fails
384                // with a count mismatch — which a real server confirmed.
385                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            // An unfiltered update rewrites the whole table. Requiring an
425            // explicit filter turns a catastrophe into a compile-time-ish error.
426            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    // --- Execution ---
457
458    /// Run the query and return every matching row.
459    pub async fn get(&self, db: &Database) -> Result<Vec<Row>> {
460        let (sql, params) = self.to_sql(db.dialect())?;
461        db.select(&sql, &params).await
462    }
463
464    /// The first matching row, if any.
465    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, &params).await
468    }
469
470    /// Rows as a JSON array, ready to return from an API handler.
471    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, &params).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    /// Insert one row, returning its `id`.
485    pub async fn insert(&self, db: &Database, values: &[(&str, Value)]) -> Result<i64> {
486        // One path for all three databases: `insert_returning` knows that the
487        // key arrives as a row on some and in the acknowledgement on others.
488        i64::from_value(&self.insert_returning(db, values, "id").await?)
489    }
490
491    /// Insert one row and return one column of it — how a model picks up the
492    /// key the database generated.
493    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, &params, 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    /// Insert one row without asking for a generated key.
513    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, &params).await
518    }
519
520    /// Insert many rows in one statement.
521    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, &params).await
527    }
528
529    /// Update the rows this query matches. Requires a filter.
530    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, &params).await
535    }
536
537    /// Delete the rows this query matches. Requires a filter.
538    pub async fn delete(&self, db: &Database) -> Result<u64> {
539        let (sql, params) = self.to_delete_sql(db.dialect(), false)?;
540        db.execute(&sql, &params).await
541    }
542
543    /// Delete every row in the table. Deliberately separate from `delete`.
544    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, &params).await
547    }
548}
549
550/// Render a condition list, threading parameter numbering through it.
551fn 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                    // `in ()` is a syntax error; an empty set matches nothing.
576                    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
617/// The operators a builder is allowed to emit.
618///
619/// An allowlist rather than escaping: there is no legitimate reason for an
620/// operator to be anything else, and a typo becomes an error instead of SQL.
621fn 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
637/// Render a column reference, which may be qualified (`users.id`), aliased
638/// (`count(*) as aggregate` is passed through) or plain.
639fn column_ref(dialect: &dyn Dialect, column: &str) -> Result<String> {
640    if column == "*" {
641        return Ok("*".to_string());
642    }
643
644    // The builder generates a handful of aggregate expressions itself; anything
645    // else must be a plain identifier.
646    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    /// The default in these tests, since the SQL they assert is PostgreSQL's.
671    /// The dialect-specific behaviour is covered by dialect.rs and by the
672    /// cross-dialect tests at the end of this module.
673    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        // The explicit forms are allowed.
830        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        // MySQL binds positionally, so every placeholder is the same token and
875        // the order of the parameter list is the only thing that matters.
876        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        // SQL Server puts it before the column list, so it cannot be appended.
891        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        // MySQL has no such clause; the key is read with a second statement.
899        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        // `offset` is a syntax error without `order by`, and the builder
906        // supplies one rather than letting the database complain.
907        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}