1use 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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
25pub struct LayerId<Id>(pub Id);
26
27#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
33pub struct LayerName {
34 value: String,
36}
37
38impl LayerName {
39 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 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
80#[non_exhaustive]
81pub enum IdFamily {
82 Element,
84 Relation,
86 Incidence,
88}
89
90#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
96#[non_exhaustive]
97pub enum LayerRole {
98 Weight,
100 Property,
102}
103
104#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
113#[non_exhaustive]
114pub enum MissingPolicy {
115 Null,
117 Default,
119}
120
121#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
127#[non_exhaustive]
128pub enum StorageMode {
129 Dense,
131 Sparse {
133 missing: MissingPolicy,
135 },
136}
137
138#[derive(Clone, Debug, PartialEq)]
144#[non_exhaustive]
145pub struct PropertyLayerDescriptor<Id, I>
146where
147 I: PropertyIndex,
148{
149 pub layer_id: LayerId<Id>,
151 pub name: LayerName,
153 pub id_family: IdFamily,
155 pub role: LayerRole,
157 pub storage: StorageMode,
159 pub arrow_field: Field,
161 index_width: core::marker::PhantomData<I>,
163}
164
165impl<Id, I> PropertyLayerDescriptor<Id, I>
166where
167 I: PropertyIndex,
168{
169 #[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#[derive(Debug, Clone, PartialEq)]
208#[non_exhaustive]
209pub enum PropertyError {
210 EmptyLayerName,
212 ExpectedDenseStorage {
214 name: LayerName,
216 },
217 ExpectedSparseStorage {
219 name: LayerName,
221 },
222 DefaultPolicyMismatch {
224 name: LayerName,
226 },
227 ArrowTypeMismatch {
229 name: LayerName,
231 },
232 IdFamilyMismatch {
234 expected: IdFamily,
236 actual: IdFamily,
238 },
239 LayerTooShort {
241 required: usize,
243 actual: usize,
245 },
246 UnexpectedNull {
248 index: usize,
250 },
251 SparseLengthMismatch {
253 indices: usize,
255 values: usize,
257 },
258 SparseIndexOrder {
260 position: usize,
262 },
263 SparseIndexOutOfBounds {
265 index: u64,
267 len: usize,
269 },
270 DuplicateName {
272 id_family: IdFamily,
274 name: LayerName,
276 },
277 SparseNullMissingNotTotal {
279 name: LayerName,
281 },
282 DuplicateLayerId {
284 layer_id: u64,
286 },
287 MissingSnapshotSection {
289 kind: u32,
291 },
292 SnapshotSectionVersion {
294 kind: u32,
296 version: u32,
298 },
299 SnapshotSectionView {
301 kind: u32,
303 error: SectionViewError,
305 },
306 SnapshotRangeOutOfBounds {
308 offset: usize,
310 len: usize,
312 available: usize,
314 },
315 SnapshotInvalidUtf8 {
317 offset: usize,
319 },
320 UnknownIdFamilyTag {
322 tag: u32,
324 },
325 UnknownLayerRoleTag {
327 tag: u32,
329 },
330 UnknownStorageTag {
332 tag: u32,
334 },
335 UnknownMissingPolicyTag {
337 tag: u32,
339 },
340 UnknownArrowFamilyTag {
342 tag: u32,
344 },
345 UnknownIdentityModeTag {
347 tag: u32,
349 },
350 SnapshotDescriptorMismatch {
352 reason: &'static str,
354 },
355 SnapshotDataLength {
357 reason: &'static str,
359 },
360 Arrow {
362 message: String,
364 },
365 MissingIdentityMap {
367 id_family: IdFamily,
369 },
370 IdentityMapLength {
372 id_family: IdFamily,
374 required: usize,
376 actual: usize,
378 },
379 LengthDoesNotFitU64 {
381 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#[non_exhaustive]
521pub enum PropertyLayerData<I>
522where
523 I: PropertyIndex,
524{
525 Dense {
527 values: ArrayRef,
529 },
530 Sparse {
532 indices: Arc<PrimitiveArray<I::ArrowType>>,
534 values: ArrayRef,
536 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#[derive(Clone, Debug)]
593#[must_use]
594pub struct PropertyLayer<Id, I>
595where
596 I: PropertyIndex,
597{
598 descriptor: PropertyLayerDescriptor<Id, I>,
600 len: usize,
602 data: PropertyLayerData<I>,
604}
605
606impl<Id, I> PropertyLayer<Id, I>
607where
608 I: PropertyIndex,
609{
610 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 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 #[must_use]
692 pub const fn descriptor(&self) -> &PropertyLayerDescriptor<Id, I> {
693 &self.descriptor
694 }
695
696 #[must_use]
702 pub const fn data(&self) -> &PropertyLayerData<I> {
703 &self.data
704 }
705
706 #[must_use]
712 pub const fn len(&self) -> usize {
713 self.len
714 }
715
716 #[must_use]
722 pub const fn is_empty(&self) -> bool {
723 self.len == 0
724 }
725}
726
727#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
733#[non_exhaustive]
734pub enum IdentityMapMode {
735 LocalEqualsCanonical,
737 ExplicitMap,
739}
740
741impl IdentityMapMode {
742 const fn tag(self) -> u32 {
748 match self {
749 Self::LocalEqualsCanonical => 0,
750 Self::ExplicitMap => 1,
751 }
752 }
753
754 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#[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: W::LittleEndianWord,
781 mode: W::LittleEndianWord,
783 local_len: W::LittleEndianWord,
785}
786
787impl<W> IdentityModeRecord<W>
788where
789 W: PropertySnapshotMetaWord,
790{
791 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 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 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 pub fn id_family(&self) -> Result<IdFamily, PropertyError> {
854 id_family_from_tag(le_word_to_u32::<W>(self.id_family)?)
855 }
856
857 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 #[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#[derive(Clone, Debug, Eq, PartialEq)]
889#[must_use]
890pub struct IdentitySnapshotSummary {
891 pub records: Vec<IdentityModeSummary>,
893}
894
895#[derive(Clone, Copy, Debug, Eq, PartialEq)]
901pub struct IdentityModeSummary {
902 pub id_family: IdFamily,
904 pub mode: IdentityMapMode,
906 pub local_len: usize,
908}
909
910pub(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
923pub(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
937pub(crate) const fn layer_role_tag(role: LayerRole) -> u32 {
943 match role {
944 LayerRole::Weight => 0,
945 LayerRole::Property => 1,
946 }
947}
948
949pub(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
962pub(crate) const fn storage_tag(storage: StorageMode) -> u32 {
968 match storage {
969 StorageMode::Dense => 0,
970 StorageMode::Sparse { .. } => 1,
971 }
972}
973
974pub(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
991pub(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
1013pub(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
1034fn 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
1067pub(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
1081pub(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#[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}