1mod 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
26fn 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 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 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 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[test]
425 fn order_by_is_omitted_without_sort_keys() {
426 assert_eq!(order_by_sql::<Postgres>(Vec::new()), "");
427 }
428
429 #[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}