pub(crate) mod check;
use crate::Junction;
use crate::sql::{LIKE_ESCAPE, like_pattern, page_offset};
use diesel::backend::Backend;
#[cfg(feature = "merge-patch")]
use diesel::expression::TypedExpressionType;
use diesel::expression::expression_types::NotSelectable;
use diesel::expression::{AsExpression, BoxableExpression, Expression};
use diesel::expression_methods::EscapeExpressionMethods;
use diesel::query_dsl::methods::{FilterDsl, LimitDsl, OffsetDsl, ThenOrderDsl};
use diesel::sql_types::{self, Bool, IntoNullable, Nullable, SingleValue, SqlType, is_nullable};
use diesel::{BoolExpressionMethods, ExpressionMethods, NullableExpressionMethods};
use diesel::{TextExpressionMethods, dsl};
#[cfg(feature = "merge-patch")]
use queryweave::FieldValue;
use queryweave::{
Conditions, FilterCondition, FilterOperator, PersistedFields, SortDirectionModel, SortKey,
SortKeys, Value, ValueKind,
};
pub type BoxedCondition<'a, QS, DB> =
Box<dyn BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a>;
pub type BoxedOrdering<'a, QS, DB> =
Box<dyn BoxableExpression<QS, DB, SqlType = NotSelectable> + 'a>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ValueMismatch {
pub field: &'static str,
}
impl std::fmt::Display for ValueMismatch {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"the value of the domain field `{}` does not fit the SQL type of its column",
self.field
)
}
}
impl std::error::Error for ValueMismatch {}
pub trait ValueSqlType: SqlType<IsNull = is_nullable::NotNull> + SingleValue + Sized {
const ACCEPTS: &'static [ValueKind];
type Rust: AsExpression<Self> + Send + 'static;
fn rust(value: Value) -> Result<Option<Self::Rust>, Value>;
}
macro_rules! value_sql_types {
($($sql:ty => $rust:ty { $($variant:ident($value:ident) => $convert:expr),+ $(,)? }),+ $(,)?) => {$(
impl ValueSqlType for $sql {
const ACCEPTS: &'static [ValueKind] = &[$(ValueKind::$variant),+];
type Rust = $rust;
fn rust(value: Value) -> Result<Option<Self::Rust>, Value> {
match value {
$(Value::$variant(value) => Ok(value.map(|$value| $convert)),)+
other => Err(other),
}
}
}
impl<'a, C, QS, DB> Comparison<'a, C, QS, DB> for $sql
where
DB: Backend,
C: Expression + NullableExpressionMethods,
C::SqlType: IntoNullable<Nullable = Nullable<$sql>>,
dsl::Nullable<C>: Expression<SqlType = Nullable<$sql>>,
dsl::Eq<dsl::Nullable<C>, Option<$rust>>:
BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
dsl::Lt<dsl::Nullable<C>, Option<$rust>>:
BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
dsl::Gt<dsl::Nullable<C>, Option<$rust>>:
BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
{
fn compare(
column: C,
operator: FilterOperator,
value: Value,
) -> Option<BoxedCondition<'a, QS, DB>> {
compare_values::<C, $sql, QS, DB>(column, operator, value)
}
}
impl ColumnValue for $sql {
const TYPE: ColumnType = ColumnType::not_null::<$sql>();
type Rust = $rust;
fn from_value(value: Value) -> Option<Self::Rust> {
<$sql as ValueSqlType>::rust(value).ok().flatten()
}
}
)+};
}
value_sql_types!(
sql_types::Bool => bool { Bool(value) => value },
sql_types::SmallInt => i16 { I16(value) => value },
sql_types::Integer => i32 { I32(value) => value, I16(value) => i32::from(value) },
sql_types::BigInt => i64 {
I64(value) => value,
I32(value) => i64::from(value),
I16(value) => i64::from(value),
},
sql_types::Float => f32 { F32(value) => value },
sql_types::Double => f64 { F64(value) => value, F32(value) => f64::from(value) },
sql_types::Timestamp => chrono::NaiveDateTime {
NaiveDateTime(value) => value,
DateTimeUtc(value) => value.naive_utc(),
},
sql_types::Date => chrono::NaiveDate { NaiveDate(value) => value },
sql_types::Time => chrono::NaiveTime { NaiveTime(value) => value },
);
#[cfg(feature = "diesel-postgres")]
value_sql_types!(
sql_types::Timestamptz => chrono::DateTime<chrono::Utc> { DateTimeUtc(value) => value },
);
#[cfg(feature = "diesel-sqlite")]
value_sql_types!(
sql_types::TimestamptzSqlite => chrono::DateTime<chrono::Utc> {
DateTimeUtc(value) => value,
},
);
#[cfg(feature = "diesel-mysql")]
value_sql_types!(
sql_types::Datetime => chrono::NaiveDateTime {
NaiveDateTime(value) => value,
DateTimeUtc(value) => value.naive_utc(),
},
);
impl ValueSqlType for sql_types::Text {
const ACCEPTS: &'static [ValueKind] = &[ValueKind::Text];
type Rust = String;
fn rust(value: Value) -> Result<Option<Self::Rust>, Value> {
match value {
Value::Text(value) => Ok(value),
other => Err(other),
}
}
}
impl ColumnValue for sql_types::Text {
const TYPE: ColumnType = ColumnType::not_null::<Self>();
type Rust = String;
fn from_value(value: Value) -> Option<Self::Rust> {
<Self as ValueSqlType>::rust(value).ok().flatten()
}
}
#[diagnostic::on_unimplemented(
message = "queryweave cannot compare a domain value with a column of SQL type `{Self}`",
label = "no `queryweave_sql::diesel::Comparison` for this SQL type",
note = "mark the entity field of a column storing no domain field `#[columns(skip)]`, or implement `ValueSqlType`, `Comparison` and `ColumnValue` for the SQL type"
)]
pub trait Comparison<'a, C, QS, DB>: ValueSqlType {
fn compare(
column: C,
operator: FilterOperator,
value: Value,
) -> Option<BoxedCondition<'a, QS, DB>>;
}
impl<'a, C, QS, DB> Comparison<'a, C, QS, DB> for sql_types::Text
where
DB: Backend,
C: Expression + NullableExpressionMethods,
C::SqlType: IntoNullable<Nullable = Nullable<Self>>,
dsl::Nullable<C>: Expression<SqlType = Nullable<Self>> + TextExpressionMethods,
dsl::Escape<dsl::Like<dsl::Nullable<C>, String>>:
BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
dsl::Eq<dsl::Nullable<C>, Option<String>>:
BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
dsl::Lt<dsl::Nullable<C>, Option<String>>:
BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
dsl::Gt<dsl::Nullable<C>, Option<String>>:
BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
{
fn compare(
column: C,
operator: FilterOperator,
value: Value,
) -> Option<BoxedCondition<'a, QS, DB>> {
if let Value::Text(Some(text)) = &value
&& let Some(pattern) = like_pattern(operator, text)
{
return Some(Box::new(
column.nullable().like(pattern).escape(LIKE_ESCAPE),
));
}
compare_values::<C, Self, QS, DB>(column, operator, value)
}
}
fn compare_values<'a, C, ST, QS, DB>(
column: C,
operator: FilterOperator,
value: Value,
) -> Option<BoxedCondition<'a, QS, DB>>
where
DB: Backend,
ST: ValueSqlType,
Option<ST::Rust>: AsExpression<Nullable<ST>>,
C: Expression + NullableExpressionMethods,
C::SqlType: IntoNullable<Nullable = Nullable<ST>>,
dsl::Nullable<C>: Expression<SqlType = Nullable<ST>>,
dsl::Eq<dsl::Nullable<C>, Option<ST::Rust>>:
BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
dsl::Lt<dsl::Nullable<C>, Option<ST::Rust>>:
BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
dsl::Gt<dsl::Nullable<C>, Option<ST::Rust>>:
BoxableExpression<QS, DB, SqlType = Nullable<Bool>> + 'a,
{
let value = ST::rust(value).ok()?;
let column = column.nullable();
match operator {
FilterOperator::LessThan => Some(Box::new(column.lt(value))),
FilterOperator::Equal => Some(Box::new(column.eq(value))),
FilterOperator::GreaterThan => Some(Box::new(column.gt(value))),
FilterOperator::StartsWith | FilterOperator::Contains | FilterOperator::EndsWith => None,
}
}
pub trait NonNull {
type SqlType;
}
impl<ST: SqlType<IsNull = is_nullable::NotNull>> NonNull for Nullable<ST> {
type SqlType = ST;
}
pub type NonNullSqlType<C> =
<<<C as Expression>::SqlType as IntoNullable>::Nullable as NonNull>::SqlType;
pub fn condition<'a, C, QS, DB>(
column: C,
filter_condition: FilterCondition,
) -> Result<BoxedCondition<'a, QS, DB>, ValueMismatch>
where
C: Expression,
C::SqlType: IntoNullable,
<C::SqlType as IntoNullable>::Nullable: NonNull,
NonNullSqlType<C>: Comparison<'a, C, QS, DB>,
{
let FilterCondition {
field,
operator,
value,
} = filter_condition;
<NonNullSqlType<C> as Comparison<'a, C, QS, DB>>::compare(column, operator, value)
.ok_or(ValueMismatch { field })
}
pub fn join<'a, QS, DB>(
conditions: impl IntoIterator<Item = BoxedCondition<'a, QS, DB>>,
junction: Junction,
) -> Option<BoxedCondition<'a, QS, DB>>
where
QS: 'a,
DB: Backend + 'a,
dsl::And<BoxedCondition<'a, QS, DB>, BoxedCondition<'a, QS, DB>>:
BoxableExpression<QS, DB, SqlType = Nullable<Bool>>,
dsl::Or<BoxedCondition<'a, QS, DB>, BoxedCondition<'a, QS, DB>>:
BoxableExpression<QS, DB, SqlType = Nullable<Bool>>,
{
conditions
.into_iter()
.reduce(|joined, condition| match junction {
Junction::And => Box::new(joined.and(condition)),
Junction::Or => Box::new(joined.or(condition)),
})
}
pub fn ordering<'a, C, QS, DB>(column: C, sort_key: SortKey) -> BoxedOrdering<'a, QS, DB>
where
DB: Backend,
C: ExpressionMethods,
dsl::Asc<C>: BoxableExpression<QS, DB, SqlType = NotSelectable> + 'a,
dsl::Desc<C>: BoxableExpression<QS, DB, SqlType = NotSelectable> + 'a,
{
match sort_key.direction {
SortDirectionModel::Ascending => Box::new(column.asc()),
SortDirectionModel::Descending => Box::new(column.desc()),
}
}
#[must_use]
pub fn page(page: Option<u32>, page_size: Option<u32>) -> Option<(i64, i64)> {
let (Some(page), Some(page_size)) = (page, page_size) else {
return None;
};
Some((i64::from(page_size), page_offset(page, page_size)))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ColumnType {
pub accepts: &'static [ValueKind],
pub nullable: bool,
}
impl ColumnType {
#[must_use]
pub const fn not_null<ST: ValueSqlType>() -> Self {
Self {
accepts: ST::ACCEPTS,
nullable: false,
}
}
}
#[diagnostic::on_unimplemented(
message = "queryweave cannot write a domain value to a column of SQL type `{Self}`",
label = "no `queryweave_sql::diesel::ColumnValue` for this SQL type",
note = "mark the entity field of a column storing no domain field `#[columns(skip)]`, or implement `ValueSqlType`, `Comparison` and `ColumnValue` for the SQL type"
)]
pub trait ColumnValue {
const TYPE: ColumnType;
type Rust;
fn from_value(value: Value) -> Option<Self::Rust>;
}
impl<ST: ValueSqlType> ColumnValue for Nullable<ST> {
const TYPE: ColumnType = ColumnType {
accepts: ST::ACCEPTS,
nullable: true,
};
type Rust = Option<ST::Rust>;
fn from_value(value: Value) -> Option<Self::Rust> {
ST::rust(value).ok()
}
}
pub type ValueOf<C> = <<C as Expression>::SqlType as ColumnValue>::Rust;
#[cfg(feature = "merge-patch")]
pub fn assignment<C>(
column: C,
field_value: FieldValue,
) -> Result<dsl::Eq<C, ValueOf<C>>, ValueMismatch>
where
C: Expression + ExpressionMethods,
C::SqlType: SqlType + TypedExpressionType + ColumnValue,
ValueOf<C>: AsExpression<C::SqlType>,
{
let FieldValue { field, value } = field_value;
let value = <C::SqlType as ColumnValue>::from_value(value).ok_or(ValueMismatch { field })?;
Ok(column.eq(value))
}
#[diagnostic::on_unimplemented(
message = "queryweave cannot filter on the column `{Self}`: its SQL type takes no domain value",
label = "a column of a SQL type with no `queryweave_sql::diesel::ValueSqlType`",
note = "mark the entity field of a column storing no domain field `#[columns(skip)]`, or implement `ValueSqlType`, `Comparison` and `ColumnValue` for its SQL type"
)]
pub trait FilterColumn<'a, QS, DB>: Sized {
const ACCEPTS: &'static [ValueKind];
fn condition(
self,
filter_condition: FilterCondition,
) -> Result<BoxedCondition<'a, QS, DB>, ValueMismatch>;
}
impl<'a, C, QS, DB> FilterColumn<'a, QS, DB> for C
where
C: Expression,
C::SqlType: IntoNullable,
<C::SqlType as IntoNullable>::Nullable: NonNull,
NonNullSqlType<C>: Comparison<'a, C, QS, DB>,
{
const ACCEPTS: &'static [ValueKind] = <NonNullSqlType<C> as ValueSqlType>::ACCEPTS;
fn condition(
self,
filter_condition: FilterCondition,
) -> Result<BoxedCondition<'a, QS, DB>, ValueMismatch> {
condition(self, filter_condition)
}
}
#[diagnostic::on_unimplemented(
message = "queryweave cannot order by the column `{Self}`",
note = "mark the entity field of a column storing no domain field `#[columns(skip)]`"
)]
pub trait OrderColumn<'a, QS, DB>: Sized {
fn ordering(self, sort_key: SortKey) -> BoxedOrdering<'a, QS, DB>;
}
impl<'a, C, QS, DB> OrderColumn<'a, QS, DB> for C
where
DB: Backend,
C: ExpressionMethods,
dsl::Asc<C>: BoxableExpression<QS, DB, SqlType = NotSelectable> + 'a,
dsl::Desc<C>: BoxableExpression<QS, DB, SqlType = NotSelectable> + 'a,
{
fn ordering(self, sort_key: SortKey) -> BoxedOrdering<'a, QS, DB> {
ordering(self, sort_key)
}
}
#[cfg(feature = "merge-patch")]
#[diagnostic::on_unimplemented(
message = "queryweave cannot write the column `{Self}`: its SQL type takes no domain value",
label = "a column of a SQL type with no `queryweave_sql::diesel::ColumnValue`",
note = "mark the entity field of a column storing no domain field `#[columns(skip)]`, or implement `ValueSqlType`, `Comparison` and `ColumnValue` for its SQL type"
)]
pub trait AssignColumn: Sized {
const TYPE: ColumnType;
type Assignment;
fn assignment(self, field_value: FieldValue) -> Result<Self::Assignment, ValueMismatch>;
}
#[cfg(feature = "merge-patch")]
impl<C> AssignColumn for C
where
C: Expression + ExpressionMethods,
C::SqlType: SqlType + TypedExpressionType + ColumnValue,
ValueOf<C>: AsExpression<C::SqlType>,
{
const TYPE: ColumnType = <C::SqlType as ColumnValue>::TYPE;
type Assignment = dsl::Eq<C, ValueOf<C>>;
fn assignment(self, field_value: FieldValue) -> Result<Self::Assignment, ValueMismatch> {
assignment(self, field_value)
}
}
#[cfg(feature = "merge-patch")]
pub type Assignment<C> = <C as AssignColumn>::Assignment;
pub trait JoinConditions<'a, QS>: Backend + Sized {
fn join(
conditions: impl IntoIterator<Item = BoxedCondition<'a, QS, Self>>,
junction: Junction,
) -> Option<BoxedCondition<'a, QS, Self>>;
}
impl<'a, QS, DB> JoinConditions<'a, QS> for DB
where
QS: 'a,
DB: Backend + 'a,
dsl::And<BoxedCondition<'a, QS, DB>, BoxedCondition<'a, QS, DB>>:
BoxableExpression<QS, DB, SqlType = Nullable<Bool>>,
dsl::Or<BoxedCondition<'a, QS, DB>, BoxedCondition<'a, QS, DB>>:
BoxableExpression<QS, DB, SqlType = Nullable<Bool>>,
{
fn join(
conditions: impl IntoIterator<Item = BoxedCondition<'a, QS, Self>>,
junction: Junction,
) -> Option<BoxedCondition<'a, QS, Self>> {
join(conditions, junction)
}
}
#[diagnostic::on_unimplemented(
message = "`{Self}` is not a Diesel entity for `{DB}`",
label = "no `queryweave_sql::diesel::Entity` for this entity",
note = "derive `Columns` on an entity with `#[diesel(table_name = ...)]`, every column of a SQL type queryweave converts (or `#[columns(skip)]`)"
)]
pub trait Entity<'a, DB> {
type Table;
fn conditions<M: PersistedFields>(
conditions: Conditions<M>,
junction: Junction,
) -> Result<Option<BoxedCondition<'a, Self::Table, DB>>, ValueMismatch>;
fn order_by<M: PersistedFields>(
sort_keys: SortKeys<M>,
) -> Vec<BoxedOrdering<'a, Self::Table, DB>>;
}
#[diagnostic::on_unimplemented(
message = "`{Self}` is not a boxed query of the entity's table",
label = "not a boxed query",
note = "box the query first, with `.into_boxed()`"
)]
pub trait BoxedQuery<'a, QS, DB>:
FilterDsl<BoxedCondition<'a, QS, DB>, Output = Self>
+ ThenOrderDsl<BoxedOrdering<'a, QS, DB>, Output = Self>
+ LimitDsl<Output = Self>
+ OffsetDsl<Output = Self>
{
}
impl<'a, QS, DB, Q> BoxedQuery<'a, QS, DB> for Q where
Q: FilterDsl<BoxedCondition<'a, QS, DB>, Output = Self>
+ ThenOrderDsl<BoxedOrdering<'a, QS, DB>, Output = Self>
+ LimitDsl<Output = Self>
+ OffsetDsl<Output = Self>
{
}
#[must_use]
pub fn apply<'a, Q, QS, DB>(
query: Q,
condition: Option<BoxedCondition<'a, QS, DB>>,
orderings: Vec<BoxedOrdering<'a, QS, DB>>,
page: Option<(i64, i64)>,
) -> Q
where
Q: BoxedQuery<'a, QS, DB>,
{
let mut query = match condition {
Some(condition) => FilterDsl::filter(query, condition),
None => query,
};
for ordering in orderings {
query = ThenOrderDsl::then_order_by(query, ordering);
}
match page {
Some((limit, offset)) => OffsetDsl::offset(LimitDsl::limit(query, limit), offset),
None => query,
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "merge-patch")]
use super::assignment;
use super::{ValueMismatch, apply, condition, join, ordering, page};
use crate::Junction;
use diesel::mysql::Mysql;
use diesel::pg::Pg;
use diesel::prelude::*;
use diesel::sqlite::Sqlite;
use pretty_assertions::assert_eq;
#[cfg(feature = "merge-patch")]
use queryweave::FieldValue;
use queryweave::{FilterCondition, FilterOperator, SortDirectionModel, SortKey, Value};
use rstest::rstest;
diesel::table! {
orders (id) {
id -> Integer,
number -> Text,
client -> Nullable<Text>,
capacity -> Integer,
value -> Double,
delivered_at -> Timestamp,
}
}
fn filter(field: &'static str, operator: FilterOperator, value: Value) -> FilterCondition {
FilterCondition {
field,
operator,
value,
}
}
#[test]
fn every_backend_renders_the_comparison() {
let less_than = || filter("capacity", FilterOperator::LessThan, Value::I32(Some(9)));
let pg = orders::table
.select(orders::id)
.into_boxed::<Pg>()
.filter(condition(orders::capacity, less_than()).unwrap());
assert_eq!(
diesel::debug_query::<Pg, _>(&pg).to_string(),
r#"SELECT "orders"."id" FROM "orders" WHERE ("orders"."capacity" < $1) -- binds: [Some(9)]"#
);
let mysql = orders::table
.select(orders::id)
.into_boxed::<Mysql>()
.filter(condition(orders::capacity, less_than()).unwrap());
assert_eq!(
diesel::debug_query::<Mysql, _>(&mysql).to_string(),
"SELECT `orders`.`id` FROM `orders` WHERE (`orders`.`capacity` < ?) -- binds: [Some(9)]"
);
let sqlite = orders::table
.select(orders::id)
.into_boxed::<Sqlite>()
.filter(condition(orders::capacity, less_than()).unwrap());
assert_eq!(
diesel::debug_query::<Sqlite, _>(&sqlite).to_string(),
"SELECT `orders`.`id` FROM `orders` WHERE (`orders`.`capacity` < ?) -- binds: [Some(9)]"
);
}
fn pg_where(condition: super::BoxedCondition<'static, orders::table, Pg>) -> String {
let query = orders::table
.select(orders::id)
.into_boxed::<Pg>()
.filter(condition);
diesel::debug_query::<Pg, _>(&query).to_string()
}
#[rstest]
#[case(
FilterOperator::LessThan,
r#""orders"."client" < $1) -- binds: [Some("50%")]"#
)]
#[case(
FilterOperator::Equal,
r#""orders"."client" = $1) -- binds: [Some("50%")]"#
)]
#[case(
FilterOperator::GreaterThan,
r#""orders"."client" > $1) -- binds: [Some("50%")]"#
)]
#[case(
FilterOperator::StartsWith,
r#""orders"."client" LIKE $1 ESCAPE $2) -- binds: ["50!%%", "!"]"#
)]
#[case(
FilterOperator::Contains,
r#""orders"."client" LIKE $1 ESCAPE $2) -- binds: ["%50!%%", "!"]"#
)]
#[case(
FilterOperator::EndsWith,
r#""orders"."client" LIKE $1 ESCAPE $2) -- binds: ["%50!%", "!"]"#
)]
fn the_operator_selects_the_comparison(
#[case] operator: FilterOperator,
#[case] expected: &str,
) {
let filter_condition = filter("client", operator, Value::Text(Some(String::from("50%"))));
assert_eq!(
pg_where(condition(orders::client, filter_condition).unwrap()),
format!(r#"SELECT "orders"."id" FROM "orders" WHERE ({expected}"#)
);
}
#[rstest]
#[case(
condition(
orders::capacity,
filter("capacity", FilterOperator::Equal, Value::I16(Some(3)))
),
r#""orders"."capacity" = $1) -- binds: [Some(3)]"#
)]
#[case(
condition(
orders::value,
filter("value", FilterOperator::GreaterThan, Value::F32(Some(1.5)))
),
r#""orders"."value" > $1) -- binds: [Some(1.5)]"#
)]
#[case(
condition(
orders::number,
filter("number", FilterOperator::Equal, Value::Text(None))
),
r#""orders"."number" = $1) -- binds: [None]"#
)]
fn values_bind_with_the_columns_type(
#[case] condition: Result<super::BoxedCondition<'static, orders::table, Pg>, ValueMismatch>,
#[case] expected: &str,
) {
assert_eq!(
pg_where(condition.unwrap()),
format!(r#"SELECT "orders"."id" FROM "orders" WHERE ({expected}"#)
);
}
#[rstest]
#[case(FilterOperator::Equal, Value::Text(Some(String::from("3"))))]
#[case(FilterOperator::Equal, Value::I64(Some(3)))]
#[case(FilterOperator::Contains, Value::I32(Some(3)))]
fn a_value_that_does_not_fit_is_a_mismatch(
#[case] operator: FilterOperator,
#[case] value: Value,
) {
let result: Result<super::BoxedCondition<'static, orders::table, Pg>, _> =
condition(orders::capacity, filter("amount", operator, value));
assert_eq!(result.err(), Some(ValueMismatch { field: "amount" }));
}
#[rstest]
#[case(Junction::And, "AND")]
#[case(Junction::Or, "OR")]
fn conditions_are_joined_by_the_junction(#[case] junction: Junction, #[case] keyword: &str) {
let conditions = [
filter("capacity", FilterOperator::GreaterThan, Value::I32(Some(1))),
filter("capacity", FilterOperator::LessThan, Value::I32(Some(9))),
]
.map(|filter_condition| condition(orders::capacity, filter_condition).unwrap());
assert_eq!(
pg_where(join(conditions, junction).unwrap()),
format!(
r#"SELECT "orders"."id" FROM "orders" WHERE (("orders"."capacity" > $1) {keyword} ("orders"."capacity" < $2)) -- binds: [Some(1), Some(9)]"#
)
);
assert!(join::<orders::table, Pg>([], junction).is_none());
}
#[test]
fn orderings_and_pages_apply_to_a_boxed_query() {
let (limit, offset) = page(Some(2), Some(10)).unwrap();
let query = orders::table
.select(orders::id)
.into_boxed::<Pg>()
.then_order_by(ordering(
orders::delivered_at,
SortKey {
field: "delivery_date",
direction: SortDirectionModel::Descending,
},
))
.then_order_by(ordering(
orders::number,
SortKey {
field: "number",
direction: SortDirectionModel::Ascending,
},
))
.limit(limit)
.offset(offset);
assert_eq!(
diesel::debug_query::<Pg, _>(&query).to_string(),
r#"SELECT "orders"."id" FROM "orders" ORDER BY "orders"."delivered_at" DESC, "orders"."number" ASC LIMIT $1 OFFSET $2 -- binds: [10, 20]"#
);
assert_eq!(page(None, Some(10)), None);
assert_eq!(page(Some(1), None), None);
}
#[test]
fn apply_adds_the_condition_orderings_and_page() {
let capacity = filter("capacity", FilterOperator::LessThan, Value::I32(Some(9)));
let number = SortKey {
field: "number",
direction: SortDirectionModel::Ascending,
};
let query = orders::table
.select(orders::id)
.into_boxed::<Pg>()
.filter(orders::id.gt(0))
.order_by(orders::id);
let query = apply(
query,
Some(condition(orders::capacity, capacity).unwrap()),
vec![ordering(orders::number, number)],
page(Some(1), Some(10)),
);
assert_eq!(
diesel::debug_query::<Pg, _>(&query).to_string(),
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]"#
);
let untouched = apply(
orders::table.select(orders::id).into_boxed::<Pg>(),
None,
Vec::new(),
None,
);
assert_eq!(
diesel::debug_query::<Pg, _>(&untouched).to_string(),
r#"SELECT "orders"."id" FROM "orders" -- binds: []"#
);
}
#[cfg(feature = "merge-patch")]
#[test]
fn assignments_make_a_changeset() {
let client = FieldValue {
field: "client",
value: Value::Text(None),
};
let capacity = FieldValue {
field: "capacity",
value: Value::I16(Some(4)),
};
let changeset = (
Some(assignment(orders::client, client).unwrap()),
Some(assignment(orders::capacity, capacity).unwrap()),
None::<diesel::dsl::Eq<orders::number, String>>,
);
let update = diesel::update(orders::table).set(changeset);
assert_eq!(
diesel::debug_query::<Pg, _>(&update).to_string(),
r#"UPDATE "orders" SET "client" = $1, "capacity" = $2 -- binds: [None, 4]"#
);
}
#[cfg(feature = "merge-patch")]
#[rstest]
#[case(Value::I32(None))]
#[case(Value::Text(Some(String::from("4"))))]
fn an_assignment_that_does_not_fit_is_a_mismatch(#[case] value: Value) {
let field_value = FieldValue {
field: "amount",
value,
};
assert_eq!(
assignment(orders::capacity, field_value).err(),
Some(ValueMismatch { field: "amount" })
);
}
#[test]
fn the_mismatch_names_the_field() {
assert_eq!(
ValueMismatch { field: "amount" }.to_string(),
"the value of the domain field `amount` does not fit the SQL type of its column"
);
}
fn utc() -> chrono::DateTime<chrono::Utc> {
chrono::DateTime::from_timestamp(1_767_268_800, 0).unwrap()
}
#[test]
fn each_sql_type_takes_its_values() {
use super::ValueSqlType;
use diesel::sql_types::{
BigInt, Bool, Date, Double, Float, Integer, SmallInt, Text, Time, Timestamp,
};
assert_eq!(Bool::rust(Value::Bool(Some(true))), Ok(Some(true)));
assert_eq!(Bool::rust(Value::I32(Some(1))), Err(Value::I32(Some(1))));
assert_eq!(SmallInt::rust(Value::I16(Some(1))), Ok(Some(1)));
assert_eq!(
SmallInt::rust(Value::I32(Some(1))),
Err(Value::I32(Some(1)))
);
assert_eq!(Integer::rust(Value::I32(Some(1))), Ok(Some(1)));
assert_eq!(Integer::rust(Value::I16(Some(2))), Ok(Some(2)));
assert_eq!(Integer::rust(Value::I64(Some(3))), Err(Value::I64(Some(3))));
assert_eq!(BigInt::rust(Value::I64(Some(1))), Ok(Some(1)));
assert_eq!(BigInt::rust(Value::I32(Some(2))), Ok(Some(2)));
assert_eq!(BigInt::rust(Value::I16(Some(3))), Ok(Some(3)));
assert_eq!(Float::rust(Value::F32(Some(1.5))), Ok(Some(1.5)));
assert_eq!(
Float::rust(Value::F64(Some(1.5))),
Err(Value::F64(Some(1.5)))
);
assert_eq!(Double::rust(Value::F64(Some(1.5))), Ok(Some(1.5)));
assert_eq!(Double::rust(Value::F32(Some(2.5))), Ok(Some(2.5)));
assert_eq!(
Text::rust(Value::Text(Some(String::from("a")))),
Ok(Some(String::from("a")))
);
assert_eq!(Text::rust(Value::Text(None)), Ok(None));
assert_eq!(Text::rust(Value::I32(Some(1))), Err(Value::I32(Some(1))));
assert_eq!(
Timestamp::rust(Value::NaiveDateTime(Some(utc().naive_utc()))),
Ok(Some(utc().naive_utc()))
);
assert_eq!(
Timestamp::rust(Value::DateTimeUtc(Some(utc()))),
Ok(Some(utc().naive_utc()))
);
assert_eq!(
Date::rust(Value::NaiveDate(Some(utc().date_naive()))),
Ok(Some(utc().date_naive()))
);
assert_eq!(
Date::rust(Value::DateTimeUtc(Some(utc()))),
Err(Value::DateTimeUtc(Some(utc())))
);
assert_eq!(
Time::rust(Value::NaiveTime(Some(utc().time()))),
Ok(Some(utc().time()))
);
}
#[cfg(feature = "diesel-postgres")]
#[test]
fn postgres_timestamptz_takes_a_utc_date_time() {
use super::ValueSqlType;
use diesel::sql_types::Timestamptz;
assert_eq!(
Timestamptz::rust(Value::DateTimeUtc(Some(utc()))),
Ok(Some(utc()))
);
assert_eq!(
Timestamptz::rust(Value::NaiveDateTime(Some(utc().naive_utc()))),
Err(Value::NaiveDateTime(Some(utc().naive_utc())))
);
}
#[cfg(feature = "diesel-sqlite")]
#[test]
fn sqlite_timestamptz_takes_a_utc_date_time() {
use super::ValueSqlType;
use diesel::sql_types::TimestamptzSqlite;
assert_eq!(
TimestamptzSqlite::rust(Value::DateTimeUtc(Some(utc()))),
Ok(Some(utc()))
);
}
#[cfg(feature = "diesel-mysql")]
#[test]
fn mysql_datetime_takes_a_naive_or_utc_date_time() {
use super::ValueSqlType;
use diesel::sql_types::Datetime;
assert_eq!(
Datetime::rust(Value::NaiveDateTime(Some(utc().naive_utc()))),
Ok(Some(utc().naive_utc()))
);
assert_eq!(
Datetime::rust(Value::DateTimeUtc(Some(utc()))),
Ok(Some(utc().naive_utc()))
);
}
#[test]
fn a_columns_value_follows_its_nullability() {
use super::ColumnValue;
use diesel::sql_types::{Integer, Nullable};
assert_eq!(Integer::from_value(Value::I32(Some(1))), Some(1));
assert_eq!(Integer::from_value(Value::I32(None)), None);
assert_eq!(
Nullable::<Integer>::from_value(Value::I32(None)),
Some(None)
);
assert_eq!(
Nullable::<Integer>::from_value(Value::I32(Some(1))),
Some(Some(1))
);
assert_eq!(Nullable::<Integer>::from_value(Value::Text(None)), None);
}
fn nulls() -> [Value; 11] {
[
Value::Bool(None),
Value::I16(None),
Value::I32(None),
Value::I64(None),
Value::F32(None),
Value::F64(None),
Value::Text(None),
Value::DateTimeUtc(None),
Value::NaiveDateTime(None),
Value::NaiveDate(None),
Value::NaiveTime(None),
]
}
fn assert_accepts<ST: super::ValueSqlType>() {
for value in nulls() {
let kind = value.kind();
assert_eq!(
ST::rust(value).is_ok(),
ST::ACCEPTS.contains(&kind),
"{kind:?} for {}",
std::any::type_name::<ST>()
);
}
}
#[test]
fn each_sql_type_accepts_the_variants_it_converts() {
use diesel::sql_types::{
BigInt, Bool, Date, Double, Float, Integer, SmallInt, Text, Time, Timestamp,
};
assert_accepts::<Bool>();
assert_accepts::<SmallInt>();
assert_accepts::<Integer>();
assert_accepts::<BigInt>();
assert_accepts::<Float>();
assert_accepts::<Double>();
assert_accepts::<Text>();
assert_accepts::<Timestamp>();
assert_accepts::<Date>();
assert_accepts::<Time>();
#[cfg(feature = "diesel-postgres")]
assert_accepts::<diesel::sql_types::Timestamptz>();
#[cfg(feature = "diesel-sqlite")]
assert_accepts::<diesel::sql_types::TimestamptzSqlite>();
#[cfg(feature = "diesel-mysql")]
assert_accepts::<diesel::sql_types::Datetime>();
}
#[test]
fn a_columns_type_follows_its_sql_type() {
use super::{ColumnType, ColumnValue, FilterColumn};
use queryweave::ValueKind;
assert_eq!(
<orders::capacity as FilterColumn<'static, orders::table, Pg>>::ACCEPTS,
&[ValueKind::I32, ValueKind::I16]
);
assert_eq!(
<diesel::sql_types::Integer as ColumnValue>::TYPE,
ColumnType {
accepts: &[ValueKind::I32, ValueKind::I16],
nullable: false,
}
);
assert_eq!(
<diesel::sql_types::Nullable<diesel::sql_types::Text> as ColumnValue>::TYPE,
ColumnType {
accepts: &[ValueKind::Text],
nullable: true,
}
);
}
}