Skip to main content

icydb_core/db/contracts/
predicate_model.rs

1use crate::{
2    model::field::FieldKind,
3    traits::FieldValueKind,
4    value::{CoercionFamily, Value},
5};
6use std::fmt;
7use thiserror::Error as ThisError;
8
9///
10/// CoercionId
11///
12/// Identifier for an explicit comparison coercion policy.
13///
14/// Coercions express *how* values may be compared, not whether a comparison
15/// is valid for a given field. Validation and planning enforce legality.
16///
17#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
18pub enum CoercionId {
19    Strict,
20    NumericWiden,
21    TextCasefold,
22    CollectionElement,
23}
24
25impl CoercionId {
26    /// Stable tag used by plan hash encodings (fingerprint/continuation).
27    #[must_use]
28    pub const fn plan_hash_tag(self) -> u8 {
29        match self {
30            Self::Strict => 0x01,
31            Self::NumericWiden => 0x02,
32            Self::TextCasefold => 0x04,
33            Self::CollectionElement => 0x05,
34        }
35    }
36}
37
38///
39/// UnsupportedQueryFeature
40///
41/// Policy-level query features intentionally rejected by the engine.
42///
43#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
44pub enum UnsupportedQueryFeature {
45    #[error(
46        "map field '{field}' is not queryable; map predicates are disabled. model queryable attributes as scalar/indexed fields or list entries"
47    )]
48    MapPredicate { field: String },
49}
50
51///
52/// ScalarType
53///
54/// Internal scalar classification used by predicate validation.
55/// This is deliberately *smaller* than the full schema/type system
56/// and exists only to support:
57/// - coercion rules
58/// - literal compatibility checks
59/// - operator validity (ordering, equality)
60///
61
62#[derive(Clone, Debug, Eq, PartialEq)]
63pub(crate) enum ScalarType {
64    Account,
65    Blob,
66    Bool,
67    Date,
68    Decimal,
69    Duration,
70    Enum,
71    Float32,
72    Float64,
73    Int,
74    Int128,
75    IntBig,
76    Principal,
77    Subaccount,
78    Text,
79    Timestamp,
80    Uint,
81    Uint128,
82    UintBig,
83    Ulid,
84    Unit,
85}
86
87// Local helpers to expand the scalar registry into match arms.
88macro_rules! scalar_coercion_family_from_registry {
89    ( @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) ),* $(,)? ) => {
90        match $self {
91            $( ScalarType::$scalar => $coercion_family, )*
92        }
93    };
94}
95
96macro_rules! scalar_matches_value_from_registry {
97    ( @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) ),* $(,)? ) => {
98        matches!(
99            ($self, $value),
100            $( (ScalarType::$scalar, $value_pat) )|*
101        )
102    };
103}
104
105macro_rules! scalar_supports_numeric_coercion_from_registry {
106    ( @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) ),* $(,)? ) => {
107        match $self {
108            $( ScalarType::$scalar => $supports_numeric_coercion, )*
109        }
110    };
111}
112
113macro_rules! scalar_is_keyable_from_registry {
114    ( @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) ),* $(,)? ) => {
115        match $self {
116            $( ScalarType::$scalar => $is_keyable, )*
117        }
118    };
119}
120
121macro_rules! scalar_supports_ordering_from_registry {
122    ( @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) ),* $(,)? ) => {
123        match $self {
124            $( ScalarType::$scalar => $supports_ordering, )*
125        }
126    };
127}
128
129impl ScalarType {
130    #[must_use]
131    pub(crate) const fn coercion_family(&self) -> CoercionFamily {
132        scalar_registry!(scalar_coercion_family_from_registry, self)
133    }
134
135    #[must_use]
136    pub(crate) const fn is_orderable(&self) -> bool {
137        // Predicate-level ordering gate.
138        // Delegates to registry-backed supports_ordering.
139        self.supports_ordering()
140    }
141
142    #[must_use]
143    pub(crate) const fn matches_value(&self, value: &Value) -> bool {
144        scalar_registry!(scalar_matches_value_from_registry, self, value)
145    }
146
147    #[must_use]
148    pub(crate) const fn supports_numeric_coercion(&self) -> bool {
149        scalar_registry!(scalar_supports_numeric_coercion_from_registry, self)
150    }
151
152    #[must_use]
153    pub(crate) const fn is_keyable(&self) -> bool {
154        scalar_registry!(scalar_is_keyable_from_registry, self)
155    }
156
157    #[must_use]
158    pub(crate) const fn supports_ordering(&self) -> bool {
159        scalar_registry!(scalar_supports_ordering_from_registry, self)
160    }
161}
162
163///
164/// FieldType
165///
166/// Reduced runtime type representation used exclusively for predicate validation.
167/// This intentionally drops:
168/// - record structure
169/// - tuple structure
170/// - validator/sanitizer metadata
171///
172
173#[derive(Clone, Debug, Eq, PartialEq)]
174pub(crate) enum FieldType {
175    Scalar(ScalarType),
176    List(Box<Self>),
177    Set(Box<Self>),
178    Map { key: Box<Self>, value: Box<Self> },
179    Structured { queryable: bool },
180}
181
182impl FieldType {
183    #[must_use]
184    pub(crate) const fn value_kind(&self) -> FieldValueKind {
185        match self {
186            Self::Scalar(_) => FieldValueKind::Atomic,
187            Self::List(_) | Self::Set(_) => FieldValueKind::Structured { queryable: true },
188            Self::Map { .. } => FieldValueKind::Structured { queryable: false },
189            Self::Structured { queryable } => FieldValueKind::Structured {
190                queryable: *queryable,
191            },
192        }
193    }
194
195    #[must_use]
196    pub(crate) const fn coercion_family(&self) -> Option<CoercionFamily> {
197        match self {
198            Self::Scalar(inner) => Some(inner.coercion_family()),
199            Self::List(_) | Self::Set(_) | Self::Map { .. } => Some(CoercionFamily::Collection),
200            Self::Structured { .. } => None,
201        }
202    }
203
204    #[must_use]
205    pub(crate) const fn is_text(&self) -> bool {
206        matches!(self, Self::Scalar(ScalarType::Text))
207    }
208
209    #[must_use]
210    pub(crate) const fn is_collection(&self) -> bool {
211        matches!(self, Self::List(_) | Self::Set(_) | Self::Map { .. })
212    }
213
214    #[must_use]
215    pub(crate) const fn is_list_like(&self) -> bool {
216        matches!(self, Self::List(_) | Self::Set(_))
217    }
218
219    #[must_use]
220    pub(crate) const fn is_orderable(&self) -> bool {
221        match self {
222            Self::Scalar(inner) => inner.is_orderable(),
223            _ => false,
224        }
225    }
226
227    #[must_use]
228    pub(crate) const fn is_keyable(&self) -> bool {
229        match self {
230            Self::Scalar(inner) => inner.is_keyable(),
231            _ => false,
232        }
233    }
234
235    #[must_use]
236    pub(crate) const fn supports_numeric_coercion(&self) -> bool {
237        match self {
238            Self::Scalar(inner) => inner.supports_numeric_coercion(),
239            _ => false,
240        }
241    }
242}
243
244pub(crate) fn literal_matches_type(literal: &Value, field_type: &FieldType) -> bool {
245    match field_type {
246        FieldType::Scalar(inner) => inner.matches_value(literal),
247        FieldType::List(element) | FieldType::Set(element) => match literal {
248            Value::List(items) => items.iter().all(|item| literal_matches_type(item, element)),
249            _ => false,
250        },
251        FieldType::Map { key, value } => match literal {
252            Value::Map(entries) => {
253                if Value::validate_map_entries(entries.as_slice()).is_err() {
254                    return false;
255                }
256
257                entries.iter().all(|(entry_key, entry_value)| {
258                    literal_matches_type(entry_key, key) && literal_matches_type(entry_value, value)
259                })
260            }
261            _ => false,
262        },
263        FieldType::Structured { .. } => {
264            // NOTE: non-queryable structured field types never match predicate literals.
265            false
266        }
267    }
268}
269
270pub(super) fn field_type_from_model_kind(kind: &FieldKind) -> FieldType {
271    match kind {
272        FieldKind::Account => FieldType::Scalar(ScalarType::Account),
273        FieldKind::Blob => FieldType::Scalar(ScalarType::Blob),
274        FieldKind::Bool => FieldType::Scalar(ScalarType::Bool),
275        FieldKind::Date => FieldType::Scalar(ScalarType::Date),
276        FieldKind::Decimal { .. } => FieldType::Scalar(ScalarType::Decimal),
277        FieldKind::Duration => FieldType::Scalar(ScalarType::Duration),
278        FieldKind::Enum { .. } => FieldType::Scalar(ScalarType::Enum),
279        FieldKind::Float32 => FieldType::Scalar(ScalarType::Float32),
280        FieldKind::Float64 => FieldType::Scalar(ScalarType::Float64),
281        FieldKind::Int => FieldType::Scalar(ScalarType::Int),
282        FieldKind::Int128 => FieldType::Scalar(ScalarType::Int128),
283        FieldKind::IntBig => FieldType::Scalar(ScalarType::IntBig),
284        FieldKind::Principal => FieldType::Scalar(ScalarType::Principal),
285        FieldKind::Subaccount => FieldType::Scalar(ScalarType::Subaccount),
286        FieldKind::Text => FieldType::Scalar(ScalarType::Text),
287        FieldKind::Timestamp => FieldType::Scalar(ScalarType::Timestamp),
288        FieldKind::Uint => FieldType::Scalar(ScalarType::Uint),
289        FieldKind::Uint128 => FieldType::Scalar(ScalarType::Uint128),
290        FieldKind::UintBig => FieldType::Scalar(ScalarType::UintBig),
291        FieldKind::Ulid => FieldType::Scalar(ScalarType::Ulid),
292        FieldKind::Unit => FieldType::Scalar(ScalarType::Unit),
293        FieldKind::Relation { key_kind, .. } => field_type_from_model_kind(key_kind),
294        FieldKind::List(inner) => FieldType::List(Box::new(field_type_from_model_kind(inner))),
295        FieldKind::Set(inner) => FieldType::Set(Box::new(field_type_from_model_kind(inner))),
296        FieldKind::Map { key, value } => FieldType::Map {
297            key: Box::new(field_type_from_model_kind(key)),
298            value: Box::new(field_type_from_model_kind(value)),
299        },
300        FieldKind::Structured { queryable } => FieldType::Structured {
301            queryable: *queryable,
302        },
303    }
304}
305
306impl fmt::Display for FieldType {
307    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308        match self {
309            Self::Scalar(inner) => write!(f, "{inner:?}"),
310            Self::List(inner) => write!(f, "List<{inner}>"),
311            Self::Set(inner) => write!(f, "Set<{inner}>"),
312            Self::Map { key, value } => write!(f, "Map<{key}, {value}>"),
313            Self::Structured { queryable } => {
314                write!(f, "Structured<queryable={queryable}>")
315            }
316        }
317    }
318}