icydb-core 0.76.6

IcyDB — A type-safe, embedded ORM and schema system for the Internet Computer
Documentation
//! Module: query::builder::field
//! Responsibility: zero-allocation field references and field-scoped predicate builders.
//! Does not own: predicate validation or runtime execution.
//! Boundary: ergonomic query-builder surface for field expressions.

use crate::{
    db::predicate::{CoercionId, CompareOp, ComparePredicate, Predicate},
    traits::FieldValue,
    value::Value,
};
use derive_more::Deref;

///
/// FieldRef
///
/// Zero-cost wrapper around a static field name used in predicates.
/// Enables method-based predicate builders without allocating.
/// Carries only a `&'static str` and derefs to `str`.
///

#[derive(Clone, Copy, Deref, Eq, Hash, PartialEq)]
pub struct FieldRef(&'static str);

impl FieldRef {
    /// Create a new field reference.
    #[must_use]
    pub const fn new(name: &'static str) -> Self {
        Self(name)
    }

    /// Return the underlying field name.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        self.0
    }

    // ------------------------------------------------------------------
    // Internal helpers
    // ------------------------------------------------------------------

    /// Internal comparison predicate builder.
    fn cmp(self, op: CompareOp, value: impl FieldValue, coercion: CoercionId) -> Predicate {
        Predicate::Compare(ComparePredicate::with_coercion(
            self.0,
            op,
            value.to_value(),
            coercion,
        ))
    }

    // ------------------------------------------------------------------
    // Comparison predicates
    // ------------------------------------------------------------------

    /// Strict equality comparison (no coercion).
    #[must_use]
    pub fn eq(self, value: impl FieldValue) -> Predicate {
        self.cmp(CompareOp::Eq, value, CoercionId::Strict)
    }

    /// Case-insensitive equality for text fields.
    #[must_use]
    pub fn text_eq_ci(self, value: impl FieldValue) -> Predicate {
        self.cmp(CompareOp::Eq, value, CoercionId::TextCasefold)
    }

    /// Strict inequality comparison.
    #[must_use]
    pub fn ne(self, value: impl FieldValue) -> Predicate {
        self.cmp(CompareOp::Ne, value, CoercionId::Strict)
    }

    /// Less-than comparison with numeric widening.
    #[must_use]
    pub fn lt(self, value: impl FieldValue) -> Predicate {
        self.cmp(CompareOp::Lt, value, CoercionId::NumericWiden)
    }

    /// Less-than-or-equal comparison with numeric widening.
    #[must_use]
    pub fn lte(self, value: impl FieldValue) -> Predicate {
        self.cmp(CompareOp::Lte, value, CoercionId::NumericWiden)
    }

    /// Greater-than comparison with numeric widening.
    #[must_use]
    pub fn gt(self, value: impl FieldValue) -> Predicate {
        self.cmp(CompareOp::Gt, value, CoercionId::NumericWiden)
    }

    /// Greater-than-or-equal comparison with numeric widening.
    #[must_use]
    pub fn gte(self, value: impl FieldValue) -> Predicate {
        self.cmp(CompareOp::Gte, value, CoercionId::NumericWiden)
    }

    /// Membership test against a fixed list (strict).
    #[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,
        ))
    }

    // ------------------------------------------------------------------
    // Structural predicates
    // ------------------------------------------------------------------

    /// Field is present and explicitly null.
    #[must_use]
    pub fn is_null(self) -> Predicate {
        Predicate::IsNull {
            field: self.0.to_string(),
        }
    }

    /// Field is present and not null.
    #[must_use]
    pub fn is_not_null(self) -> Predicate {
        Predicate::IsNotNull {
            field: self.0.to_string(),
        }
    }

    /// Field is not present at all.
    #[must_use]
    pub fn is_missing(self) -> Predicate {
        Predicate::IsMissing {
            field: self.0.to_string(),
        }
    }

    /// Field is present but empty (collection- or string-specific).
    #[must_use]
    pub fn is_empty(self) -> Predicate {
        Predicate::IsEmpty {
            field: self.0.to_string(),
        }
    }

    /// Field is present and non-empty.
    #[must_use]
    pub fn is_not_empty(self) -> Predicate {
        Predicate::IsNotEmpty {
            field: self.0.to_string(),
        }
    }

    /// Case-sensitive substring match for text fields.
    #[must_use]
    pub fn text_contains(self, value: impl FieldValue) -> Predicate {
        Predicate::TextContains {
            field: self.0.to_string(),
            value: value.to_value(),
        }
    }

    /// Case-insensitive substring match for text fields.
    #[must_use]
    pub fn text_contains_ci(self, value: impl FieldValue) -> Predicate {
        Predicate::TextContainsCi {
            field: self.0.to_string(),
            value: value.to_value(),
        }
    }

    /// Case-sensitive prefix match for text fields.
    #[must_use]
    pub fn text_starts_with(self, value: impl FieldValue) -> Predicate {
        self.cmp(CompareOp::StartsWith, value, CoercionId::Strict)
    }

    /// Case-insensitive prefix match for text fields.
    #[must_use]
    pub fn text_starts_with_ci(self, value: impl FieldValue) -> Predicate {
        self.cmp(CompareOp::StartsWith, value, CoercionId::TextCasefold)
    }

    /// Inclusive range predicate lowered as `field >= lower AND field <= upper`.
    #[must_use]
    pub fn between(self, lower: impl FieldValue, upper: impl FieldValue) -> Predicate {
        Predicate::and(vec![self.gte(lower), self.lte(upper)])
    }
}

// ----------------------------------------------------------------------
// Boundary traits
// ----------------------------------------------------------------------

impl AsRef<str> for FieldRef {
    fn as_ref(&self) -> &str {
        self.0
    }
}

///
/// TESTS
///

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