use crate::model::Model;
use crate::query::condition::{Condition, FilterValue, Leaf, LookupOp};
use crate::query::field::FieldRef;
use crate::query::predicate::PortablePredicate;
use sassi::BasicPredicate;
use std::ops::{BitAnd, BitOr, BitXor, Not};
#[must_use = "Q<T> describes a filter predicate; use it in a QuerySet::filter_struct call or it has no effect"]
#[non_exhaustive]
pub enum Q<T: Model> {
Portable(PortablePredicate<T>),
Ilike(FieldRef<T, String>, String),
JsonbPath(crate::jsonb::path::JsonbPathLeaf),
Regex(FieldRef<T, String>, String, bool),
Expression(crate::expr::Expr<bool>),
Array(ArrayPredicate<T>),
Condition(Condition),
Compound { op: CompoundOp, parts: Vec<Q<T>> },
Xor(Box<Q<T>>, Box<Q<T>>),
Negated(Box<Q<T>>),
}
impl<T: Model> Clone for Q<T> {
fn clone(&self) -> Self {
match self {
Self::Portable(predicate) => Self::Portable(predicate.clone()),
Self::Ilike(field, value) => Self::Ilike(*field, value.clone()),
Self::JsonbPath(leaf) => Self::JsonbPath(leaf.clone()),
Self::Regex(field, value, case_sensitive) => {
Self::Regex(*field, value.clone(), *case_sensitive)
}
Self::Expression(expr) => Self::Expression(expr.clone()),
Self::Array(array) => Self::Array(array.clone()),
Self::Condition(condition) => Self::Condition(condition.clone()),
Self::Compound { op, parts } => Self::Compound {
op: *op,
parts: parts.clone(),
},
Self::Xor(left, right) => {
Self::Xor(Box::new((**left).clone()), Box::new((**right).clone()))
}
Self::Negated(inner) => Self::Negated(Box::new((**inner).clone())),
}
}
}
impl<T: Model> std::fmt::Debug for Q<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Portable(predicate) => f.debug_tuple("Portable").field(predicate).finish(),
Self::Ilike(field, value) => f
.debug_tuple("Ilike")
.field(&field.column())
.field(value)
.finish(),
Self::JsonbPath(leaf) => f.debug_tuple("JsonbPath").field(leaf).finish(),
Self::Regex(field, value, case_sensitive) => f
.debug_tuple("Regex")
.field(&field.column())
.field(value)
.field(case_sensitive)
.finish(),
Self::Expression(_) => f.debug_struct("Expression").finish_non_exhaustive(),
Self::Array(array) => f.debug_tuple("Array").field(array).finish(),
Self::Condition(condition) => f.debug_tuple("Condition").field(condition).finish(),
Self::Compound { op, parts } => f
.debug_struct("Compound")
.field("op", op)
.field("parts", parts)
.finish(),
Self::Xor(left, right) => f.debug_tuple("Xor").field(left).field(right).finish(),
Self::Negated(inner) => f.debug_tuple("Negated").field(inner).finish(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CompoundOp {
And,
Or,
}
#[non_exhaustive]
pub enum ArrayPredicate<T: Model> {
Contains(crate::array::ArrayContainsLeaf, std::marker::PhantomData<T>),
ContainedBy(
crate::array::ArrayContainedByLeaf,
std::marker::PhantomData<T>,
),
Overlap(crate::array::ArrayOverlapLeaf, std::marker::PhantomData<T>),
}
impl<T: Model> Clone for ArrayPredicate<T> {
fn clone(&self) -> Self {
match self {
Self::Contains(leaf, _) => Self::Contains(leaf.clone(), std::marker::PhantomData),
Self::ContainedBy(leaf, _) => Self::ContainedBy(leaf.clone(), std::marker::PhantomData),
Self::Overlap(leaf, _) => Self::Overlap(leaf.clone(), std::marker::PhantomData),
}
}
}
impl<T: Model> std::fmt::Debug for ArrayPredicate<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Contains(leaf, _) => f.debug_tuple("Contains").field(leaf).finish(),
Self::ContainedBy(leaf, _) => f.debug_tuple("ContainedBy").field(leaf).finish(),
Self::Overlap(leaf, _) => f.debug_tuple("Overlap").field(leaf).finish(),
}
}
}
impl<T: Model> From<crate::array::ArrayContainsLeaf> for ArrayPredicate<T> {
fn from(leaf: crate::array::ArrayContainsLeaf) -> Self {
ArrayPredicate::Contains(leaf, std::marker::PhantomData)
}
}
impl<T: Model> From<crate::array::ArrayContainedByLeaf> for ArrayPredicate<T> {
fn from(leaf: crate::array::ArrayContainedByLeaf) -> Self {
ArrayPredicate::ContainedBy(leaf, std::marker::PhantomData)
}
}
impl<T: Model> From<crate::array::ArrayOverlapLeaf> for ArrayPredicate<T> {
fn from(leaf: crate::array::ArrayOverlapLeaf) -> Self {
ArrayPredicate::Overlap(leaf, std::marker::PhantomData)
}
}
impl<T: Model> From<ArrayPredicate<T>> for Q<T> {
fn from(p: ArrayPredicate<T>) -> Self {
Q::Array(p)
}
}
impl<T: Model> From<crate::array::ArrayContainsLeaf> for Q<T> {
fn from(leaf: crate::array::ArrayContainsLeaf) -> Self {
Q::Array(leaf.into())
}
}
impl<T: Model> From<crate::array::ArrayContainedByLeaf> for Q<T> {
fn from(leaf: crate::array::ArrayContainedByLeaf) -> Self {
Q::Array(leaf.into())
}
}
impl<T: Model> From<crate::array::ArrayOverlapLeaf> for Q<T> {
fn from(leaf: crate::array::ArrayOverlapLeaf) -> Self {
Q::Array(leaf.into())
}
}
mod sealed_into_q {
pub trait Sealed {}
}
#[doc(hidden)]
pub use sealed_into_q::Sealed as __SealedIntoQ;
pub trait IntoQ<T: Model>: sealed_into_q::Sealed {
fn into_q(self) -> Q<T>;
}
impl<T: Model> sealed_into_q::Sealed for Q<T> {}
impl<T: Model> IntoQ<T> for Q<T> {
#[inline]
fn into_q(self) -> Q<T> {
self
}
}
impl sealed_into_q::Sealed for Condition {}
impl<T: Model> IntoQ<T> for Condition {
#[inline]
fn into_q(self) -> Q<T> {
Q::Condition(self)
}
}
impl sealed_into_q::Sealed for crate::expr::Expr<bool> {}
impl<T: Model> IntoQ<T> for crate::expr::Expr<bool> {
#[inline]
fn into_q(self) -> Q<T> {
Q::Expression(self)
}
}
impl<T: Model> Q<T> {
pub fn always_true() -> Self {
Q::Portable(PortablePredicate::always_true())
}
pub fn always_false() -> Self {
Q::Portable(PortablePredicate::always_false())
}
}
impl<T: Model> BitAnd for Q<T> {
type Output = Q<T>;
fn bitand(self, rhs: Self) -> Q<T> {
compose_compound::<T, _>(self, rhs, CompoundOp::And, |a, b| a & b)
}
}
impl<T: Model> BitOr for Q<T> {
type Output = Q<T>;
fn bitor(self, rhs: Self) -> Q<T> {
compose_compound::<T, _>(self, rhs, CompoundOp::Or, |a, b| a | b)
}
}
impl<T: Model> BitXor for Q<T> {
type Output = Q<T>;
fn bitxor(self, rhs: Self) -> Q<T> {
match (self, rhs) {
(Q::Portable(a), Q::Portable(b)) => Q::Portable(a ^ b),
(lhs, rhs) => Q::Xor(Box::new(lhs), Box::new(rhs)),
}
}
}
impl<T: Model> Not for Q<T> {
type Output = Q<T>;
fn not(self) -> Q<T> {
match self {
Q::Portable(p) => Q::Portable(!p),
Q::Negated(inner) => *inner,
other => Q::Negated(Box::new(other)),
}
}
}
fn compose_compound<T: Model, F>(lhs: Q<T>, rhs: Q<T>, op: CompoundOp, portable_op: F) -> Q<T>
where
F: FnOnce(PortablePredicate<T>, PortablePredicate<T>) -> PortablePredicate<T>,
{
match (lhs, rhs) {
(Q::Portable(a), Q::Portable(b)) => Q::Portable(portable_op(a, b)),
(
Q::Compound {
op: lop,
parts: mut l,
},
Q::Compound { op: rop, parts: r },
) if lop == op && rop == op => {
l.extend(r);
Q::Compound { op, parts: l }
}
(
Q::Compound {
op: lop,
parts: mut l,
},
other,
) if lop == op => {
l.push(other);
Q::Compound { op, parts: l }
}
(other, Q::Compound { op: rop, parts: r }) if rop == op => {
let mut v = Vec::with_capacity(r.len() + 1);
v.push(other);
v.extend(r);
Q::Compound { op, parts: v }
}
(lhs, rhs) => Q::Compound {
op,
parts: vec![lhs, rhs],
},
}
}
#[allow(dead_code)]
pub(crate) fn q_to_condition<T: Model>(q: Q<T>) -> Condition {
match q {
Q::Portable(p) => basic_predicate_to_condition(p.into_inner()),
Q::Ilike(field, pattern) => Condition::Leaf(Leaf::new(
field.column(),
LookupOp::IContains,
FilterValue::String(pattern),
)),
Q::JsonbPath(leaf) => Condition::JsonbPath(leaf),
Q::Regex(field, pattern, true) => Condition::Leaf(Leaf::new(
field.column(),
LookupOp::Regex,
FilterValue::String(pattern),
)),
Q::Regex(field, pattern, false) => Condition::Leaf(Leaf::new(
field.column(),
LookupOp::IRegex,
FilterValue::String(pattern),
)),
Q::Expression(expr) => Condition::Expr(expr),
Q::Array(ArrayPredicate::Contains(leaf, _)) => Condition::ArrayContains(leaf),
Q::Array(ArrayPredicate::ContainedBy(leaf, _)) => Condition::ArrayContainedBy(leaf),
Q::Array(ArrayPredicate::Overlap(leaf, _)) => Condition::ArrayOverlap(leaf),
Q::Condition(c) => c,
Q::Compound { op, parts } => {
let lowered: Vec<Condition> = parts.into_iter().map(q_to_condition).collect();
match op {
CompoundOp::And => Condition::And(lowered),
CompoundOp::Or => Condition::Or(lowered),
}
}
Q::Xor(a, b) => xor_to_condition(*a, *b),
Q::Negated(inner) => Condition::Not(Box::new(q_to_condition(*inner))),
#[allow(unreachable_patterns)]
_ => panic!(
"djogi internal: unhandled Q<T> variant in q_to_condition — \
update the bridge when a new variant is added."
),
}
}
fn basic_predicate_to_condition<T: Model>(bp: BasicPredicate<T>) -> Condition {
match bp {
BasicPredicate::True => Condition::True,
BasicPredicate::False => Condition::Or(Vec::new()),
BasicPredicate::And(parts) => Condition::And(
parts
.into_iter()
.map(basic_predicate_to_condition)
.collect(),
),
BasicPredicate::Or(parts) => Condition::Or(
parts
.into_iter()
.map(basic_predicate_to_condition)
.collect(),
),
BasicPredicate::Not(inner) => {
Condition::Not(Box::new(basic_predicate_to_condition(*inner)))
}
BasicPredicate::Xor(a, b) => xor_to_condition_basic(*a, *b),
BasicPredicate::Field(_fp) => {
panic!(
"djogi internal: cannot lower BasicPredicate::Field to \
Condition — FieldPredicate is pub(crate) in sassi. No djogi \
FieldRef API constructs this variant; reaching this panic \
means a future cluster must expose the sassi constructor. \
Use the closure-based filter API (QuerySet::filter) or \
Q<T> directly."
)
}
#[allow(unreachable_patterns)]
_ => panic!(
"djogi internal: unhandled BasicPredicate variant in \
basic_predicate_to_condition — update the bridge when sassi \
adds a new variant."
),
}
}
fn xor_to_condition<T: Model>(a: Q<T>, b: Q<T>) -> Condition {
let ca = q_to_condition(a);
let cb = q_to_condition(b);
Condition::Or(vec![
Condition::And(vec![Condition::Not(Box::new(ca.clone())), cb.clone()]),
Condition::And(vec![ca, Condition::Not(Box::new(cb))]),
])
}
fn xor_to_condition_basic<T: Model>(a: BasicPredicate<T>, b: BasicPredicate<T>) -> Condition {
let ca = basic_predicate_to_condition(a);
let cb = basic_predicate_to_condition(b);
Condition::Or(vec![
Condition::And(vec![Condition::Not(Box::new(ca.clone())), cb.clone()]),
Condition::And(vec![ca, Condition::Not(Box::new(cb))]),
])
}
#[allow(dead_code)]
pub(crate) fn q_to_condition_ref<T: Model>(q: &Q<T>) -> Condition {
match q {
Q::Portable(p) => basic_predicate_ref_to_condition(p.inner_ref()),
Q::Ilike(field, pattern) => Condition::Leaf(Leaf::new(
field.column(),
LookupOp::IContains,
FilterValue::String(pattern.clone()),
)),
Q::JsonbPath(leaf) => Condition::JsonbPath(leaf.clone()),
Q::Regex(field, pattern, true) => Condition::Leaf(Leaf::new(
field.column(),
LookupOp::Regex,
FilterValue::String(pattern.clone()),
)),
Q::Regex(field, pattern, false) => Condition::Leaf(Leaf::new(
field.column(),
LookupOp::IRegex,
FilterValue::String(pattern.clone()),
)),
Q::Expression(expr) => Condition::Expr(expr.clone()),
Q::Array(ArrayPredicate::Contains(leaf, _)) => Condition::ArrayContains(leaf.clone()),
Q::Array(ArrayPredicate::ContainedBy(leaf, _)) => Condition::ArrayContainedBy(leaf.clone()),
Q::Array(ArrayPredicate::Overlap(leaf, _)) => Condition::ArrayOverlap(leaf.clone()),
Q::Condition(c) => c.clone(),
Q::Compound { op, parts } => {
let lowered: Vec<Condition> = parts.iter().map(q_to_condition_ref).collect();
match op {
CompoundOp::And => Condition::And(lowered),
CompoundOp::Or => Condition::Or(lowered),
}
}
Q::Xor(a, b) => xor_to_condition_ref(a, b),
Q::Negated(inner) => Condition::Not(Box::new(q_to_condition_ref(inner))),
#[allow(unreachable_patterns)]
_ => panic!(
"djogi internal: unhandled Q<T> variant in q_to_condition_ref — \
update the bridge when a new variant is added."
),
}
}
fn basic_predicate_ref_to_condition<T: Model>(bp: &BasicPredicate<T>) -> Condition {
match bp {
BasicPredicate::True => Condition::True,
BasicPredicate::False => Condition::Or(Vec::new()),
BasicPredicate::And(parts) => {
Condition::And(parts.iter().map(basic_predicate_ref_to_condition).collect())
}
BasicPredicate::Or(parts) => {
Condition::Or(parts.iter().map(basic_predicate_ref_to_condition).collect())
}
BasicPredicate::Not(inner) => {
Condition::Not(Box::new(basic_predicate_ref_to_condition(inner)))
}
BasicPredicate::Xor(a, b) => xor_basic_ref_to_condition(a, b),
BasicPredicate::Field(_fp) => {
panic!(
"djogi internal: cannot lower BasicPredicate::Field to \
Condition (by-ref path) — FieldPredicate is pub(crate) in \
sassi. Use the closure-based filter API or Q<T> directly."
)
}
#[allow(unreachable_patterns)]
_ => panic!(
"djogi internal: unhandled BasicPredicate variant in \
basic_predicate_ref_to_condition — update the bridge when \
sassi adds a new variant."
),
}
}
fn xor_to_condition_ref<T: Model>(a: &Q<T>, b: &Q<T>) -> Condition {
let ca = q_to_condition_ref(a);
let cb = q_to_condition_ref(b);
Condition::Or(vec![
Condition::And(vec![Condition::Not(Box::new(ca.clone())), cb.clone()]),
Condition::And(vec![ca, Condition::Not(Box::new(cb))]),
])
}
fn xor_basic_ref_to_condition<T: Model>(a: &BasicPredicate<T>, b: &BasicPredicate<T>) -> Condition {
let ca = basic_predicate_ref_to_condition(a);
let cb = basic_predicate_ref_to_condition(b);
Condition::Or(vec![
Condition::And(vec![Condition::Not(Box::new(ca.clone())), cb.clone()]),
Condition::And(vec![ca, Condition::Not(Box::new(cb))]),
])
}
#[cfg(test)]
#[allow(clippy::manual_async_fn)]
mod tests {
use super::*;
use crate::DjogiError;
use crate::descriptor::ModelDescriptor;
#[derive(Clone, Debug)]
struct TestModel;
impl crate::model::__sealed::Sealed for TestModel {}
impl Model for TestModel {
type Pk = crate::types::HeerId;
type Fields = ();
fn table_name() -> &'static str {
"test_models"
}
fn pk_value(&self) -> &Self::Pk {
unreachable!("algebra tests do not invoke pk_value")
}
fn descriptor() -> &'static ModelDescriptor {
unreachable!("algebra tests do not invoke descriptor")
}
fn get(
_ctx: &mut crate::context::DjogiContext,
_id: Self::Pk,
) -> impl std::future::Future<Output = Result<Self, DjogiError>> + Send {
async { unreachable!() }
}
fn create(
_ctx: &mut crate::context::DjogiContext,
_v: Self,
) -> impl std::future::Future<Output = Result<Self, DjogiError>> + Send {
async { unreachable!() }
}
fn save<'ctx>(
&'ctx mut self,
_ctx: &'ctx mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<(), DjogiError>> + Send + 'ctx {
async { unreachable!() }
}
fn delete(
self,
_ctx: &mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<(), DjogiError>> + Send {
async { unreachable!() }
}
fn refresh_from_db<'ctx>(
&'ctx self,
_ctx: &'ctx mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<Self, DjogiError>> + Send + 'ctx {
async { unreachable!() }
}
}
fn portable_true<T: Model>() -> Q<T> {
Q::Portable(PortablePredicate::<T>::always_true())
}
fn portable_false<T: Model>() -> Q<T> {
Q::Portable(PortablePredicate::<T>::always_false())
}
#[test]
fn q_skeleton_constructs_portable_variant() {
let q: Q<TestModel> = portable_true();
assert!(matches!(q, Q::Portable(_)));
}
#[test]
fn q_skeleton_carries_portable_false() {
let q: Q<TestModel> = portable_false();
assert!(matches!(q, Q::Portable(_)));
}
#[test]
fn q_skeleton_is_clone_and_debug() {
let q: Q<TestModel> = portable_true();
let _ = format!("{:?}", q.clone());
}
#[test]
fn q_always_true_is_portable_true() {
let q: Q<TestModel> = Q::always_true();
assert!(matches!(q, Q::Portable(_)));
}
#[test]
fn q_always_false_is_portable_false() {
let q: Q<TestModel> = Q::always_false();
assert!(matches!(q, Q::Portable(_)));
}
#[test]
fn q_portable_and_portable_flattens_through_sassi() {
let a: Q<TestModel> = portable_true();
let b: Q<TestModel> = portable_false();
match a & b {
Q::Portable(p) => match p.into_inner() {
BasicPredicate::And(parts) => {
assert_eq!(parts.len(), 2);
assert!(matches!(parts[0], BasicPredicate::True));
assert!(matches!(parts[1], BasicPredicate::False));
}
other => panic!("expected BasicPredicate::And(_), got {other:?}"),
},
other => panic!("expected Q::Portable(_), got {other:?}"),
}
}
#[test]
fn q_portable_or_portable_flattens_through_sassi() {
let a: Q<TestModel> = portable_true();
let b: Q<TestModel> = portable_false();
match a | b {
Q::Portable(p) => match p.into_inner() {
BasicPredicate::Or(_) => {}
other => panic!("expected BasicPredicate::Or(_), got {other:?}"),
},
other => panic!("expected Q::Portable(_), got {other:?}"),
}
}
#[test]
fn q_portable_xor_portable_uses_sassi_xor() {
let a: Q<TestModel> = portable_true();
let b: Q<TestModel> = portable_false();
match a ^ b {
Q::Portable(p) => match p.into_inner() {
BasicPredicate::Xor(_, _) => {}
other => panic!("expected BasicPredicate::Xor(_, _), got {other:?}"),
},
other => panic!("expected Q::Portable(_), got {other:?}"),
}
}
#[test]
fn q_portable_double_negation_collapses_via_sassi() {
let p: Q<TestModel> = portable_true();
let result = !!p;
match result {
Q::Portable(p) => match p.into_inner() {
BasicPredicate::True => {}
other => panic!("expected BasicPredicate::True, got {other:?}"),
},
other => panic!("expected Q::Portable(_) after !!, got {other:?}"),
}
}
#[test]
fn q_mixed_and_creates_compound_node() {
let a: Q<TestModel> = portable_true();
let b: Q<TestModel> = Q::Negated(Box::new(portable_false()));
match a & b {
Q::Compound {
op: CompoundOp::And,
parts,
} => {
assert_eq!(parts.len(), 2);
assert!(matches!(parts[0], Q::Portable(_)));
assert!(matches!(parts[1], Q::Negated(_)));
}
other => panic!("expected Q::Compound{{op: And, ..}}, got {other:?}"),
}
}
#[test]
fn q_chained_compound_and_flattens() {
let neg = || Q::<TestModel>::Negated(Box::new(portable_true()));
let combined = neg() & neg() & neg();
match combined {
Q::Compound {
op: CompoundOp::And,
parts,
} => assert_eq!(parts.len(), 3, "expected flat 3-element parts"),
other => panic!("expected Q::Compound{{op: And, ..}}, got {other:?}"),
}
}
#[test]
fn q_or_with_two_compounds_flattens() {
let neg = || Q::<TestModel>::Negated(Box::new(portable_true()));
let lhs = neg() | neg();
let rhs = neg() | neg();
match lhs | rhs {
Q::Compound {
op: CompoundOp::Or,
parts,
} => assert_eq!(parts.len(), 4),
other => panic!("expected Q::Compound{{op: Or, ..}}, got {other:?}"),
}
}
#[test]
fn q_xor_mixed_lands_in_q_xor_variant() {
let basic: Q<TestModel> = portable_true();
let neg: Q<TestModel> = Q::Negated(Box::new(portable_false()));
match basic ^ neg {
Q::Xor(lhs, rhs) => {
assert!(matches!(*lhs, Q::Portable(_)));
assert!(matches!(*rhs, Q::Negated(_)));
}
other => panic!("expected Q::Xor(_, _), got {other:?}"),
}
}
#[test]
fn q_not_negated_negated_collapses() {
let inner: Q<TestModel> = Q::Compound {
op: CompoundOp::And,
parts: vec![
Q::Negated(Box::new(portable_true())),
Q::Negated(Box::new(portable_false())),
],
};
let result = !!inner.clone();
assert!(matches!(result, Q::Compound { .. }));
}
#[test]
fn q_operator_precedence_xor_binds_tighter_than_or() {
let a: Q<TestModel> = portable_true();
let b: Q<TestModel> = Q::Negated(Box::new(portable_false()));
let c: Q<TestModel> = Q::Negated(Box::new(portable_true()));
let composed = a ^ b | c;
match composed {
Q::Compound {
op: CompoundOp::Or,
parts,
} => {
assert_eq!(parts.len(), 2);
assert!(matches!(parts[0], Q::Xor(_, _)));
assert!(matches!(parts[1], Q::Negated(_)));
}
other => panic!("expected outer Q::Compound{{op: Or}}, got {other:?}"),
}
}
#[test]
fn q_array_contains_lifts_via_into() {
use crate::array::ArrayContainsLeaf;
use crate::query::condition::FilterValue;
let leaf = ArrayContainsLeaf {
column: "tags",
values: FilterValue::ArrayString(vec!["a".to_string()]),
};
let q: Q<TestModel> = leaf.into();
match q {
Q::Array(ArrayPredicate::Contains(inner, _)) => {
assert_eq!(inner.column, "tags");
}
other => panic!("expected Q::Array(ArrayPredicate::Contains), got {other:?}"),
}
}
#[test]
fn q_array_contained_by_lifts_via_into() {
use crate::array::ArrayContainedByLeaf;
use crate::query::condition::FilterValue;
let leaf = ArrayContainedByLeaf {
column: "tags",
values: FilterValue::ArrayI32(vec![1, 2, 3]),
};
let q: Q<TestModel> = leaf.into();
assert!(matches!(q, Q::Array(ArrayPredicate::ContainedBy(_, _))));
}
#[test]
fn q_array_overlap_lifts_via_into() {
use crate::array::ArrayOverlapLeaf;
use crate::query::condition::FilterValue;
let leaf = ArrayOverlapLeaf {
column: "tags",
values: FilterValue::ArrayBool(vec![true]),
};
let q: Q<TestModel> = leaf.into();
assert!(matches!(q, Q::Array(ArrayPredicate::Overlap(_, _))));
}
#[test]
fn q_array_three_variants_exhaust() {
use crate::array::{ArrayContainedByLeaf, ArrayContainsLeaf, ArrayOverlapLeaf};
use crate::query::condition::FilterValue;
let leaves: [ArrayPredicate<TestModel>; 3] = [
ArrayContainsLeaf {
column: "tags",
values: FilterValue::ArrayI32(vec![1]),
}
.into(),
ArrayContainedByLeaf {
column: "tags",
values: FilterValue::ArrayI32(vec![1]),
}
.into(),
ArrayOverlapLeaf {
column: "tags",
values: FilterValue::ArrayI32(vec![1]),
}
.into(),
];
for p in leaves {
match p {
ArrayPredicate::Contains(_, _) => {}
ArrayPredicate::ContainedBy(_, _) => {}
ArrayPredicate::Overlap(_, _) => {}
}
}
}
#[test]
fn q_operator_precedence_and_binds_tighter_than_xor() {
let a: Q<TestModel> = portable_true();
let b: Q<TestModel> = Q::Negated(Box::new(portable_false()));
let c: Q<TestModel> = Q::Negated(Box::new(portable_true()));
let composed = a & b ^ c;
match composed {
Q::Xor(lhs, rhs) => {
assert!(matches!(
*lhs,
Q::Compound {
op: CompoundOp::And,
..
}
));
assert!(matches!(*rhs, Q::Negated(_)));
}
other => panic!("expected outer Q::Xor, got {other:?}"),
}
}
#[test]
fn operator_precedence_a_and_b_xor_c_or_d() {
let a: Q<TestModel> = Q::always_true();
let b: Q<TestModel> = Q::always_false();
let c: Q<TestModel> = Q::always_true();
let d: Q<TestModel> = Q::always_false();
let result = a & b ^ c | d;
match result {
Q::Portable(p) => match p.into_inner() {
BasicPredicate::Or(or_parts) => {
assert_eq!(or_parts.len(), 2, "outer Or must have 2 branches");
match &or_parts[0] {
BasicPredicate::Xor(lhs, rhs_c) => {
assert!(
matches!(rhs_c.as_ref(), BasicPredicate::True),
"XOR rhs must be c = True, got {rhs_c:?}"
);
match lhs.as_ref() {
BasicPredicate::And(and_parts) => {
assert_eq!(and_parts.len(), 2, "And must have 2 parts (a, b)");
assert!(
matches!(and_parts[0], BasicPredicate::True),
"And[0] must be a = True"
);
assert!(
matches!(and_parts[1], BasicPredicate::False),
"And[1] must be b = False"
);
}
other => {
panic!("expected And([True, False]) as XOR lhs, got {other:?}")
}
}
}
other => panic!("expected Xor as left Or branch, got {other:?}"),
}
assert!(
matches!(or_parts[1], BasicPredicate::False),
"right Or branch must be d = False"
);
}
other => panic!("expected BasicPredicate::Or([...]), got {other:?}"),
},
other => panic!("expected Q::Portable(_), got {other:?}"),
}
}
#[test]
fn eight_term_composition_flattens_same_op() {
let a: Q<TestModel> = Q::always_true();
let b: Q<TestModel> = Q::always_false();
let c: Q<TestModel> = Q::always_true();
let d: Q<TestModel> = Q::always_false();
let e: Q<TestModel> = Q::always_true();
let f: Q<TestModel> = Q::always_false();
let g: Q<TestModel> = Q::always_true();
let h: Q<TestModel> = Q::always_false();
let result = a & b & c & d | e | f ^ g ^ h;
match result {
Q::Portable(p) => match p.into_inner() {
BasicPredicate::Or(or_parts) => {
assert!(
or_parts.len() >= 2,
"outer Or must have ≥ 2 parts; got {}",
or_parts.len()
);
let has_4_way_and = or_parts
.iter()
.any(|p| matches!(p, BasicPredicate::And(ap) if ap.len() == 4));
assert!(
has_4_way_and,
"expected a 4-part And in Or parts (a&b&c&d flattened); parts: {or_parts:?}"
);
}
other => panic!("expected BasicPredicate::Or([...]), got {other:?}"),
},
other => panic!("expected Q::Portable(_), got {other:?}"),
}
}
#[test]
fn q_portable_true_lowers_to_condition_true() {
let q: Q<TestModel> = portable_true();
assert!(matches!(q_to_condition(q), Condition::True));
}
#[test]
fn q_portable_false_lowers_to_empty_or() {
let q: Q<TestModel> = portable_false();
match q_to_condition(q) {
Condition::Or(v) => assert!(v.is_empty(), "expected empty Or, got {v:?}"),
other => panic!("expected Condition::Or(empty), got {other:?}"),
}
}
#[test]
fn q_compound_and_lowers_preserves_order() {
let a =
Q::<TestModel>::Condition(Condition::Leaf(Leaf::eq_raw("a", FilterValue::Bool(true))));
let b =
Q::<TestModel>::Condition(Condition::Leaf(Leaf::eq_raw("b", FilterValue::Bool(false))));
let c =
Q::<TestModel>::Condition(Condition::Leaf(Leaf::eq_raw("c", FilterValue::Bool(true))));
let q = Q::Compound {
op: CompoundOp::And,
parts: vec![a, b, c],
};
match q_to_condition(q) {
Condition::And(parts) => {
assert_eq!(parts.len(), 3);
let names: Vec<&'static str> = parts
.iter()
.map(|p| match p {
Condition::Leaf(l) => l.column(),
_ => panic!("expected leaf, got {p:?}"),
})
.collect();
assert_eq!(names, vec!["a", "b", "c"]);
}
other => panic!("expected Condition::And, got {other:?}"),
}
}
#[test]
fn q_compound_or_lowers_to_or() {
let q = Q::<TestModel>::Compound {
op: CompoundOp::Or,
parts: vec![portable_true(), portable_false()],
};
match q_to_condition(q) {
Condition::Or(parts) => assert_eq!(parts.len(), 2),
other => panic!("expected Condition::Or, got {other:?}"),
}
}
#[test]
fn q_negated_lowers_to_condition_not() {
let q = Q::<TestModel>::Negated(Box::new(portable_true()));
match q_to_condition(q) {
Condition::Not(inner) => assert!(matches!(*inner, Condition::True)),
other => panic!("expected Condition::Not, got {other:?}"),
}
}
#[test]
fn q_xor_lowers_to_general_form() {
let a = portable_true::<TestModel>();
let b = portable_false::<TestModel>();
let q = Q::Xor(Box::new(a), Box::new(b));
match q_to_condition(q) {
Condition::Or(or_parts) => {
assert_eq!(or_parts.len(), 2, "outer Or must have 2 branches");
match &or_parts[0] {
Condition::And(and_parts) => {
assert_eq!(and_parts.len(), 2);
assert!(matches!(and_parts[0], Condition::Not(_)));
}
other => panic!("expected And as first Or branch, got {other:?}"),
}
match &or_parts[1] {
Condition::And(and_parts) => {
assert_eq!(and_parts.len(), 2);
assert!(matches!(and_parts[1], Condition::Not(_)));
}
other => panic!("expected And as second Or branch, got {other:?}"),
}
}
other => panic!("expected Condition::Or for XOR general form, got {other:?}"),
}
}
#[test]
fn q_regex_case_sensitive_lowers_to_lookup_op_regex() {
use crate::query::field::__macro_support::__make_field_ref;
let field: FieldRef<TestModel, String> = __make_field_ref(None, "slug");
let q: Q<TestModel> = Q::Regex(field, "^foo".to_string(), true);
match q_to_condition(q) {
Condition::Leaf(leaf) => {
assert_eq!(leaf.op(), LookupOp::Regex);
assert_eq!(leaf.column(), "slug");
assert!(matches!(leaf.value(), FilterValue::String(s) if s == "^foo"));
}
other => panic!("expected Condition::Leaf with LookupOp::Regex, got {other:?}"),
}
}
#[test]
fn q_regex_case_insensitive_lowers_to_lookup_op_iregex() {
use crate::query::field::__macro_support::__make_field_ref;
let field: FieldRef<TestModel, String> = __make_field_ref(None, "slug");
let q: Q<TestModel> = Q::Regex(field, "^foo".to_string(), false);
match q_to_condition(q) {
Condition::Leaf(leaf) => {
assert_eq!(leaf.op(), LookupOp::IRegex);
}
other => panic!("expected Condition::Leaf with LookupOp::IRegex, got {other:?}"),
}
}
#[test]
fn q_ilike_lowers_to_lookup_op_icontains() {
use crate::query::field::__macro_support::__make_field_ref;
let field: FieldRef<TestModel, String> = __make_field_ref(None, "title");
let q: Q<TestModel> = Q::Ilike(field, "rust%".to_string());
match q_to_condition(q) {
Condition::Leaf(leaf) => {
assert_eq!(leaf.op(), LookupOp::IContains);
assert_eq!(leaf.column(), "title");
}
other => panic!("expected Condition::Leaf with LookupOp::IContains, got {other:?}"),
}
}
#[test]
fn q_condition_round_trips_as_identity() {
let original = Condition::Leaf(Leaf::eq_raw("status", FilterValue::Bool(true)));
let q: Q<TestModel> = Q::Condition(original.clone());
match q_to_condition(q) {
Condition::Leaf(leaf) => {
assert_eq!(leaf.column(), "status");
assert_eq!(leaf.op(), LookupOp::Eq);
}
other => panic!("expected identity round-trip, got {other:?}"),
}
}
#[test]
fn q_array_contains_lowers_to_condition_array_contains() {
use crate::array::ArrayContainsLeaf;
let leaf = ArrayContainsLeaf {
column: "tags",
values: FilterValue::ArrayString(vec!["a".to_string()]),
};
let q: Q<TestModel> = leaf.into();
match q_to_condition(q) {
Condition::ArrayContains(l) => assert_eq!(l.column, "tags"),
other => panic!("expected Condition::ArrayContains, got {other:?}"),
}
}
#[test]
fn q_array_contained_by_lowers_to_condition_array_contained_by() {
use crate::array::ArrayContainedByLeaf;
let leaf = ArrayContainedByLeaf {
column: "tags",
values: FilterValue::ArrayI32(vec![1, 2]),
};
let q: Q<TestModel> = leaf.into();
assert!(matches!(q_to_condition(q), Condition::ArrayContainedBy(_)));
}
#[test]
fn q_array_overlap_lowers_to_condition_array_overlap() {
use crate::array::ArrayOverlapLeaf;
let leaf = ArrayOverlapLeaf {
column: "tags",
values: FilterValue::ArrayBool(vec![true]),
};
let q: Q<TestModel> = leaf.into();
assert!(matches!(q_to_condition(q), Condition::ArrayOverlap(_)));
}
#[test]
fn q_portable_and_or_not_round_trip() {
let composed: PortablePredicate<TestModel> =
PortablePredicate::always_true() & PortablePredicate::always_false();
let q: Q<TestModel> = Q::Portable(composed);
match q_to_condition(q) {
Condition::And(parts) => assert_eq!(parts.len(), 2),
other => panic!("expected Condition::And, got {other:?}"),
}
let negated: PortablePredicate<TestModel> = !PortablePredicate::always_true();
let q: Q<TestModel> = Q::Portable(negated);
match q_to_condition(q) {
Condition::Or(parts) => assert!(parts.is_empty()),
other => panic!("expected Condition::Or(empty), got {other:?}"),
}
}
}