1use crate::control::{ForkBasis, NamespaceStatus, WriterBlock};
6use crate::envelope::EnvelopeCodecError;
7use crate::sst_blocks::BlockHandle;
8use crate::{
9 AccessGrants, AccessRevisionNo, ActorId, AttributeRevisionNo, Attributes, ChangeSeq, CommitId,
10 ContentId, ContentRef, DisplayName, InodeId, InodeKind, ManifestNo, MetadataSegmentId, NameKey,
11 NamespaceId, RevisionNo, RunNo,
12};
13use crate::{ContentStoreId, PrincipalScope, WalNo, WriterEpoch};
14use serde::{Deserialize, Serialize};
15use std::fmt;
16
17pub const NAMESPACE_MANIFEST_FORMAT_VERSION: u32 = 1;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum NamespaceManifestKind {
27 NamespaceManifest,
29}
30
31impl NamespaceManifestKind {
32 pub const fn as_str(self) -> &'static str {
34 match self {
35 Self::NamespaceManifest => "namespace_manifest",
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum MetadataRowFamily {
46 Inodes,
48 DirentryBinds,
50 DirentryChildBinds,
52 DirentryUnbinds,
54 Revisions,
56 Tombstones,
58 ActiveDeletions,
61 CommitReceipts,
63 ContentPublications,
65 Attributes,
70 Access,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
79#[serde(rename_all = "snake_case")]
80pub enum MetadataFamilyGroup {
81 Bindings,
83 Revisions,
85 Inodes,
87 Tombstones,
89 ActiveDeletions,
91 CommitReceipts,
93 ContentPublications,
95 Attributes,
97 Access,
99}
100
101impl MetadataFamilyGroup {
102 pub const ALL: [Self; 9] = [
104 Self::Bindings,
105 Self::Revisions,
106 Self::Inodes,
107 Self::Tombstones,
108 Self::ActiveDeletions,
109 Self::CommitReceipts,
110 Self::ContentPublications,
111 Self::Attributes,
112 Self::Access,
113 ];
114
115 pub const fn as_str(self) -> &'static str {
117 match self {
118 Self::Bindings => "bindings",
119 Self::Revisions => "revisions",
120 Self::Inodes => "inodes",
121 Self::Tombstones => "tombstones",
122 Self::ActiveDeletions => "active_deletions",
123 Self::CommitReceipts => "commit_receipts",
124 Self::ContentPublications => "content_publications",
125 Self::Attributes => "attributes",
126 Self::Access => "access",
127 }
128 }
129
130 pub const fn families(self) -> &'static [MetadataRowFamily] {
132 match self {
133 Self::Bindings => &[
134 MetadataRowFamily::DirentryBinds,
135 MetadataRowFamily::DirentryChildBinds,
136 MetadataRowFamily::DirentryUnbinds,
137 ],
138 Self::Revisions => &[MetadataRowFamily::Revisions],
139 Self::Inodes => &[MetadataRowFamily::Inodes],
140 Self::Tombstones => &[MetadataRowFamily::Tombstones],
141 Self::ActiveDeletions => &[MetadataRowFamily::ActiveDeletions],
142 Self::CommitReceipts => &[MetadataRowFamily::CommitReceipts],
143 Self::ContentPublications => &[MetadataRowFamily::ContentPublications],
144 Self::Attributes => &[MetadataRowFamily::Attributes],
145 Self::Access => &[MetadataRowFamily::Access],
146 }
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
152#[serde(rename_all = "snake_case")]
153pub enum RunTier {
154 Delta,
156 Base,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164#[serde(deny_unknown_fields)]
165pub struct MetadataRunRef {
166 pub run_no: RunNo,
168 pub run_seq: ChangeSeq,
170 pub tier: RunTier,
172 pub segments: Vec<MetadataSegmentRef>,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
180#[serde(deny_unknown_fields)]
181pub struct MetadataSegmentRef {
182 pub owner_namespace_id: NamespaceId,
184 pub segment_id: MetadataSegmentId,
186 pub family: MetadataRowFamily,
188 pub segment_index: u32,
190 pub row_count: u64,
192 pub min_row_key: String,
194 pub max_row_key: String,
196 pub index_block: BlockHandle,
200 pub filter_block: BlockHandle,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub filter_inline: Option<String>,
209 pub object_checksum: String,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
220pub enum MetadataRow {
221 Inode(InodeRecord),
223 DirentryBind(DirentryBindRecord),
225 DirentryUnbind(DirentryUnbindRecord),
227 FileRevision(RevisionRecord),
229 Tombstone(SubtreeTombstoneRecord),
231 ActiveDeletion(ActiveDeletionRecord),
237 CommitReceipt(CommitReceiptRecord),
239 ContentPublication(ContentPublicationRecord),
241 AttributesRevision(AttributesRevisionRecord),
248 AccessRevision(AccessRevisionRecord),
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258#[serde(deny_unknown_fields)]
259pub struct InodeRecord {
260 pub inode_id: InodeId,
262 pub inode_kind: InodeKind,
264 pub created_seq: ChangeSeq,
266 pub commit_id: CommitId,
268 pub created_by: crate::ActorId,
270 pub created_at_ms: u64,
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
276#[serde(deny_unknown_fields)]
277pub struct DirentryBindRecord {
278 pub parent_inode_id: InodeId,
280 pub name_key: NameKey,
282 pub display_name: DisplayName,
284 pub child_inode_id: InodeId,
286 pub bind_seq: ChangeSeq,
288 pub bind_delta_index: u32,
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
294#[serde(deny_unknown_fields)]
295pub struct DirentryUnbindRecord {
296 pub parent_inode_id: InodeId,
298 pub name_key: NameKey,
300 pub display_name: DisplayName,
302 pub child_inode_id: InodeId,
304 pub bind_seq: ChangeSeq,
306 pub bind_delta_index: u32,
308 pub unbind_seq: ChangeSeq,
310 pub unbind_delta_index: u32,
312}
313
314#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
316#[serde(deny_unknown_fields)]
317pub struct RevisionRecord {
318 pub inode_id: InodeId,
320 pub revision_no: RevisionNo,
322 pub committed_seq: ChangeSeq,
324 pub commit_id: CommitId,
326 pub committed_at_ms: u64,
328 pub committed_by: crate::ActorId,
330 pub delta_index: u32,
332 pub content_ref: ContentRef,
334}
335
336#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
338#[serde(deny_unknown_fields)]
339pub struct SubtreeTombstoneRecord {
340 pub root_inode_id: InodeId,
342 pub generation: TombstoneGeneration,
344 pub commit_id: CommitId,
346 pub action: TombstoneRowAction,
348 pub deleted_at_ms: u64,
350 pub deleted_by: crate::ActorId,
352}
353
354#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
356#[serde(deny_unknown_fields)]
357pub struct ActiveDeletionRecord {
358 pub root_inode_id: InodeId,
360 pub deletion_seq: ChangeSeq,
362 pub action: ActiveDeletionRowAction,
364}
365
366impl ActiveDeletionRecord {
367 pub fn row_key(&self) -> String {
369 lookup_keys::active_deletion_row_key(
370 self.deletion_seq,
371 self.root_inode_id,
372 self.action.sort_rank(),
373 )
374 }
375}
376
377#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
379#[serde(deny_unknown_fields)]
380pub struct ContentPublicationRecord {
381 pub content_id: ContentId,
383 pub committed_seq: ChangeSeq,
385 pub delta_index: u32,
387}
388
389#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
391#[serde(deny_unknown_fields)]
392pub struct CommitReceiptRecord {
393 pub commit_id: CommitId,
395 pub committed_by: crate::ActorId,
397 pub semantic_commit_fingerprint: crate::CommitFingerprint,
399 pub committed_seq: ChangeSeq,
401 pub committed_at_ms: u64,
403 #[serde(default, skip_serializing_if = "Option::is_none")]
405 pub message: Option<String>,
406}
407
408#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
410#[serde(deny_unknown_fields)]
411pub struct AttributesRevisionRecord {
412 pub inode_id: InodeId,
414 pub attributes_revision_no: AttributeRevisionNo,
416 pub committed_seq: ChangeSeq,
418 pub commit_id: CommitId,
420 pub delta_index: u32,
422 pub updated_by: crate::ActorId,
424 pub updated_at_ms: u64,
426 pub attributes: Attributes,
428}
429
430#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
432#[serde(deny_unknown_fields)]
433pub struct AccessRevisionRecord {
434 pub inode_id: InodeId,
436 pub access_revision_no: AccessRevisionNo,
438 pub committed_seq: ChangeSeq,
440 pub commit_id: CommitId,
442 pub delta_index: u32,
444 pub updated_by: crate::ActorId,
446 pub updated_at_ms: u64,
448 pub boundary: bool,
450 pub grants: AccessGrants,
452}
453
454#[derive(
460 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
461)]
462#[serde(deny_unknown_fields)]
463pub struct TombstoneGeneration {
464 pub seq: ChangeSeq,
466 pub delta_index: u32,
468}
469
470#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
475#[serde(deny_unknown_fields)]
476pub struct DeletedDirentry {
477 pub parent_inode_id: InodeId,
479 pub name_key: NameKey,
481 pub display_name: DisplayName,
483}
484
485#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
487#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
488pub enum TombstoneRowAction {
489 Set {
491 deleted_direntry: DeletedDirentry,
493 },
494 Revoke {
497 target: TombstoneGeneration,
499 },
500}
501
502#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
509#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
510pub enum ActiveDeletionRowAction {
511 Listed {
514 inode_kind: InodeKind,
516 deleted_at_ms: u64,
519 deleted_by: crate::ActorId,
521 deleted_direntry: DeletedDirentry,
524 },
525 Removed {
527 revocation_seq: ChangeSeq,
529 },
530}
531
532impl ActiveDeletionRowAction {
533 fn sort_rank(&self) -> u32 {
536 match self {
537 Self::Removed { .. } => lookup_keys::ACTIVE_DELETION_RANK_REMOVED,
538 Self::Listed { .. } => lookup_keys::ACTIVE_DELETION_RANK_LISTED,
539 }
540 }
541}
542
543impl MetadataRowFamily {
544 pub const fn as_str(self) -> &'static str {
546 match self {
547 Self::Inodes => "inodes",
548 Self::DirentryBinds => "direntry_binds",
549 Self::DirentryChildBinds => "direntry_child_binds",
550 Self::DirentryUnbinds => "direntry_unbinds",
551 Self::Revisions => "revisions",
552 Self::Tombstones => "tombstones",
553 Self::ActiveDeletions => "active_deletions",
554 Self::CommitReceipts => "commit_receipts",
555 Self::ContentPublications => "content_publications",
556 Self::Attributes => "attributes",
557 Self::Access => "access",
558 }
559 }
560
561 pub const fn row_key_prefix(self) -> &'static str {
565 match self {
566 Self::Inodes => lookup_keys::INODE_ROW_PREFIX,
567 Self::DirentryBinds => lookup_keys::DIRENTRY_BIND_ROW_PREFIX,
568 Self::DirentryChildBinds => lookup_keys::DIRENTRY_CHILD_BIND_ROW_PREFIX,
569 Self::DirentryUnbinds => lookup_keys::DIRENTRY_UNBIND_ROW_PREFIX,
570 Self::Revisions => lookup_keys::REVISION_ROW_PREFIX,
571 Self::Tombstones => lookup_keys::TOMBSTONE_ROW_PREFIX,
572 Self::ActiveDeletions => lookup_keys::ACTIVE_DELETION_ROW_PREFIX,
573 Self::CommitReceipts => lookup_keys::COMMIT_RECEIPT_ROW_PREFIX,
574 Self::ContentPublications => lookup_keys::CONTENT_PUBLICATION_ROW_PREFIX,
575 Self::Attributes => lookup_keys::ATTRIBUTE_ROW_PREFIX,
576 Self::Access => lookup_keys::ACCESS_ROW_PREFIX,
577 }
578 }
579}
580
581impl MetadataRow {
582 pub fn row_key(&self) -> String {
586 self.row_key_for_family(match self {
587 Self::Inode(_) => MetadataRowFamily::Inodes,
588 Self::DirentryBind(_) => MetadataRowFamily::DirentryBinds,
589 Self::DirentryUnbind(_) => MetadataRowFamily::DirentryUnbinds,
590 Self::FileRevision(_) => MetadataRowFamily::Revisions,
591 Self::Tombstone(_) => MetadataRowFamily::Tombstones,
592 Self::ActiveDeletion(_) => MetadataRowFamily::ActiveDeletions,
593 Self::CommitReceipt(_) => MetadataRowFamily::CommitReceipts,
594 Self::ContentPublication(_) => MetadataRowFamily::ContentPublications,
595 Self::AttributesRevision(_) => MetadataRowFamily::Attributes,
596 Self::AccessRevision(_) => MetadataRowFamily::Access,
597 })
598 }
599
600 pub fn row_key_for_family(&self, family: MetadataRowFamily) -> String {
604 match self {
605 Self::Inode(record) => lookup_keys::inode_key(record.inode_id),
606 Self::DirentryBind(record) => match family {
607 MetadataRowFamily::DirentryBinds => Some(lookup_keys::direntry_bind_row_key(
608 record.parent_inode_id,
609 record.name_key.as_str(),
610 record.bind_seq,
611 record.bind_delta_index,
612 )),
613 MetadataRowFamily::DirentryChildBinds => {
614 Some(lookup_keys::direntry_child_bind_row_key(
615 record.child_inode_id,
616 record.bind_seq,
617 record.bind_delta_index,
618 record.parent_inode_id,
619 record.name_key.as_str(),
620 ))
621 }
622 MetadataRowFamily::Inodes
623 | MetadataRowFamily::DirentryUnbinds
624 | MetadataRowFamily::Revisions
625 | MetadataRowFamily::Tombstones
626 | MetadataRowFamily::ActiveDeletions
627 | MetadataRowFamily::CommitReceipts
628 | MetadataRowFamily::ContentPublications
629 | MetadataRowFamily::Attributes
630 | MetadataRowFamily::Access => None,
631 }
632 .expect("a direntry bind row should use a direntry bind family"),
633 Self::DirentryUnbind(record) => lookup_keys::direntry_unbind_row_key(
634 record.parent_inode_id,
635 record.name_key.as_str(),
636 record.bind_seq,
637 record.bind_delta_index,
638 record.unbind_seq,
639 record.unbind_delta_index,
640 ),
641 Self::FileRevision(record) => lookup_keys::revision_row_key(
642 record.inode_id,
643 record.revision_no,
644 record.committed_seq,
645 record.delta_index,
646 ),
647 Self::Tombstone(record) => {
648 lookup_keys::tombstone_row_key(record.root_inode_id, record.generation)
649 }
650 Self::ActiveDeletion(record) => lookup_keys::active_deletion_row_key(
651 record.deletion_seq,
652 record.root_inode_id,
653 record.action.sort_rank(),
654 ),
655 Self::CommitReceipt(record) => {
656 lookup_keys::commit_receipt_row_key(record.commit_id.as_str(), record.committed_seq)
657 }
658 Self::ContentPublication(record) => {
659 lookup_keys::content_publication_row_key(&record.content_id, record.committed_seq)
660 }
661 Self::AttributesRevision(record) => lookup_keys::attributes_row_key(
662 record.inode_id,
663 record.attributes_revision_no,
664 record.committed_seq,
665 record.delta_index,
666 ),
667 Self::AccessRevision(record) => lookup_keys::access_row_key(
668 record.inode_id,
669 record.access_revision_no,
670 record.committed_seq,
671 record.delta_index,
672 ),
673 }
674 }
675
676 pub fn filter_key_for_family(&self, family: MetadataRowFamily) -> String {
678 match self {
679 Self::Inode(_) => self.row_key_for_family(family),
680 Self::DirentryBind(record) => match family {
681 MetadataRowFamily::DirentryBinds => Some(lookup_keys::direntry_bind_probe(
682 record.parent_inode_id,
683 record.name_key.as_str(),
684 )),
685 MetadataRowFamily::DirentryChildBinds => {
686 Some(lookup_keys::direntry_child_probe(record.child_inode_id))
687 }
688 MetadataRowFamily::Inodes
689 | MetadataRowFamily::DirentryUnbinds
690 | MetadataRowFamily::Revisions
691 | MetadataRowFamily::Tombstones
692 | MetadataRowFamily::ActiveDeletions
693 | MetadataRowFamily::CommitReceipts
694 | MetadataRowFamily::ContentPublications
695 | MetadataRowFamily::Attributes
696 | MetadataRowFamily::Access => None,
697 }
698 .expect("a direntry bind row should use a direntry bind family"),
699 Self::DirentryUnbind(record) => {
700 lookup_keys::direntry_unbind_probe(record.parent_inode_id, record.name_key.as_str())
701 }
702 Self::FileRevision(record) => lookup_keys::revision_probe(record.inode_id),
703 Self::Tombstone(record) => lookup_keys::tombstone_probe(record.root_inode_id),
704 Self::ActiveDeletion(_) => self.row_key_for_family(family),
707 Self::CommitReceipt(record) => {
708 lookup_keys::commit_receipt_probe(record.commit_id.as_str())
709 }
710 Self::ContentPublication(record) => {
711 lookup_keys::content_publication_probe(&record.content_id)
712 }
713 Self::AttributesRevision(record) => lookup_keys::attributes_probe(record.inode_id),
714 Self::AccessRevision(record) => lookup_keys::access_probe(record.inode_id),
715 }
716 }
717}
718
719pub fn hex_encode_row_key_component(value: &str) -> String {
723 crate::hex::hex_encode_bytes(value.as_bytes())
724}
725
726pub mod lookup_keys {
730 use super::{hex_encode_row_key_component, TombstoneGeneration};
731 use crate::{AccessRevisionNo, AttributeRevisionNo, ChangeSeq, ContentId, InodeId, RevisionNo};
732
733 pub const INODE_ROW_PREFIX: &str = "inode-";
735
736 pub const REVISION_ROW_PREFIX: &str = "revision-";
738
739 pub(super) const DIRENTRY_BIND_ROW_PREFIX: &str = "direntry-bind-";
740 pub(super) const DIRENTRY_CHILD_BIND_ROW_PREFIX: &str = "direntry-child-bind-";
741 pub(super) const DIRENTRY_UNBIND_ROW_PREFIX: &str = "direntry-unbind-";
742 pub(super) const TOMBSTONE_ROW_PREFIX: &str = "tombstone-";
743 pub(super) const CONTENT_PUBLICATION_ROW_PREFIX: &str = "content-publication-";
744 pub(super) const COMMIT_RECEIPT_ROW_PREFIX: &str = "commit-receipt-";
745 pub(super) const ATTRIBUTE_ROW_PREFIX: &str = "attribute-";
746 pub(super) const ACCESS_ROW_PREFIX: &str = "access-";
747
748 pub fn after_row_key(row_key: &str) -> String {
750 format!("{row_key}\0")
751 }
752
753 pub fn inode_key(inode_id: InodeId) -> String {
755 format!("{INODE_ROW_PREFIX}{:020}", inode_id.0)
756 }
757
758 pub fn inode_key_after(inode_id: InodeId) -> String {
760 after_row_key(&inode_key(inode_id))
761 }
762
763 pub fn direntry_parent_prefix(parent_inode_id: InodeId) -> String {
765 format!("{DIRENTRY_BIND_ROW_PREFIX}{:020}-", parent_inode_id.0)
766 }
767
768 pub fn direntry_bind_probe(parent_inode_id: InodeId, name_key: &str) -> String {
770 format!(
771 "{}{}",
772 direntry_parent_prefix(parent_inode_id),
773 hex_encode_row_key_component(name_key)
774 )
775 }
776
777 pub fn direntry_bind_prefix(parent_inode_id: InodeId, name_key: &str) -> String {
779 format!("{}-", direntry_bind_probe(parent_inode_id, name_key))
780 }
781
782 pub fn direntry_bind_row_key(
784 parent_inode_id: InodeId,
785 name_key: &str,
786 bind_seq: ChangeSeq,
787 bind_delta_index: u32,
788 ) -> String {
789 format!(
790 "{}{:020}-{bind_delta_index:010}",
791 direntry_bind_prefix(parent_inode_id, name_key),
792 bind_seq.0
793 )
794 }
795
796 pub fn direntry_child_probe(child_inode_id: InodeId) -> String {
798 format!("{DIRENTRY_CHILD_BIND_ROW_PREFIX}{:020}", child_inode_id.0)
799 }
800
801 pub fn direntry_child_prefix(child_inode_id: InodeId) -> String {
803 format!("{}-", direntry_child_probe(child_inode_id))
804 }
805
806 pub(super) fn direntry_child_bind_row_key(
808 child_inode_id: InodeId,
809 bind_seq: ChangeSeq,
810 bind_delta_index: u32,
811 parent_inode_id: InodeId,
812 name_key: &str,
813 ) -> String {
814 format!(
815 "{}{:020}-{bind_delta_index:010}-{:020}-{}",
816 direntry_child_prefix(child_inode_id),
817 bind_seq.0,
818 parent_inode_id.0,
819 hex_encode_row_key_component(name_key)
820 )
821 }
822
823 pub fn direntry_unbind_probe(parent_inode_id: InodeId, name_key: &str) -> String {
825 format!(
826 "{}{}",
827 direntry_unbind_parent_prefix(parent_inode_id),
828 hex_encode_row_key_component(name_key)
829 )
830 }
831
832 pub fn direntry_unbind_binding_prefix(
834 parent_inode_id: InodeId,
835 name_key: &str,
836 bind_seq: ChangeSeq,
837 bind_delta_index: u32,
838 ) -> String {
839 format!(
840 "{}{:020}-{bind_delta_index:010}-",
841 direntry_unbind_name_prefix(parent_inode_id, name_key),
842 bind_seq.0
843 )
844 }
845
846 pub(super) fn direntry_unbind_row_key(
848 parent_inode_id: InodeId,
849 name_key: &str,
850 bind_seq: ChangeSeq,
851 bind_delta_index: u32,
852 unbind_seq: ChangeSeq,
853 unbind_delta_index: u32,
854 ) -> String {
855 format!(
856 "{}{:020}-{unbind_delta_index:010}",
857 direntry_unbind_binding_prefix(parent_inode_id, name_key, bind_seq, bind_delta_index),
858 unbind_seq.0
859 )
860 }
861
862 pub(super) fn direntry_unbind_parent_prefix(parent_inode_id: InodeId) -> String {
864 format!("{DIRENTRY_UNBIND_ROW_PREFIX}{:020}-", parent_inode_id.0)
865 }
866
867 pub fn direntry_unbind_name_prefix(parent_inode_id: InodeId, name_key: &str) -> String {
869 format!("{}-", direntry_unbind_probe(parent_inode_id, name_key))
870 }
871
872 pub fn tombstone_probe(root_inode_id: InodeId) -> String {
874 format!("{TOMBSTONE_ROW_PREFIX}{:020}", root_inode_id.0)
875 }
876
877 pub fn tombstone_prefix(root_inode_id: InodeId) -> String {
879 format!("{}-", tombstone_probe(root_inode_id))
880 }
881
882 pub(super) fn tombstone_row_key(
887 root_inode_id: InodeId,
888 generation: TombstoneGeneration,
889 ) -> String {
890 format!(
891 "{}{:020}-{:010}",
892 tombstone_prefix(root_inode_id),
893 generation.seq.0,
894 generation.delta_index
895 )
896 }
897
898 pub const ACTIVE_DELETION_ROW_PREFIX: &str = "active-deletion-";
900
901 pub(super) const ACTIVE_DELETION_RANK_REMOVED: u32 = 0;
906
907 pub(super) const ACTIVE_DELETION_RANK_LISTED: u32 = 1;
910
911 pub(super) fn active_deletion_row_key(
913 deletion_seq: ChangeSeq,
914 root_inode_id: InodeId,
915 sort_rank: u32,
916 ) -> String {
917 format!(
918 "{ACTIVE_DELETION_ROW_PREFIX}{:020}-{:020}-{sort_rank:010}",
919 deletion_seq.0, root_inode_id.0
920 )
921 }
922
923 pub fn active_deletion_key_after(deletion_seq: ChangeSeq, root_inode_id: InodeId) -> String {
925 after_row_key(&active_deletion_row_key(
926 deletion_seq,
927 root_inode_id,
928 ACTIVE_DELETION_RANK_LISTED,
929 ))
930 }
931
932 pub fn content_publication_probe(content_id: &ContentId) -> String {
934 format!("{CONTENT_PUBLICATION_ROW_PREFIX}{content_id}")
935 }
936
937 pub fn content_publication_prefix(content_id: &ContentId) -> String {
939 format!("{}-", content_publication_probe(content_id))
940 }
941
942 pub(super) fn content_publication_row_key(
944 content_id: &ContentId,
945 committed_seq: ChangeSeq,
946 ) -> String {
947 format!(
948 "{}{:020}",
949 content_publication_prefix(content_id),
950 committed_seq.0
951 )
952 }
953
954 pub fn commit_receipt_probe(commit_id: &str) -> String {
956 format!(
957 "{COMMIT_RECEIPT_ROW_PREFIX}{}",
958 hex_encode_row_key_component(commit_id)
959 )
960 }
961
962 pub fn commit_receipt_prefix(commit_id: &str) -> String {
964 format!("{}-", commit_receipt_probe(commit_id))
965 }
966
967 pub(super) fn commit_receipt_row_key(commit_id: &str, committed_seq: ChangeSeq) -> String {
969 format!(
970 "{}{:020}",
971 commit_receipt_prefix(commit_id),
972 committed_seq.0
973 )
974 }
975
976 pub fn revision_probe(inode_id: InodeId) -> String {
978 format!("{REVISION_ROW_PREFIX}{:020}", inode_id.0)
979 }
980
981 pub fn revision_prefix(inode_id: InodeId) -> String {
983 format!("{}-", revision_probe(inode_id))
984 }
985
986 pub fn revision_number_prefix(inode_id: InodeId, revision_no: RevisionNo) -> String {
988 format!(
989 "{}{:020}-",
990 revision_prefix(inode_id),
991 u64::MAX - revision_no.0
992 )
993 }
994
995 pub fn revision_row_key(
997 inode_id: InodeId,
998 revision_no: RevisionNo,
999 committed_seq: ChangeSeq,
1000 delta_index: u32,
1001 ) -> String {
1002 format!(
1003 "{}{:020}-{:010}",
1004 revision_number_prefix(inode_id, revision_no),
1005 u64::MAX - committed_seq.0,
1006 u32::MAX - delta_index
1007 )
1008 }
1009
1010 pub fn attributes_probe(inode_id: InodeId) -> String {
1012 format!("{ATTRIBUTE_ROW_PREFIX}{:020}", inode_id.0)
1013 }
1014
1015 pub fn attributes_prefix(inode_id: InodeId) -> String {
1017 format!("{}-", attributes_probe(inode_id))
1018 }
1019
1020 pub(super) fn attributes_row_key(
1022 inode_id: InodeId,
1023 attributes_revision_no: AttributeRevisionNo,
1024 committed_seq: ChangeSeq,
1025 delta_index: u32,
1026 ) -> String {
1027 format!(
1028 "{}{:020}-{:020}-{:010}",
1029 attributes_prefix(inode_id),
1030 u64::MAX - attributes_revision_no.0,
1031 u64::MAX - committed_seq.0,
1032 u32::MAX - delta_index
1033 )
1034 }
1035
1036 pub fn access_probe(inode_id: InodeId) -> String {
1038 format!("{ACCESS_ROW_PREFIX}{:020}", inode_id.0)
1039 }
1040
1041 pub fn access_prefix(inode_id: InodeId) -> String {
1043 format!("{}-", access_probe(inode_id))
1044 }
1045
1046 pub(super) fn access_row_key(
1048 inode_id: InodeId,
1049 access_revision_no: AccessRevisionNo,
1050 committed_seq: ChangeSeq,
1051 delta_index: u32,
1052 ) -> String {
1053 format!(
1054 "{}{:020}-{:020}-{:010}",
1055 access_prefix(inode_id),
1056 u64::MAX - access_revision_no.0,
1057 u64::MAX - committed_seq.0,
1058 u32::MAX - delta_index
1059 )
1060 }
1061}
1062
1063#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1065#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1066#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1067pub enum NamespaceAccess {
1068 Unrestricted {},
1070 Acl {
1072 principal_scope: PrincipalScope,
1074 root_grants: AccessGrants,
1077 },
1078}
1079
1080impl NamespaceAccess {
1081 pub fn unrestricted() -> Self {
1083 Self::Unrestricted {}
1084 }
1085
1086 pub const fn is_unrestricted(&self) -> bool {
1088 matches!(self, Self::Unrestricted {})
1089 }
1090}
1091
1092#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1096#[serde(deny_unknown_fields)]
1097pub struct NamespaceManifestPayload {
1098 pub namespace_id: NamespaceId,
1100 pub content_store_id: ContentStoreId,
1102 pub created_at_ms: u64,
1104 pub created_by: ActorId,
1106 pub access: NamespaceAccess,
1108 #[serde(default, skip_serializing_if = "Option::is_none")]
1110 pub fork_basis: Option<ForkBasis>,
1111 pub status: NamespaceStatus,
1113 #[serde(default, skip_serializing_if = "Option::is_none")]
1115 pub writer: Option<WriterBlock>,
1116 pub last_folded_wal_no: WalNo,
1118 pub retention_floor_wal_no: WalNo,
1120 pub manifest_no: ManifestNo,
1122 pub compactor_epoch: u64,
1126 pub head_seq: ChangeSeq,
1128 pub head_commit_id: CommitId,
1130 pub base_seq: ChangeSeq,
1132 pub writer_epoch: WriterEpoch,
1134 pub next_inode_id: InodeId,
1136 pub next_run_no: RunNo,
1138 pub retention_floor_seq: ChangeSeq,
1140 pub runs: Vec<MetadataRunRef>,
1142}
1143
1144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1148pub struct ManifestIdentityDrift {
1149 pub field: String,
1151}
1152
1153impl fmt::Display for ManifestIdentityDrift {
1154 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1155 write!(
1156 formatter,
1157 "successor manifest changes the namespace's immutable `{}`",
1158 self.field
1159 )
1160 }
1161}
1162
1163impl std::error::Error for ManifestIdentityDrift {}
1164
1165impl NamespaceManifestPayload {
1166 pub fn initial(
1168 namespace_id: NamespaceId,
1169 content_store_id: ContentStoreId,
1170 created_at_ms: u64,
1171 created_by: ActorId,
1172 access: NamespaceAccess,
1173 ) -> Self {
1174 Self {
1175 namespace_id,
1176 content_store_id,
1177 created_at_ms,
1178 created_by,
1179 access,
1180 fork_basis: None,
1181 status: NamespaceStatus::Active {},
1182 writer: None,
1183 manifest_no: ManifestNo(1),
1184 compactor_epoch: 0,
1185 head_seq: ChangeSeq(0),
1186 head_commit_id: crate::control::genesis_commit_id(),
1187 base_seq: ChangeSeq(0),
1188 writer_epoch: WriterEpoch(0),
1189 next_inode_id: crate::FIRST_ALLOCATABLE_INODE_ID,
1190 next_run_no: RunNo(0),
1191 last_folded_wal_no: WalNo(0),
1192 retention_floor_wal_no: WalNo(0),
1193 retention_floor_seq: ChangeSeq(0),
1194 runs: Vec::new(),
1195 }
1196 }
1197
1198 pub fn ensure_successor_identity(
1200 &self,
1201 successor: &NamespaceManifestPayload,
1202 ) -> Result<(), ManifestIdentityDrift> {
1203 let drift = |field: &str| {
1204 Err(ManifestIdentityDrift {
1205 field: field.to_owned(),
1206 })
1207 };
1208 if successor.namespace_id != self.namespace_id {
1209 return drift("namespace_id");
1210 }
1211 if successor.content_store_id != self.content_store_id {
1212 return drift("content_store_id");
1213 }
1214 if successor.created_at_ms != self.created_at_ms {
1215 return drift("created_at_ms");
1216 }
1217 if successor.created_by != self.created_by {
1218 return drift("created_by");
1219 }
1220 if successor.access != self.access {
1221 return drift("access");
1222 }
1223 if successor.fork_basis != self.fork_basis {
1224 return drift("fork_basis");
1225 }
1226 if self.status.is_deleted() && !successor.status.is_deleted() {
1227 return drift("status");
1228 }
1229 if self.status.reclaim_after_ms().is_some()
1230 && self.status.reclaim_after_ms() != successor.status.reclaim_after_ms()
1231 {
1232 return drift("reclaim_after_ms");
1233 }
1234 Ok(())
1235 }
1236}
1237
1238pub type NamespaceManifestEnvelope = crate::envelope::VerifiedEnvelope<NamespaceManifestPayload>;
1240
1241pub fn encode_namespace_manifest_json(
1243 payload: NamespaceManifestPayload,
1244) -> Result<crate::envelope::EncodedEnvelope<NamespaceManifestPayload>, EnvelopeCodecError> {
1245 crate::envelope::encode_json_envelope(
1246 NamespaceManifestKind::NamespaceManifest.as_str(),
1247 NAMESPACE_MANIFEST_FORMAT_VERSION,
1248 payload,
1249 )
1250}
1251
1252pub fn decode_namespace_manifest_json(
1258 bytes: &[u8],
1259) -> Result<NamespaceManifestEnvelope, EnvelopeCodecError> {
1260 let expected_kind = NamespaceManifestKind::NamespaceManifest;
1261 let decoded =
1262 crate::envelope::decode_json_envelope(bytes, NAMESPACE_MANIFEST_FORMAT_VERSION, |found| {
1263 crate::envelope::verify_kind(expected_kind.as_str(), found)
1264 })?;
1265
1266 Ok(decoded)
1267}
1268
1269#[cfg(test)]
1270mod tests {
1271 use super::{
1272 decode_namespace_manifest_json, encode_namespace_manifest_json, BlockHandle,
1273 MetadataRowFamily, MetadataRunRef, MetadataSegmentRef, NamespaceManifestPayload, RunTier,
1274 };
1275 use crate::{
1276 ChangeSeq, CommitId, InodeId, ManifestNo, MetadataSegmentId, NameKey, NamespaceId, RunNo,
1277 WriterEpoch,
1278 };
1279
1280 fn row_commit_id() -> CommitId {
1281 CommitId::parse("c_metadata_row").expect("commit id")
1282 }
1283
1284 fn deleted_direntry() -> super::DeletedDirentry {
1285 super::DeletedDirentry {
1286 parent_inode_id: InodeId(9),
1287 name_key: NameKey::parse("report.txt").expect("valid name key"),
1288 display_name: crate::DisplayName::parse("report.txt").expect("valid display name"),
1289 }
1290 }
1291
1292 #[test]
1293 fn successor_preserves_identity_and_terminal_status() {
1294 let initial = NamespaceManifestPayload::initial(
1295 NamespaceId::parse("original").expect("namespace"),
1296 crate::ContentStoreId::parse("cs_00000000000000000000000000000001")
1297 .expect("content store"),
1298 1_000,
1299 crate::ActorId::parse("test").expect("actor"),
1300 super::NamespaceAccess::Unrestricted {},
1301 );
1302 for (field, change) in [
1303 ("namespace_id", 0),
1304 ("content_store_id", 1),
1305 ("created_at_ms", 2),
1306 ("fork_basis", 3),
1307 ("access", 4),
1308 ] {
1309 let mut successor = initial.clone();
1310 match change {
1311 0 => successor.namespace_id = NamespaceId::parse("changed").expect("namespace"),
1312 1 => {
1313 successor.content_store_id =
1314 crate::ContentStoreId::parse("cs_00000000000000000000000000000002")
1315 .expect("content store")
1316 }
1317 2 => successor.created_at_ms += 1,
1318 3 => {
1319 successor.fork_basis = Some(crate::control::ForkBasis {
1320 manifest: crate::control::ManifestRef {
1321 owner_namespace_id: NamespaceId::parse("source").expect("namespace"),
1322 manifest_no: ManifestNo(1),
1323 manifest_head_seq: ChangeSeq(0),
1324 manifest_payload_checksum: "sha256:source".to_owned(),
1325 },
1326 source_checkpoint_id: crate::CheckpointId::parse(
1327 "pin_00000000000000000001-0000000000000001",
1328 )
1329 .expect("checkpoint"),
1330 })
1331 }
1332 _ => {
1333 successor.access = super::NamespaceAccess::Acl {
1334 principal_scope: crate::PrincipalScope::parse("org_test").expect("scope"),
1335 root_grants: crate::AccessGrants::default(),
1336 }
1337 }
1338 }
1339 assert_eq!(
1340 initial
1341 .ensure_successor_identity(&successor)
1342 .expect_err("identity drift")
1343 .field,
1344 field
1345 );
1346 }
1347 let mut deleted = initial.clone();
1348 deleted.status = crate::control::NamespaceStatus::Deleted {
1349 reclaim_after_ms: None,
1350 };
1351 initial.ensure_successor_identity(&deleted).expect("delete");
1352 assert!(deleted.ensure_successor_identity(&initial).is_err());
1353 let mut retired = deleted.clone();
1354 retired.status = crate::control::NamespaceStatus::Deleted {
1355 reclaim_after_ms: Some(2_000),
1356 };
1357 deleted.ensure_successor_identity(&retired).expect("retire");
1358 retired
1359 .ensure_successor_identity(&retired)
1360 .expect("same deadline");
1361 for deadline in [None, Some(1_999), Some(2_001)] {
1362 let mut successor = retired.clone();
1363 successor.status = crate::control::NamespaceStatus::Deleted {
1364 reclaim_after_ms: deadline,
1365 };
1366 assert_eq!(
1367 retired
1368 .ensure_successor_identity(&successor)
1369 .expect_err("fixed deadline")
1370 .field,
1371 "reclaim_after_ms"
1372 );
1373 }
1374 }
1375
1376 #[test]
1377 fn inode_row_keys_sort_by_ascending_inode_id() {
1378 let ids = [9_u64, 1, 100, 10, 2];
1381 let key_of = |id: u64| super::lookup_keys::inode_key(InodeId(id));
1382 let mut keys: Vec<String> = ids.iter().copied().map(key_of).collect();
1383 keys.sort();
1384
1385 let mut ascending_ids = ids;
1386 ascending_ids.sort_unstable();
1387 assert_eq!(
1388 keys,
1389 ascending_ids
1390 .iter()
1391 .copied()
1392 .map(key_of)
1393 .collect::<Vec<_>>(),
1394 "row-key order must agree with inode-id order"
1395 );
1396 assert!(keys
1397 .iter()
1398 .all(|key| key.starts_with(super::lookup_keys::INODE_ROW_PREFIX)));
1399 }
1400
1401 #[test]
1402 fn the_inode_resume_bound_skips_its_own_row_and_nothing_after_it() {
1403 let resume = super::lookup_keys::inode_key_after(InodeId(7));
1404 assert!(resume > super::lookup_keys::inode_key(InodeId(7)));
1405 assert!(resume < super::lookup_keys::inode_key(InodeId(8)));
1406 }
1407
1408 #[test]
1409 fn namespace_manifest_kind_string_matches_serde() {
1410 let kind = super::NamespaceManifestKind::NamespaceManifest;
1411 let serialized = serde_json::to_value(kind).expect("serialize kind");
1412 assert_eq!(serialized, serde_json::Value::from(kind.as_str()));
1413 }
1414
1415 #[test]
1416 fn namespace_manifest_codec_round_trips_base_only_materialization() {
1417 let (envelope, encoded) = encode_namespace_manifest_json(NamespaceManifestPayload {
1418 content_store_id: crate::ContentStoreId::parse("cs_0123456789abcdef0123456789abcdef")
1419 .expect("content store"),
1420 created_at_ms: 1_000,
1421 created_by: crate::ActorId::parse("test").expect("actor"),
1422 access: super::NamespaceAccess::Unrestricted {},
1423 fork_basis: None,
1424 status: crate::control::NamespaceStatus::Active {},
1425 writer: None,
1426 last_folded_wal_no: crate::WalNo(0),
1427 retention_floor_wal_no: crate::WalNo(0),
1428 compactor_epoch: 0,
1429 namespace_id: NamespaceId::parse("demo").expect("valid namespace id"),
1430 manifest_no: ManifestNo(10),
1431
1432 head_seq: ChangeSeq(10),
1433 head_commit_id: CommitId::parse("c_00000000000000000000000000000001")
1434 .expect("commit id"),
1435 base_seq: ChangeSeq(10),
1436 writer_epoch: WriterEpoch(2),
1437 next_inode_id: InodeId(42),
1438 next_run_no: RunNo(1),
1439 retention_floor_seq: ChangeSeq(0),
1440 runs: vec![metadata_run_ref(
1441 "demo",
1442 "seg_00000000000000000000000000000001",
1443 RunNo(0),
1444 ChangeSeq(10),
1445 RunTier::Base,
1446 )],
1447 })
1448 .expect("manifest")
1449 .into_parts();
1450 let document: serde_json::Value =
1451 serde_json::from_slice(&encoded).expect("decode manifest document");
1452 assert!(document["payload"]
1453 .get("frozen_base_delta_merges")
1454 .is_none());
1455 let decoded = decode_namespace_manifest_json(&encoded).expect("decode manifest");
1456
1457 assert_eq!(decoded, envelope);
1458 assert_eq!(decoded.payload.base_seq, ChangeSeq(10));
1459 assert_eq!(decoded.payload.runs.len(), 1);
1460 assert_eq!(decoded.payload.runs[0].run_seq, ChangeSeq(10));
1461 }
1462
1463 #[test]
1464 fn namespace_manifest_codec_round_trips_inherited_source_segments() {
1465 let (envelope, encoded) = encode_namespace_manifest_json(NamespaceManifestPayload {
1466 content_store_id: crate::ContentStoreId::parse("cs_0123456789abcdef0123456789abcdef")
1467 .expect("content store"),
1468 created_at_ms: 1_000,
1469 created_by: crate::ActorId::parse("test").expect("actor"),
1470 access: super::NamespaceAccess::Unrestricted {},
1471 fork_basis: None,
1472 status: crate::control::NamespaceStatus::Active {},
1473 writer: None,
1474 last_folded_wal_no: crate::WalNo(0),
1475 retention_floor_wal_no: crate::WalNo(0),
1476 compactor_epoch: 0,
1477 namespace_id: NamespaceId::parse("demo").expect("valid namespace id"),
1478 manifest_no: ManifestNo(12),
1479
1480 head_seq: ChangeSeq(12),
1481 head_commit_id: CommitId::parse("c_00000000000000000000000000000002")
1482 .expect("commit id"),
1483 base_seq: ChangeSeq(10),
1484 writer_epoch: WriterEpoch(2),
1485 next_inode_id: InodeId(42),
1486 next_run_no: RunNo(2),
1487 retention_floor_seq: ChangeSeq(0),
1488 runs: vec![
1489 metadata_run_ref(
1490 "source",
1491 "seg_00000000000000000000000000000001",
1492 RunNo(0),
1493 ChangeSeq(10),
1494 RunTier::Base,
1495 ),
1496 metadata_run_ref(
1497 "demo",
1498 "seg_00000000000000000000000000000002",
1499 RunNo(1),
1500 ChangeSeq(12),
1501 RunTier::Delta,
1502 ),
1503 ],
1504 })
1505 .expect("manifest")
1506 .into_parts();
1507 let decoded = decode_namespace_manifest_json(&encoded).expect("decode manifest");
1508
1509 assert_eq!(decoded, envelope);
1510 assert_eq!(decoded.payload.runs[0].tier, RunTier::Base);
1511 assert_eq!(decoded.payload.runs[1].tier, RunTier::Delta);
1512 assert_eq!(decoded.payload.runs[1].run_seq, ChangeSeq(12));
1513 assert_eq!(
1514 decoded.payload.runs[0].segments[0].owner_namespace_id,
1515 NamespaceId::parse("source").expect("valid namespace id")
1516 );
1517 }
1518
1519 #[test]
1520 fn direntry_bind_row_key_supports_parent_and_child_indexes() {
1521 let row = super::MetadataRow::DirentryBind(super::DirentryBindRecord {
1522 parent_inode_id: InodeId(9),
1523 name_key: NameKey::parse("report.txt").expect("valid name key"),
1524 display_name: crate::DisplayName::parse("Report.txt").expect("valid display name"),
1525 child_inode_id: InodeId(42),
1526 bind_seq: ChangeSeq(17),
1527 bind_delta_index: 3,
1528 });
1529
1530 assert_eq!(
1531 row.row_key_for_family(MetadataRowFamily::DirentryBinds),
1532 "direntry-bind-00000000000000000009-7265706f72742e747874-00000000000000000017-0000000003"
1533 );
1534 assert_eq!(
1535 row.row_key_for_family(MetadataRowFamily::DirentryChildBinds),
1536 "direntry-child-bind-00000000000000000042-00000000000000000017-0000000003-00000000000000000009-7265706f72742e747874"
1537 );
1538 }
1539
1540 #[test]
1541 fn row_keys_hex_encode_dash_containing_variable_components() {
1542 let row = super::MetadataRow::DirentryBind(super::DirentryBindRecord {
1543 parent_inode_id: InodeId(9),
1544 name_key: NameKey::parse("report-2024").expect("valid name key"),
1545 display_name: crate::DisplayName::parse("report-2024").expect("valid display name"),
1546 child_inode_id: InodeId(42),
1547 bind_seq: ChangeSeq(17),
1548 bind_delta_index: 3,
1549 });
1550
1551 assert_eq!(
1552 row.row_key_for_family(MetadataRowFamily::DirentryBinds),
1553 "direntry-bind-00000000000000000009-7265706f72742d32303234-00000000000000000017-0000000003"
1554 );
1555 }
1556
1557 #[test]
1558 fn revision_row_key_orders_newest_first_within_each_inode() {
1559 let row = super::MetadataRow::FileRevision(super::RevisionRecord {
1560 inode_id: InodeId(42),
1561 revision_no: crate::RevisionNo(7),
1562 committed_seq: ChangeSeq(12),
1563 commit_id: row_commit_id(),
1564 committed_at_ms: 12_000,
1565 committed_by: crate::ActorId::loonfs(),
1566 delta_index: 3,
1567 content_ref: crate::ContentRef::blob_v1(
1568 crate::NamespaceId::parse("demo").expect("namespace id"),
1569 crate::ContentId::parse("con_0123456789abcdef0123456789abcdef")
1570 .expect("valid content id"),
1571 b"row key sample",
1572 ),
1573 });
1574
1575 assert_eq!(
1576 row.row_key_for_family(MetadataRowFamily::Revisions),
1577 "revision-00000000000000000042-18446744073709551608-18446744073709551603-4294967292"
1578 );
1579 }
1580
1581 #[test]
1582 fn whole_state_row_keys_sort_newest_revision_first_under_the_inode_prefix() {
1583 let row_of = |revision: u64, seq: u64, delta_index: u32| {
1584 super::MetadataRow::AttributesRevision(super::AttributesRevisionRecord {
1585 inode_id: InodeId(42),
1586 attributes_revision_no: crate::AttributeRevisionNo(revision),
1587 committed_seq: ChangeSeq(seq),
1588 commit_id: row_commit_id(),
1589 delta_index,
1590 updated_by: crate::ActorId::loonfs(),
1591 updated_at_ms: 12_000 + seq,
1592 attributes: crate::Attributes::default(),
1593 })
1594 };
1595 let newest = row_of(3, 12, 1);
1596 let older = row_of(2, 11, 0);
1597
1598 assert_eq!(
1599 newest.row_key_for_family(MetadataRowFamily::Attributes),
1600 "attribute-00000000000000000042-18446744073709551612-18446744073709551603-4294967294"
1601 );
1602 assert_eq!(
1603 newest.row_key(),
1604 newest.row_key_for_family(MetadataRowFamily::Attributes)
1605 );
1606 assert!(
1607 newest.row_key() < older.row_key(),
1608 "an ascending scan must reach the newest revision first"
1609 );
1610 let prefix = super::lookup_keys::attributes_prefix(InodeId(42));
1611 assert!(newest.row_key().starts_with(&prefix));
1612 assert!(older.row_key().starts_with(&prefix));
1613 assert_eq!(
1616 newest.filter_key_for_family(MetadataRowFamily::Attributes),
1617 super::lookup_keys::attributes_probe(InodeId(42))
1618 );
1619 assert!(!row_of(3, 12, 1)
1621 .row_key()
1622 .starts_with(&super::lookup_keys::attributes_prefix(InodeId(43))));
1623
1624 let access_row = |revision, seq, delta_index| {
1625 super::MetadataRow::AccessRevision(super::AccessRevisionRecord {
1626 inode_id: InodeId(42),
1627 access_revision_no: crate::AccessRevisionNo(revision),
1628 committed_seq: ChangeSeq(seq),
1629 commit_id: crate::CommitId::parse("c_access").expect("commit"),
1630 delta_index,
1631 updated_by: crate::ActorId::loonfs(),
1632 updated_at_ms: 1_000,
1633 boundary: false,
1634 grants: crate::AccessGrants::default(),
1635 })
1636 };
1637 let newest = access_row(3, 12, 1);
1638 let older = access_row(2, 11, 0);
1639 assert_eq!(
1640 newest.row_key(),
1641 "access-00000000000000000042-18446744073709551612-18446744073709551603-4294967294"
1642 );
1643 assert!(newest.row_key() < older.row_key());
1644 assert_eq!(
1645 newest.filter_key_for_family(MetadataRowFamily::Access),
1646 super::lookup_keys::access_probe(InodeId(42))
1647 );
1648 }
1649
1650 #[test]
1651 fn row_key_prefixes_match_the_row_keys_they_front() {
1652 let name_key = NameKey::parse("report.txt").expect("valid name key");
1653 let display_name = crate::DisplayName::parse("report.txt").expect("valid display name");
1654 let bind = super::MetadataRow::DirentryBind(super::DirentryBindRecord {
1655 parent_inode_id: InodeId(9),
1656 name_key: name_key.clone(),
1657 display_name: display_name.clone(),
1658 child_inode_id: InodeId(42),
1659 bind_seq: ChangeSeq(17),
1660 bind_delta_index: 3,
1661 });
1662 let revision = super::MetadataRow::FileRevision(super::RevisionRecord {
1663 inode_id: InodeId(42),
1664 revision_no: crate::RevisionNo(7),
1665 committed_seq: ChangeSeq(12),
1666 commit_id: row_commit_id(),
1667 committed_at_ms: 12_000,
1668 committed_by: crate::ActorId::loonfs(),
1669 delta_index: 3,
1670 content_ref: crate::ContentRef::blob_v1(
1671 crate::NamespaceId::parse("demo").expect("namespace id"),
1672 crate::ContentId::parse("con_0123456789abcdef0123456789abcdef")
1673 .expect("valid content id"),
1674 b"row key prefix sample",
1675 ),
1676 });
1677 let rows: [(MetadataRowFamily, super::MetadataRow); 10] = [
1678 (
1679 MetadataRowFamily::Inodes,
1680 super::MetadataRow::Inode(super::InodeRecord {
1681 inode_id: InodeId(42),
1682 inode_kind: crate::InodeKind::File,
1683 created_seq: ChangeSeq(3),
1684 commit_id: row_commit_id(),
1685 created_by: crate::ActorId::loonfs(),
1686 created_at_ms: 3_000,
1687 }),
1688 ),
1689 (MetadataRowFamily::DirentryBinds, bind.clone()),
1690 (MetadataRowFamily::DirentryChildBinds, bind),
1691 (
1692 MetadataRowFamily::DirentryUnbinds,
1693 super::MetadataRow::DirentryUnbind(super::DirentryUnbindRecord {
1694 parent_inode_id: InodeId(9),
1695 name_key,
1696 display_name,
1697 child_inode_id: InodeId(42),
1698 bind_seq: ChangeSeq(17),
1699 bind_delta_index: 3,
1700 unbind_seq: ChangeSeq(19),
1701 unbind_delta_index: 0,
1702 }),
1703 ),
1704 (MetadataRowFamily::Revisions, revision),
1705 (
1706 MetadataRowFamily::ContentPublications,
1707 super::MetadataRow::ContentPublication(super::ContentPublicationRecord {
1708 content_id: crate::ContentId::parse("con_0123456789abcdef0123456789abcdef")
1709 .expect("valid content id"),
1710 committed_seq: ChangeSeq(12),
1711 delta_index: 3,
1712 }),
1713 ),
1714 (
1715 MetadataRowFamily::Tombstones,
1716 super::MetadataRow::Tombstone(super::SubtreeTombstoneRecord {
1717 root_inode_id: InodeId(42),
1718 generation: super::TombstoneGeneration {
1719 seq: ChangeSeq(12),
1720 delta_index: 0,
1721 },
1722 commit_id: row_commit_id(),
1723 action: super::TombstoneRowAction::Set {
1724 deleted_direntry: deleted_direntry(),
1725 },
1726 deleted_at_ms: 12_000,
1727 deleted_by: crate::ActorId::loonfs(),
1728 }),
1729 ),
1730 (
1731 MetadataRowFamily::ActiveDeletions,
1732 super::MetadataRow::ActiveDeletion(super::ActiveDeletionRecord {
1733 root_inode_id: InodeId(42),
1734 deletion_seq: ChangeSeq(12),
1735 action: super::ActiveDeletionRowAction::Removed {
1736 revocation_seq: ChangeSeq(15),
1737 },
1738 }),
1739 ),
1740 (
1741 MetadataRowFamily::CommitReceipts,
1742 super::MetadataRow::CommitReceipt(super::CommitReceiptRecord {
1743 commit_id: CommitId::parse("c_00000000000000000000000000000001")
1744 .expect("commit id"),
1745 committed_by: crate::ActorId::loonfs(),
1746 semantic_commit_fingerprint: serde_json::from_str(r#""sha256:unused""#)
1747 .expect("fingerprint"),
1748 committed_seq: ChangeSeq(12),
1749 committed_at_ms: 12_000,
1750 message: None,
1751 }),
1752 ),
1753 (
1754 MetadataRowFamily::Attributes,
1755 super::MetadataRow::AttributesRevision(super::AttributesRevisionRecord {
1756 inode_id: InodeId(42),
1757 attributes_revision_no: crate::AttributeRevisionNo(3),
1758 committed_seq: ChangeSeq(12),
1759 commit_id: row_commit_id(),
1760 delta_index: 0,
1761 updated_by: crate::ActorId::loonfs(),
1762 updated_at_ms: 12_000,
1763 attributes: crate::Attributes::default(),
1764 }),
1765 ),
1766 ];
1767
1768 for (family, row) in rows {
1769 let row_key = row.row_key_for_family(family);
1770 let prefix = family.row_key_prefix();
1771 assert!(
1772 !prefix.is_empty(),
1773 "`{family:?}` declares no row-key prefix"
1774 );
1775 assert!(
1776 row_key.starts_with(prefix),
1777 "row key `{row_key}` for `{family:?}` does not start with `{prefix}`"
1778 );
1779 }
1780 }
1781
1782 #[test]
1783 fn attribution_values_never_change_row_or_index_keys() {
1784 fn rows(actor: crate::ActorId) -> Vec<(MetadataRowFamily, super::MetadataRow)> {
1785 vec![
1786 (
1787 MetadataRowFamily::Inodes,
1788 super::MetadataRow::Inode(super::InodeRecord {
1789 inode_id: InodeId(42),
1790 inode_kind: crate::InodeKind::File,
1791 created_seq: ChangeSeq(3),
1792 commit_id: row_commit_id(),
1793 created_by: actor.clone(),
1794 created_at_ms: 3_000,
1795 }),
1796 ),
1797 (
1798 MetadataRowFamily::Revisions,
1799 super::MetadataRow::FileRevision(super::RevisionRecord {
1800 inode_id: InodeId(42),
1801 revision_no: crate::RevisionNo(7),
1802 committed_seq: ChangeSeq(12),
1803 commit_id: row_commit_id(),
1804 committed_at_ms: 12_000,
1805 committed_by: actor.clone(),
1806 delta_index: 3,
1807 content_ref: crate::ContentRef::blob_v1(
1808 crate::NamespaceId::parse("demo").expect("namespace id"),
1809 crate::ContentId::parse("con_0123456789abcdef0123456789abcdef")
1810 .expect("content id"),
1811 b"attribution key test",
1812 ),
1813 }),
1814 ),
1815 (
1816 MetadataRowFamily::Tombstones,
1817 super::MetadataRow::Tombstone(super::SubtreeTombstoneRecord {
1818 root_inode_id: InodeId(42),
1819 generation: super::TombstoneGeneration {
1820 seq: ChangeSeq(12),
1821 delta_index: 3,
1822 },
1823 commit_id: row_commit_id(),
1824 action: super::TombstoneRowAction::Set {
1825 deleted_direntry: deleted_direntry(),
1826 },
1827 deleted_at_ms: 12_000,
1828 deleted_by: actor.clone(),
1829 }),
1830 ),
1831 (
1832 MetadataRowFamily::ActiveDeletions,
1833 super::MetadataRow::ActiveDeletion(super::ActiveDeletionRecord {
1834 root_inode_id: InodeId(42),
1835 deletion_seq: ChangeSeq(12),
1836 action: super::ActiveDeletionRowAction::Listed {
1837 inode_kind: crate::InodeKind::File,
1838 deleted_at_ms: 12_000,
1839 deleted_by: actor.clone(),
1840 deleted_direntry: deleted_direntry(),
1841 },
1842 }),
1843 ),
1844 (
1845 MetadataRowFamily::Attributes,
1846 super::MetadataRow::AttributesRevision(super::AttributesRevisionRecord {
1847 inode_id: InodeId(42),
1848 attributes_revision_no: crate::AttributeRevisionNo(2),
1849 committed_seq: ChangeSeq(12),
1850 commit_id: row_commit_id(),
1851 delta_index: 3,
1852 updated_by: actor,
1853 updated_at_ms: 12_000,
1854 attributes: crate::Attributes::default(),
1855 }),
1856 ),
1857 ]
1858 }
1859
1860 let actors = [
1861 crate::ActorId::parse("auth0|x").expect("actor id"),
1862 crate::ActorId::parse("x".repeat(256)).expect("256-byte actor id"),
1863 crate::ActorId::parse("external|actor").expect("external actor id"),
1864 ];
1865 let baseline = rows(actors[0].clone());
1866 for actor in actors.into_iter().skip(1) {
1867 let changed = rows(actor);
1868 for ((family, baseline), (changed_family, changed)) in baseline.iter().zip(&changed) {
1869 assert_eq!(family, changed_family);
1870 assert_eq!(
1871 baseline.row_key_for_family(*family),
1872 changed.row_key_for_family(*family)
1873 );
1874 assert_eq!(
1875 baseline.filter_key_for_family(*family),
1876 changed.filter_key_for_family(*family)
1877 );
1878 }
1879 }
1880 }
1881
1882 fn metadata_run_ref(
1883 owner_namespace_id: &str,
1884 segment_id: &str,
1885 run_no: RunNo,
1886 run_seq: ChangeSeq,
1887 tier: RunTier,
1888 ) -> MetadataRunRef {
1889 MetadataRunRef {
1890 run_no,
1891 run_seq,
1892 tier,
1893 segments: vec![metadata_segment_ref(owner_namespace_id, segment_id)],
1894 }
1895 }
1896
1897 fn metadata_segment_ref(owner_namespace_id: &str, segment_id: &str) -> MetadataSegmentRef {
1898 MetadataSegmentRef {
1899 owner_namespace_id: NamespaceId::parse(owner_namespace_id).expect("valid namespace id"),
1900 segment_id: MetadataSegmentId::parse(segment_id).expect("valid segment id"),
1901 family: MetadataRowFamily::Inodes,
1902 segment_index: 0,
1903 row_count: 0,
1904 min_row_key: String::new(),
1905 max_row_key: String::new(),
1906 index_block: BlockHandle {
1907 offset: 0,
1908 stored_len: 0,
1909 decoded_len: 0,
1910 crc32c: 0,
1911 },
1912 filter_block: BlockHandle {
1913 offset: 0,
1914 stored_len: 0,
1915 decoded_len: 0,
1916 crc32c: 0,
1917 },
1918 filter_inline: None,
1919 object_checksum: "sha256:unused".to_owned(),
1920 }
1921 }
1922}