1pub(crate) mod check;
29
30use crate::Junction;
31use crate::sql::{LIKE_ESCAPE, like_pattern, page_offset};
32use diesel::backend::Backend;
33#[cfg(feature = "merge-patch")]
34use diesel::expression::TypedExpressionType;
35use diesel::expression::expression_types::NotSelectable;
36use diesel::expression::{AsExpression, BoxableExpression, Expression};
37use diesel::expression_methods::EscapeExpressionMethods;
38use diesel::query_dsl::methods::{FilterDsl, LimitDsl, OffsetDsl, ThenOrderDsl};
39use diesel::sql_types::{self, Bool, IntoNullable, Nullable, SingleValue, SqlType, is_nullable};
40use diesel::{BoolExpressionMethods, ExpressionMethods, NullableExpressionMethods};
41use diesel::{TextExpressionMethods, dsl};
42#[cfg(feature = "merge-patch")]
43use queryweave::FieldValue;
44use queryweave::{
45 Conditions, FilterCondition, FilterOperator, PersistedFields, SortDirectionModel, SortKey,
46 SortKeys, Value, ValueKind,
47};
48
49pub type BoxedCondition<'a, QS, DB> =
54 Box<dyn BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a>;
55
56pub type BoxedOrdering<'a, QS, DB> =
58 Box<dyn BoxableExpression<QS, DB, SqlType = NotSelectable> + 'a>;
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct ValueMismatch {
65 pub field: &'static str,
67}
68
69impl std::fmt::Display for ValueMismatch {
70 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 write!(
72 formatter,
73 "the value of the domain field `{}` does not fit the SQL type of its column",
74 self.field
75 )
76 }
77}
78
79impl std::error::Error for ValueMismatch {}
80
81pub trait ValueSqlType: SqlType<IsNull = is_nullable::NotNull> + SingleValue + Sized {
90 const ACCEPTS: &'static [ValueKind];
93
94 type Rust: AsExpression<Self> + Send + 'static;
96
97 fn rust(value: Value) -> Result<Option<Self::Rust>, Value>;
103}
104
105macro_rules! value_sql_types {
108 ($($sql:ty => $rust:ty { $($variant:ident($value:ident) => $convert:expr),+ $(,)? }),+ $(,)?) => {$(
109 impl ValueSqlType for $sql {
110 const ACCEPTS: &'static [ValueKind] = &[$(ValueKind::$variant),+];
111
112 type Rust = $rust;
113
114 fn rust(value: Value) -> Result<Option<Self::Rust>, Value> {
115 match value {
116 $(Value::$variant(value) => Ok(value.map(|$value| $convert)),)+
117 other => Err(other),
118 }
119 }
120 }
121
122 impl<'a, C, QS, DB> Comparison<'a, C, QS, DB> for $sql
123 where
124 DB: Backend,
125 C: Expression + NullableExpressionMethods,
126 C::SqlType: IntoNullable<Nullable = Nullable<$sql>>,
127 dsl::Nullable<C>: Expression<SqlType = Nullable<$sql>>,
128 dsl::Eq<dsl::Nullable<C>, Option<$rust>>:
129 BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
130 dsl::Lt<dsl::Nullable<C>, Option<$rust>>:
131 BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
132 dsl::Gt<dsl::Nullable<C>, Option<$rust>>:
133 BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
134 {
135 fn compare(
136 column: C,
137 operator: FilterOperator,
138 value: Value,
139 ) -> Option<BoxedCondition<'a, QS, DB>> {
140 compare_values::<C, $sql, QS, DB>(column, operator, value)
141 }
142 }
143
144 impl ColumnValue for $sql {
145 const TYPE: ColumnType = ColumnType::not_null::<$sql>();
146
147 type Rust = $rust;
148
149 fn from_value(value: Value) -> Option<Self::Rust> {
150 <$sql as ValueSqlType>::rust(value).ok().flatten()
152 }
153 }
154 )+};
155}
156
157value_sql_types!(
158 sql_types::Bool => bool { Bool(value) => value },
159 sql_types::SmallInt => i16 { I16(value) => value },
160 sql_types::Integer => i32 { I32(value) => value, I16(value) => i32::from(value) },
161 sql_types::BigInt => i64 {
162 I64(value) => value,
163 I32(value) => i64::from(value),
164 I16(value) => i64::from(value),
165 },
166 sql_types::Float => f32 { F32(value) => value },
167 sql_types::Double => f64 { F64(value) => value, F32(value) => f64::from(value) },
168 sql_types::Timestamp => chrono::NaiveDateTime {
169 NaiveDateTime(value) => value,
170 DateTimeUtc(value) => value.naive_utc(),
171 },
172 sql_types::Date => chrono::NaiveDate { NaiveDate(value) => value },
173 sql_types::Time => chrono::NaiveTime { NaiveTime(value) => value },
174);
175
176#[cfg(feature = "diesel-postgres")]
177value_sql_types!(
178 sql_types::Timestamptz => chrono::DateTime<chrono::Utc> { DateTimeUtc(value) => value },
179);
180
181#[cfg(feature = "diesel-sqlite")]
182value_sql_types!(
183 sql_types::TimestamptzSqlite => chrono::DateTime<chrono::Utc> {
184 DateTimeUtc(value) => value,
185 },
186);
187
188#[cfg(feature = "diesel-mysql")]
189value_sql_types!(
190 sql_types::Datetime => chrono::NaiveDateTime {
191 NaiveDateTime(value) => value,
192 DateTimeUtc(value) => value.naive_utc(),
193 },
194);
195
196impl ValueSqlType for sql_types::Text {
198 const ACCEPTS: &'static [ValueKind] = &[ValueKind::Text];
199
200 type Rust = String;
201
202 fn rust(value: Value) -> Result<Option<Self::Rust>, Value> {
203 match value {
204 Value::Text(value) => Ok(value),
205 other => Err(other),
206 }
207 }
208}
209
210impl ColumnValue for sql_types::Text {
211 const TYPE: ColumnType = ColumnType::not_null::<Self>();
212
213 type Rust = String;
214
215 fn from_value(value: Value) -> Option<Self::Rust> {
216 <Self as ValueSqlType>::rust(value).ok().flatten()
217 }
218}
219
220#[diagnostic::on_unimplemented(
227 message = "queryweave cannot compare a domain value with a column of SQL type `{Self}`",
228 label = "no `queryweave_sql::diesel::Comparison` for this SQL type",
229 note = "mark the entity field of a column storing no domain field `#[columns(skip)]`, or implement `ValueSqlType`, `Comparison` and `ColumnValue` for the SQL type"
230)]
231pub trait Comparison<'a, C, QS, DB>: ValueSqlType {
232 fn compare(
235 column: C,
236 operator: FilterOperator,
237 value: Value,
238 ) -> Option<BoxedCondition<'a, QS, DB>>;
239}
240
241impl<'a, C, QS, DB> Comparison<'a, C, QS, DB> for sql_types::Text
242where
243 DB: Backend,
244 C: Expression + NullableExpressionMethods,
245 C::SqlType: IntoNullable<Nullable = Nullable<Self>>,
246 dsl::Nullable<C>: Expression<SqlType = Nullable<Self>> + TextExpressionMethods,
247 dsl::Escape<dsl::Like<dsl::Nullable<C>, String>>:
248 BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
249 dsl::Eq<dsl::Nullable<C>, Option<String>>:
250 BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
251 dsl::Lt<dsl::Nullable<C>, Option<String>>:
252 BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
253 dsl::Gt<dsl::Nullable<C>, Option<String>>:
254 BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
255{
256 fn compare(
257 column: C,
258 operator: FilterOperator,
259 value: Value,
260 ) -> Option<BoxedCondition<'a, QS, DB>> {
261 if let Value::Text(Some(text)) = &value
264 && let Some(pattern) = like_pattern(operator, text)
265 {
266 return Some(Box::new(
267 column.nullable().like(pattern).escape(LIKE_ESCAPE),
268 ));
269 }
270
271 compare_values::<C, Self, QS, DB>(column, operator, value)
272 }
273}
274
275fn compare_values<'a, C, ST, QS, DB>(
279 column: C,
280 operator: FilterOperator,
281 value: Value,
282) -> Option<BoxedCondition<'a, QS, DB>>
283where
284 DB: Backend,
285 ST: ValueSqlType,
286 Option<ST::Rust>: AsExpression<Nullable<ST>>,
287 C: Expression + NullableExpressionMethods,
288 C::SqlType: IntoNullable<Nullable = Nullable<ST>>,
289 dsl::Nullable<C>: Expression<SqlType = Nullable<ST>>,
290 dsl::Eq<dsl::Nullable<C>, Option<ST::Rust>>:
291 BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
292 dsl::Lt<dsl::Nullable<C>, Option<ST::Rust>>:
293 BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
294 dsl::Gt<dsl::Nullable<C>, Option<ST::Rust>>:
295 BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
296{
297 let value = ST::rust(value).ok()?;
298 let column = column.nullable();
299
300 match operator {
301 FilterOperator::LessThan => Some(Box::new(column.lt(value))),
302 FilterOperator::Equal => Some(Box::new(column.eq(value))),
303 FilterOperator::GreaterThan => Some(Box::new(column.gt(value))),
304 FilterOperator::StartsWith | FilterOperator::Contains | FilterOperator::EndsWith => None,
305 }
306}
307
308pub trait NonNull {
311 type SqlType;
313}
314
315impl<ST: SqlType<IsNull = is_nullable::NotNull>> NonNull for Nullable<ST> {
316 type SqlType = ST;
317}
318
319pub type NonNullSqlType<C> =
321 <<<C as Expression>::SqlType as IntoNullable>::Nullable as NonNull>::SqlType;
322
323pub fn condition<'a, C, QS, DB>(
331 column: C,
332 filter_condition: FilterCondition,
333) -> Result<BoxedCondition<'a, QS, DB>, ValueMismatch>
334where
335 C: Expression,
336 C::SqlType: IntoNullable,
337 <C::SqlType as IntoNullable>::Nullable: NonNull,
338 NonNullSqlType<C>: Comparison<'a, C, QS, DB>,
339{
340 let FilterCondition {
341 field,
342 operator,
343 value,
344 } = filter_condition;
345
346 <NonNullSqlType<C> as Comparison<'a, C, QS, DB>>::compare(column, operator, value)
347 .ok_or(ValueMismatch { field })
348}
349
350pub fn join<'a, QS, DB>(
353 conditions: impl IntoIterator<Item = BoxedCondition<'a, QS, DB>>,
354 junction: Junction,
355) -> Option<BoxedCondition<'a, QS, DB>>
356where
357 QS: 'a,
358 DB: Backend + 'a,
359 dsl::And<BoxedCondition<'a, QS, DB>, BoxedCondition<'a, QS, DB>>:
360 BoxableExpression<QS, DB, SqlType = Nullable<Bool>>,
361 dsl::Or<BoxedCondition<'a, QS, DB>, BoxedCondition<'a, QS, DB>>:
362 BoxableExpression<QS, DB, SqlType = Nullable<Bool>>,
363{
364 conditions
365 .into_iter()
366 .reduce(|joined, condition| match junction {
367 Junction::And => Box::new(joined.and(condition)),
368 Junction::Or => Box::new(joined.or(condition)),
369 })
370}
371
372pub fn ordering<'a, C, QS, DB>(column: C, sort_key: SortKey) -> BoxedOrdering<'a, QS, DB>
374where
375 DB: Backend,
376 C: ExpressionMethods,
377 dsl::Asc<C>: BoxableExpression<QS, DB, SqlType = NotSelectable> + 'a,
378 dsl::Desc<C>: BoxableExpression<QS, DB, SqlType = NotSelectable> + 'a,
379{
380 match sort_key.direction {
381 SortDirectionModel::Ascending => Box::new(column.asc()),
382 SortDirectionModel::Descending => Box::new(column.desc()),
383 }
384}
385
386#[must_use]
389pub fn page(page: Option<u32>, page_size: Option<u32>) -> Option<(i64, i64)> {
390 let (Some(page), Some(page_size)) = (page, page_size) else {
391 return None;
392 };
393
394 Some((i64::from(page_size), page_offset(page, page_size)))
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401pub struct ColumnType {
402 pub accepts: &'static [ValueKind],
404 pub nullable: bool,
406}
407
408impl ColumnType {
409 #[must_use]
411 pub const fn not_null<ST: ValueSqlType>() -> Self {
412 Self {
413 accepts: ST::ACCEPTS,
414 nullable: false,
415 }
416 }
417}
418
419#[diagnostic::on_unimplemented(
422 message = "queryweave cannot write a domain value to a column of SQL type `{Self}`",
423 label = "no `queryweave_sql::diesel::ColumnValue` for this SQL type",
424 note = "mark the entity field of a column storing no domain field `#[columns(skip)]`, or implement `ValueSqlType`, `Comparison` and `ColumnValue` for the SQL type"
425)]
426pub trait ColumnValue {
427 const TYPE: ColumnType;
431
432 type Rust;
434
435 fn from_value(value: Value) -> Option<Self::Rust>;
438}
439
440impl<ST: ValueSqlType> ColumnValue for Nullable<ST> {
441 const TYPE: ColumnType = ColumnType {
442 accepts: ST::ACCEPTS,
443 nullable: true,
444 };
445
446 type Rust = Option<ST::Rust>;
447
448 fn from_value(value: Value) -> Option<Self::Rust> {
449 ST::rust(value).ok()
450 }
451}
452
453pub type ValueOf<C> = <<C as Expression>::SqlType as ColumnValue>::Rust;
455
456#[cfg(feature = "merge-patch")]
466pub fn assignment<C>(
467 column: C,
468 field_value: FieldValue,
469) -> Result<dsl::Eq<C, ValueOf<C>>, ValueMismatch>
470where
471 C: Expression + ExpressionMethods,
472 C::SqlType: SqlType + TypedExpressionType + ColumnValue,
473 ValueOf<C>: AsExpression<C::SqlType>,
474{
475 let FieldValue { field, value } = field_value;
476 let value = <C::SqlType as ColumnValue>::from_value(value).ok_or(ValueMismatch { field })?;
477
478 Ok(column.eq(value))
479}
480
481#[diagnostic::on_unimplemented(
488 message = "queryweave cannot filter on the column `{Self}`: its SQL type takes no domain value",
489 label = "a column of a SQL type with no `queryweave_sql::diesel::ValueSqlType`",
490 note = "mark the entity field of a column storing no domain field `#[columns(skip)]`, or implement `ValueSqlType`, `Comparison` and `ColumnValue` for its SQL type"
491)]
492pub trait FilterColumn<'a, QS, DB>: Sized {
493 const ACCEPTS: &'static [ValueKind];
496
497 fn condition(
503 self,
504 filter_condition: FilterCondition,
505 ) -> Result<BoxedCondition<'a, QS, DB>, ValueMismatch>;
506}
507
508impl<'a, C, QS, DB> FilterColumn<'a, QS, DB> for C
509where
510 C: Expression,
511 C::SqlType: IntoNullable,
512 <C::SqlType as IntoNullable>::Nullable: NonNull,
513 NonNullSqlType<C>: Comparison<'a, C, QS, DB>,
514{
515 const ACCEPTS: &'static [ValueKind] = <NonNullSqlType<C> as ValueSqlType>::ACCEPTS;
516
517 fn condition(
518 self,
519 filter_condition: FilterCondition,
520 ) -> Result<BoxedCondition<'a, QS, DB>, ValueMismatch> {
521 condition(self, filter_condition)
522 }
523}
524
525#[diagnostic::on_unimplemented(
529 message = "queryweave cannot order by the column `{Self}`",
530 note = "mark the entity field of a column storing no domain field `#[columns(skip)]`"
531)]
532pub trait OrderColumn<'a, QS, DB>: Sized {
533 fn ordering(self, sort_key: SortKey) -> BoxedOrdering<'a, QS, DB>;
535}
536
537impl<'a, C, QS, DB> OrderColumn<'a, QS, DB> for C
538where
539 DB: Backend,
540 C: ExpressionMethods,
541 dsl::Asc<C>: BoxableExpression<QS, DB, SqlType = NotSelectable> + 'a,
542 dsl::Desc<C>: BoxableExpression<QS, DB, SqlType = NotSelectable> + 'a,
543{
544 fn ordering(self, sort_key: SortKey) -> BoxedOrdering<'a, QS, DB> {
545 ordering(self, sort_key)
546 }
547}
548
549#[cfg(feature = "merge-patch")]
554#[diagnostic::on_unimplemented(
555 message = "queryweave cannot write the column `{Self}`: its SQL type takes no domain value",
556 label = "a column of a SQL type with no `queryweave_sql::diesel::ColumnValue`",
557 note = "mark the entity field of a column storing no domain field `#[columns(skip)]`, or implement `ValueSqlType`, `Comparison` and `ColumnValue` for its SQL type"
558)]
559pub trait AssignColumn: Sized {
560 const TYPE: ColumnType;
563
564 type Assignment;
566
567 fn assignment(self, field_value: FieldValue) -> Result<Self::Assignment, ValueMismatch>;
573}
574
575#[cfg(feature = "merge-patch")]
576impl<C> AssignColumn for C
577where
578 C: Expression + ExpressionMethods,
579 C::SqlType: SqlType + TypedExpressionType + ColumnValue,
580 ValueOf<C>: AsExpression<C::SqlType>,
581{
582 const TYPE: ColumnType = <C::SqlType as ColumnValue>::TYPE;
583
584 type Assignment = dsl::Eq<C, ValueOf<C>>;
585
586 fn assignment(self, field_value: FieldValue) -> Result<Self::Assignment, ValueMismatch> {
587 assignment(self, field_value)
588 }
589}
590
591#[cfg(feature = "merge-patch")]
594pub type Assignment<C> = <C as AssignColumn>::Assignment;
595
596pub trait JoinConditions<'a, QS>: Backend + Sized {
602 fn join(
604 conditions: impl IntoIterator<Item = BoxedCondition<'a, QS, Self>>,
605 junction: Junction,
606 ) -> Option<BoxedCondition<'a, QS, Self>>;
607}
608
609impl<'a, QS, DB> JoinConditions<'a, QS> for DB
610where
611 QS: 'a,
612 DB: Backend + 'a,
613 dsl::And<BoxedCondition<'a, QS, DB>, BoxedCondition<'a, QS, DB>>:
614 BoxableExpression<QS, DB, SqlType = Nullable<Bool>>,
615 dsl::Or<BoxedCondition<'a, QS, DB>, BoxedCondition<'a, QS, DB>>:
616 BoxableExpression<QS, DB, SqlType = Nullable<Bool>>,
617{
618 fn join(
619 conditions: impl IntoIterator<Item = BoxedCondition<'a, QS, Self>>,
620 junction: Junction,
621 ) -> Option<BoxedCondition<'a, QS, Self>> {
622 join(conditions, junction)
623 }
624}
625
626#[diagnostic::on_unimplemented(
635 message = "`{Self}` is not a Diesel entity for `{DB}`",
636 label = "no `queryweave_sql::diesel::Entity` for this entity",
637 note = "derive `Columns` on an entity with `#[diesel(table_name = ...)]`, every column of a SQL type queryweave converts (or `#[columns(skip)]`)"
638)]
639pub trait Entity<'a, DB> {
640 type Table;
642
643 fn conditions<M: PersistedFields>(
651 conditions: Conditions<M>,
652 junction: Junction,
653 ) -> Result<Option<BoxedCondition<'a, Self::Table, DB>>, ValueMismatch>;
654
655 fn order_by<M: PersistedFields>(
657 sort_keys: SortKeys<M>,
658 ) -> Vec<BoxedOrdering<'a, Self::Table, DB>>;
659}
660
661#[diagnostic::on_unimplemented(
668 message = "`{Self}` is not a boxed query of the entity's table",
669 label = "not a boxed query",
670 note = "box the query first, with `.into_boxed()`"
671)]
672pub trait BoxedQuery<'a, QS, DB>:
673 FilterDsl<BoxedCondition<'a, QS, DB>, Output = Self>
674 + ThenOrderDsl<BoxedOrdering<'a, QS, DB>, Output = Self>
675 + LimitDsl<Output = Self>
676 + OffsetDsl<Output = Self>
677{
678}
679
680impl<'a, QS, DB, Q> BoxedQuery<'a, QS, DB> for Q where
681 Q: FilterDsl<BoxedCondition<'a, QS, DB>, Output = Self>
682 + ThenOrderDsl<BoxedOrdering<'a, QS, DB>, Output = Self>
683 + LimitDsl<Output = Self>
684 + OffsetDsl<Output = Self>
685{
686}
687
688#[must_use]
692pub fn apply<'a, Q, QS, DB>(
693 query: Q,
694 condition: Option<BoxedCondition<'a, QS, DB>>,
695 orderings: Vec<BoxedOrdering<'a, QS, DB>>,
696 page: Option<(i64, i64)>,
697) -> Q
698where
699 Q: BoxedQuery<'a, QS, DB>,
700{
701 let mut query = match condition {
702 Some(condition) => FilterDsl::filter(query, condition),
703 None => query,
704 };
705 for ordering in orderings {
706 query = ThenOrderDsl::then_order_by(query, ordering);
707 }
708 match page {
709 Some((limit, offset)) => OffsetDsl::offset(LimitDsl::limit(query, limit), offset),
710 None => query,
711 }
712}
713
714#[cfg(test)]
715mod tests {
716 #[cfg(feature = "merge-patch")]
717 use super::assignment;
718 use super::{ValueMismatch, apply, condition, join, ordering, page};
719 use crate::Junction;
720 use diesel::mysql::Mysql;
721 use diesel::pg::Pg;
722 use diesel::prelude::*;
723 use diesel::sqlite::Sqlite;
724 use pretty_assertions::assert_eq;
725 #[cfg(feature = "merge-patch")]
726 use queryweave::FieldValue;
727 use queryweave::{FilterCondition, FilterOperator, SortDirectionModel, SortKey, Value};
728 use rstest::rstest;
729
730 diesel::table! {
731 orders (id) {
732 id -> Integer,
733 number -> Text,
734 client -> Nullable<Text>,
735 capacity -> Integer,
736 value -> Double,
737 delivered_at -> Timestamp,
738 }
739 }
740
741 fn filter(field: &'static str, operator: FilterOperator, value: Value) -> FilterCondition {
742 FilterCondition {
743 field,
744 operator,
745 value,
746 }
747 }
748
749 #[test]
752 fn every_backend_renders_the_comparison() {
753 let less_than = || filter("capacity", FilterOperator::LessThan, Value::I32(Some(9)));
754
755 let pg = orders::table
756 .select(orders::id)
757 .into_boxed::<Pg>()
758 .filter(condition(orders::capacity, less_than()).unwrap());
759 assert_eq!(
760 diesel::debug_query::<Pg, _>(&pg).to_string(),
761 r#"SELECT "orders"."id" FROM "orders" WHERE ("orders"."capacity" < $1) -- binds: [Some(9)]"#
762 );
763
764 let mysql = orders::table
765 .select(orders::id)
766 .into_boxed::<Mysql>()
767 .filter(condition(orders::capacity, less_than()).unwrap());
768 assert_eq!(
769 diesel::debug_query::<Mysql, _>(&mysql).to_string(),
770 "SELECT `orders`.`id` FROM `orders` WHERE (`orders`.`capacity` < ?) -- binds: [Some(9)]"
771 );
772
773 let sqlite = orders::table
774 .select(orders::id)
775 .into_boxed::<Sqlite>()
776 .filter(condition(orders::capacity, less_than()).unwrap());
777 assert_eq!(
778 diesel::debug_query::<Sqlite, _>(&sqlite).to_string(),
779 "SELECT `orders`.`id` FROM `orders` WHERE (`orders`.`capacity` < ?) -- binds: [Some(9)]"
780 );
781 }
782
783 fn pg_where(condition: super::BoxedCondition<'static, orders::table, Pg>) -> String {
785 let query = orders::table
786 .select(orders::id)
787 .into_boxed::<Pg>()
788 .filter(condition);
789 diesel::debug_query::<Pg, _>(&query).to_string()
790 }
791
792 #[rstest]
795 #[case(
796 FilterOperator::LessThan,
797 r#""orders"."client" < $1) -- binds: [Some("50%")]"#
798 )]
799 #[case(
800 FilterOperator::Equal,
801 r#""orders"."client" = $1) -- binds: [Some("50%")]"#
802 )]
803 #[case(
804 FilterOperator::GreaterThan,
805 r#""orders"."client" > $1) -- binds: [Some("50%")]"#
806 )]
807 #[case(
808 FilterOperator::StartsWith,
809 r#""orders"."client" LIKE $1 ESCAPE $2) -- binds: ["50!%%", "!"]"#
810 )]
811 #[case(
812 FilterOperator::Contains,
813 r#""orders"."client" LIKE $1 ESCAPE $2) -- binds: ["%50!%%", "!"]"#
814 )]
815 #[case(
816 FilterOperator::EndsWith,
817 r#""orders"."client" LIKE $1 ESCAPE $2) -- binds: ["%50!%", "!"]"#
818 )]
819 fn the_operator_selects_the_comparison(
820 #[case] operator: FilterOperator,
821 #[case] expected: &str,
822 ) {
823 let filter_condition = filter("client", operator, Value::Text(Some(String::from("50%"))));
824
825 assert_eq!(
826 pg_where(condition(orders::client, filter_condition).unwrap()),
827 format!(r#"SELECT "orders"."id" FROM "orders" WHERE ({expected}"#)
828 );
829 }
830
831 #[rstest]
833 #[case(
834 condition(
835 orders::capacity,
836 filter("capacity", FilterOperator::Equal, Value::I16(Some(3)))
837 ),
838 r#""orders"."capacity" = $1) -- binds: [Some(3)]"#
839 )]
840 #[case(
841 condition(
842 orders::value,
843 filter("value", FilterOperator::GreaterThan, Value::F32(Some(1.5)))
844 ),
845 r#""orders"."value" > $1) -- binds: [Some(1.5)]"#
846 )]
847 #[case(
848 condition(
849 orders::number,
850 filter("number", FilterOperator::Equal, Value::Text(None))
851 ),
852 r#""orders"."number" = $1) -- binds: [None]"#
853 )]
854 fn values_bind_with_the_columns_type(
855 #[case] condition: Result<super::BoxedCondition<'static, orders::table, Pg>, ValueMismatch>,
856 #[case] expected: &str,
857 ) {
858 assert_eq!(
859 pg_where(condition.unwrap()),
860 format!(r#"SELECT "orders"."id" FROM "orders" WHERE ({expected}"#)
861 );
862 }
863
864 #[rstest]
867 #[case(FilterOperator::Equal, Value::Text(Some(String::from("3"))))]
868 #[case(FilterOperator::Equal, Value::I64(Some(3)))]
869 #[case(FilterOperator::Contains, Value::I32(Some(3)))]
870 fn a_value_that_does_not_fit_is_a_mismatch(
871 #[case] operator: FilterOperator,
872 #[case] value: Value,
873 ) {
874 let result: Result<super::BoxedCondition<'static, orders::table, Pg>, _> =
875 condition(orders::capacity, filter("amount", operator, value));
876
877 assert_eq!(result.err(), Some(ValueMismatch { field: "amount" }));
878 }
879
880 #[rstest]
882 #[case(Junction::And, "AND")]
883 #[case(Junction::Or, "OR")]
884 fn conditions_are_joined_by_the_junction(#[case] junction: Junction, #[case] keyword: &str) {
885 let conditions = [
886 filter("capacity", FilterOperator::GreaterThan, Value::I32(Some(1))),
887 filter("capacity", FilterOperator::LessThan, Value::I32(Some(9))),
888 ]
889 .map(|filter_condition| condition(orders::capacity, filter_condition).unwrap());
890
891 assert_eq!(
892 pg_where(join(conditions, junction).unwrap()),
893 format!(
894 r#"SELECT "orders"."id" FROM "orders" WHERE (("orders"."capacity" > $1) {keyword} ("orders"."capacity" < $2)) -- binds: [Some(1), Some(9)]"#
895 )
896 );
897 assert!(join::<orders::table, Pg>([], junction).is_none());
898 }
899
900 #[test]
902 fn orderings_and_pages_apply_to_a_boxed_query() {
903 let (limit, offset) = page(Some(2), Some(10)).unwrap();
904 let query = orders::table
905 .select(orders::id)
906 .into_boxed::<Pg>()
907 .then_order_by(ordering(
908 orders::delivered_at,
909 SortKey {
910 field: "delivery_date",
911 direction: SortDirectionModel::Descending,
912 },
913 ))
914 .then_order_by(ordering(
915 orders::number,
916 SortKey {
917 field: "number",
918 direction: SortDirectionModel::Ascending,
919 },
920 ))
921 .limit(limit)
922 .offset(offset);
923
924 assert_eq!(
925 diesel::debug_query::<Pg, _>(&query).to_string(),
926 r#"SELECT "orders"."id" FROM "orders" ORDER BY "orders"."delivered_at" DESC, "orders"."number" ASC LIMIT $1 OFFSET $2 -- binds: [10, 20]"#
927 );
928 assert_eq!(page(None, Some(10)), None);
929 assert_eq!(page(Some(1), None), None);
930 }
931
932 #[test]
935 fn apply_adds_the_condition_orderings_and_page() {
936 let capacity = filter("capacity", FilterOperator::LessThan, Value::I32(Some(9)));
937 let number = SortKey {
938 field: "number",
939 direction: SortDirectionModel::Ascending,
940 };
941 let query = orders::table
942 .select(orders::id)
943 .into_boxed::<Pg>()
944 .filter(orders::id.gt(0))
945 .order_by(orders::id);
946
947 let query = apply(
948 query,
949 Some(condition(orders::capacity, capacity).unwrap()),
950 vec![ordering(orders::number, number)],
951 page(Some(1), Some(10)),
952 );
953
954 assert_eq!(
955 diesel::debug_query::<Pg, _>(&query).to_string(),
956 r#"SELECT "orders"."id" FROM "orders" WHERE (("orders"."id" > $1) AND ("orders"."capacity" < $2)) ORDER BY "orders"."id", "orders"."number" ASC LIMIT $3 OFFSET $4 -- binds: [0, Some(9), 10, 10]"#
957 );
958
959 let untouched = apply(
960 orders::table.select(orders::id).into_boxed::<Pg>(),
961 None,
962 Vec::new(),
963 None,
964 );
965 assert_eq!(
966 diesel::debug_query::<Pg, _>(&untouched).to_string(),
967 r#"SELECT "orders"."id" FROM "orders" -- binds: []"#
968 );
969 }
970
971 #[cfg(feature = "merge-patch")]
974 #[test]
975 fn assignments_make_a_changeset() {
976 let client = FieldValue {
977 field: "client",
978 value: Value::Text(None),
979 };
980 let capacity = FieldValue {
981 field: "capacity",
982 value: Value::I16(Some(4)),
983 };
984 let changeset = (
985 Some(assignment(orders::client, client).unwrap()),
986 Some(assignment(orders::capacity, capacity).unwrap()),
987 None::<diesel::dsl::Eq<orders::number, String>>,
988 );
989 let update = diesel::update(orders::table).set(changeset);
990
991 assert_eq!(
992 diesel::debug_query::<Pg, _>(&update).to_string(),
993 r#"UPDATE "orders" SET "client" = $1, "capacity" = $2 -- binds: [None, 4]"#
994 );
995 }
996
997 #[cfg(feature = "merge-patch")]
999 #[rstest]
1000 #[case(Value::I32(None))]
1001 #[case(Value::Text(Some(String::from("4"))))]
1002 fn an_assignment_that_does_not_fit_is_a_mismatch(#[case] value: Value) {
1003 let field_value = FieldValue {
1004 field: "amount",
1005 value,
1006 };
1007
1008 assert_eq!(
1009 assignment(orders::capacity, field_value).err(),
1010 Some(ValueMismatch { field: "amount" })
1011 );
1012 }
1013
1014 #[test]
1015 fn the_mismatch_names_the_field() {
1016 assert_eq!(
1017 ValueMismatch { field: "amount" }.to_string(),
1018 "the value of the domain field `amount` does not fit the SQL type of its column"
1019 );
1020 }
1021
1022 fn utc() -> chrono::DateTime<chrono::Utc> {
1023 chrono::DateTime::from_timestamp(1_767_268_800, 0).unwrap()
1024 }
1025
1026 #[test]
1030 fn each_sql_type_takes_its_values() {
1031 use super::ValueSqlType;
1032 use diesel::sql_types::{
1033 BigInt, Bool, Date, Double, Float, Integer, SmallInt, Text, Time, Timestamp,
1034 };
1035
1036 assert_eq!(Bool::rust(Value::Bool(Some(true))), Ok(Some(true)));
1037 assert_eq!(Bool::rust(Value::I32(Some(1))), Err(Value::I32(Some(1))));
1038
1039 assert_eq!(SmallInt::rust(Value::I16(Some(1))), Ok(Some(1)));
1040 assert_eq!(
1041 SmallInt::rust(Value::I32(Some(1))),
1042 Err(Value::I32(Some(1)))
1043 );
1044
1045 assert_eq!(Integer::rust(Value::I32(Some(1))), Ok(Some(1)));
1046 assert_eq!(Integer::rust(Value::I16(Some(2))), Ok(Some(2)));
1047 assert_eq!(Integer::rust(Value::I64(Some(3))), Err(Value::I64(Some(3))));
1048
1049 assert_eq!(BigInt::rust(Value::I64(Some(1))), Ok(Some(1)));
1050 assert_eq!(BigInt::rust(Value::I32(Some(2))), Ok(Some(2)));
1051 assert_eq!(BigInt::rust(Value::I16(Some(3))), Ok(Some(3)));
1052
1053 assert_eq!(Float::rust(Value::F32(Some(1.5))), Ok(Some(1.5)));
1054 assert_eq!(
1055 Float::rust(Value::F64(Some(1.5))),
1056 Err(Value::F64(Some(1.5)))
1057 );
1058
1059 assert_eq!(Double::rust(Value::F64(Some(1.5))), Ok(Some(1.5)));
1060 assert_eq!(Double::rust(Value::F32(Some(2.5))), Ok(Some(2.5)));
1061
1062 assert_eq!(
1063 Text::rust(Value::Text(Some(String::from("a")))),
1064 Ok(Some(String::from("a")))
1065 );
1066 assert_eq!(Text::rust(Value::Text(None)), Ok(None));
1067 assert_eq!(Text::rust(Value::I32(Some(1))), Err(Value::I32(Some(1))));
1068
1069 assert_eq!(
1070 Timestamp::rust(Value::NaiveDateTime(Some(utc().naive_utc()))),
1071 Ok(Some(utc().naive_utc()))
1072 );
1073 assert_eq!(
1074 Timestamp::rust(Value::DateTimeUtc(Some(utc()))),
1075 Ok(Some(utc().naive_utc()))
1076 );
1077
1078 assert_eq!(
1079 Date::rust(Value::NaiveDate(Some(utc().date_naive()))),
1080 Ok(Some(utc().date_naive()))
1081 );
1082 assert_eq!(
1083 Date::rust(Value::DateTimeUtc(Some(utc()))),
1084 Err(Value::DateTimeUtc(Some(utc())))
1085 );
1086
1087 assert_eq!(
1088 Time::rust(Value::NaiveTime(Some(utc().time()))),
1089 Ok(Some(utc().time()))
1090 );
1091 }
1092
1093 #[cfg(feature = "diesel-postgres")]
1096 #[test]
1097 fn postgres_timestamptz_takes_a_utc_date_time() {
1098 use super::ValueSqlType;
1099 use diesel::sql_types::Timestamptz;
1100
1101 assert_eq!(
1102 Timestamptz::rust(Value::DateTimeUtc(Some(utc()))),
1103 Ok(Some(utc()))
1104 );
1105 assert_eq!(
1106 Timestamptz::rust(Value::NaiveDateTime(Some(utc().naive_utc()))),
1107 Err(Value::NaiveDateTime(Some(utc().naive_utc())))
1108 );
1109 }
1110
1111 #[cfg(feature = "diesel-sqlite")]
1112 #[test]
1113 fn sqlite_timestamptz_takes_a_utc_date_time() {
1114 use super::ValueSqlType;
1115 use diesel::sql_types::TimestamptzSqlite;
1116
1117 assert_eq!(
1118 TimestamptzSqlite::rust(Value::DateTimeUtc(Some(utc()))),
1119 Ok(Some(utc()))
1120 );
1121 }
1122
1123 #[cfg(feature = "diesel-mysql")]
1124 #[test]
1125 fn mysql_datetime_takes_a_naive_or_utc_date_time() {
1126 use super::ValueSqlType;
1127 use diesel::sql_types::Datetime;
1128
1129 assert_eq!(
1130 Datetime::rust(Value::NaiveDateTime(Some(utc().naive_utc()))),
1131 Ok(Some(utc().naive_utc()))
1132 );
1133 assert_eq!(
1134 Datetime::rust(Value::DateTimeUtc(Some(utc()))),
1135 Ok(Some(utc().naive_utc()))
1136 );
1137 }
1138
1139 #[test]
1142 fn a_columns_value_follows_its_nullability() {
1143 use super::ColumnValue;
1144 use diesel::sql_types::{Integer, Nullable};
1145
1146 assert_eq!(Integer::from_value(Value::I32(Some(1))), Some(1));
1147 assert_eq!(Integer::from_value(Value::I32(None)), None);
1148 assert_eq!(
1149 Nullable::<Integer>::from_value(Value::I32(None)),
1150 Some(None)
1151 );
1152 assert_eq!(
1153 Nullable::<Integer>::from_value(Value::I32(Some(1))),
1154 Some(Some(1))
1155 );
1156 assert_eq!(Nullable::<Integer>::from_value(Value::Text(None)), None);
1157 }
1158
1159 fn nulls() -> [Value; 11] {
1161 [
1162 Value::Bool(None),
1163 Value::I16(None),
1164 Value::I32(None),
1165 Value::I64(None),
1166 Value::F32(None),
1167 Value::F64(None),
1168 Value::Text(None),
1169 Value::DateTimeUtc(None),
1170 Value::NaiveDateTime(None),
1171 Value::NaiveDate(None),
1172 Value::NaiveTime(None),
1173 ]
1174 }
1175
1176 fn assert_accepts<ST: super::ValueSqlType>() {
1178 for value in nulls() {
1179 let kind = value.kind();
1180 assert_eq!(
1181 ST::rust(value).is_ok(),
1182 ST::ACCEPTS.contains(&kind),
1183 "{kind:?} for {}",
1184 std::any::type_name::<ST>()
1185 );
1186 }
1187 }
1188
1189 #[test]
1192 fn each_sql_type_accepts_the_variants_it_converts() {
1193 use diesel::sql_types::{
1194 BigInt, Bool, Date, Double, Float, Integer, SmallInt, Text, Time, Timestamp,
1195 };
1196
1197 assert_accepts::<Bool>();
1198 assert_accepts::<SmallInt>();
1199 assert_accepts::<Integer>();
1200 assert_accepts::<BigInt>();
1201 assert_accepts::<Float>();
1202 assert_accepts::<Double>();
1203 assert_accepts::<Text>();
1204 assert_accepts::<Timestamp>();
1205 assert_accepts::<Date>();
1206 assert_accepts::<Time>();
1207 #[cfg(feature = "diesel-postgres")]
1208 assert_accepts::<diesel::sql_types::Timestamptz>();
1209 #[cfg(feature = "diesel-sqlite")]
1210 assert_accepts::<diesel::sql_types::TimestamptzSqlite>();
1211 #[cfg(feature = "diesel-mysql")]
1212 assert_accepts::<diesel::sql_types::Datetime>();
1213 }
1214
1215 #[test]
1217 fn a_columns_type_follows_its_sql_type() {
1218 use super::{ColumnType, ColumnValue, FilterColumn};
1219 use queryweave::ValueKind;
1220
1221 assert_eq!(
1222 <orders::capacity as FilterColumn<'static, orders::table, Pg>>::ACCEPTS,
1223 &[ValueKind::I32, ValueKind::I16]
1224 );
1225 assert_eq!(
1226 <diesel::sql_types::Integer as ColumnValue>::TYPE,
1227 ColumnType {
1228 accepts: &[ValueKind::I32, ValueKind::I16],
1229 nullable: false,
1230 }
1231 );
1232 assert_eq!(
1233 <diesel::sql_types::Nullable<diesel::sql_types::Text> as ColumnValue>::TYPE,
1234 ColumnType {
1235 accepts: &[ValueKind::Text],
1236 nullable: true,
1237 }
1238 );
1239 }
1240}