icydb-core 0.78.0

IcyDB — A type-safe, embedded ORM and schema system for the Internet Computer
Documentation
//! Module: predicate::model
//! Responsibility: public predicate AST and construction helpers.
//! Does not own: schema validation or runtime slot resolution.
//! Boundary: user/query-facing predicate model.

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),
    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 {
    /// Build an `And` predicate from child predicates.
    #[must_use]
    pub const fn and(preds: Vec<Self>) -> Self {
        Self::And(preds)
    }

    /// Build an `Or` predicate from child predicates.
    #[must_use]
    pub const fn or(preds: Vec<Self>) -> Self {
        Self::Or(preds)
    }

    /// Negate one predicate.
    #[must_use]
    #[expect(clippy::should_implement_trait)]
    pub fn not(pred: Self) -> Self {
        Self::Not(Box::new(pred))
    }

    /// Compare `field == value`.
    #[must_use]
    pub fn eq(field: String, value: Value) -> Self {
        Self::Compare(ComparePredicate::eq(field, value))
    }

    /// Compare `field != value`.
    #[must_use]
    pub fn ne(field: String, value: Value) -> Self {
        Self::Compare(ComparePredicate::ne(field, value))
    }

    /// Compare `field < value`.
    #[must_use]
    pub fn lt(field: String, value: Value) -> Self {
        Self::Compare(ComparePredicate::lt(field, value))
    }

    /// Compare `field <= value`.
    #[must_use]
    pub fn lte(field: String, value: Value) -> Self {
        Self::Compare(ComparePredicate::lte(field, value))
    }

    /// Compare `field > value`.
    #[must_use]
    pub fn gt(field: String, value: Value) -> Self {
        Self::Compare(ComparePredicate::gt(field, value))
    }

    /// Compare `field >= value`.
    #[must_use]
    pub fn gte(field: String, value: Value) -> Self {
        Self::Compare(ComparePredicate::gte(field, value))
    }

    /// Compare `field IN values`.
    #[must_use]
    pub fn in_(field: String, values: Vec<Value>) -> Self {
        Self::Compare(ComparePredicate::in_(field, values))
    }

    /// Compare `field NOT IN values`.
    #[must_use]
    pub fn not_in(field: String, values: Vec<Value>) -> Self {
        Self::Compare(ComparePredicate::not_in(field, values))
    }

    /// Compare `field IS NOT NULL`.
    #[must_use]
    pub const fn is_not_null(field: String) -> Self {
        Self::IsNotNull { field }
    }

    /// Compare `field BETWEEN lower AND upper`.
    #[must_use]
    pub fn between(field: String, lower: Value, upper: Value) -> Self {
        Self::And(vec![
            Self::gte(field.clone(), lower),
            Self::lte(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 {
    /// Return the stable wire tag for this compare operator.
    #[must_use]
    pub const fn tag(self) -> u8 {
        self as u8
    }
}

#[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(),
        }
    }

    /// Construct a comparison predicate with an explicit coercion policy.
    #[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),
        }
    }

    /// Build `Eq` comparison.
    #[must_use]
    pub fn eq(field: String, value: Value) -> Self {
        Self::new(field, CompareOp::Eq, value)
    }

    /// Build `Ne` comparison.
    #[must_use]
    pub fn ne(field: String, value: Value) -> Self {
        Self::new(field, CompareOp::Ne, value)
    }

    /// Build `Lt` comparison.
    #[must_use]
    pub fn lt(field: String, value: Value) -> Self {
        Self::new(field, CompareOp::Lt, value)
    }

    /// Build `Lte` comparison.
    #[must_use]
    pub fn lte(field: String, value: Value) -> Self {
        Self::new(field, CompareOp::Lte, value)
    }

    /// Build `Gt` comparison.
    #[must_use]
    pub fn gt(field: String, value: Value) -> Self {
        Self::new(field, CompareOp::Gt, value)
    }

    /// Build `Gte` comparison.
    #[must_use]
    pub fn gte(field: String, value: Value) -> Self {
        Self::new(field, CompareOp::Gte, value)
    }

    /// Build `In` comparison.
    #[must_use]
    pub fn in_(field: String, values: Vec<Value>) -> Self {
        Self::new(field, CompareOp::In, Value::List(values))
    }

    /// Build `NotIn` comparison.
    #[must_use]
    pub fn not_in(field: String, values: Vec<Value>) -> Self {
        Self::new(field, CompareOp::NotIn, Value::List(values))
    }

    /// Borrow the compared field name.
    #[must_use]
    pub fn field(&self) -> &str {
        &self.field
    }

    /// Return the compare operator.
    #[must_use]
    pub const fn op(&self) -> CompareOp {
        self.op
    }

    /// Borrow the compared literal value.
    #[must_use]
    pub const fn value(&self) -> &Value {
        &self.value
    }

    /// Borrow the comparison coercion policy.
    #[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 },
}