use crate::{
db::predicate::coercion::{CoercionId, CoercionSpec},
value::Value,
};
use std::ops::{BitAnd, BitOr};
use thiserror::Error as ThisError;
#[cfg_attr(doc, doc = "Predicate")]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Predicate {
True,
False,
And(Vec<Self>),
Or(Vec<Self>),
Not(Box<Self>),
Compare(ComparePredicate),
CompareFields(CompareFieldsPredicate),
IsNull { field: String },
IsNotNull { field: String },
IsMissing { field: String },
IsEmpty { field: String },
IsNotEmpty { field: String },
TextContains { field: String, value: Value },
TextContainsCi { field: String, value: Value },
}
impl Predicate {
#[must_use]
pub const fn and(preds: Vec<Self>) -> Self {
Self::And(preds)
}
#[must_use]
pub const fn or(preds: Vec<Self>) -> Self {
Self::Or(preds)
}
#[must_use]
#[expect(clippy::should_implement_trait)]
pub fn not(pred: Self) -> Self {
Self::Not(Box::new(pred))
}
#[must_use]
pub fn eq(field: String, value: Value) -> Self {
Self::Compare(ComparePredicate::eq(field, value))
}
#[must_use]
pub fn ne(field: String, value: Value) -> Self {
Self::Compare(ComparePredicate::ne(field, value))
}
#[must_use]
pub fn lt(field: String, value: Value) -> Self {
Self::Compare(ComparePredicate::lt(field, value))
}
#[must_use]
pub fn lte(field: String, value: Value) -> Self {
Self::Compare(ComparePredicate::lte(field, value))
}
#[must_use]
pub fn gt(field: String, value: Value) -> Self {
Self::Compare(ComparePredicate::gt(field, value))
}
#[must_use]
pub fn gte(field: String, value: Value) -> Self {
Self::Compare(ComparePredicate::gte(field, value))
}
#[must_use]
pub fn eq_fields(left_field: String, right_field: String) -> Self {
Self::CompareFields(CompareFieldsPredicate::eq(left_field, right_field))
}
#[must_use]
pub fn ne_fields(left_field: String, right_field: String) -> Self {
Self::CompareFields(CompareFieldsPredicate::ne(left_field, right_field))
}
#[must_use]
pub fn lt_fields(left_field: String, right_field: String) -> Self {
Self::CompareFields(CompareFieldsPredicate::with_coercion(
left_field,
CompareOp::Lt,
right_field,
CoercionId::NumericWiden,
))
}
#[must_use]
pub fn lte_fields(left_field: String, right_field: String) -> Self {
Self::CompareFields(CompareFieldsPredicate::with_coercion(
left_field,
CompareOp::Lte,
right_field,
CoercionId::NumericWiden,
))
}
#[must_use]
pub fn gt_fields(left_field: String, right_field: String) -> Self {
Self::CompareFields(CompareFieldsPredicate::with_coercion(
left_field,
CompareOp::Gt,
right_field,
CoercionId::NumericWiden,
))
}
#[must_use]
pub fn gte_fields(left_field: String, right_field: String) -> Self {
Self::CompareFields(CompareFieldsPredicate::with_coercion(
left_field,
CompareOp::Gte,
right_field,
CoercionId::NumericWiden,
))
}
#[must_use]
pub fn in_(field: String, values: Vec<Value>) -> Self {
Self::Compare(ComparePredicate::in_(field, values))
}
#[must_use]
pub fn not_in(field: String, values: Vec<Value>) -> Self {
Self::Compare(ComparePredicate::not_in(field, values))
}
#[must_use]
pub const fn is_not_null(field: String) -> Self {
Self::IsNotNull { field }
}
#[must_use]
pub fn between(field: String, lower: Value, upper: Value) -> Self {
Self::And(vec![
Self::gte(field.clone(), lower),
Self::lte(field, upper),
])
}
#[must_use]
pub fn not_between(field: String, lower: Value, upper: Value) -> Self {
Self::Or(vec![Self::lt(field.clone(), lower), Self::gt(field, upper)])
}
}
impl BitAnd for Predicate {
type Output = Self;
fn bitand(self, rhs: Self) -> Self::Output {
Self::And(vec![self, rhs])
}
}
impl BitAnd for &Predicate {
type Output = Predicate;
fn bitand(self, rhs: Self) -> Self::Output {
Predicate::And(vec![self.clone(), rhs.clone()])
}
}
impl BitOr for Predicate {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
Self::Or(vec![self, rhs])
}
}
impl BitOr for &Predicate {
type Output = Predicate;
fn bitor(self, rhs: Self) -> Self::Output {
Predicate::Or(vec![self.clone(), rhs.clone()])
}
}
#[cfg_attr(
doc,
doc = "Neutral predicate model consumed by executor/index layers."
)]
pub(crate) type PredicateExecutionModel = Predicate;
#[cfg_attr(doc, doc = "CompareOp")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u8)]
pub enum CompareOp {
Eq = 0x01,
Ne = 0x02,
Lt = 0x03,
Lte = 0x04,
Gt = 0x05,
Gte = 0x06,
In = 0x07,
NotIn = 0x08,
Contains = 0x09,
StartsWith = 0x0a,
EndsWith = 0x0b,
}
impl CompareOp {
#[must_use]
pub const fn tag(self) -> u8 {
self as u8
}
#[must_use]
pub const fn flipped(self) -> Self {
match self {
Self::Eq => Self::Eq,
Self::Ne => Self::Ne,
Self::Lt => Self::Gt,
Self::Lte => Self::Gte,
Self::Gt => Self::Lt,
Self::Gte => Self::Lte,
Self::In => Self::In,
Self::NotIn => Self::NotIn,
Self::Contains => Self::Contains,
Self::StartsWith => Self::StartsWith,
Self::EndsWith => Self::EndsWith,
}
}
}
#[cfg_attr(doc, doc = "ComparePredicate")]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ComparePredicate {
pub(crate) field: String,
pub(crate) op: CompareOp,
pub(crate) value: Value,
pub(crate) coercion: CoercionSpec,
}
impl ComparePredicate {
fn new(field: String, op: CompareOp, value: Value) -> Self {
Self {
field,
op,
value,
coercion: CoercionSpec::default(),
}
}
#[must_use]
pub fn with_coercion(
field: impl Into<String>,
op: CompareOp,
value: Value,
coercion: CoercionId,
) -> Self {
Self {
field: field.into(),
op,
value,
coercion: CoercionSpec::new(coercion),
}
}
#[must_use]
pub fn eq(field: String, value: Value) -> Self {
Self::new(field, CompareOp::Eq, value)
}
#[must_use]
pub fn ne(field: String, value: Value) -> Self {
Self::new(field, CompareOp::Ne, value)
}
#[must_use]
pub fn lt(field: String, value: Value) -> Self {
Self::new(field, CompareOp::Lt, value)
}
#[must_use]
pub fn lte(field: String, value: Value) -> Self {
Self::new(field, CompareOp::Lte, value)
}
#[must_use]
pub fn gt(field: String, value: Value) -> Self {
Self::new(field, CompareOp::Gt, value)
}
#[must_use]
pub fn gte(field: String, value: Value) -> Self {
Self::new(field, CompareOp::Gte, value)
}
#[must_use]
pub fn in_(field: String, values: Vec<Value>) -> Self {
Self::new(field, CompareOp::In, Value::List(values))
}
#[must_use]
pub fn not_in(field: String, values: Vec<Value>) -> Self {
Self::new(field, CompareOp::NotIn, Value::List(values))
}
#[must_use]
pub fn field(&self) -> &str {
&self.field
}
#[must_use]
pub const fn op(&self) -> CompareOp {
self.op
}
#[must_use]
pub const fn value(&self) -> &Value {
&self.value
}
#[must_use]
pub const fn coercion(&self) -> &CoercionSpec {
&self.coercion
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CompareFieldsPredicate {
pub(crate) left_field: String,
pub(crate) op: CompareOp,
pub(crate) right_field: String,
pub(crate) coercion: CoercionSpec,
}
impl CompareFieldsPredicate {
fn canonicalize_symmetric_fields(
op: CompareOp,
left_field: String,
right_field: String,
) -> (String, String) {
if matches!(op, CompareOp::Eq | CompareOp::Ne) && left_field < right_field {
(right_field, left_field)
} else {
(left_field, right_field)
}
}
fn new(left_field: String, op: CompareOp, right_field: String) -> Self {
let (left_field, right_field) =
Self::canonicalize_symmetric_fields(op, left_field, right_field);
Self {
left_field,
op,
right_field,
coercion: CoercionSpec::default(),
}
}
#[must_use]
pub fn with_coercion(
left_field: impl Into<String>,
op: CompareOp,
right_field: impl Into<String>,
coercion: CoercionId,
) -> Self {
let (left_field, right_field) =
Self::canonicalize_symmetric_fields(op, left_field.into(), right_field.into());
Self {
left_field,
op,
right_field,
coercion: CoercionSpec::new(coercion),
}
}
#[must_use]
pub fn eq(left_field: String, right_field: String) -> Self {
Self::new(left_field, CompareOp::Eq, right_field)
}
#[must_use]
pub fn ne(left_field: String, right_field: String) -> Self {
Self::new(left_field, CompareOp::Ne, right_field)
}
#[must_use]
pub fn lt(left_field: String, right_field: String) -> Self {
Self::new(left_field, CompareOp::Lt, right_field)
}
#[must_use]
pub fn lte(left_field: String, right_field: String) -> Self {
Self::new(left_field, CompareOp::Lte, right_field)
}
#[must_use]
pub fn gt(left_field: String, right_field: String) -> Self {
Self::new(left_field, CompareOp::Gt, right_field)
}
#[must_use]
pub fn gte(left_field: String, right_field: String) -> Self {
Self::new(left_field, CompareOp::Gte, right_field)
}
#[must_use]
pub fn left_field(&self) -> &str {
&self.left_field
}
#[must_use]
pub const fn op(&self) -> CompareOp {
self.op
}
#[must_use]
pub fn right_field(&self) -> &str {
&self.right_field
}
#[must_use]
pub const fn coercion(&self) -> &CoercionSpec {
&self.coercion
}
}
#[cfg_attr(
doc,
doc = "UnsupportedQueryFeature\n\nPolicy-level query features intentionally rejected by the engine."
)]
#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
pub enum UnsupportedQueryFeature {
#[error("map field '{field}' is not queryable; use scalar/indexed fields or list entries")]
MapPredicate { field: String },
}