use crate::{
db::predicate::{CoercionId, CompareFieldsPredicate, CompareOp, ComparePredicate, Predicate},
traits::FieldValue,
value::Value,
};
use derive_more::Deref;
#[derive(Clone, Copy, Deref, Eq, Hash, PartialEq)]
pub struct FieldRef(&'static str);
impl FieldRef {
#[must_use]
pub const fn new(name: &'static str) -> Self {
Self(name)
}
#[must_use]
pub const fn as_str(self) -> &'static str {
self.0
}
fn cmp(self, op: CompareOp, value: impl FieldValue, coercion: CoercionId) -> Predicate {
Predicate::Compare(ComparePredicate::with_coercion(
self.0,
op,
value.to_value(),
coercion,
))
}
fn cmp_field(self, op: CompareOp, other: impl AsRef<str>, coercion: CoercionId) -> Predicate {
Predicate::CompareFields(CompareFieldsPredicate::with_coercion(
self.0,
op,
other.as_ref(),
coercion,
))
}
#[must_use]
pub fn eq(self, value: impl FieldValue) -> Predicate {
self.cmp(CompareOp::Eq, value, CoercionId::Strict)
}
#[must_use]
pub fn text_eq_ci(self, value: impl FieldValue) -> Predicate {
self.cmp(CompareOp::Eq, value, CoercionId::TextCasefold)
}
#[must_use]
pub fn ne(self, value: impl FieldValue) -> Predicate {
self.cmp(CompareOp::Ne, value, CoercionId::Strict)
}
#[must_use]
pub fn lt(self, value: impl FieldValue) -> Predicate {
self.cmp(CompareOp::Lt, value, CoercionId::NumericWiden)
}
#[must_use]
pub fn lte(self, value: impl FieldValue) -> Predicate {
self.cmp(CompareOp::Lte, value, CoercionId::NumericWiden)
}
#[must_use]
pub fn gt(self, value: impl FieldValue) -> Predicate {
self.cmp(CompareOp::Gt, value, CoercionId::NumericWiden)
}
#[must_use]
pub fn gte(self, value: impl FieldValue) -> Predicate {
self.cmp(CompareOp::Gte, value, CoercionId::NumericWiden)
}
#[must_use]
pub fn eq_field(self, other: impl AsRef<str>) -> Predicate {
self.cmp_field(CompareOp::Eq, other, CoercionId::Strict)
}
#[must_use]
pub fn ne_field(self, other: impl AsRef<str>) -> Predicate {
self.cmp_field(CompareOp::Ne, other, CoercionId::Strict)
}
#[must_use]
pub fn lt_field(self, other: impl AsRef<str>) -> Predicate {
self.cmp_field(CompareOp::Lt, other, CoercionId::NumericWiden)
}
#[must_use]
pub fn lte_field(self, other: impl AsRef<str>) -> Predicate {
self.cmp_field(CompareOp::Lte, other, CoercionId::NumericWiden)
}
#[must_use]
pub fn gt_field(self, other: impl AsRef<str>) -> Predicate {
self.cmp_field(CompareOp::Gt, other, CoercionId::NumericWiden)
}
#[must_use]
pub fn gte_field(self, other: impl AsRef<str>) -> Predicate {
self.cmp_field(CompareOp::Gte, other, CoercionId::NumericWiden)
}
#[must_use]
pub fn in_list<I, V>(self, values: I) -> Predicate
where
I: IntoIterator<Item = V>,
V: FieldValue,
{
Predicate::Compare(ComparePredicate::with_coercion(
self.0,
CompareOp::In,
Value::List(values.into_iter().map(|v| v.to_value()).collect()),
CoercionId::Strict,
))
}
#[must_use]
pub fn is_null(self) -> Predicate {
Predicate::IsNull {
field: self.0.to_string(),
}
}
#[must_use]
pub fn is_not_null(self) -> Predicate {
Predicate::IsNotNull {
field: self.0.to_string(),
}
}
#[must_use]
pub fn is_missing(self) -> Predicate {
Predicate::IsMissing {
field: self.0.to_string(),
}
}
#[must_use]
pub fn is_empty(self) -> Predicate {
Predicate::IsEmpty {
field: self.0.to_string(),
}
}
#[must_use]
pub fn is_not_empty(self) -> Predicate {
Predicate::IsNotEmpty {
field: self.0.to_string(),
}
}
#[must_use]
pub fn text_contains(self, value: impl FieldValue) -> Predicate {
Predicate::TextContains {
field: self.0.to_string(),
value: value.to_value(),
}
}
#[must_use]
pub fn text_contains_ci(self, value: impl FieldValue) -> Predicate {
Predicate::TextContainsCi {
field: self.0.to_string(),
value: value.to_value(),
}
}
#[must_use]
pub fn text_starts_with(self, value: impl FieldValue) -> Predicate {
self.cmp(CompareOp::StartsWith, value, CoercionId::Strict)
}
#[must_use]
pub fn text_starts_with_ci(self, value: impl FieldValue) -> Predicate {
self.cmp(CompareOp::StartsWith, value, CoercionId::TextCasefold)
}
#[must_use]
pub fn between(self, lower: impl FieldValue, upper: impl FieldValue) -> Predicate {
Predicate::and(vec![self.gte(lower), self.lte(upper)])
}
#[must_use]
pub fn between_fields(self, lower: impl AsRef<str>, upper: impl AsRef<str>) -> Predicate {
Predicate::and(vec![self.gte_field(lower), self.lte_field(upper)])
}
#[must_use]
pub fn not_between(self, lower: impl FieldValue, upper: impl FieldValue) -> Predicate {
Predicate::or(vec![self.lt(lower), self.gt(upper)])
}
#[must_use]
pub fn not_between_fields(self, lower: impl AsRef<str>, upper: impl AsRef<str>) -> Predicate {
Predicate::or(vec![self.lt_field(lower), self.gt_field(upper)])
}
}
impl AsRef<str> for FieldRef {
fn as_ref(&self) -> &str {
self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn field_ref_text_starts_with_uses_strict_prefix_compare() {
let predicate = FieldRef::new("name").text_starts_with("Al");
let Predicate::Compare(compare) = predicate else {
panic!("expected compare predicate");
};
assert_eq!(compare.field, "name");
assert_eq!(compare.op, CompareOp::StartsWith);
assert_eq!(compare.coercion.id, CoercionId::Strict);
assert_eq!(compare.value, Value::Text("Al".to_string()));
}
#[test]
fn field_ref_text_starts_with_ci_uses_casefold_prefix_compare() {
let predicate = FieldRef::new("name").text_starts_with_ci("AL");
let Predicate::Compare(compare) = predicate else {
panic!("expected compare predicate");
};
assert_eq!(compare.field, "name");
assert_eq!(compare.op, CompareOp::StartsWith);
assert_eq!(compare.coercion.id, CoercionId::TextCasefold);
assert_eq!(compare.value, Value::Text("AL".to_string()));
}
#[test]
fn field_ref_gt_field_builds_compare_fields_predicate() {
let predicate = FieldRef::new("age").gt_field("rank");
let Predicate::CompareFields(compare) = predicate else {
panic!("expected field-to-field compare predicate");
};
assert_eq!(compare.left_field(), "age");
assert_eq!(compare.op(), CompareOp::Gt);
assert_eq!(compare.right_field(), "rank");
assert_eq!(compare.coercion().id, CoercionId::NumericWiden);
}
#[test]
fn field_ref_not_between_builds_outside_range_predicate() {
let predicate = FieldRef::new("age").not_between(10_u64, 20_u64);
assert_eq!(
predicate,
Predicate::or(vec![
Predicate::Compare(ComparePredicate::with_coercion(
"age",
CompareOp::Lt,
Value::Uint(10),
CoercionId::NumericWiden,
)),
Predicate::Compare(ComparePredicate::with_coercion(
"age",
CompareOp::Gt,
Value::Uint(20),
CoercionId::NumericWiden,
)),
])
);
}
#[test]
fn field_ref_between_fields_builds_field_bound_range_predicate() {
let predicate = FieldRef::new("age").between_fields("min_age", "max_age");
assert_eq!(
predicate,
Predicate::and(vec![
Predicate::CompareFields(CompareFieldsPredicate::with_coercion(
"age",
CompareOp::Gte,
"min_age",
CoercionId::NumericWiden,
)),
Predicate::CompareFields(CompareFieldsPredicate::with_coercion(
"age",
CompareOp::Lte,
"max_age",
CoercionId::NumericWiden,
)),
])
);
}
}