Skip to main content

oxgraph_property/
model.rs

1//! Core property-layer data model, error type, and shared validation helpers.
2//!
3//! Holds the domain newtypes/enums, the Arrow-backed layer types, the
4//! identity-mode snapshot records, the concrete [`PropertyError`] enum, the
5//! snapshot tag codecs for those enums, and the layer-construction validation
6//! helpers shared across the crate.
7
8use std::{error::Error, fmt, string::String, sync::Arc, vec::Vec};
9
10use arrow_array::{Array, ArrayRef, PrimitiveArray};
11use arrow_schema::Field;
12use oxgraph_snapshot::SectionViewError;
13use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
14
15use crate::width::{
16    PropertyIndex, PropertySnapshotMetaWord, le_word, le_word_to_u32, le_word_to_usize,
17};
18
19/// Stable numeric identifier for one property layer.
20///
21/// # Performance
22///
23/// Copying, comparing, ordering, hashing, and debug-formatting are `O(1)`.
24#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
25pub struct LayerId<Id>(pub Id);
26
27/// Human-facing property layer name.
28///
29/// # Performance
30///
31/// Cloning is `O(name.len())`; comparison and display are `O(name.len())`.
32#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
33pub struct LayerName {
34    /// Owned layer name.
35    value: String,
36}
37
38impl LayerName {
39    /// Builds a non-empty layer name.
40    ///
41    /// # Errors
42    ///
43    /// Returns [`PropertyError::EmptyLayerName`] when `value` is empty.
44    ///
45    /// # Performance
46    ///
47    /// This function is `O(value.len())`.
48    pub fn try_new(value: &str) -> Result<Self, PropertyError> {
49        if value.is_empty() {
50            return Err(PropertyError::EmptyLayerName);
51        }
52        Ok(Self {
53            value: String::from(value),
54        })
55    }
56
57    /// Returns the layer name as a borrowed string.
58    ///
59    /// # Performance
60    ///
61    /// This function is `O(1)`.
62    #[must_use]
63    pub const fn as_str(&self) -> &str {
64        self.value.as_str()
65    }
66}
67
68impl fmt::Display for LayerName {
69    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70        formatter.write_str(self.as_str())
71    }
72}
73
74/// Topology ID family keyed by a property layer.
75///
76/// # Performance
77///
78/// Copying, comparing, ordering, hashing, and debug-formatting are `O(1)`.
79#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
80#[non_exhaustive]
81pub enum IdFamily {
82    /// Element/node/vertex-keyed layer.
83    Element,
84    /// Relation/edge/hyperedge-keyed layer.
85    Relation,
86    /// Incidence/endpoint/participant-keyed layer.
87    Incidence,
88}
89
90/// Declared role of a property layer.
91///
92/// # Performance
93///
94/// Copying, comparing, ordering, hashing, and debug-formatting are `O(1)`.
95#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
96#[non_exhaustive]
97pub enum LayerRole {
98    /// Layer is intended to be selected as a topology weight capability.
99    Weight,
100    /// Layer is a named property with no required weight interpretation.
101    Property,
102}
103
104/// Missing-value policy for sparse property layers.
105///
106/// The actual default scalar, when present, is stored in Arrow data for the
107/// sparse layer. This enum records whether a total default exists.
108///
109/// # Performance
110///
111/// Copying, comparing, and debug-formatting are `O(1)`.
112#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
113#[non_exhaustive]
114pub enum MissingPolicy {
115    /// Missing positions are null and therefore not directly weight-total.
116    Null,
117    /// Missing positions read from an Arrow scalar default stored with the layer.
118    Default,
119}
120
121/// Physical storage mode for a property layer.
122///
123/// # Performance
124///
125/// Copying, comparing, and debug-formatting are `O(1)`.
126#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
127#[non_exhaustive]
128pub enum StorageMode {
129    /// Dense array with one slot per ID index.
130    Dense,
131    /// Sparse array keyed by explicit indexes plus a missing-value policy.
132    Sparse {
133        /// Policy used for indexes not present in the sparse index array.
134        missing: MissingPolicy,
135    },
136}
137
138/// Descriptor for one Arrow-backed property layer.
139///
140/// # Performance
141///
142/// Cloning is `O(name.len() + arrow field clone cost)`.
143#[derive(Clone, Debug, PartialEq)]
144#[non_exhaustive]
145pub struct PropertyLayerDescriptor<Id, I>
146where
147    I: PropertyIndex,
148{
149    /// Stable layer identifier.
150    pub layer_id: LayerId<Id>,
151    /// Human-facing layer name.
152    pub name: LayerName,
153    /// Topology ID family keyed by this layer.
154    pub id_family: IdFamily,
155    /// Declared layer role.
156    pub role: LayerRole,
157    /// Physical storage mode.
158    pub storage: StorageMode,
159    /// Arrow schema field for stored values.
160    pub arrow_field: Field,
161    /// Sparse/logical index width selected for this layer.
162    index_width: core::marker::PhantomData<I>,
163}
164
165impl<Id, I> PropertyLayerDescriptor<Id, I>
166where
167    I: PropertyIndex,
168{
169    /// Constructs a descriptor and validates the layer name.
170    ///
171    /// # Errors
172    ///
173    /// Returns [`PropertyError::EmptyLayerName`] when `name` is empty.
174    ///
175    /// # Performance
176    ///
177    /// This function is `O(name.len())` plus Arrow field move cost.
178    #[expect(
179        clippy::too_many_arguments,
180        reason = "descriptor constructor mirrors the six-field descriptor contract"
181    )]
182    pub fn try_new(
183        layer_id: LayerId<Id>,
184        name: &str,
185        id_family: IdFamily,
186        role: LayerRole,
187        storage: StorageMode,
188        arrow_field: Field,
189    ) -> Result<Self, PropertyError> {
190        Ok(Self {
191            layer_id,
192            name: LayerName::try_new(name)?,
193            id_family,
194            role,
195            storage,
196            arrow_field,
197            index_width: core::marker::PhantomData,
198        })
199    }
200}
201
202/// Errors raised while validating property descriptors, layers, or snapshots.
203///
204/// # Performance
205///
206/// Formatting is `O(message length)`.
207#[derive(Debug, Clone, PartialEq)]
208#[non_exhaustive]
209pub enum PropertyError {
210    /// Layer names must not be empty.
211    EmptyLayerName,
212    /// Dense layers must use dense descriptors.
213    ExpectedDenseStorage {
214        /// Name of the offending layer.
215        name: LayerName,
216    },
217    /// Sparse layers must use sparse descriptors.
218    ExpectedSparseStorage {
219        /// Name of the offending layer.
220        name: LayerName,
221    },
222    /// A sparse descriptor and default value disagreed.
223    DefaultPolicyMismatch {
224        /// Name of the offending layer.
225        name: LayerName,
226    },
227    /// A layer's Arrow data type did not match the descriptor field type.
228    ArrowTypeMismatch {
229        /// Name of the offending layer.
230        name: LayerName,
231    },
232    /// A layer's ID family did not match the requested adapter family.
233    IdFamilyMismatch {
234        /// Expected ID family.
235        expected: IdFamily,
236        /// Actual ID family.
237        actual: IdFamily,
238    },
239    /// A layer had too few values for the topology index bound.
240    LayerTooShort {
241        /// Required minimum length.
242        required: usize,
243        /// Actual layer length.
244        actual: usize,
245    },
246    /// A non-nullable selected layer contained a null slot.
247    UnexpectedNull {
248        /// Index of the null slot.
249        index: usize,
250    },
251    /// Sparse index and value arrays differed in length.
252    SparseLengthMismatch {
253        /// Sparse index count.
254        indices: usize,
255        /// Sparse value count.
256        values: usize,
257    },
258    /// Sparse indexes must be strictly increasing.
259    SparseIndexOrder {
260        /// Sparse array position where order failed.
261        position: usize,
262    },
263    /// Sparse index was outside the declared logical length.
264    SparseIndexOutOfBounds {
265        /// Invalid sparse index.
266        index: u64,
267        /// Logical layer length.
268        len: usize,
269    },
270    /// A name was reused within an ID-family namespace.
271    DuplicateName {
272        /// ID family namespace.
273        id_family: IdFamily,
274        /// Duplicate layer name.
275        name: LayerName,
276    },
277    /// Sparse null-missing policy cannot be selected as a total weight view.
278    SparseNullMissingNotTotal {
279        /// Name of the offending layer.
280        name: LayerName,
281    },
282    /// A layer ID was reused within one descriptor set.
283    DuplicateLayerId {
284        /// Duplicate layer ID.
285        layer_id: u64,
286    },
287    /// A snapshot section was missing.
288    MissingSnapshotSection {
289        /// Missing section kind.
290        kind: u32,
291    },
292    /// A snapshot section had an unsupported version.
293    SnapshotSectionVersion {
294        /// Section kind.
295        kind: u32,
296        /// Actual section version.
297        version: u32,
298    },
299    /// A snapshot section could not be borrowed as the expected record type.
300    SnapshotSectionView {
301        /// Section kind.
302        kind: u32,
303        /// Underlying typed-view error.
304        error: SectionViewError,
305    },
306    /// Snapshot bytes ended before a declared range.
307    SnapshotRangeOutOfBounds {
308        /// Byte range start.
309        offset: usize,
310        /// Byte range length.
311        len: usize,
312        /// Available section byte length.
313        available: usize,
314    },
315    /// Snapshot string table bytes were not valid UTF-8.
316    SnapshotInvalidUtf8 {
317        /// Byte offset of the invalid string.
318        offset: usize,
319    },
320    /// Snapshot metadata used an unknown ID family tag.
321    UnknownIdFamilyTag {
322        /// Invalid tag.
323        tag: u32,
324    },
325    /// Snapshot metadata used an unknown layer role tag.
326    UnknownLayerRoleTag {
327        /// Invalid tag.
328        tag: u32,
329    },
330    /// Snapshot metadata used an unknown storage tag.
331    UnknownStorageTag {
332        /// Invalid tag.
333        tag: u32,
334    },
335    /// Snapshot metadata used an unknown missing-policy tag.
336    UnknownMissingPolicyTag {
337        /// Invalid tag.
338        tag: u32,
339    },
340    /// Snapshot metadata used an unknown Arrow value-family tag.
341    UnknownArrowFamilyTag {
342        /// Invalid tag.
343        tag: u32,
344    },
345    /// Snapshot metadata used an unknown identity-map mode tag.
346    UnknownIdentityModeTag {
347        /// Invalid tag.
348        tag: u32,
349    },
350    /// A property snapshot descriptor was structurally inconsistent.
351    SnapshotDescriptorMismatch {
352        /// Human-readable mismatch reason.
353        reason: &'static str,
354    },
355    /// A property data payload had an invalid byte length.
356    SnapshotDataLength {
357        /// Human-readable mismatch reason.
358        reason: &'static str,
359    },
360    /// Arrow IPC/schema validation failed.
361    Arrow {
362        /// Arrow error message.
363        message: String,
364    },
365    /// An explicit identity map was required but missing.
366    MissingIdentityMap {
367        /// ID family whose map was missing.
368        id_family: IdFamily,
369    },
370    /// An identity map length did not match its mode metadata.
371    IdentityMapLength {
372        /// ID family whose map had the wrong length.
373        id_family: IdFamily,
374        /// Required map length.
375        required: usize,
376        /// Actual map length.
377        actual: usize,
378    },
379    /// A `usize` value could not be represented as `u64`.
380    LengthDoesNotFitU64 {
381        /// Value that did not fit.
382        value: usize,
383    },
384}
385
386impl fmt::Display for PropertyError {
387    #[expect(
388        clippy::too_many_lines,
389        reason = "property validation has one display branch per concrete error variant"
390    )]
391    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
392        match self {
393            Self::EmptyLayerName => formatter.write_str("property layer name is empty"),
394            Self::ExpectedDenseStorage { name } => {
395                write!(formatter, "property layer '{name}' is not dense")
396            }
397            Self::ExpectedSparseStorage { name } => {
398                write!(formatter, "property layer '{name}' is not sparse")
399            }
400            Self::DefaultPolicyMismatch { name } => {
401                write!(formatter, "property layer '{name}' default policy mismatch")
402            }
403            Self::ArrowTypeMismatch { name } => {
404                write!(formatter, "property layer '{name}' Arrow type mismatch")
405            }
406            Self::IdFamilyMismatch { expected, actual } => write!(
407                formatter,
408                "property ID family mismatch: expected {expected:?}, got {actual:?}"
409            ),
410            Self::LayerTooShort { required, actual } => write!(
411                formatter,
412                "property layer too short: required {required}, got {actual}"
413            ),
414            Self::UnexpectedNull { index } => write!(
415                formatter,
416                "property layer has unexpected null at index {index}"
417            ),
418            Self::SparseLengthMismatch { indices, values } => write!(
419                formatter,
420                "sparse property length mismatch: {indices} indexes for {values} values"
421            ),
422            Self::SparseIndexOrder { position } => write!(
423                formatter,
424                "sparse property indexes are not strictly increasing at position {position}"
425            ),
426            Self::SparseIndexOutOfBounds { index, len } => write!(
427                formatter,
428                "sparse property index {index} is outside logical length {len}"
429            ),
430            Self::DuplicateName { id_family, name } => write!(
431                formatter,
432                "duplicate property name '{name}' in {id_family:?} namespace"
433            ),
434            Self::SparseNullMissingNotTotal { name } => write!(
435                formatter,
436                "sparse property layer '{name}' has null missing policy and is not total"
437            ),
438            Self::DuplicateLayerId { layer_id } => {
439                write!(formatter, "duplicate property layer ID {layer_id:?}")
440            }
441            Self::MissingSnapshotSection { kind } => {
442                write!(formatter, "snapshot is missing section kind {kind:#x}")
443            }
444            Self::SnapshotSectionVersion { kind, version } => write!(
445                formatter,
446                "snapshot section {kind:#x} has unsupported version {version}"
447            ),
448            Self::SnapshotSectionView { kind, error } => write!(
449                formatter,
450                "snapshot section {kind:#x} cannot be borrowed as expected records: {error}"
451            ),
452            Self::SnapshotRangeOutOfBounds {
453                offset,
454                len,
455                available,
456            } => write!(
457                formatter,
458                "snapshot range {offset}..{} exceeds available {available} bytes",
459                offset.saturating_add(*len)
460            ),
461            Self::SnapshotInvalidUtf8 { offset } => {
462                write!(
463                    formatter,
464                    "snapshot string at byte offset {offset} is not UTF-8"
465                )
466            }
467            Self::UnknownIdFamilyTag { tag } => {
468                write!(formatter, "unknown property ID-family tag {tag}")
469            }
470            Self::UnknownLayerRoleTag { tag } => {
471                write!(formatter, "unknown property layer-role tag {tag}")
472            }
473            Self::UnknownStorageTag { tag } => {
474                write!(formatter, "unknown property storage tag {tag}")
475            }
476            Self::UnknownMissingPolicyTag { tag } => {
477                write!(formatter, "unknown property missing-policy tag {tag}")
478            }
479            Self::UnknownArrowFamilyTag { tag } => {
480                write!(formatter, "unknown Arrow value-family tag {tag}")
481            }
482            Self::UnknownIdentityModeTag { tag } => {
483                write!(formatter, "unknown identity-map mode tag {tag}")
484            }
485            Self::SnapshotDescriptorMismatch { reason } => {
486                write!(formatter, "property snapshot descriptor mismatch: {reason}")
487            }
488            Self::SnapshotDataLength { reason } => {
489                write!(
490                    formatter,
491                    "property snapshot data length mismatch: {reason}"
492                )
493            }
494            Self::Arrow { message } => write!(formatter, "Arrow property error: {message}"),
495            Self::MissingIdentityMap { id_family } => {
496                write!(formatter, "missing explicit identity map for {id_family:?}")
497            }
498            Self::IdentityMapLength {
499                id_family,
500                required,
501                actual,
502            } => write!(
503                formatter,
504                "identity map for {id_family:?} has length {actual}, required {required}"
505            ),
506            Self::LengthDoesNotFitU64 { value } => {
507                write!(formatter, "length {value} does not fit u64")
508            }
509        }
510    }
511}
512
513impl Error for PropertyError {}
514
515/// Data backing one property layer.
516///
517/// # Performance
518///
519/// Cloning is `O(1)` because Arrow arrays are reference-counted.
520#[non_exhaustive]
521pub enum PropertyLayerData<I>
522where
523    I: PropertyIndex,
524{
525    /// Dense Arrow array with one slot per ID index.
526    Dense {
527        /// Dense values.
528        values: ArrayRef,
529    },
530    /// Sparse Arrow array keyed by explicit indexes.
531    Sparse {
532        /// Strictly ascending sparse indexes.
533        indices: Arc<PrimitiveArray<I::ArrowType>>,
534        /// Values aligned with `indices`.
535        values: ArrayRef,
536        /// Optional Arrow scalar default encoded as a length-one array.
537        default: Option<ArrayRef>,
538    },
539}
540
541impl<I> Clone for PropertyLayerData<I>
542where
543    I: PropertyIndex,
544{
545    fn clone(&self) -> Self {
546        match self {
547            Self::Dense { values } => Self::Dense {
548                values: Arc::clone(values),
549            },
550            Self::Sparse {
551                indices,
552                values,
553                default,
554            } => Self::Sparse {
555                indices: Arc::clone(indices),
556                values: Arc::clone(values),
557                default: default.clone(),
558            },
559        }
560    }
561}
562
563impl<I> fmt::Debug for PropertyLayerData<I>
564where
565    I: PropertyIndex,
566{
567    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
568        match self {
569            Self::Dense { values } => formatter
570                .debug_struct("Dense")
571                .field("len", &values.len())
572                .finish(),
573            Self::Sparse {
574                indices,
575                values,
576                default,
577            } => formatter
578                .debug_struct("Sparse")
579                .field("indices", &indices.len())
580                .field("values", &values.len())
581                .field("has_default", &default.is_some())
582                .finish(),
583        }
584    }
585}
586
587/// Arrow-backed property layer.
588///
589/// # Performance
590///
591/// Cloning is `O(1)` for Arrow buffers plus descriptor clone cost.
592#[derive(Clone, Debug)]
593#[must_use]
594pub struct PropertyLayer<Id, I>
595where
596    I: PropertyIndex,
597{
598    /// Layer descriptor.
599    descriptor: PropertyLayerDescriptor<Id, I>,
600    /// Logical layer length.
601    len: usize,
602    /// Layer data.
603    data: PropertyLayerData<I>,
604}
605
606impl<Id, I> PropertyLayer<Id, I>
607where
608    I: PropertyIndex,
609{
610    /// Builds a dense Arrow-backed property layer.
611    ///
612    /// # Errors
613    ///
614    /// Returns [`PropertyError`] when storage, Arrow type, or nullability is invalid.
615    ///
616    /// # Performance
617    ///
618    /// Validation is `O(values.len())` only when nullability must be checked.
619    pub fn try_new_dense(
620        descriptor: PropertyLayerDescriptor<Id, I>,
621        values: ArrayRef,
622    ) -> Result<Self, PropertyError> {
623        if descriptor.storage != StorageMode::Dense {
624            return Err(PropertyError::ExpectedDenseStorage {
625                name: descriptor.name,
626            });
627        }
628        ensure_arrow_type(&descriptor, values.as_ref())?;
629        if !descriptor.arrow_field.is_nullable() {
630            ensure_no_nulls(values.as_ref())?;
631        }
632        let len = values.len();
633        Ok(Self {
634            descriptor,
635            len,
636            data: PropertyLayerData::Dense { values },
637        })
638    }
639
640    /// Builds a sparse Arrow-backed property layer.
641    ///
642    /// # Errors
643    ///
644    /// Returns [`PropertyError`] when storage, Arrow type, default policy,
645    /// sparse index ordering, or nullability is invalid.
646    ///
647    /// # Performance
648    ///
649    /// Validation is `O(indices.len() + default length)`.
650    pub fn try_new_sparse(
651        descriptor: PropertyLayerDescriptor<Id, I>,
652        len: usize,
653        indices: Arc<PrimitiveArray<I::ArrowType>>,
654        values: ArrayRef,
655        default: Option<ArrayRef>,
656    ) -> Result<Self, PropertyError> {
657        let StorageMode::Sparse { missing } = descriptor.storage else {
658            return Err(PropertyError::ExpectedSparseStorage {
659                name: descriptor.name,
660            });
661        };
662        validate_default_policy(&descriptor, missing, default.as_ref())?;
663        ensure_arrow_type(&descriptor, values.as_ref())?;
664        if indices.len() != values.len() {
665            return Err(PropertyError::SparseLengthMismatch {
666                indices: indices.len(),
667                values: values.len(),
668            });
669        }
670        ensure_no_nulls(indices.as_ref())?;
671        if !descriptor.arrow_field.is_nullable() {
672            ensure_no_nulls(values.as_ref())?;
673        }
674        validate_sparse_indices::<I>(indices.as_ref(), len)?;
675        Ok(Self {
676            descriptor,
677            len,
678            data: PropertyLayerData::Sparse {
679                indices,
680                values,
681                default,
682            },
683        })
684    }
685
686    /// Returns this layer's descriptor.
687    ///
688    /// # Performance
689    ///
690    /// This function is `O(1)`.
691    #[must_use]
692    pub const fn descriptor(&self) -> &PropertyLayerDescriptor<Id, I> {
693        &self.descriptor
694    }
695
696    /// Returns this layer's data.
697    ///
698    /// # Performance
699    ///
700    /// This function is `O(1)`.
701    #[must_use]
702    pub const fn data(&self) -> &PropertyLayerData<I> {
703        &self.data
704    }
705
706    /// Returns the logical layer length.
707    ///
708    /// # Performance
709    ///
710    /// This function is `O(1)`.
711    #[must_use]
712    pub const fn len(&self) -> usize {
713        self.len
714    }
715
716    /// Returns whether the logical layer is empty.
717    ///
718    /// # Performance
719    ///
720    /// This function is `O(1)`.
721    #[must_use]
722    pub const fn is_empty(&self) -> bool {
723        self.len == 0
724    }
725}
726
727/// Identity snapshot map mode.
728///
729/// # Performance
730///
731/// Copying, comparing, and debug-formatting are `O(1)`.
732#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
733#[non_exhaustive]
734pub enum IdentityMapMode {
735    /// Local IDs are identical to canonical IDs for this family.
736    LocalEqualsCanonical,
737    /// The snapshot stores an explicit local-to-canonical map section.
738    ExplicitMap,
739}
740
741impl IdentityMapMode {
742    /// Returns the snapshot tag for this mode.
743    ///
744    /// # Performance
745    ///
746    /// This function is `O(1)`.
747    const fn tag(self) -> u32 {
748        match self {
749            Self::LocalEqualsCanonical => 0,
750            Self::ExplicitMap => 1,
751        }
752    }
753
754    /// Decodes a snapshot mode tag.
755    ///
756    /// # Performance
757    ///
758    /// This function is `O(1)`.
759    const fn from_tag(tag: u32) -> Option<Self> {
760        match tag {
761            0 => Some(Self::LocalEqualsCanonical),
762            1 => Some(Self::ExplicitMap),
763            _ => None,
764        }
765    }
766}
767
768/// Wire record declaring one identity family map mode.
769///
770/// # Performance
771///
772/// Copying and reading fields are `O(1)`.
773#[derive(Clone, Copy, Debug, Eq, FromBytes, Immutable, IntoBytes, KnownLayout, PartialEq)]
774#[repr(C)]
775pub struct IdentityModeRecord<W>
776where
777    W: PropertySnapshotMetaWord,
778{
779    /// ID-family tag.
780    id_family: W::LittleEndianWord,
781    /// Map-mode tag.
782    mode: W::LittleEndianWord,
783    /// Number of local IDs covered by the mode.
784    local_len: W::LittleEndianWord,
785}
786
787impl<W> IdentityModeRecord<W>
788where
789    W: PropertySnapshotMetaWord,
790{
791    /// Builds a local-equals-canonical identity mode record.
792    ///
793    /// # Errors
794    ///
795    /// Returns [`PropertyError`] when `local_len` cannot be represented by the
796    /// selected metadata width.
797    ///
798    /// # Performance
799    ///
800    /// This function is `O(1)`.
801    pub fn local_equals_canonical(
802        id_family: IdFamily,
803        local_len: usize,
804    ) -> Result<Self, PropertyError> {
805        Self::new(id_family, IdentityMapMode::LocalEqualsCanonical, local_len)
806    }
807
808    /// Builds an explicit-map identity mode record.
809    ///
810    /// # Errors
811    ///
812    /// Returns [`PropertyError`] when `local_len` cannot be represented by the
813    /// selected metadata width.
814    ///
815    /// # Performance
816    ///
817    /// This function is `O(1)`.
818    pub fn explicit_map(id_family: IdFamily, local_len: usize) -> Result<Self, PropertyError> {
819        Self::new(id_family, IdentityMapMode::ExplicitMap, local_len)
820    }
821
822    /// Builds an identity mode record.
823    ///
824    /// # Errors
825    ///
826    /// Returns [`PropertyError`] when `local_len` cannot be represented by the
827    /// selected metadata width.
828    ///
829    /// # Performance
830    ///
831    /// This function is `O(1)`.
832    pub fn new(
833        id_family: IdFamily,
834        mode: IdentityMapMode,
835        local_len: usize,
836    ) -> Result<Self, PropertyError> {
837        Ok(Self {
838            id_family: le_word::<W>(id_family_tag(id_family) as usize)?,
839            mode: le_word::<W>(mode.tag() as usize)?,
840            local_len: le_word::<W>(local_len)?,
841        })
842    }
843
844    /// Returns this record's ID family.
845    ///
846    /// # Errors
847    ///
848    /// Returns [`PropertyError::UnknownIdFamilyTag`] if the record tag is unknown.
849    ///
850    /// # Performance
851    ///
852    /// This function is `O(1)`.
853    pub fn id_family(&self) -> Result<IdFamily, PropertyError> {
854        id_family_from_tag(le_word_to_u32::<W>(self.id_family)?)
855    }
856
857    /// Returns this record's identity map mode.
858    ///
859    /// # Errors
860    ///
861    /// Returns [`PropertyError::UnknownIdentityModeTag`] if the record tag is unknown.
862    ///
863    /// # Performance
864    ///
865    /// This function is `O(1)`.
866    pub fn mode(&self) -> Result<IdentityMapMode, PropertyError> {
867        let tag = le_word_to_u32::<W>(self.mode)?;
868        IdentityMapMode::from_tag(tag).ok_or(PropertyError::UnknownIdentityModeTag { tag })
869    }
870
871    /// Returns the local ID count covered by this mode.
872    ///
873    /// # Performance
874    ///
875    /// This function is `O(1)` on targets where `u64` to `usize` fits; values
876    /// above `usize::MAX` saturate to `usize::MAX` for validation errors.
877    #[must_use]
878    pub fn local_len(&self) -> usize {
879        le_word_to_usize::<W>(self.local_len).unwrap_or(usize::MAX)
880    }
881}
882
883/// Summary returned after identity snapshot validation.
884///
885/// # Performance
886///
887/// Cloning is `O(f)` for `f` identity-family records.
888#[derive(Clone, Debug, Eq, PartialEq)]
889#[must_use]
890pub struct IdentitySnapshotSummary {
891    /// Validated identity records.
892    pub records: Vec<IdentityModeSummary>,
893}
894
895/// Decoded identity mode summary.
896///
897/// # Performance
898///
899/// Copying is `O(1)`.
900#[derive(Clone, Copy, Debug, Eq, PartialEq)]
901pub struct IdentityModeSummary {
902    /// ID family covered by this record.
903    pub id_family: IdFamily,
904    /// Identity map mode.
905    pub mode: IdentityMapMode,
906    /// Number of local IDs covered.
907    pub local_len: usize,
908}
909
910/// Converts an ID family to its snapshot tag.
911///
912/// # Performance
913///
914/// This function is `O(1)`.
915pub(crate) const fn id_family_tag(id_family: IdFamily) -> u32 {
916    match id_family {
917        IdFamily::Element => 0,
918        IdFamily::Relation => 1,
919        IdFamily::Incidence => 2,
920    }
921}
922
923/// Decodes an ID family snapshot tag.
924///
925/// # Performance
926///
927/// This function is `O(1)`.
928pub(crate) const fn id_family_from_tag(tag: u32) -> Result<IdFamily, PropertyError> {
929    match tag {
930        0 => Ok(IdFamily::Element),
931        1 => Ok(IdFamily::Relation),
932        2 => Ok(IdFamily::Incidence),
933        _ => Err(PropertyError::UnknownIdFamilyTag { tag }),
934    }
935}
936
937/// Converts a layer role to its snapshot tag.
938///
939/// # Performance
940///
941/// This function is `O(1)`.
942pub(crate) const fn layer_role_tag(role: LayerRole) -> u32 {
943    match role {
944        LayerRole::Weight => 0,
945        LayerRole::Property => 1,
946    }
947}
948
949/// Decodes a layer role snapshot tag.
950///
951/// # Performance
952///
953/// This function is `O(1)`.
954pub(crate) const fn layer_role_from_tag(tag: u32) -> Result<LayerRole, PropertyError> {
955    match tag {
956        0 => Ok(LayerRole::Weight),
957        1 => Ok(LayerRole::Property),
958        _ => Err(PropertyError::UnknownLayerRoleTag { tag }),
959    }
960}
961
962/// Converts storage mode to its snapshot tag.
963///
964/// # Performance
965///
966/// This function is `O(1)`.
967pub(crate) const fn storage_tag(storage: StorageMode) -> u32 {
968    match storage {
969        StorageMode::Dense => 0,
970        StorageMode::Sparse { .. } => 1,
971    }
972}
973
974/// Converts missing policy to its snapshot tag.
975///
976/// # Performance
977///
978/// This function is `O(1)`.
979pub(crate) const fn missing_policy_tag(storage: StorageMode) -> u32 {
980    match storage {
981        StorageMode::Dense => 0,
982        StorageMode::Sparse {
983            missing: MissingPolicy::Null,
984        } => 1,
985        StorageMode::Sparse {
986            missing: MissingPolicy::Default,
987        } => 2,
988    }
989}
990
991/// Decodes storage and missing policy tags.
992///
993/// # Performance
994///
995/// This function is `O(1)`.
996pub(crate) const fn storage_from_tags(
997    storage: u32,
998    missing: u32,
999) -> Result<StorageMode, PropertyError> {
1000    match (storage, missing) {
1001        (0, 0) => Ok(StorageMode::Dense),
1002        (1, 1) => Ok(StorageMode::Sparse {
1003            missing: MissingPolicy::Null,
1004        }),
1005        (1, 2) => Ok(StorageMode::Sparse {
1006            missing: MissingPolicy::Default,
1007        }),
1008        (0, _) => Err(PropertyError::UnknownMissingPolicyTag { tag: missing }),
1009        (_, _) => Err(PropertyError::UnknownStorageTag { tag: storage }),
1010    }
1011}
1012
1013/// Ensures an Arrow array matches a descriptor field data type.
1014///
1015/// # Performance
1016///
1017/// This function is `O(1)`.
1018pub(crate) fn ensure_arrow_type<Id, I>(
1019    descriptor: &PropertyLayerDescriptor<Id, I>,
1020    values: &dyn Array,
1021) -> Result<(), PropertyError>
1022where
1023    I: PropertyIndex,
1024{
1025    if descriptor.arrow_field.data_type() == values.data_type() {
1026        Ok(())
1027    } else {
1028        Err(PropertyError::ArrowTypeMismatch {
1029            name: descriptor.name.clone(),
1030        })
1031    }
1032}
1033
1034/// Validates sparse default policy and Arrow type.
1035///
1036/// # Performance
1037///
1038/// This function is `O(1)`.
1039fn validate_default_policy<Id, I>(
1040    descriptor: &PropertyLayerDescriptor<Id, I>,
1041    missing: MissingPolicy,
1042    default: Option<&ArrayRef>,
1043) -> Result<(), PropertyError>
1044where
1045    I: PropertyIndex,
1046{
1047    match (missing, default) {
1048        (MissingPolicy::Null, None) => Ok(()),
1049        (MissingPolicy::Default, Some(array)) => {
1050            ensure_arrow_type(descriptor, array.as_ref())?;
1051            if array.len() == 1 && !array.is_null(0) {
1052                Ok(())
1053            } else {
1054                Err(PropertyError::DefaultPolicyMismatch {
1055                    name: descriptor.name.clone(),
1056                })
1057            }
1058        }
1059        (MissingPolicy::Null | MissingPolicy::Default, _) => {
1060            Err(PropertyError::DefaultPolicyMismatch {
1061                name: descriptor.name.clone(),
1062            })
1063        }
1064    }
1065}
1066
1067/// Ensures an Arrow array has no null slots.
1068///
1069/// # Performance
1070///
1071/// This function is `O(array.len())`.
1072pub(crate) fn ensure_no_nulls(array: &dyn Array) -> Result<(), PropertyError> {
1073    for index in 0..array.len() {
1074        if array.is_null(index) {
1075            return Err(PropertyError::UnexpectedNull { index });
1076        }
1077    }
1078    Ok(())
1079}
1080
1081/// Validates sparse index ordering and bounds.
1082///
1083/// # Performance
1084///
1085/// This function is `O(indices.len())`.
1086pub(crate) fn validate_sparse_indices<I>(
1087    indices: &PrimitiveArray<I::ArrowType>,
1088    len: usize,
1089) -> Result<(), PropertyError>
1090where
1091    I: PropertyIndex,
1092{
1093    let mut previous = None;
1094    for position in 0..indices.len() {
1095        let index = indices.value(position);
1096        let Some(index_usize) = index.to_usize() else {
1097            return Err(PropertyError::SparseIndexOutOfBounds {
1098                index: index.to_u64(),
1099                len,
1100            });
1101        };
1102        if index_usize >= len {
1103            return Err(PropertyError::SparseIndexOutOfBounds {
1104                index: index.to_u64(),
1105                len,
1106            });
1107        }
1108        if let Some(prior) = previous
1109            && index <= prior
1110        {
1111            return Err(PropertyError::SparseIndexOrder { position });
1112        }
1113        previous = Some(index);
1114    }
1115    Ok(())
1116}
1117
1118/// Converts an Arrow error into a property error.
1119///
1120/// # Performance
1121///
1122/// This function is `O(error message length)`.
1123#[expect(
1124    clippy::needless_pass_by_value,
1125    reason = "Arrow result adapters hand over owned errors and this helper consumes them into messages"
1126)]
1127pub(crate) fn map_arrow_error(error: arrow_schema::ArrowError) -> PropertyError {
1128    PropertyError::Arrow {
1129        message: error.to_string(),
1130    }
1131}