Skip to main content

queryweave_sql/backend/
sqlx.rs

1//! sqlx's [`QueryBuilder`] as a query the SQL is written into, for any
2//! database whose driver binds the domain's raw values ([`BindValues`]):
3//! Postgres, MySQL/MariaDB and SQLite.
4//!
5//! The clauses are appended as SQL text, the values bound with the builder's
6//! placeholders (`$1` on Postgres, `?` elsewhere).
7
8mod bind;
9
10pub use bind::BindValues;
11
12use super::column;
13#[cfg(feature = "merge-patch")]
14use crate::AssignmentsTarget;
15use crate::sql::{
16    LIKE_ESCAPE, like_pattern, page_offset, sql_direction, sql_junction, sql_operator,
17};
18use crate::{Columns, ConditionsTarget, Junction, OrderByTarget, PageTarget};
19use queryweave::{Conditions, FilterCondition, SortKey, SortKeys, Value};
20#[cfg(feature = "merge-patch")]
21use queryweave::{FieldValue, FieldValues};
22use sqlx::QueryBuilder;
23#[cfg(feature = "merge-patch")]
24use sqlx::query_builder::Separated;
25
26/// Push a `column operator value` comparison and bind the value, the column
27/// being the one `C` stores the condition's domain field in.
28///
29/// Comparisons bind the value with its own SQL type (binding its text form
30/// would compare e.g. an integer column against text, which Postgres
31/// rejects); the `LIKE` operators bind an escaped `%` pattern of the text and
32/// name the escape character.
33fn push_condition<C: Columns + ?Sized, DB: BindValues>(
34    query_builder: &mut QueryBuilder<'_, DB>,
35    condition: FilterCondition,
36) {
37    let FilterCondition {
38        field,
39        operator,
40        value,
41    } = condition;
42
43    query_builder.push(format!(
44        "{} {} ",
45        column::<C>(field),
46        sql_operator(operator)
47    ));
48
49    // The `Text` filter kind guarantees the `LIKE` operators only ever carry
50    // text; anything else is bound as it is.
51    match value {
52        Value::Text(Some(text)) => match like_pattern(operator, &text) {
53            Some(pattern) => {
54                DB::push_bind_value(query_builder, Value::Text(Some(pattern)));
55                query_builder.push(format!(" ESCAPE '{LIKE_ESCAPE}'"));
56            },
57            None => DB::push_bind_value(query_builder, Value::Text(Some(text))),
58        },
59        value => DB::push_bind_value(query_builder, value),
60    }
61    query_builder.push(" ");
62}
63
64impl<DB: BindValues> ConditionsTarget for QueryBuilder<'_, DB> {
65    fn write_where(&mut self) {
66        self.push("WHERE ");
67    }
68
69    fn write_conditions<C: Columns + ?Sized, M>(
70        &mut self,
71        conditions: Conditions<M>,
72        junction: Junction,
73    ) {
74        for (index, condition) in conditions.into_iter().enumerate() {
75            if index > 0 {
76                self.push(format!("{} ", sql_junction(junction)));
77            }
78            push_condition::<C, DB>(self, condition);
79        }
80    }
81}
82
83impl<DB: BindValues> OrderByTarget for QueryBuilder<'_, DB> {
84    fn write_order_by<C: Columns + ?Sized, M>(&mut self, sort_keys: SortKeys<M>) {
85        if sort_keys.is_empty() {
86            return;
87        }
88
89        let order_by = sort_keys
90            .into_iter()
91            .map(|SortKey { field, direction }| {
92                format!("{} {}", column::<C>(field), sql_direction(direction))
93            })
94            .collect::<Vec<_>>()
95            .join(", ");
96        self.push(format!("ORDER BY {order_by} "));
97    }
98}
99
100#[cfg(feature = "merge-patch")]
101impl<'args, DB: BindValues> AssignmentsTarget for QueryBuilder<'args, DB> {
102    type Assignments<'target>
103        = Separated<'target, 'args, DB, &'static str>
104    where
105        Self: 'target;
106
107    fn write_assignments<C: Columns + ?Sized, M>(
108        &mut self,
109        assignments: FieldValues<M>,
110    ) -> Self::Assignments<'_> {
111        let mut separated = self.separated(", ");
112
113        for FieldValue { field, value } in assignments {
114            separated.push(format!("{} = ", column::<C>(field)));
115            DB::push_bind_value_unseparated(&mut separated, value);
116        }
117
118        separated
119    }
120}
121
122impl<DB: BindValues> PageTarget for QueryBuilder<'_, DB> {
123    fn write_page(&mut self, page: Option<u32>, page_size: Option<u32>) {
124        let (Some(page), Some(page_size)) = (page, page_size) else {
125            return;
126        };
127
128        self.push("LIMIT ");
129        DB::push_bind_value(self, Value::I64(Some(i64::from(page_size))));
130        self.push(" OFFSET ");
131        DB::push_bind_value(self, Value::I64(Some(page_offset(page, page_size))));
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::BindValues;
138    use crate::{Columns, Junction, push_page};
139    use queryweave::{
140        FieldValue, FilterCondition, FilterOperator, PersistedFields, SortDirectionModel, SortKey,
141        Value,
142    };
143    use rstest::rstest;
144    use sqlx::{MySql, Postgres, QueryBuilder, Sqlite};
145
146    /// Each field stored in the column of its name, except `delivery_date`,
147    /// stored in `delivered_at`.
148    struct Table;
149
150    impl Columns for Table {
151        const NAME: &'static str = "Table";
152        const COLUMNS: &'static [(&'static str, &'static str)] = &[
153            ("number", "number"),
154            ("client", "client"),
155            ("capacity", "capacity"),
156            ("value", "value"),
157            ("is_closed", "is_closed"),
158            ("user_count", "user_count"),
159            ("is_sweet", "is_sweet"),
160            ("is_equal_to", "is_equal_to"),
161            ("name_c", "name_c"),
162            ("delivery_date", "delivered_at"),
163        ];
164    }
165
166    /// A model whose every field has a column on [`Table`].
167    struct Model;
168
169    impl PersistedFields for Model {
170        const FIELD_NAMES: &'static [&'static str] = &[
171            "number",
172            "client",
173            "capacity",
174            "value",
175            "is_closed",
176            "user_count",
177            "is_sweet",
178            "is_equal_to",
179            "name_c",
180            "delivery_date",
181        ];
182    }
183
184    fn condition(field: &'static str, operator: FilterOperator) -> FilterCondition {
185        FilterCondition {
186            field,
187            operator,
188            value: Value::Text(Some(String::from("needle"))),
189        }
190    }
191
192    fn conditions_sql<DB: BindValues>(conditions: Vec<FilterCondition>) -> String {
193        let mut query_builder = QueryBuilder::<DB>::default();
194        Table::push_conditions(
195            &mut query_builder,
196            queryweave::__private::model_data::<Model, _>(conditions),
197            Junction::And,
198        );
199        query_builder.sql().to_string()
200    }
201
202    fn order_by_sql<DB: BindValues>(sort_keys: Vec<SortKey>) -> String {
203        let mut query_builder = QueryBuilder::<DB>::default();
204        Table::push_order_by(
205            &mut query_builder,
206            queryweave::__private::model_data::<Model, _>(sort_keys),
207        );
208        query_builder.sql().to_string()
209    }
210
211    fn page_sql<DB: BindValues>(page: Option<u32>, page_size: Option<u32>) -> String {
212        let mut query_builder = QueryBuilder::<DB>::default();
213        push_page(&mut query_builder, page, page_size);
214        query_builder.sql().to_string()
215    }
216
217    /// Conditions, sorting and a page, as one `WHERE … ORDER BY … LIMIT …`
218    /// tail.
219    fn query_tail_sql<DB: BindValues>() -> String {
220        let mut query_builder = QueryBuilder::<DB>::default();
221        Table::push_conditions(
222            &mut query_builder,
223            queryweave::__private::model_data::<Model, _>(vec![
224                condition("number", FilterOperator::Equal),
225                condition("client", FilterOperator::StartsWith),
226            ]),
227            Junction::And,
228        );
229        Table::push_order_by(
230            &mut query_builder,
231            queryweave::__private::model_data::<Model, _>(vec![SortKey {
232                field: "delivery_date",
233                direction: SortDirectionModel::Descending,
234            }]),
235        );
236        push_page(&mut query_builder, Some(2), Some(10));
237        query_builder.sql().to_string()
238    }
239
240    // The same data renders the same SQL on every database, with its own
241    // placeholders.
242    #[test]
243    fn every_database_gets_the_same_sql_with_its_own_placeholders() {
244        assert_eq!(
245            query_tail_sql::<Postgres>(),
246            "number = $1 AND client LIKE $2 ESCAPE '!' ORDER BY delivered_at DESC LIMIT $3 OFFSET $4"
247        );
248        let with_question_marks =
249            "number = ? AND client LIKE ? ESCAPE '!' ORDER BY delivered_at DESC LIMIT ? OFFSET ?";
250        assert_eq!(query_tail_sql::<MySql>(), with_question_marks);
251        assert_eq!(query_tail_sql::<Sqlite>(), with_question_marks);
252    }
253
254    #[test]
255    fn assignments_render_on_every_database() {
256        fn assignments_sql<DB: BindValues>() -> String {
257            let mut query_builder = QueryBuilder::<DB>::new("UPDATE t SET ");
258            let _ = Table::push_assignments(
259                &mut query_builder,
260                queryweave::__private::model_data::<Model, _>(vec![
261                    FieldValue {
262                        field: "number",
263                        value: Value::Text(None),
264                    },
265                    FieldValue {
266                        field: "delivery_date",
267                        value: Value::DateTimeUtc(None),
268                    },
269                ]),
270            );
271            query_builder.sql().to_string()
272        }
273
274        assert_eq!(
275            assignments_sql::<Postgres>(),
276            "UPDATE t SET number = $1, delivered_at = $2"
277        );
278        assert_eq!(
279            assignments_sql::<MySql>(),
280            "UPDATE t SET number = ?, delivered_at = ?"
281        );
282        assert_eq!(
283            assignments_sql::<Sqlite>(),
284            "UPDATE t SET number = ?, delivered_at = ?"
285        );
286    }
287
288    // A renamed field is filtered, sorted and written by its column.
289    #[test]
290    fn a_renamed_field_is_written_as_its_column() {
291        assert_eq!(
292            conditions_sql::<Postgres>(vec![condition(
293                "delivery_date",
294                FilterOperator::GreaterThan
295            )]),
296            "delivered_at > $1 "
297        );
298        assert_eq!(
299            order_by_sql::<Postgres>(vec![SortKey {
300                field: "delivery_date",
301                direction: SortDirectionModel::Descending,
302            }]),
303            "ORDER BY delivered_at DESC "
304        );
305    }
306
307    // The operator selects the SQL comparison; the `LIKE` ones name the
308    // escape character of their pattern.
309    #[rstest]
310    #[case(FilterOperator::LessThan, "value < $1 ")]
311    #[case(FilterOperator::GreaterThan, "value > $1 ")]
312    #[case(FilterOperator::Equal, "value = $1 ")]
313    #[case(FilterOperator::StartsWith, "value LIKE $1 ESCAPE '!' ")]
314    #[case(FilterOperator::Contains, "value LIKE $1 ESCAPE '!' ")]
315    #[case(FilterOperator::EndsWith, "value LIKE $1 ESCAPE '!' ")]
316    fn the_operator_selects_the_sql_comparison(
317        #[case] operator: FilterOperator,
318        #[case] expected_sql: &str,
319    ) {
320        assert_eq!(
321            conditions_sql::<Postgres>(vec![condition("value", operator)]),
322            expected_sql
323        );
324    }
325
326    // The column is written verbatim, even when its name merely contains a
327    // filter suffix (the old suffix-stripping turned `is_closed` into
328    // `islosed`).
329    #[rstest]
330    #[case("is_closed")]
331    #[case("user_count")]
332    #[case("is_sweet")]
333    #[case("is_equal_to")]
334    #[case("name_c")]
335    fn the_column_is_written_verbatim(#[case] column: &'static str) {
336        assert_eq!(
337            conditions_sql::<Postgres>(vec![condition(column, FilterOperator::Equal)]),
338            format!("{column} = $1 ")
339        );
340    }
341
342    // Conditions are joined by the junction's keyword, and only between them.
343    #[rstest]
344    #[case(Junction::And, "number = $1 AND client = $2 ")]
345    #[case(Junction::Or, "number = $1 OR client = $2 ")]
346    fn conditions_are_joined_by_the_junction(
347        #[case] junction: Junction,
348        #[case] expected_sql: &str,
349    ) {
350        let mut query_builder = QueryBuilder::<Postgres>::default();
351        Table::push_conditions(
352            &mut query_builder,
353            queryweave::__private::model_data::<Model, _>(vec![
354                condition("number", FilterOperator::Equal),
355                condition("client", FilterOperator::Equal),
356            ]),
357            junction,
358        );
359
360        assert_eq!(query_builder.sql(), expected_sql);
361        assert_eq!(conditions_sql::<Postgres>(Vec::new()), "");
362    }
363
364    // A non-text value is bound as it is, whatever the operator.
365    #[test]
366    fn a_non_text_value_is_bound_with_its_own_type() {
367        let sql = conditions_sql::<Postgres>(vec![FilterCondition {
368            field: "value",
369            operator: FilterOperator::LessThan,
370            value: Value::F64(Some(9.5)),
371        }]);
372
373        assert_eq!(sql, "value < $1 ");
374    }
375
376    // Assignments are comma separated, and more can follow on the returned
377    // builder.
378    #[test]
379    fn assignments_are_comma_separated_and_extendable() {
380        let mut query_builder = QueryBuilder::<Postgres>::new("UPDATE orders SET ");
381        let mut separated = Table::push_assignments(
382            &mut query_builder,
383            queryweave::__private::model_data::<Model, _>(vec![
384                FieldValue {
385                    field: "number",
386                    value: Value::Text(None),
387                },
388                FieldValue {
389                    field: "capacity",
390                    value: Value::I32(Some(3)),
391                },
392            ]),
393        );
394        separated
395            .push("last_modified_at = ")
396            .push_bind_unseparated(1_i64);
397
398        assert_eq!(
399            query_builder.sql(),
400            "UPDATE orders SET number = $1, capacity = $2, last_modified_at = $3"
401        );
402    }
403
404    // Sort keys are written in order, each with its direction, comma
405    // separated only between them.
406    #[test]
407    fn order_by_lists_the_sort_keys_in_order() {
408        assert_eq!(
409            order_by_sql::<Postgres>(vec![
410                SortKey {
411                    field: "number",
412                    direction: SortDirectionModel::Ascending,
413                },
414                SortKey {
415                    field: "value",
416                    direction: SortDirectionModel::Descending,
417                },
418            ]),
419            "ORDER BY number ASC, value DESC "
420        );
421    }
422
423    // Without sort keys there is no `ORDER BY` at all.
424    #[test]
425    fn order_by_is_omitted_without_sort_keys() {
426        assert_eq!(order_by_sql::<Postgres>(Vec::new()), "");
427    }
428
429    // A page needs both its number and its size.
430    #[rstest]
431    #[case(Some(1), Some(10), "LIMIT $1 OFFSET $2")]
432    #[case(None, Some(10), "")]
433    #[case(Some(1), None, "")]
434    #[case(None, None, "")]
435    fn a_page_needs_both_its_number_and_size(
436        #[case] page: Option<u32>,
437        #[case] page_size: Option<u32>,
438        #[case] expected_sql: &str,
439    ) {
440        assert_eq!(page_sql::<Postgres>(page, page_size), expected_sql);
441    }
442}