Skip to main content

icydb_core/db/contracts/
predicate_model.rs

1use crate::{
2    model::field::FieldKind,
3    traits::FieldValueKind,
4    value::{CoercionFamily, TextMode, Value},
5};
6use std::{
7    cmp::Ordering,
8    collections::BTreeMap,
9    fmt,
10    mem::discriminant,
11    ops::{BitAnd, BitOr},
12};
13use thiserror::Error as ThisError;
14
15///
16/// CoercionId
17///
18/// Identifier for an explicit comparison coercion policy.
19///
20/// Coercions express *how* values may be compared, not whether a comparison
21/// is valid for a given field. Validation and planning enforce legality.
22///
23#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
24pub enum CoercionId {
25    Strict,
26    NumericWiden,
27    TextCasefold,
28    CollectionElement,
29}
30
31impl CoercionId {
32    /// Stable tag used by plan hash encodings (fingerprint/continuation).
33    #[must_use]
34    pub const fn plan_hash_tag(self) -> u8 {
35        match self {
36            Self::Strict => 0x01,
37            Self::NumericWiden => 0x02,
38            Self::TextCasefold => 0x04,
39            Self::CollectionElement => 0x05,
40        }
41    }
42}
43
44///
45/// UnsupportedQueryFeature
46///
47/// Policy-level query features intentionally rejected by the engine.
48///
49#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
50pub enum UnsupportedQueryFeature {
51    #[error(
52        "map field '{field}' is not queryable; map predicates are disabled. model queryable attributes as scalar/indexed fields or list entries"
53    )]
54    MapPredicate { field: String },
55}
56
57///
58/// ScalarType
59///
60/// Internal scalar classification used by predicate validation.
61/// This is deliberately *smaller* than the full schema/type system
62/// and exists only to support:
63/// - coercion rules
64/// - literal compatibility checks
65/// - operator validity (ordering, equality)
66///
67
68#[derive(Clone, Debug, Eq, PartialEq)]
69pub(crate) enum ScalarType {
70    Account,
71    Blob,
72    Bool,
73    Date,
74    Decimal,
75    Duration,
76    Enum,
77    Float32,
78    Float64,
79    Int,
80    Int128,
81    IntBig,
82    Principal,
83    Subaccount,
84    Text,
85    Timestamp,
86    Uint,
87    Uint128,
88    UintBig,
89    Ulid,
90    Unit,
91}
92
93// Local helpers to expand the scalar registry into match arms.
94macro_rules! scalar_coercion_family_from_registry {
95    ( @args $self:expr; @entries $( ($scalar:ident, $coercion_family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
96        match $self {
97            $( ScalarType::$scalar => $coercion_family, )*
98        }
99    };
100}
101
102macro_rules! scalar_matches_value_from_registry {
103    ( @args $self:expr, $value:expr; @entries $( ($scalar:ident, $coercion_family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
104        matches!(
105            ($self, $value),
106            $( (ScalarType::$scalar, $value_pat) )|*
107        )
108    };
109}
110
111macro_rules! scalar_supports_numeric_coercion_from_registry {
112    ( @args $self:expr; @entries $( ($scalar:ident, $coercion_family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
113        match $self {
114            $( ScalarType::$scalar => $supports_numeric_coercion, )*
115        }
116    };
117}
118
119macro_rules! scalar_is_keyable_from_registry {
120    ( @args $self:expr; @entries $( ($scalar:ident, $coercion_family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
121        match $self {
122            $( ScalarType::$scalar => $is_keyable, )*
123        }
124    };
125}
126
127macro_rules! scalar_supports_ordering_from_registry {
128    ( @args $self:expr; @entries $( ($scalar:ident, $coercion_family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
129        match $self {
130            $( ScalarType::$scalar => $supports_ordering, )*
131        }
132    };
133}
134
135impl ScalarType {
136    #[must_use]
137    pub(crate) const fn coercion_family(&self) -> CoercionFamily {
138        scalar_registry!(scalar_coercion_family_from_registry, self)
139    }
140
141    #[must_use]
142    pub(crate) const fn is_orderable(&self) -> bool {
143        // Predicate-level ordering gate.
144        // Delegates to registry-backed supports_ordering.
145        self.supports_ordering()
146    }
147
148    #[must_use]
149    pub(crate) const fn matches_value(&self, value: &Value) -> bool {
150        scalar_registry!(scalar_matches_value_from_registry, self, value)
151    }
152
153    #[must_use]
154    pub(crate) const fn supports_numeric_coercion(&self) -> bool {
155        scalar_registry!(scalar_supports_numeric_coercion_from_registry, self)
156    }
157
158    #[must_use]
159    pub(crate) const fn is_keyable(&self) -> bool {
160        scalar_registry!(scalar_is_keyable_from_registry, self)
161    }
162
163    #[must_use]
164    pub(crate) const fn supports_ordering(&self) -> bool {
165        scalar_registry!(scalar_supports_ordering_from_registry, self)
166    }
167}
168
169///
170/// FieldType
171///
172/// Reduced runtime type representation used exclusively for predicate validation.
173/// This intentionally drops:
174/// - record structure
175/// - tuple structure
176/// - validator/sanitizer metadata
177///
178
179#[derive(Clone, Debug, Eq, PartialEq)]
180pub(crate) enum FieldType {
181    Scalar(ScalarType),
182    List(Box<Self>),
183    Set(Box<Self>),
184    Map { key: Box<Self>, value: Box<Self> },
185    Structured { queryable: bool },
186}
187
188impl FieldType {
189    #[must_use]
190    pub(crate) const fn value_kind(&self) -> FieldValueKind {
191        match self {
192            Self::Scalar(_) => FieldValueKind::Atomic,
193            Self::List(_) | Self::Set(_) => FieldValueKind::Structured { queryable: true },
194            Self::Map { .. } => FieldValueKind::Structured { queryable: false },
195            Self::Structured { queryable } => FieldValueKind::Structured {
196                queryable: *queryable,
197            },
198        }
199    }
200
201    #[must_use]
202    pub(crate) const fn coercion_family(&self) -> Option<CoercionFamily> {
203        match self {
204            Self::Scalar(inner) => Some(inner.coercion_family()),
205            Self::List(_) | Self::Set(_) | Self::Map { .. } => Some(CoercionFamily::Collection),
206            Self::Structured { .. } => None,
207        }
208    }
209
210    #[must_use]
211    pub(crate) const fn is_text(&self) -> bool {
212        matches!(self, Self::Scalar(ScalarType::Text))
213    }
214
215    #[must_use]
216    pub(crate) const fn is_collection(&self) -> bool {
217        matches!(self, Self::List(_) | Self::Set(_) | Self::Map { .. })
218    }
219
220    #[must_use]
221    pub(crate) const fn is_list_like(&self) -> bool {
222        matches!(self, Self::List(_) | Self::Set(_))
223    }
224
225    #[must_use]
226    pub(crate) const fn is_orderable(&self) -> bool {
227        match self {
228            Self::Scalar(inner) => inner.is_orderable(),
229            _ => false,
230        }
231    }
232
233    #[must_use]
234    pub(crate) const fn is_keyable(&self) -> bool {
235        match self {
236            Self::Scalar(inner) => inner.is_keyable(),
237            _ => false,
238        }
239    }
240
241    #[must_use]
242    pub(crate) const fn supports_numeric_coercion(&self) -> bool {
243        match self {
244            Self::Scalar(inner) => inner.supports_numeric_coercion(),
245            _ => false,
246        }
247    }
248}
249
250pub(crate) fn literal_matches_type(literal: &Value, field_type: &FieldType) -> bool {
251    match field_type {
252        FieldType::Scalar(inner) => inner.matches_value(literal),
253        FieldType::List(element) | FieldType::Set(element) => match literal {
254            Value::List(items) => items.iter().all(|item| literal_matches_type(item, element)),
255            _ => false,
256        },
257        FieldType::Map { key, value } => match literal {
258            Value::Map(entries) => {
259                if Value::validate_map_entries(entries.as_slice()).is_err() {
260                    return false;
261                }
262
263                entries.iter().all(|(entry_key, entry_value)| {
264                    literal_matches_type(entry_key, key) && literal_matches_type(entry_value, value)
265                })
266            }
267            _ => false,
268        },
269        FieldType::Structured { .. } => {
270            // NOTE: non-queryable structured field types never match predicate literals.
271            false
272        }
273    }
274}
275
276pub(super) fn field_type_from_model_kind(kind: &FieldKind) -> FieldType {
277    match kind {
278        FieldKind::Account => FieldType::Scalar(ScalarType::Account),
279        FieldKind::Blob => FieldType::Scalar(ScalarType::Blob),
280        FieldKind::Bool => FieldType::Scalar(ScalarType::Bool),
281        FieldKind::Date => FieldType::Scalar(ScalarType::Date),
282        FieldKind::Decimal { .. } => FieldType::Scalar(ScalarType::Decimal),
283        FieldKind::Duration => FieldType::Scalar(ScalarType::Duration),
284        FieldKind::Enum { .. } => FieldType::Scalar(ScalarType::Enum),
285        FieldKind::Float32 => FieldType::Scalar(ScalarType::Float32),
286        FieldKind::Float64 => FieldType::Scalar(ScalarType::Float64),
287        FieldKind::Int => FieldType::Scalar(ScalarType::Int),
288        FieldKind::Int128 => FieldType::Scalar(ScalarType::Int128),
289        FieldKind::IntBig => FieldType::Scalar(ScalarType::IntBig),
290        FieldKind::Principal => FieldType::Scalar(ScalarType::Principal),
291        FieldKind::Subaccount => FieldType::Scalar(ScalarType::Subaccount),
292        FieldKind::Text => FieldType::Scalar(ScalarType::Text),
293        FieldKind::Timestamp => FieldType::Scalar(ScalarType::Timestamp),
294        FieldKind::Uint => FieldType::Scalar(ScalarType::Uint),
295        FieldKind::Uint128 => FieldType::Scalar(ScalarType::Uint128),
296        FieldKind::UintBig => FieldType::Scalar(ScalarType::UintBig),
297        FieldKind::Ulid => FieldType::Scalar(ScalarType::Ulid),
298        FieldKind::Unit => FieldType::Scalar(ScalarType::Unit),
299        FieldKind::Relation { key_kind, .. } => field_type_from_model_kind(key_kind),
300        FieldKind::List(inner) => FieldType::List(Box::new(field_type_from_model_kind(inner))),
301        FieldKind::Set(inner) => FieldType::Set(Box::new(field_type_from_model_kind(inner))),
302        FieldKind::Map { key, value } => FieldType::Map {
303            key: Box::new(field_type_from_model_kind(key)),
304            value: Box::new(field_type_from_model_kind(value)),
305        },
306        FieldKind::Structured { queryable } => FieldType::Structured {
307            queryable: *queryable,
308        },
309    }
310}
311
312impl fmt::Display for FieldType {
313    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314        match self {
315            Self::Scalar(inner) => write!(f, "{inner:?}"),
316            Self::List(inner) => write!(f, "List<{inner}>"),
317            Self::Set(inner) => write!(f, "Set<{inner}>"),
318            Self::Map { key, value } => write!(f, "Map<{key}, {value}>"),
319            Self::Structured { queryable } => {
320                write!(f, "Structured<queryable={queryable}>")
321            }
322        }
323    }
324}
325
326///
327/// CoercionSpec
328///
329/// Fully-specified coercion policy for predicate comparisons.
330///
331#[derive(Clone, Debug, Eq, PartialEq)]
332pub struct CoercionSpec {
333    pub id: CoercionId,
334    pub params: BTreeMap<String, String>,
335}
336
337impl CoercionSpec {
338    #[must_use]
339    pub const fn new(id: CoercionId) -> Self {
340        Self {
341            id,
342            params: BTreeMap::new(),
343        }
344    }
345}
346
347impl Default for CoercionSpec {
348    fn default() -> Self {
349        Self::new(CoercionId::Strict)
350    }
351}
352
353///
354/// CoercionRuleFamily
355///
356/// Rule-side matcher for coercion routing families.
357///
358#[derive(Clone, Copy, Debug, Eq, PartialEq)]
359pub(crate) enum CoercionRuleFamily {
360    Any,
361    Family(CoercionFamily),
362}
363
364///
365/// CoercionRule
366///
367/// Declarative coercion routing rule between value families.
368///
369#[derive(Clone, Copy, Debug, Eq, PartialEq)]
370pub(crate) struct CoercionRule {
371    pub left: CoercionRuleFamily,
372    pub right: CoercionRuleFamily,
373    pub id: CoercionId,
374}
375
376pub(crate) const COERCION_TABLE: &[CoercionRule] = &[
377    CoercionRule {
378        left: CoercionRuleFamily::Any,
379        right: CoercionRuleFamily::Any,
380        id: CoercionId::Strict,
381    },
382    CoercionRule {
383        left: CoercionRuleFamily::Family(CoercionFamily::Numeric),
384        right: CoercionRuleFamily::Family(CoercionFamily::Numeric),
385        id: CoercionId::NumericWiden,
386    },
387    CoercionRule {
388        left: CoercionRuleFamily::Family(CoercionFamily::Textual),
389        right: CoercionRuleFamily::Family(CoercionFamily::Textual),
390        id: CoercionId::TextCasefold,
391    },
392    CoercionRule {
393        left: CoercionRuleFamily::Any,
394        right: CoercionRuleFamily::Any,
395        id: CoercionId::CollectionElement,
396    },
397];
398
399/// Returns whether a coercion rule exists for the provided routing families.
400#[must_use]
401pub(in crate::db) fn supports_coercion(
402    left: CoercionFamily,
403    right: CoercionFamily,
404    id: CoercionId,
405) -> bool {
406    COERCION_TABLE.iter().any(|rule| {
407        rule.id == id && family_matches(rule.left, left) && family_matches(rule.right, right)
408    })
409}
410
411fn family_matches(rule: CoercionRuleFamily, value: CoercionFamily) -> bool {
412    match rule {
413        CoercionRuleFamily::Any => true,
414        CoercionRuleFamily::Family(expected) => expected == value,
415    }
416}
417
418///
419/// TextOp
420///
421#[derive(Clone, Copy, Debug, Eq, PartialEq)]
422pub(in crate::db) enum TextOp {
423    StartsWith,
424    EndsWith,
425}
426
427/// Perform equality comparison under an explicit coercion policy.
428#[must_use]
429pub(in crate::db) fn compare_eq(
430    left: &Value,
431    right: &Value,
432    coercion: &CoercionSpec,
433) -> Option<bool> {
434    match coercion.id {
435        CoercionId::Strict | CoercionId::CollectionElement => {
436            same_variant(left, right).then_some(left == right)
437        }
438        CoercionId::NumericWiden => {
439            if !left.supports_numeric_coercion() || !right.supports_numeric_coercion() {
440                return None;
441            }
442
443            left.cmp_numeric(right).map(|ord| ord == Ordering::Equal)
444        }
445        CoercionId::TextCasefold => compare_casefold(left, right),
446    }
447}
448
449/// Perform ordering comparison under an explicit coercion policy.
450#[must_use]
451pub(in crate::db) fn compare_order(
452    left: &Value,
453    right: &Value,
454    coercion: &CoercionSpec,
455) -> Option<Ordering> {
456    match coercion.id {
457        CoercionId::Strict | CoercionId::CollectionElement => {
458            if !same_variant(left, right) {
459                return None;
460            }
461            Value::strict_order_cmp(left, right)
462        }
463        CoercionId::NumericWiden => {
464            if !left.supports_numeric_coercion() || !right.supports_numeric_coercion() {
465                return None;
466            }
467
468            left.cmp_numeric(right)
469        }
470        CoercionId::TextCasefold => {
471            let left = casefold_value(left)?;
472            let right = casefold_value(right)?;
473            Some(left.cmp(&right))
474        }
475    }
476}
477
478/// Canonical total ordering for database predicate semantics.
479#[must_use]
480pub(in crate::db) fn canonical_cmp(left: &Value, right: &Value) -> Ordering {
481    if let Some(ordering) = Value::strict_order_cmp(left, right) {
482        return ordering;
483    }
484
485    left.canonical_rank().cmp(&right.canonical_rank())
486}
487
488/// Perform text-specific comparison operations.
489#[must_use]
490pub(in crate::db) fn compare_text(
491    left: &Value,
492    right: &Value,
493    coercion: &CoercionSpec,
494    op: TextOp,
495) -> Option<bool> {
496    if !matches!(left, Value::Text(_)) || !matches!(right, Value::Text(_)) {
497        return None;
498    }
499
500    let mode = match coercion.id {
501        CoercionId::Strict => TextMode::Cs,
502        CoercionId::TextCasefold => TextMode::Ci,
503        _ => return None,
504    };
505
506    match op {
507        TextOp::StartsWith => left.text_starts_with(right, mode),
508        TextOp::EndsWith => left.text_ends_with(right, mode),
509    }
510}
511
512fn same_variant(left: &Value, right: &Value) -> bool {
513    discriminant(left) == discriminant(right)
514}
515
516fn compare_casefold(left: &Value, right: &Value) -> Option<bool> {
517    let left = casefold_value(left)?;
518    let right = casefold_value(right)?;
519    Some(left == right)
520}
521
522fn casefold_value(value: &Value) -> Option<String> {
523    match value {
524        Value::Text(text) => Some(casefold(text)),
525        _ => None,
526    }
527}
528
529fn casefold(input: &str) -> String {
530    if input.is_ascii() {
531        return input.to_ascii_lowercase();
532    }
533
534    input.to_lowercase()
535}
536
537///
538/// CompareOp
539///
540#[derive(Clone, Copy, Debug, Eq, PartialEq)]
541#[repr(u8)]
542pub enum CompareOp {
543    Eq = 0x01,
544    Ne = 0x02,
545    Lt = 0x03,
546    Lte = 0x04,
547    Gt = 0x05,
548    Gte = 0x06,
549    In = 0x07,
550    NotIn = 0x08,
551    Contains = 0x09,
552    StartsWith = 0x0a,
553    EndsWith = 0x0b,
554}
555
556impl CompareOp {
557    #[must_use]
558    pub const fn tag(self) -> u8 {
559        self as u8
560    }
561}
562
563///
564/// ComparePredicate
565///
566#[derive(Clone, Debug, Eq, PartialEq)]
567pub struct ComparePredicate {
568    pub field: String,
569    pub op: CompareOp,
570    pub value: Value,
571    pub coercion: CoercionSpec,
572}
573
574impl ComparePredicate {
575    fn new(field: String, op: CompareOp, value: Value) -> Self {
576        Self {
577            field,
578            op,
579            value,
580            coercion: CoercionSpec::default(),
581        }
582    }
583
584    /// Construct a comparison predicate with an explicit coercion policy.
585    #[must_use]
586    pub fn with_coercion(
587        field: impl Into<String>,
588        op: CompareOp,
589        value: Value,
590        coercion: CoercionId,
591    ) -> Self {
592        Self {
593            field: field.into(),
594            op,
595            value,
596            coercion: CoercionSpec::new(coercion),
597        }
598    }
599
600    #[must_use]
601    pub fn eq(field: String, value: Value) -> Self {
602        Self::new(field, CompareOp::Eq, value)
603    }
604
605    #[must_use]
606    pub fn ne(field: String, value: Value) -> Self {
607        Self::new(field, CompareOp::Ne, value)
608    }
609
610    #[must_use]
611    pub fn lt(field: String, value: Value) -> Self {
612        Self::new(field, CompareOp::Lt, value)
613    }
614
615    #[must_use]
616    pub fn lte(field: String, value: Value) -> Self {
617        Self::new(field, CompareOp::Lte, value)
618    }
619
620    #[must_use]
621    pub fn gt(field: String, value: Value) -> Self {
622        Self::new(field, CompareOp::Gt, value)
623    }
624
625    #[must_use]
626    pub fn gte(field: String, value: Value) -> Self {
627        Self::new(field, CompareOp::Gte, value)
628    }
629
630    #[must_use]
631    pub fn in_(field: String, values: Vec<Value>) -> Self {
632        Self::new(field, CompareOp::In, Value::List(values))
633    }
634
635    #[must_use]
636    pub fn not_in(field: String, values: Vec<Value>) -> Self {
637        Self::new(field, CompareOp::NotIn, Value::List(values))
638    }
639}
640
641///
642/// Predicate
643///
644#[derive(Clone, Debug, Eq, PartialEq)]
645pub enum Predicate {
646    True,
647    False,
648    And(Vec<Self>),
649    Or(Vec<Self>),
650    Not(Box<Self>),
651    Compare(ComparePredicate),
652    IsNull { field: String },
653    IsMissing { field: String },
654    IsEmpty { field: String },
655    IsNotEmpty { field: String },
656    TextContains { field: String, value: Value },
657    TextContainsCi { field: String, value: Value },
658}
659
660impl Predicate {
661    #[must_use]
662    pub const fn and(preds: Vec<Self>) -> Self {
663        Self::And(preds)
664    }
665
666    #[must_use]
667    pub const fn or(preds: Vec<Self>) -> Self {
668        Self::Or(preds)
669    }
670
671    #[must_use]
672    #[expect(clippy::should_implement_trait)]
673    pub fn not(pred: Self) -> Self {
674        Self::Not(Box::new(pred))
675    }
676
677    #[must_use]
678    pub fn eq(field: String, value: Value) -> Self {
679        Self::Compare(ComparePredicate::eq(field, value))
680    }
681
682    #[must_use]
683    pub fn ne(field: String, value: Value) -> Self {
684        Self::Compare(ComparePredicate::ne(field, value))
685    }
686
687    #[must_use]
688    pub fn lt(field: String, value: Value) -> Self {
689        Self::Compare(ComparePredicate::lt(field, value))
690    }
691
692    #[must_use]
693    pub fn lte(field: String, value: Value) -> Self {
694        Self::Compare(ComparePredicate::lte(field, value))
695    }
696
697    #[must_use]
698    pub fn gt(field: String, value: Value) -> Self {
699        Self::Compare(ComparePredicate::gt(field, value))
700    }
701
702    #[must_use]
703    pub fn gte(field: String, value: Value) -> Self {
704        Self::Compare(ComparePredicate::gte(field, value))
705    }
706
707    #[must_use]
708    pub fn in_(field: String, values: Vec<Value>) -> Self {
709        Self::Compare(ComparePredicate::in_(field, values))
710    }
711
712    #[must_use]
713    pub fn not_in(field: String, values: Vec<Value>) -> Self {
714        Self::Compare(ComparePredicate::not_in(field, values))
715    }
716}
717
718impl BitAnd for Predicate {
719    type Output = Self;
720
721    fn bitand(self, rhs: Self) -> Self::Output {
722        Self::And(vec![self, rhs])
723    }
724}
725
726impl BitAnd for &Predicate {
727    type Output = Predicate;
728
729    fn bitand(self, rhs: Self) -> Self::Output {
730        Predicate::And(vec![self.clone(), rhs.clone()])
731    }
732}
733
734impl BitOr for Predicate {
735    type Output = Self;
736
737    fn bitor(self, rhs: Self) -> Self::Output {
738        Self::Or(vec![self, rhs])
739    }
740}
741
742impl BitOr for &Predicate {
743    type Output = Predicate;
744
745    fn bitor(self, rhs: Self) -> Self::Output {
746        Predicate::Or(vec![self.clone(), rhs.clone()])
747    }
748}
749
750/// Neutral predicate model consumed by executor/index layers.
751pub(crate) type PredicateExecutionModel = Predicate;