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    /// `or column <operator> value`.
127    ///
128    /// The `or_` half of [`filter_op`](Self::filter_op), which was missing:
129    /// `or_filter` could only ever mean equality, so a search across three
130    /// columns with `like` had no way to say so.
131    pub fn or_filter_op(mut self, column: &str, operator: &str, value: impl Into<Value>) -> Self {
132        self.conditions.push(Condition::Comparison {
133            column: column.to_string(),
134            operator: operator.to_string(),
135            value: value.into(),
136            or: true,
137        });
138        self
139    }
140
141    /// `or column like pattern`.
142    pub fn or_filter_like(self, column: &str, pattern: impl Into<Value>) -> Self {
143        self.or_filter_op(column, "like", pattern)
144    }
145
146    pub fn filter_in(mut self, column: &str, values: Vec<Value>) -> Self {
147        self.conditions.push(Condition::In {
148            column: column.to_string(),
149            values,
150            negated: false,
151            or: false,
152        });
153        self
154    }
155
156    pub fn filter_not_in(mut self, column: &str, values: Vec<Value>) -> Self {
157        self.conditions.push(Condition::In {
158            column: column.to_string(),
159            values,
160            negated: true,
161            or: false,
162        });
163        self
164    }
165
166    pub fn filter_null(mut self, column: &str) -> Self {
167        self.conditions.push(Condition::Null { column: column.to_string(), negated: false, or: false });
168        self
169    }
170
171    pub fn filter_not_null(mut self, column: &str) -> Self {
172        self.conditions.push(Condition::Null { column: column.to_string(), negated: true, or: false });
173        self
174    }
175
176    pub fn filter_between(mut self, column: &str, low: impl Into<Value>, high: impl Into<Value>) -> Self {
177        self.conditions.push(Condition::Between {
178            column: column.to_string(),
179            low: low.into(),
180            high: high.into(),
181            or: false,
182        });
183        self
184    }
185
186    /// `where column like pattern`.
187    pub fn filter_like(self, column: &str, pattern: impl Into<Value>) -> Self {
188        self.filter_op(column, "like", pattern)
189    }
190
191    /// A parenthesised group of conditions.
192    pub fn group_filter(mut self, build: impl FnOnce(QueryBuilder) -> QueryBuilder) -> Self {
193        let nested = build(QueryBuilder::new(self.table.clone()));
194        if !nested.conditions.is_empty() {
195            self.conditions.push(Condition::Group { conditions: nested.conditions, or: false });
196        }
197        self
198    }
199
200    pub fn or_group_filter(mut self, build: impl FnOnce(QueryBuilder) -> QueryBuilder) -> Self {
201        let nested = build(QueryBuilder::new(self.table.clone()));
202        if !nested.conditions.is_empty() {
203            self.conditions.push(Condition::Group { conditions: nested.conditions, or: true });
204        }
205        self
206    }
207
208    pub fn join(self, table: &str, left: &str, operator: &str, right: &str) -> Self {
209        self.add_join("inner join", table, left, operator, right)
210    }
211
212    pub fn left_join(self, table: &str, left: &str, operator: &str, right: &str) -> Self {
213        self.add_join("left join", table, left, operator, right)
214    }
215
216    fn add_join(
217        mut self,
218        kind: &'static str,
219        table: &str,
220        left: &str,
221        operator: &str,
222        right: &str,
223    ) -> Self {
224        self.joins.push(Join {
225            kind,
226            table: table.to_string(),
227            left: left.to_string(),
228            operator: operator.to_string(),
229            right: right.to_string(),
230        });
231        self
232    }
233
234    pub fn order_by(mut self, column: &str, direction: Direction) -> Self {
235        self.order.push((column.to_string(), direction));
236        self
237    }
238
239    pub fn latest(self, column: &str) -> Self {
240        self.order_by(column, Direction::Desc)
241    }
242
243    pub fn group_by(mut self, columns: &[&str]) -> Self {
244        self.group = columns.iter().map(|c| (*c).to_string()).collect();
245        self
246    }
247
248    pub fn limit(mut self, limit: i64) -> Self {
249        self.limit = Some(limit);
250        self
251    }
252
253    pub fn offset(mut self, offset: i64) -> Self {
254        self.offset = Some(offset);
255        self
256    }
257
258    /// Limit and offset for a 1-based page.
259    pub fn page(self, page: i64, per_page: i64) -> Self {
260        let page = page.max(1);
261        self.limit(per_page).offset((page - 1) * per_page)
262    }
263
264    // --- SQL generation ---
265
266    /// Build the `select` statement and its parameters for one database.
267    ///
268    /// The dialect supplies the quoting, the placeholders and the paging
269    /// syntax, so the same builder chain is correct on PostgreSQL, MySQL and
270    /// SQL Server without the caller knowing which is underneath.
271    pub fn to_sql(&self, dialect: &dyn Dialect) -> Result<(String, Vec<Value>)> {
272        let mut params = Vec::new();
273        let mut sql = String::from("select ");
274
275        if self.distinct {
276            sql.push_str("distinct ");
277        }
278
279        if self.columns.is_empty() {
280            sql.push('*');
281        } else {
282            let rendered: Result<Vec<String>> =
283                self.columns.iter().map(|c| column_ref(dialect, c)).collect();
284            sql.push_str(&rendered?.join(", "));
285        }
286
287        sql.push_str(" from ");
288        sql.push_str(&quote_qualified(dialect, &self.table)?);
289
290        for join in &self.joins {
291            check_operator(&join.operator)?;
292            sql.push_str(&format!(
293                " {} {} on {} {} {}",
294                join.kind,
295                quote_qualified(dialect, &join.table)?,
296                column_ref(dialect, &join.left)?,
297                join.operator,
298                column_ref(dialect, &join.right)?
299            ));
300        }
301
302        if !self.conditions.is_empty() {
303            sql.push_str(" where ");
304            sql.push_str(&render_conditions(dialect, &self.conditions, &mut params)?);
305        }
306
307        if !self.group.is_empty() {
308            let rendered: Result<Vec<String>> =
309                self.group.iter().map(|c| column_ref(dialect, c)).collect();
310            sql.push_str(&format!(" group by {}", rendered?.join(", ")));
311        }
312
313        if !self.order.is_empty() {
314            let rendered: Result<Vec<String>> = self
315                .order
316                .iter()
317                .map(|(column, direction)| {
318                    Ok(format!("{} {}", column_ref(dialect, column)?, direction.as_sql()))
319                })
320                .collect();
321            sql.push_str(&format!(" order by {}", rendered?.join(", ")));
322        }
323
324        // Limit and offset are numbers the builder controls, never user text.
325        // The dialect decides how they are spelled, and whether an ordering has
326        // to be invented for them to be legal.
327        sql.push_str(&dialect.limit_offset(self.limit, self.offset, !self.order.is_empty()));
328
329        Ok((sql, params))
330    }
331
332    /// The `select count(*)` form of this query, ignoring order and paging.
333    pub fn to_count_sql(&self, dialect: &dyn Dialect) -> Result<(String, Vec<Value>)> {
334        let counting = QueryBuilder {
335            columns: vec!["count(*) as aggregate".to_string()],
336            order: Vec::new(),
337            limit: None,
338            offset: None,
339            ..self.clone()
340        };
341        counting.to_sql(dialect)
342    }
343
344    fn to_insert_sql(
345        &self,
346        dialect: &dyn Dialect,
347        rows: &[Vec<(String, Value)>],
348        returning: Option<&str>,
349    ) -> Result<(String, Vec<Value>)> {
350        let first = rows.first().ok_or_else(|| Error::msg("insert needs at least one row"))?;
351        let columns: Vec<String> = first.iter().map(|(name, _)| name.clone()).collect();
352
353        let rendered: Result<Vec<String>> = columns
354            .iter()
355            .map(|c| {
356                validate_identifier(c, dialect.max_identifier_length()).map(|_| dialect.quote(c))
357            })
358            .collect();
359
360        let mut params = Vec::new();
361        let mut placeholders = Vec::new();
362
363        for row in rows {
364            if row.len() != columns.len() {
365                return Err(Error::msg(
366                    "every row in a bulk insert must have the same columns".to_string(),
367                ));
368            }
369            let mut slots = Vec::with_capacity(row.len());
370            for (name, value) in row {
371                if !columns.contains(name) {
372                    return Err(Error::msg(format!(
373                        "column `{name}` is missing from the first row of this insert"
374                    )));
375                }
376                params.push(value.clone());
377                slots.push(dialect.placeholder(params.len()));
378            }
379            placeholders.push(format!("({})", slots.join(", ")));
380        }
381
382        let table = quote_qualified(dialect, &self.table)?;
383        let columns_sql = rendered?.join(", ");
384        let values_sql = placeholders.join(", ");
385
386        // Where the "hand the new key back" clause goes is structural, not
387        // cosmetic: PostgreSQL appends it, SQL Server puts it before the column
388        // list, and MySQL has no clause at all.
389        let sql = match (returning, dialect.returning()) {
390            (None, _) | (Some(_), ReturningStyle::SeparateQuery(_)) => {
391                format!("insert into {table} ({columns_sql}) values {values_sql}")
392            }
393            (Some(column), ReturningStyle::Suffix) => {
394                validate_identifier(column, dialect.max_identifier_length())?;
395                format!(
396                    "insert into {table} ({columns_sql}) values {values_sql} returning {}",
397                    dialect.quote(column)
398                )
399            }
400            (Some(column), ReturningStyle::OutputClause) => {
401                validate_identifier(column, dialect.max_identifier_length())?;
402                // T-SQL puts OUTPUT between the column list and VALUES. Before
403                // the column list it parses as a second column list and fails
404                // with a count mismatch — which a real server confirmed.
405                format!(
406                    "insert into {table} ({columns_sql}) output inserted.{} values {values_sql}",
407                    dialect.quote(column)
408                )
409            }
410        };
411
412        Ok((sql, params))
413    }
414
415    fn to_update_sql(
416        &self,
417        dialect: &dyn Dialect,
418        values: &[(String, Value)],
419    ) -> Result<(String, Vec<Value>)> {
420        if values.is_empty() {
421            return Err(Error::msg("update needs at least one column"));
422        }
423
424        let mut params = Vec::new();
425        let mut assignments = Vec::with_capacity(values.len());
426
427        for (column, value) in values {
428            validate_identifier(column, dialect.max_identifier_length())?;
429            params.push(value.clone());
430            assignments.push(format!(
431                "{} = {}",
432                dialect.quote(column),
433                dialect.placeholder(params.len())
434            ));
435        }
436
437        let mut sql = format!(
438            "update {} set {}",
439            quote_qualified(dialect, &self.table)?,
440            assignments.join(", ")
441        );
442
443        if self.conditions.is_empty() {
444            // An unfiltered update rewrites the whole table. Requiring an
445            // explicit filter turns a catastrophe into a compile-time-ish error.
446            return Err(Error::msg(
447                "refusing to update every row: add a filter, or call `update_all` if that is really intended"
448                    .to_string(),
449            ));
450        }
451
452        sql.push_str(" where ");
453        sql.push_str(&render_conditions(dialect, &self.conditions, &mut params)?);
454        Ok((sql, params))
455    }
456
457    fn to_delete_sql(&self, dialect: &dyn Dialect, allow_all: bool) -> Result<(String, Vec<Value>)> {
458        let mut params = Vec::new();
459        let mut sql = format!("delete from {}", quote_qualified(dialect, &self.table)?);
460
461        if self.conditions.is_empty() {
462            if !allow_all {
463                return Err(Error::msg(
464                    "refusing to delete every row: add a filter, or call `delete_all` if that is really intended"
465                        .to_string(),
466                ));
467            }
468        } else {
469            sql.push_str(" where ");
470            sql.push_str(&render_conditions(dialect, &self.conditions, &mut params)?);
471        }
472
473        Ok((sql, params))
474    }
475
476    // --- Execution ---
477
478    /// Run the query and return every matching row.
479    pub async fn get(&self, db: &Database) -> Result<Vec<Row>> {
480        let (sql, params) = self.to_sql(db.dialect())?;
481        db.select(&sql, &params).await
482    }
483
484    /// The first matching row, if any.
485    pub async fn first(&self, db: &Database) -> Result<Option<Row>> {
486        let (sql, params) = self.clone().limit(1).to_sql(db.dialect())?;
487        db.select_one(&sql, &params).await
488    }
489
490    /// Rows as a JSON array, ready to return from an API handler.
491    pub async fn get_json(&self, db: &Database) -> Result<Json> {
492        Ok(crate::rows_to_json(&self.get(db).await?))
493    }
494
495    pub async fn count(&self, db: &Database) -> Result<i64> {
496        let (sql, params) = self.to_count_sql(db.dialect())?;
497        Ok(db.scalar::<i64>(&sql, &params).await?.unwrap_or(0))
498    }
499
500    pub async fn exists(&self, db: &Database) -> Result<bool> {
501        Ok(self.count(db).await? > 0)
502    }
503
504    /// Insert one row, returning its `id`.
505    pub async fn insert(&self, db: &Database, values: &[(&str, Value)]) -> Result<i64> {
506        // One path for all three databases: `insert_returning` knows that the
507        // key arrives as a row on some and in the acknowledgement on others.
508        i64::from_value(&self.insert_returning(db, values, "id").await?)
509    }
510
511    /// Insert one row and return one column of it — how a model picks up the
512    /// key the database generated.
513    pub async fn insert_returning(
514        &self,
515        db: &Database,
516        values: &[(&str, Value)],
517        column: &str,
518    ) -> Result<Value> {
519        let row: Vec<(String, Value)> =
520            values.iter().map(|(name, value)| ((*name).to_string(), value.clone())).collect();
521        let (sql, params) =
522            self.to_insert_sql(db.dialect(), std::slice::from_ref(&row), Some(column))?;
523
524        db.insert_returning_key(&sql, &params, column).await?.ok_or_else(|| {
525            Error::msg(format!(
526                "the insert did not report a `{column}`. The column has to be generated by the \
527                 database — an auto-incrementing key or a default — for it to have one to report."
528            ))
529        })
530    }
531
532    /// Insert one row without asking for a generated key.
533    pub async fn insert_without_id(&self, db: &Database, values: &[(&str, Value)]) -> Result<u64> {
534        let row: Vec<(String, Value)> =
535            values.iter().map(|(name, value)| ((*name).to_string(), value.clone())).collect();
536        let (sql, params) = self.to_insert_sql(db.dialect(), std::slice::from_ref(&row), None)?;
537        db.execute(&sql, &params).await
538    }
539
540    /// Insert many rows in one statement.
541    pub async fn insert_many(&self, db: &Database, rows: &[Vec<(String, Value)>]) -> Result<u64> {
542        if rows.is_empty() {
543            return Ok(0);
544        }
545        let (sql, params) = self.to_insert_sql(db.dialect(), rows, None)?;
546        db.execute(&sql, &params).await
547    }
548
549    /// Update the rows this query matches. Requires a filter.
550    pub async fn update(&self, db: &Database, values: &[(&str, Value)]) -> Result<u64> {
551        let owned: Vec<(String, Value)> =
552            values.iter().map(|(name, value)| ((*name).to_string(), value.clone())).collect();
553        let (sql, params) = self.to_update_sql(db.dialect(), &owned)?;
554        db.execute(&sql, &params).await
555    }
556
557    /// Delete the rows this query matches. Requires a filter.
558    pub async fn delete(&self, db: &Database) -> Result<u64> {
559        let (sql, params) = self.to_delete_sql(db.dialect(), false)?;
560        db.execute(&sql, &params).await
561    }
562
563    /// Delete every row in the table. Deliberately separate from `delete`.
564    pub async fn delete_all(&self, db: &Database) -> Result<u64> {
565        let (sql, params) = self.to_delete_sql(db.dialect(), true)?;
566        db.execute(&sql, &params).await
567    }
568}
569
570/// Render a condition list, threading parameter numbering through it.
571fn render_conditions(
572    dialect: &dyn Dialect,
573    conditions: &[Condition],
574    params: &mut Vec<Value>,
575) -> Result<String> {
576    let mut out = String::new();
577
578    for (index, condition) in conditions.iter().enumerate() {
579        if index > 0 {
580            out.push_str(if condition.is_or() { " or " } else { " and " });
581        }
582
583        match condition {
584            Condition::Comparison { column, operator, value, .. } => {
585                check_operator(operator)?;
586                params.push(value.clone());
587                out.push_str(&format!(
588                    "{} {operator} {}",
589                    column_ref(dialect, column)?,
590                    dialect.placeholder(params.len())
591                ));
592            }
593            Condition::In { column, values, negated, .. } => {
594                if values.is_empty() {
595                    // `in ()` is a syntax error; an empty set matches nothing.
596                    out.push_str(if *negated { "true" } else { "false" });
597                    continue;
598                }
599                let mut slots = Vec::with_capacity(values.len());
600                for value in values {
601                    params.push(value.clone());
602                    slots.push(dialect.placeholder(params.len()));
603                }
604                out.push_str(&format!(
605                    "{} {}in ({})",
606                    column_ref(dialect, column)?,
607                    if *negated { "not " } else { "" },
608                    slots.join(", ")
609                ));
610            }
611            Condition::Null { column, negated, .. } => {
612                out.push_str(&format!(
613                    "{} is {}null",
614                    column_ref(dialect, column)?,
615                    if *negated { "not " } else { "" }
616                ));
617            }
618            Condition::Between { column, low, high, .. } => {
619                params.push(low.clone());
620                let low_slot = dialect.placeholder(params.len());
621                params.push(high.clone());
622                out.push_str(&format!(
623                    "{} between {low_slot} and {}",
624                    column_ref(dialect, column)?,
625                    dialect.placeholder(params.len())
626                ));
627            }
628            Condition::Group { conditions, .. } => {
629                out.push_str(&format!("({})", render_conditions(dialect, conditions, params)?));
630            }
631        }
632    }
633
634    Ok(out)
635}
636
637/// The operators a builder is allowed to emit.
638///
639/// An allowlist rather than escaping: there is no legitimate reason for an
640/// operator to be anything else, and a typo becomes an error instead of SQL.
641fn check_operator(operator: &str) -> Result<()> {
642    const ALLOWED: &[&str] = &[
643        "=", "!=", "<>", "<", "<=", ">", ">=", "like", "not like", "ilike", "not ilike", "@>", "<@",
644        "?", "is distinct from",
645    ];
646
647    if ALLOWED.contains(&operator.to_ascii_lowercase().as_str()) {
648        Ok(())
649    } else {
650        Err(Error::msg(format!(
651            "`{operator}` is not an allowed comparison operator. Allowed: {}",
652            ALLOWED.join(", ")
653        )))
654    }
655}
656
657/// Render a column reference, which may be qualified (`users.id`), aliased
658/// (`count(*) as aggregate` is passed through) or plain.
659fn column_ref(dialect: &dyn Dialect, column: &str) -> Result<String> {
660    if column == "*" {
661        return Ok("*".to_string());
662    }
663
664    // The builder generates a handful of aggregate expressions itself; anything
665    // else must be a plain identifier.
666    if let Some(rest) = column.strip_prefix("count(*) as ") {
667        validate_identifier(rest, dialect.max_identifier_length())?;
668        return Ok(format!("count(*) as {}", dialect.quote(rest)));
669    }
670
671    for prefix in ["count", "sum", "avg", "min", "max"] {
672        if let Some(inner) = column
673            .strip_prefix(&format!("{prefix}("))
674            .and_then(|rest| rest.strip_suffix(')'))
675        {
676            let rendered =
677                if inner == "*" { "*".to_string() } else { quote_qualified(dialect, inner)? };
678            return Ok(format!("{prefix}({rendered})"));
679        }
680    }
681
682    quote_qualified(dialect, column)
683}
684
685#[cfg(test)]
686mod tests {
687    use super::*;
688    use crate::dialect::{MySql, Postgres, SqlServer};
689
690    /// The default in these tests, since the SQL they assert is PostgreSQL's.
691    /// The dialect-specific behaviour is covered by dialect.rs and by the
692    /// cross-dialect tests at the end of this module.
693    fn sql_of(builder: QueryBuilder) -> (String, Vec<Value>) {
694        builder.to_sql(&Postgres).unwrap()
695    }
696
697    #[test]
698    fn builds_a_plain_select() {
699        let (sql, params) = sql_of(QueryBuilder::new("users"));
700
701        assert_eq!(sql, r#"select * from "users""#);
702        assert!(params.is_empty());
703    }
704
705    #[test]
706    fn values_become_numbered_parameters() {
707        let (sql, params) = sql_of(
708            QueryBuilder::new("users")
709                .select(&["id", "name"])
710                .filter("email", "ada@example.com")
711                .filter_op("age", ">", 18),
712        );
713
714        assert_eq!(
715            sql,
716            r#"select "id", "name" from "users" where "email" = $1 and "age" > $2"#
717        );
718        assert_eq!(params, vec![Value::Text("ada@example.com".into()), Value::Int(18)]);
719    }
720
721    #[test]
722    fn a_value_can_never_change_the_statement() {
723        let (sql, params) = sql_of(QueryBuilder::new("users").filter("name", "'; drop table users; --"));
724
725        assert_eq!(sql, r#"select * from "users" where "name" = $1"#);
726        assert_eq!(params[0], Value::Text("'; drop table users; --".into()));
727    }
728
729    #[test]
730    fn an_injected_identifier_is_rejected() {
731        let error = QueryBuilder::new("users").filter("name; drop table users", 1).to_sql(&Postgres).unwrap_err();
732        assert!(error.to_string().contains("not a valid SQL identifier"));
733
734        assert!(QueryBuilder::new("users; drop table users").to_sql(&Postgres).is_err());
735    }
736
737    #[test]
738    fn an_unknown_operator_is_rejected() {
739        let error = QueryBuilder::new("users").filter_op("id", "; drop", 1).to_sql(&Postgres).unwrap_err();
740        assert!(error.to_string().contains("not an allowed comparison operator"));
741    }
742
743    #[test]
744    fn renders_in_null_and_between() {
745        let (sql, params) = sql_of(
746            QueryBuilder::new("posts")
747                .filter_in("status", vec![Value::from("draft"), Value::from("live")])
748                .filter_not_null("published_at")
749                .filter_between("views", 10, 100),
750        );
751
752        assert_eq!(
753            sql,
754            r#"select * from "posts" where "status" in ($1, $2) and "published_at" is not null and "views" between $3 and $4"#
755        );
756        assert_eq!(params.len(), 4);
757    }
758
759    #[test]
760    fn an_empty_in_list_matches_nothing_instead_of_breaking() {
761        let (sql, _) = sql_of(QueryBuilder::new("posts").filter_in("id", vec![]));
762        assert_eq!(sql, r#"select * from "posts" where false"#);
763    }
764
765    #[test]
766    fn groups_keep_or_conditions_together() {
767        let (sql, params) = sql_of(
768            QueryBuilder::new("users")
769                .filter("active", true)
770                .group_filter(|q| q.filter("role", "admin").or_filter("role", "owner")),
771        );
772
773        assert_eq!(
774            sql,
775            r#"select * from "users" where "active" = $1 and ("role" = $2 or "role" = $3)"#
776        );
777        assert_eq!(params.len(), 3);
778    }
779
780    #[test]
781    fn joins_order_and_paging() {
782        let (sql, _) = sql_of(
783            QueryBuilder::new("posts")
784                .select(&["posts.title", "users.name"])
785                .join("users", "posts.user_id", "=", "users.id")
786                .latest("posts.created_at")
787                .page(3, 20),
788        );
789
790        assert_eq!(
791            sql,
792            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"#
793        );
794    }
795
796    #[test]
797    fn counting_drops_order_and_paging() {
798        let (sql, _) = QueryBuilder::new("users")
799            .filter("active", true)
800            .latest("created_at")
801            .page(2, 10)
802            .to_count_sql(&Postgres)
803            .unwrap();
804
805        assert_eq!(
806            sql,
807            r#"select count(*) as "aggregate" from "users" where "active" = $1"#
808        );
809    }
810
811    #[test]
812    fn builds_an_insert_with_a_returning_clause() {
813        let row = vec![
814            ("name".to_string(), Value::from("Ada")),
815            ("email".to_string(), Value::from("ada@example.com")),
816        ];
817        let (sql, params) =
818            QueryBuilder::new("users").to_insert_sql(&Postgres, std::slice::from_ref(&row), Some("id")).unwrap();
819
820        assert_eq!(
821            sql,
822            r#"insert into "users" ("name", "email") values ($1, $2) returning "id""#
823        );
824        assert_eq!(params.len(), 2);
825    }
826
827    #[test]
828    fn a_bulk_insert_numbers_every_row() {
829        let rows = vec![
830            vec![("name".to_string(), Value::from("a"))],
831            vec![("name".to_string(), Value::from("b"))],
832        ];
833        let (sql, params) = QueryBuilder::new("users").to_insert_sql(&Postgres, &rows, None).unwrap();
834
835        assert_eq!(sql, r#"insert into "users" ("name") values ($1), ($2)"#);
836        assert_eq!(params.len(), 2);
837    }
838
839    #[test]
840    fn update_and_delete_refuse_to_touch_every_row_by_accident() {
841        let values = vec![("name".to_string(), Value::from("x"))];
842
843        let error = QueryBuilder::new("users").to_update_sql(&Postgres, &values).unwrap_err();
844        assert!(error.to_string().contains("refusing to update every row"));
845
846        let error = QueryBuilder::new("users").to_delete_sql(&Postgres, false).unwrap_err();
847        assert!(error.to_string().contains("refusing to delete every row"));
848
849        // The explicit forms are allowed.
850        assert!(QueryBuilder::new("users").to_delete_sql(&Postgres, true).is_ok());
851        assert!(
852            QueryBuilder::new("users")
853                .filter("id", 1)
854                .to_update_sql(&Postgres, &values)
855                .is_ok()
856        );
857    }
858
859    #[test]
860    fn one_chain_produces_correct_sql_for_every_database() {
861        let query = QueryBuilder::new("users")
862            .select(&["id", "name"])
863            .filter("active", true)
864            .latest("created_at")
865            .page(2, 10);
866
867        let (postgres, _) = query.to_sql(&Postgres).unwrap();
868        assert_eq!(
869            postgres,
870            r#"select "id", "name" from "users" where "active" = $1 order by "created_at" desc limit 10 offset 10"#
871        );
872
873        let (mysql, _) = query.to_sql(&MySql).unwrap();
874        assert_eq!(
875            mysql,
876            "select `id`, `name` from `users` where `active` = ? order by `created_at` desc limit 10 offset 10"
877        );
878
879        let (sqlserver, _) = query.to_sql(&SqlServer).unwrap();
880        assert_eq!(
881            sqlserver,
882            "select [id], [name] from [users] where [active] = @P1 order by [created_at] desc offset 10 rows fetch next 10 rows only"
883        );
884    }
885
886    #[test]
887    fn parameters_are_numbered_or_not_according_to_the_database() {
888        let query = QueryBuilder::new("posts").filter("a", 1).filter("b", 2).filter("c", 3);
889
890        let (postgres, params) = query.to_sql(&Postgres).unwrap();
891        assert!(postgres.ends_with("$1 and \"b\" = $2 and \"c\" = $3"), "{postgres}");
892        assert_eq!(params.len(), 3);
893
894        // MySQL binds positionally, so every placeholder is the same token and
895        // the order of the parameter list is the only thing that matters.
896        let (mysql, params) = query.to_sql(&MySql).unwrap();
897        assert!(mysql.ends_with("? and `b` = ? and `c` = ?"), "{mysql}");
898        assert_eq!(params.len(), 3);
899    }
900
901    #[test]
902    fn a_generated_key_is_asked_for_in_each_databases_own_way() {
903        let row = vec![("name".to_string(), Value::from("Ada"))];
904        let rows = std::slice::from_ref(&row);
905
906        let (postgres, _) =
907            QueryBuilder::new("users").to_insert_sql(&Postgres, rows, Some("id")).unwrap();
908        assert!(postgres.ends_with(r#"returning "id""#), "{postgres}");
909
910        // SQL Server puts it before the column list, so it cannot be appended.
911        let (sqlserver, _) =
912            QueryBuilder::new("users").to_insert_sql(&SqlServer, rows, Some("id")).unwrap();
913        assert_eq!(
914            sqlserver,
915            "insert into [users] ([name]) output inserted.[id] values (@P1)"
916        );
917
918        // MySQL has no such clause; the key is read with a second statement.
919        let (mysql, _) = QueryBuilder::new("users").to_insert_sql(&MySql, rows, Some("id")).unwrap();
920        assert_eq!(mysql, "insert into `users` (`name`) values (?)");
921    }
922
923    #[test]
924    fn sql_server_gets_an_ordering_it_can_page_with() {
925        // `offset` is a syntax error without `order by`, and the builder
926        // supplies one rather than letting the database complain.
927        let (sql, _) = QueryBuilder::new("users").page(3, 20).to_sql(&SqlServer).unwrap();
928
929        assert!(sql.contains("order by (select null)"), "{sql}");
930        assert!(sql.ends_with("offset 40 rows fetch next 20 rows only"), "{sql}");
931    }
932
933    #[test]
934    fn an_injected_identifier_is_rejected_whatever_the_database() {
935        for dialect in [&Postgres as &dyn Dialect, &MySql, &SqlServer] {
936            let error = QueryBuilder::new("users")
937                .filter("name; drop table users", 1)
938                .to_sql(dialect)
939                .unwrap_err();
940
941            assert!(
942                error.to_string().contains("not a valid SQL identifier"),
943                "{} accepted it",
944                dialect.name()
945            );
946        }
947    }
948
949    #[test]
950    fn an_update_numbers_values_before_conditions() {
951        let values = vec![("name".to_string(), Value::from("Ada"))];
952        let (sql, params) =
953            QueryBuilder::new("users").filter("id", 7).to_update_sql(&Postgres, &values).unwrap();
954
955        assert_eq!(sql, r#"update "users" set "name" = $1 where "id" = $2"#);
956        assert_eq!(params, vec![Value::Text("Ada".into()), Value::Int(7)]);
957    }
958}