Skip to main content

icydb_core/model/
field.rs

1//! Module: model::field
2//! Responsibility: module-local ownership and contracts for model::field.
3//! Does not own: cross-module orchestration outside this module.
4//! Boundary: exposes this module API while keeping implementation details internal.
5
6use crate::{traits::FieldValueKind, types::EntityTag};
7
8///
9/// FieldStorageDecode
10///
11/// FieldStorageDecode captures how one persisted field payload must be
12/// interpreted at structural decode boundaries.
13/// Semantic `FieldKind` alone is not always authoritative for persisted decode:
14/// some fields intentionally store raw `Value` payloads even when their planner
15/// shape is narrower.
16///
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum FieldStorageDecode {
20    /// Decode the persisted field payload according to semantic `FieldKind`.
21    ByKind,
22    /// Decode the persisted field payload directly into `Value`.
23    Value,
24}
25
26///
27/// ScalarCodec
28///
29/// ScalarCodec identifies the canonical binary leaf encoding used for one
30/// scalar persisted field payload.
31/// These codecs are fixed-width or span-bounded by the surrounding row slot
32/// container; they do not perform map/array/value dispatch.
33///
34
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub enum ScalarCodec {
37    Blob,
38    Bool,
39    Date,
40    Duration,
41    Float32,
42    Float64,
43    Int64,
44    Principal,
45    Subaccount,
46    Text,
47    Timestamp,
48    Uint64,
49    Ulid,
50    Unit,
51}
52
53///
54/// LeafCodec
55///
56/// LeafCodec declares whether one persisted field payload uses a dedicated
57/// scalar codec or falls back to CBOR leaf decoding.
58/// The row container consults this metadata before deciding whether a slot can
59/// stay on the scalar fast path.
60///
61
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum LeafCodec {
64    Scalar(ScalarCodec),
65    CborFallback,
66}
67
68///
69/// EnumVariantModel
70///
71/// EnumVariantModel carries structural decode metadata for one generated enum
72/// variant payload.
73/// Runtime structural decode uses this to stay on the field-kind contract for
74/// enum payloads instead of falling back to generic untyped CBOR decoding.
75///
76
77#[derive(Clone, Copy, Debug)]
78pub struct EnumVariantModel {
79    /// Stable schema variant tag.
80    pub(crate) ident: &'static str,
81    /// Declared payload kind when this variant carries data.
82    pub(crate) payload_kind: Option<&'static FieldKind>,
83    /// Persisted payload decode contract for the carried data.
84    pub(crate) payload_storage_decode: FieldStorageDecode,
85}
86
87impl EnumVariantModel {
88    /// Build one enum variant structural decode descriptor.
89    #[must_use]
90    pub const fn new(
91        ident: &'static str,
92        payload_kind: Option<&'static FieldKind>,
93        payload_storage_decode: FieldStorageDecode,
94    ) -> Self {
95        Self {
96            ident,
97            payload_kind,
98            payload_storage_decode,
99        }
100    }
101
102    /// Return the stable schema variant tag.
103    #[must_use]
104    pub const fn ident(&self) -> &'static str {
105        self.ident
106    }
107
108    /// Return the declared payload kind when this variant carries data.
109    #[must_use]
110    pub const fn payload_kind(&self) -> Option<&'static FieldKind> {
111        self.payload_kind
112    }
113
114    /// Return the persisted payload decode contract for this variant.
115    #[must_use]
116    pub const fn payload_storage_decode(&self) -> FieldStorageDecode {
117        self.payload_storage_decode
118    }
119}
120
121///
122/// FieldModel
123///
124/// Runtime field metadata surfaced by macro-generated `EntityModel` values.
125///
126/// This is the smallest unit consumed by predicate validation, planning,
127/// and executor-side plan checks.
128///
129
130#[derive(Debug)]
131pub struct FieldModel {
132    /// Field name as used in predicates and indexing.
133    pub(crate) name: &'static str,
134    /// Runtime type shape (no schema-layer graph nodes).
135    pub(crate) kind: FieldKind,
136    /// Persisted field decode contract used by structural runtime decoders.
137    pub(crate) storage_decode: FieldStorageDecode,
138    /// Leaf payload codec used by slot readers and writers.
139    pub(crate) leaf_codec: LeafCodec,
140}
141
142impl FieldModel {
143    /// Build one runtime field descriptor.
144    #[must_use]
145    pub const fn new(name: &'static str, kind: FieldKind) -> Self {
146        Self::new_with_storage_decode(name, kind, FieldStorageDecode::ByKind)
147    }
148
149    /// Build one runtime field descriptor with an explicit persisted decode contract.
150    #[must_use]
151    pub const fn new_with_storage_decode(
152        name: &'static str,
153        kind: FieldKind,
154        storage_decode: FieldStorageDecode,
155    ) -> Self {
156        Self {
157            name,
158            kind,
159            storage_decode,
160            leaf_codec: leaf_codec_for(kind, storage_decode),
161        }
162    }
163
164    /// Return the stable field name.
165    #[must_use]
166    pub const fn name(&self) -> &'static str {
167        self.name
168    }
169
170    /// Return the runtime type-kind descriptor.
171    #[must_use]
172    pub const fn kind(&self) -> FieldKind {
173        self.kind
174    }
175
176    /// Return the persisted field decode contract.
177    #[must_use]
178    pub const fn storage_decode(&self) -> FieldStorageDecode {
179        self.storage_decode
180    }
181
182    /// Return the persisted leaf payload codec.
183    #[must_use]
184    pub const fn leaf_codec(&self) -> LeafCodec {
185        self.leaf_codec
186    }
187}
188
189// Resolve the canonical leaf codec from semantic field kind plus storage
190// contract. Fields that intentionally persist as `Value` or that still require
191// recursive payload decoding remain on the shared CBOR fallback.
192const fn leaf_codec_for(kind: FieldKind, storage_decode: FieldStorageDecode) -> LeafCodec {
193    if matches!(storage_decode, FieldStorageDecode::Value) {
194        return LeafCodec::CborFallback;
195    }
196
197    match kind {
198        FieldKind::Blob => LeafCodec::Scalar(ScalarCodec::Blob),
199        FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
200        FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
201        FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
202        FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
203        FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
204        FieldKind::Int => LeafCodec::Scalar(ScalarCodec::Int64),
205        FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
206        FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
207        FieldKind::Text => LeafCodec::Scalar(ScalarCodec::Text),
208        FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
209        FieldKind::Uint => LeafCodec::Scalar(ScalarCodec::Uint64),
210        FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
211        FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
212        FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
213        FieldKind::Account
214        | FieldKind::Decimal { .. }
215        | FieldKind::Enum { .. }
216        | FieldKind::Int128
217        | FieldKind::IntBig
218        | FieldKind::List(_)
219        | FieldKind::Map { .. }
220        | FieldKind::Set(_)
221        | FieldKind::Structured { .. }
222        | FieldKind::Uint128
223        | FieldKind::UintBig => LeafCodec::CborFallback,
224    }
225}
226
227///
228/// RelationStrength
229///
230/// Explicit relation intent for save-time referential integrity.
231///
232
233#[derive(Clone, Copy, Debug, Eq, PartialEq)]
234pub enum RelationStrength {
235    Strong,
236    Weak,
237}
238
239///
240/// FieldKind
241///
242/// Minimal runtime type surface needed by planning, validation, and execution.
243///
244/// This is aligned with `Value` variants and intentionally lossy: it encodes
245/// only the shape required for predicate compatibility and index planning.
246///
247
248#[derive(Clone, Copy, Debug)]
249pub enum FieldKind {
250    // Scalar primitives
251    Account,
252    Blob,
253    Bool,
254    Date,
255    Decimal {
256        /// Required schema-declared fractional scale for decimal fields.
257        scale: u32,
258    },
259    Duration,
260    Enum {
261        /// Fully-qualified enum type path used for strict filter normalization.
262        path: &'static str,
263        /// Declared per-variant payload decode metadata.
264        variants: &'static [EnumVariantModel],
265    },
266    Float32,
267    Float64,
268    Int,
269    Int128,
270    IntBig,
271    Principal,
272    Subaccount,
273    Text,
274    Timestamp,
275    Uint,
276    Uint128,
277    UintBig,
278    Ulid,
279    Unit,
280
281    /// Typed relation; `key_kind` reflects the referenced key type.
282    /// `strength` encodes strong vs. weak relation intent.
283    Relation {
284        /// Fully-qualified Rust type path for diagnostics.
285        target_path: &'static str,
286        /// Stable external name used in storage keys.
287        target_entity_name: &'static str,
288        /// Stable runtime identity used on hot execution paths.
289        target_entity_tag: EntityTag,
290        /// Data store path where the target entity is persisted.
291        target_store_path: &'static str,
292        key_kind: &'static Self,
293        strength: RelationStrength,
294    },
295
296    // Collections
297    List(&'static Self),
298    Set(&'static Self),
299    /// Deterministic, unordered key/value collection.
300    ///
301    /// Map fields are persistable and patchable, but not queryable or indexable.
302    Map {
303        key: &'static Self,
304        value: &'static Self,
305    },
306
307    /// Structured (non-atomic) value.
308    /// Queryability here controls whether predicates may target this field,
309    /// not whether it may be stored or updated.
310    Structured {
311        queryable: bool,
312    },
313}
314
315impl FieldKind {
316    #[must_use]
317    pub const fn value_kind(&self) -> FieldValueKind {
318        match self {
319            Self::Account
320            | Self::Blob
321            | Self::Bool
322            | Self::Date
323            | Self::Duration
324            | Self::Enum { .. }
325            | Self::Float32
326            | Self::Float64
327            | Self::Int
328            | Self::Int128
329            | Self::IntBig
330            | Self::Principal
331            | Self::Subaccount
332            | Self::Text
333            | Self::Timestamp
334            | Self::Uint
335            | Self::Uint128
336            | Self::UintBig
337            | Self::Ulid
338            | Self::Unit
339            | Self::Decimal { .. }
340            | Self::Relation { .. } => FieldValueKind::Atomic,
341            Self::List(_) | Self::Set(_) => FieldValueKind::Structured { queryable: true },
342            Self::Map { .. } => FieldValueKind::Structured { queryable: false },
343            Self::Structured { queryable } => FieldValueKind::Structured {
344                queryable: *queryable,
345            },
346        }
347    }
348
349    /// Returns `true` if this field shape is permitted in
350    /// persisted or query-visible schemas under the current
351    /// determinism policy.
352    ///
353    /// This shape-level check is structural only; query-time policy
354    /// enforcement (for example, map predicate fencing) is applied at
355    /// query construction and validation boundaries.
356    #[must_use]
357    pub const fn is_deterministic_collection_shape(&self) -> bool {
358        match self {
359            Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
360
361            Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
362
363            Self::Map { key, value } => {
364                key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
365            }
366
367            _ => true,
368        }
369    }
370}