Skip to main content

queryweave_sql/backend/
diesel.rs

1//! Diesel's typed query DSL as the target of a model's data.
2//!
3//! Filter conditions become boxed expressions on a table, sort keys boxed
4//! orderings, a page `LIMIT` / `OFFSET` values, and merge-patch writes typed
5//! `column = value` assignments for a changeset.
6//!
7//! Diesel types every column statically, so a column is named by the code
8//! `Columns` generates for a Diesel entity (one `match` arm per column), not
9//! looked up by name here. The functions below take that typed column and an
10//! item of the model's data, and convert the item's `Value` into the column's
11//! own Rust type, picked by the column's SQL type through [`ValueSqlType`]: no
12//! SQL is written by hand, and Diesel renders it for each backend.
13//!
14//! The wrappers reach a Diesel entity's methods through [`Entity`], which
15//! `Columns` implements for it, and [`apply`] them to a [`BoxedQuery`].
16//!
17//! A domain field whose value does not fit its column's SQL type (a domain
18//! field and its column disagree on the type, or a nullable field is written
19//! to a `NOT NULL` column) fails to build where the entity's methods are
20//! called for its model: the domain derives publish each field's value type
21//! (`PersistedFields::FIELD_TYPES`), and each column's SQL type the values it
22//! takes ([`ValueSqlType::ACCEPTS`], through [`FilterColumn::ACCEPTS`] and
23//! [`ColumnValue::TYPE`]). A value that
24//! still does not fit at run time (a model implementing `PersistedFields` by
25//! hand, or a `NULL` written to a `NOT NULL` column) is a [`ValueMismatch`]
26//! error, not a panic.
27
28pub(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
49/// A filter condition on the table `QS`, boxed for a boxed query's `filter`.
50///
51/// Nullable: a comparison with a nullable column, or with a `NULL` value, may
52/// be `NULL`.
53pub type BoxedCondition<'a, QS, DB> =
54    Box<dyn BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a>;
55
56/// An ordering on the table `QS`, boxed for a boxed query's `then_order_by`.
57pub type BoxedOrdering<'a, QS, DB> =
58    Box<dyn BoxableExpression<QS, DB, SqlType = NotSelectable> + 'a>;
59
60/// A value that does not fit its column: the domain field's raw type and the
61/// column's SQL type disagree, or the value is a `NULL` for a `NOT NULL`
62/// column.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct ValueMismatch {
65    /// The domain field whose value does not fit.
66    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
81/// A non-null SQL type a domain value binds as: the Rust type it binds, taken
82/// from a [`Value`].
83///
84/// Implemented for Diesel's SQL types queryweave's values fit, each accepting
85/// the variants it holds without loss (an `Integer` column takes `I32` and
86/// `I16`; a `Timestamp` column a `NaiveDateTime`, or a UTC `DateTimeUtc` as its
87/// naive UTC time). Implement it for a SQL type of your own (with
88/// [`Comparison`] and [`ColumnValue`]) to filter and write its columns.
89pub trait ValueSqlType: SqlType<IsNull = is_nullable::NotNull> + SingleValue + Sized {
90    /// The [`Value`] variants [`rust`](Self::rust) takes, for the compile-time
91    /// check that a domain field's value fits its column.
92    const ACCEPTS: &'static [ValueKind];
93
94    /// The Rust type the value binds as.
95    type Rust: AsExpression<Self> + Send + 'static;
96
97    /// The value as this SQL type's Rust type (`None` for a `NULL`).
98    ///
99    /// # Errors
100    ///
101    /// The value itself, if it is of another type.
102    fn rust(value: Value) -> Result<Option<Self::Rust>, Value>;
103}
104
105/// Implement [`ValueSqlType`] (and [`Comparison`], [`ColumnValue`]) for SQL
106/// types, each listing the `Value` variants it accepts and how each converts.
107macro_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                // A `NOT NULL` column cannot be set to `NULL`.
151                <$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
196// Text is a `ValueSqlType` like the others, but compares with `LIKE` too.
197impl 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/// How a column whose non-null SQL type is `Self` is compared with a value:
221/// `<`, `=`, `>`, and for text the `LIKE` operators.
222///
223/// Implemented per SQL type, so the code generated for a column is the same
224/// call whatever its type: [`condition`] picks the implementation by the
225/// column's SQL type.
226#[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    /// `column operator value`, the value bound with the column's type; `None`
233    /// if the value is of another type, or the operator does not apply.
234    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        // The `Text` filter kind guarantees the `LIKE` operators only ever
262        // carry text: an escaped `%` pattern, matched literally.
263        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
275/// `column < value`, `=` or `>`, the column made nullable so a `NULL` value
276/// (or column) compares as in SQL; `None` for a value of another type, or a
277/// `LIKE` operator (text only).
278fn 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
308/// `Nullable<ST>` → `ST`: the non-null SQL type of a column, nullable or not
309/// (through `IntoNullable`, which leaves a nullable type as it is).
310pub trait NonNull {
311    /// The non-null SQL type.
312    type SqlType;
313}
314
315impl<ST: SqlType<IsNull = is_nullable::NotNull>> NonNull for Nullable<ST> {
316    type SqlType = ST;
317}
318
319/// The non-null SQL type of the column `C`.
320pub type NonNullSqlType<C> =
321    <<<C as Expression>::SqlType as IntoNullable>::Nullable as NonNull>::SqlType;
322
323/// The condition `column operator value` for a filter condition of the model,
324/// the column being the one the entity stores its domain field in.
325///
326/// # Errors
327///
328/// [`ValueMismatch`] if the condition's value does not fit the column's SQL
329/// type.
330pub 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
350/// The conditions joined by `junction` (`AND` or `OR`), in order; `None` when
351/// there are none.
352pub 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
372/// The ordering `column ASC` or `column DESC` for a sort key of the model.
373pub 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/// The `LIMIT` and `OFFSET` of page `page` (0 based) of `page_size` rows, for
387/// a boxed query's `limit` and `offset`; `None` unless both are requested.
388#[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/// What a column takes, at compile time: the [`Value`] variants of its
398/// non-null SQL type ([`ValueSqlType::ACCEPTS`]), and whether it may be
399/// `NULL`.
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401pub struct ColumnType {
402    /// The variants it takes.
403    pub accepts: &'static [ValueKind],
404    /// Whether it may be `NULL`.
405    pub nullable: bool,
406}
407
408impl ColumnType {
409    /// A `NOT NULL` column of the SQL type `ST`.
410    #[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/// The Rust value a column whose SQL type is `Self` is set to: the SQL type's
420/// Rust type, or an `Option` of it for a nullable column.
421#[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    /// What the column takes, for the compile-time check that a domain
428    /// field's value fits it: [`ColumnType::not_null`] for a `NOT NULL` SQL
429    /// type.
430    const TYPE: ColumnType;
431
432    /// The Rust type.
433    type Rust;
434
435    /// The value as that type; `None` if it does not fit (another type, or a
436    /// `NULL` for a `NOT NULL` column).
437    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
453/// The Rust value type of the column `C`, by its SQL type.
454pub type ValueOf<C> = <<C as Expression>::SqlType as ColumnValue>::Rust;
455
456/// The assignment `column = value` for a merge-patch write of the model.
457///
458/// The column is the one the entity stores the domain field in; the
459/// assignment is one item of a changeset (`Option`s of these, `None` for the
460/// unchanged columns).
461///
462/// # Errors
463///
464/// [`ValueMismatch`] if the value does not fit the column's SQL type.
465#[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/// A Diesel column a model's filter conditions can compare: one whose SQL
482/// type converts a domain value ([`ValueSqlType`] and [`Comparison`]).
483///
484/// Blanket-implemented over [`condition`]: the code generated for an entity
485/// requires it of every column, so a column of another SQL type is reported
486/// where the conditions are written, naming the column.
487#[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    /// The [`Value`] variants the column's SQL type takes, for the
494    /// compile-time check that a domain field's value fits it.
495    const ACCEPTS: &'static [ValueKind];
496
497    /// See [`condition`].
498    ///
499    /// # Errors
500    ///
501    /// See [`condition`].
502    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/// A Diesel column a model's sort keys can order by.
526///
527/// Blanket-implemented over [`ordering`], for every column of the table.
528#[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    /// See [`ordering`].
534    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/// A Diesel column a model's merge-patch writes can set: one whose SQL type
550/// converts a domain value ([`ColumnValue`]).
551///
552/// Blanket-implemented over [`assignment`].
553#[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    /// What the column takes, for the compile-time check that a domain
561    /// field's value fits it ([`ColumnValue::TYPE`]).
562    const TYPE: ColumnType;
563
564    /// The assignment `column = value`.
565    type Assignment;
566
567    /// See [`assignment`].
568    ///
569    /// # Errors
570    ///
571    /// See [`assignment`].
572    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/// The assignment `column = value` of the column `C`: one slot of an entity's
592/// changeset (an `Option` of it, `None` when unchanged).
593#[cfg(feature = "merge-patch")]
594pub type Assignment<C> = <C as AssignColumn>::Assignment;
595
596/// A backend whose conditions on the table `QS` can be joined: every Diesel
597/// backend.
598///
599/// [`join`] as a method, so the code generated for an entity needs this one
600/// bound rather than [`join`]'s.
601pub trait JoinConditions<'a, QS>: Backend + Sized {
602    /// See [`join`].
603    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/// A Diesel entity: one whose columns the model's data can name, implemented
627/// by `#[derive(Columns)]` for an entity with `#[diesel(table_name = ..)]`.
628///
629/// Its methods are the entity's `diesel_conditions` and `diesel_order_by` as
630/// a trait, so the code generated for the wrappers (`AppendToQuery`,
631/// `AppendConditions`), which knows the entity but not its table,
632/// can name them. `DB` is the backend, `'a` how long the boxed expressions
633/// live.
634#[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    /// The entity's table (`table_name::table`).
641    type Table;
642
643    /// The conditions of the model `M`, joined by `junction`, as one
644    /// condition on the table; `None` when there are none.
645    ///
646    /// # Errors
647    ///
648    /// [`ValueMismatch`] if a condition's value does not fit its column's SQL
649    /// type.
650    fn conditions<M: PersistedFields>(
651        conditions: Conditions<M>,
652        junction: Junction,
653    ) -> Result<Option<BoxedCondition<'a, Self::Table, DB>>, ValueMismatch>;
654
655    /// The sort keys of the model `M`, in order, as orderings of the table.
656    fn order_by<M: PersistedFields>(
657        sort_keys: SortKeys<M>,
658    ) -> Vec<BoxedOrdering<'a, Self::Table, DB>>;
659}
660
661/// A boxed query on the table `QS` (`table.into_boxed()`, after any `select`
662/// or `filter`): one taking conditions, orderings, a limit and an offset as
663/// the same type, which [`apply`] adds.
664///
665/// Blanket-implemented for every query that does: Diesel's boxed select
666/// statements.
667#[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/// The boxed query `query` with the condition (its `filter`, so `AND`ed to
689/// the query's own), the orderings after its own (`then_order_by`), and the
690/// page (`limit` and `offset`, from [`page`]).
691#[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    /// A select of the ids, filtered by a condition on `capacity`, rendered
750    /// for each backend.
751    #[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    /// The Postgres `WHERE` of a select of the ids with the condition.
784    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    // The operator selects the comparison; the `LIKE` ones bind an escaped
793    // pattern and the escape character, nullable column or not.
794    #[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    // A narrower value widens to its column's type; a `NULL` binds as one.
832    #[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    // A value of another type, or `LIKE` on a column that is not text, is a
865    // mismatch naming the domain field.
866    #[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    // Conditions are joined by the junction; without any there is none.
881    #[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    // Sort keys order in their direction, a page is a limit and an offset.
901    #[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    // `apply` adds a condition after the query's own, orderings after its
933    // own, and a page; nothing it is not given.
934    #[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    // Assignments set each column with its type: a nullable column to `NULL`,
972    // the unchanged columns left out of the changeset.
973    #[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    // A `NOT NULL` column cannot be set to `NULL`, nor to another type.
998    #[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    // Each SQL type takes its own variant and the narrower ones, without loss
1027    // (a UTC date-time as its naive UTC time); a `NULL` stays one, and any
1028    // other variant is handed back.
1029    #[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    // Each backend's own date-time type takes a UTC date-time (MySQL's, which
1094    // has no time zone, as its naive UTC time).
1095    #[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    // A column's value: a `NOT NULL` column takes no `NULL`, a nullable one
1140    // does; a value of another type fits neither.
1141    #[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    /// A `NULL` of every `Value` variant.
1160    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    /// `ST::ACCEPTS` lists exactly the variants `ST::rust` takes.
1177    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    // The compile-time check and the conversion agree on what a SQL type
1190    // takes.
1191    #[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    // A column's type: its SQL type's variants, nullable or not.
1216    #[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}