Skip to main content

loonfs_api/
manifest.rs

1//! The namespace manifest format: the durable document naming the
2//! metadata segment runs that materialize one namespace file-set version
3//! (format spec, "Namespace manifests").
4
5use 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
17/// Version 1 is an uncompressed JSON envelope document carrying the payload as
18/// a raw JSON fragment. `payload_checksum` covers the fragment's exact bytes.
19pub const NAMESPACE_MANIFEST_FORMAT_VERSION: u32 = 1;
20
21/// Identifies the durable payload family carried by a namespace-manifest envelope.
22///
23/// See [durable object families](../../../docs/specs/format.md#a8-object-keys).
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum NamespaceManifestKind {
27    /// Marks the file-set descriptor used to materialize a namespace snapshot.
28    NamespaceManifest,
29}
30
31impl NamespaceManifestKind {
32    /// Returns the frozen envelope discriminator written to durable storage.
33    pub const fn as_str(self) -> &'static str {
34        match self {
35            Self::NamespaceManifest => "namespace_manifest",
36        }
37    }
38}
39
40/// Selects a metadata row family and its durable lookup ordering.
41///
42/// See [metadata rows and row keys](../../../docs/specs/format.md#a6-metadata-rows-and-row-keys).
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum MetadataRowFamily {
46    /// Stores inode identity, kind, and creation position.
47    Inodes,
48    /// Orders directory bindings for parent-and-name visibility lookups.
49    DirentryBinds,
50    /// Re-indexes directory bindings by child for parent discovery.
51    DirentryChildBinds,
52    /// Stores immutable events that retire exact historical bindings.
53    DirentryUnbinds,
54    /// Stores file revisions newest-first within each inode.
55    Revisions,
56    /// Stores set and revoke events used to determine active subtree tombstones.
57    Tombstones,
58    /// Names the deletions that are recoverable right now, derived from the
59    /// tombstone family and ordered by deletion time.
60    ActiveDeletions,
61    /// Preserves commit idempotency evidence independently of retained WAL history.
62    CommitReceipts,
63    /// Preserves evidence that content was published.
64    ContentPublications,
65    /// Stores inode attribute revisions newest-first.
66    ///
67    /// Attributes are read only in this order, so the family has no secondary
68    /// index and requires no cross-family parity check.
69    Attributes,
70    /// Stores inode access revisions newest-first.
71    ///
72    /// Access rows are read only in this order, so the family has no
73    /// secondary index and requires no cross-family parity check.
74    Access,
75}
76
77/// Metadata families merged together as one consistency unit.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
79#[serde(rename_all = "snake_case")]
80pub enum MetadataFamilyGroup {
81    /// Directory bindings, their child index, and unbinds.
82    Bindings,
83    /// File revisions.
84    Revisions,
85    /// Inodes.
86    Inodes,
87    /// Tombstones.
88    Tombstones,
89    /// Active deletions.
90    ActiveDeletions,
91    /// Commit receipts.
92    CommitReceipts,
93    /// Preserves evidence that content was published.
94    ContentPublications,
95    /// Attributes.
96    Attributes,
97    /// Access rows.
98    Access,
99}
100
101impl MetadataFamilyGroup {
102    /// Every family group in serialized declaration order.
103    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    /// Returns the snake-case name used in durable keys and serialized values.
116    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    /// Returns the families this group merges together.
131    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/// Identifies the compaction tier that holds a metadata run.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
152#[serde(rename_all = "snake_case")]
153pub enum RunTier {
154    /// Holds rows that no compaction has dropped.
155    Delta,
156    /// Holds rows produced by a compaction over the oldest run.
157    Base,
158}
159
160/// Reference to one immutable metadata run in a namespace manifest.
161///
162/// See [control and manifest payloads](../../../docs/specs/format.md#a4-control-and-manifest-payloads).
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164#[serde(deny_unknown_fields)]
165pub struct MetadataRunRef {
166    /// Run identity allocated by the manifest.
167    pub run_no: RunNo,
168    /// Namespace sequence at which this run was produced.
169    pub run_seq: ChangeSeq,
170    /// Compaction tier used to order overlapping runs.
171    pub tier: RunTier,
172    /// Segments written as part of this run.
173    pub segments: Vec<MetadataSegmentRef>,
174}
175
176/// Reference to one immutable metadata segment in a namespace manifest.
177///
178/// See [control and manifest payloads](../../../docs/specs/format.md#a4-control-and-manifest-payloads).
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
180#[serde(deny_unknown_fields)]
181pub struct MetadataSegmentRef {
182    /// Namespace that stores the segment. This may be a fork source.
183    pub owner_namespace_id: NamespaceId,
184    /// Immutable segment id used in the durable object key.
185    pub segment_id: MetadataSegmentId,
186    /// Row schema and lookup ordering encoded in this segment.
187    pub family: MetadataRowFamily,
188    /// Zero-based shard position among segments emitted for the same family and run.
189    pub segment_index: u32,
190    /// Number of row payloads in the segment, used for validation and planning.
191    pub row_count: u64,
192    /// Inclusive least durable row key; the segment is corrupt if decoded rows disagree.
193    pub min_row_key: String,
194    /// Inclusive greatest durable row key; range planning skips disjoint segments.
195    pub max_row_key: String,
196    /// Location and verification data for the segment index block.
197    ///
198    /// Segments have no footer, so readers begin with this handle.
199    pub index_block: BlockHandle,
200    /// Where the segment's bloom filter block lives and how to verify it.
201    pub filter_block: BlockHandle,
202    /// The filter block's stored bytes inlined as hex, present when the
203    /// filter is small (small delta runs). Point lookups consult it to skip
204    /// the segment without any object fetch; `filter_block` still names and
205    /// verifies the same bytes, so the inline copy must decode byte-for-byte
206    /// identical (same length and CRC32C) or the manifest is corrupt.
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub filter_inline: Option<String>,
209    /// SHA-256 of the complete stored segment, formatted as
210    /// `sha256:<64 lowercase hex>`. Caches and offline verification use this
211    /// value. Ranged reads verify each block with its CRC32C instead.
212    pub object_checksum: String,
213}
214
215/// One materialized metadata row stored in a segment.
216///
217/// See [metadata rows and row keys](../../../docs/specs/format.md#a6-metadata-rows-and-row-keys).
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
220pub enum MetadataRow {
221    /// Establishes one inode's immutable identity and kind.
222    Inode(InodeRecord),
223    /// Records one generation of a directory name binding.
224    DirentryBind(DirentryBindRecord),
225    /// Retires one exact directory-binding generation.
226    DirentryUnbind(DirentryUnbindRecord),
227    /// Publishes one immutable content revision for a file inode.
228    FileRevision(RevisionRecord),
229    /// Changes whether one root inode has an active subtree tombstone.
230    Tombstone(SubtreeTombstoneRecord),
231    /// Derived row used to list currently recoverable deletions.
232    ///
233    /// Materialization writes `listed` for each tombstone set and `removed` for
234    /// each revoke. This lets trash listing use an ordered range scan instead of
235    /// replaying all historical deletion events.
236    ActiveDeletion(ActiveDeletionRecord),
237    /// Preserves the evidence needed to answer a retried logical commit.
238    CommitReceipt(CommitReceiptRecord),
239    /// Records a published content identity independently of its revisions.
240    ContentPublication(ContentPublicationRecord),
241    /// Publishes one inode's complete attribute map at one revision.
242    ///
243    /// The row is whole state, not a change: a reader takes the newest row
244    /// for an inode and needs nothing older. An inode with no row anywhere is
245    /// at revision 0 with an empty map, so nothing is written until a caller
246    /// writes an attribute.
247    AttributesRevision(AttributesRevisionRecord),
248    /// Publishes one inode's complete access state at one revision.
249    ///
250    /// Whole state, like an attribute revision: a reader takes the newest
251    /// row for an inode. An inode with no row anywhere is at revision 0
252    /// with no boundary and no grants.
253    AccessRevision(AccessRevisionRecord),
254}
255
256/// One inode's immutable identity and creation metadata.
257#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258#[serde(deny_unknown_fields)]
259pub struct InodeRecord {
260    /// Namespace-scoped inode identity allocated by the publishing writer.
261    pub inode_id: InodeId,
262    /// Classification fixed when the inode was created.
263    pub inode_kind: InodeKind,
264    /// Commit sequence from which the inode can become visible.
265    pub created_seq: ChangeSeq,
266    /// Commit ID associated with this row.
267    pub commit_id: CommitId,
268    /// Actor that created the inode, as supplied by the application.
269    pub created_by: crate::ActorId,
270    /// Time the inode was created, in Unix milliseconds.
271    pub created_at_ms: u64,
272}
273
274/// One generation of a directory name binding.
275#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
276#[serde(deny_unknown_fields)]
277pub struct DirentryBindRecord {
278    /// Directory in which the name was bound.
279    pub parent_inode_id: InodeId,
280    /// Policy-derived key used for uniqueness and lookup.
281    pub name_key: NameKey,
282    /// User-facing component spelling retained for directory responses.
283    pub display_name: DisplayName,
284    /// Inode reached while this binding generation remains active.
285    pub child_inode_id: InodeId,
286    /// Commit sequence that created this binding generation.
287    pub bind_seq: ChangeSeq,
288    /// Position that disambiguates the binding within `bind_seq`.
289    pub bind_delta_index: u32,
290}
291
292/// One event that retires an exact directory-binding generation.
293#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
294#[serde(deny_unknown_fields)]
295pub struct DirentryUnbindRecord {
296    /// Directory that held the targeted binding.
297    pub parent_inode_id: InodeId,
298    /// Canonical name key of the targeted binding.
299    pub name_key: NameKey,
300    /// User-facing spelling the retired binding carried.
301    pub display_name: DisplayName,
302    /// Child identity recorded by the targeted binding.
303    pub child_inode_id: InodeId,
304    /// Commit sequence that created the binding being retired.
305    pub bind_seq: ChangeSeq,
306    /// Delta position of the binding being retired.
307    pub bind_delta_index: u32,
308    /// Commit sequence from which this unbind takes effect.
309    pub unbind_seq: ChangeSeq,
310    /// Position that disambiguates the unbind within `unbind_seq`.
311    pub unbind_delta_index: u32,
312}
313
314/// One immutable content revision for a file inode.
315#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
316#[serde(deny_unknown_fields)]
317pub struct RevisionRecord {
318    /// File inode whose history contains the revision.
319    pub inode_id: InodeId,
320    /// Monotonic revision number within that file's history.
321    pub revision_no: RevisionNo,
322    /// Namespace sequence that published the revision.
323    pub committed_seq: ChangeSeq,
324    /// Commit ID associated with this row.
325    pub commit_id: CommitId,
326    /// The owning commit's observational wall-clock stamp.
327    pub committed_at_ms: u64,
328    /// Actor that committed this revision, as supplied by the application.
329    pub committed_by: crate::ActorId,
330    /// Delta position that disambiguates the revision within `committed_seq`.
331    pub delta_index: u32,
332    /// Immutable bytes published by the revision.
333    pub content_ref: ContentRef,
334}
335
336/// One event that changes whether a root inode has an active subtree tombstone.
337#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
338#[serde(deny_unknown_fields)]
339pub struct SubtreeTombstoneRecord {
340    /// Inode whose rooted subtree the event governs.
341    pub root_inode_id: InodeId,
342    /// Position of the event in namespace history.
343    pub generation: TombstoneGeneration,
344    /// Commit ID associated with this row.
345    pub commit_id: CommitId,
346    /// What this event did.
347    pub action: TombstoneRowAction,
348    /// Wall-clock stamp of the recording commit.
349    pub deleted_at_ms: u64,
350    /// Actor that recorded this tombstone event.
351    pub deleted_by: crate::ActorId,
352}
353
354/// One current-state row for a recoverable deletion.
355#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
356#[serde(deny_unknown_fields)]
357pub struct ActiveDeletionRecord {
358    /// Subtree root the deletion covers.
359    pub root_inode_id: InodeId,
360    /// Commit sequence of the deletion.
361    pub deletion_seq: ChangeSeq,
362    /// Current listing state for the deletion.
363    pub action: ActiveDeletionRowAction,
364}
365
366impl ActiveDeletionRecord {
367    /// Builds this row's durable key in trash-listing order.
368    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/// Evidence retained at every retention floor.
378#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
379#[serde(deny_unknown_fields)]
380pub struct ContentPublicationRecord {
381    /// Stored directly in the row key and Bloom filter key.
382    pub content_id: ContentId,
383    /// Distinguishes later publications of the same content.
384    pub committed_seq: ChangeSeq,
385    /// First publishing delta when a commit uses this content more than once.
386    pub delta_index: u32,
387}
388
389/// One durable commit idempotency receipt.
390#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
391#[serde(deny_unknown_fields)]
392pub struct CommitReceiptRecord {
393    /// Caller idempotency key whose later reuse is checked against this row.
394    pub commit_id: CommitId,
395    /// Actor that committed the change, as supplied by the application.
396    pub committed_by: crate::ActorId,
397    /// Digest used to distinguish a safe retry from conflicting ID reuse.
398    pub semantic_commit_fingerprint: crate::CommitFingerprint,
399    /// Namespace sequence assigned to the accepted commit.
400    pub committed_seq: ChangeSeq,
401    /// The commit's observational wall-clock stamp.
402    pub committed_at_ms: u64,
403    /// Caller annotation preserved for idempotent response reconstruction.
404    #[serde(default, skip_serializing_if = "Option::is_none")]
405    pub message: Option<String>,
406}
407
408/// One inode's complete attribute map at one revision.
409#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
410#[serde(deny_unknown_fields)]
411pub struct AttributesRevisionRecord {
412    /// Inode whose attributes this revision states.
413    pub inode_id: InodeId,
414    /// Monotonic per-inode attribute revision.
415    pub attributes_revision_no: AttributeRevisionNo,
416    /// Namespace sequence that published the revision.
417    pub committed_seq: ChangeSeq,
418    /// Commit ID associated with this row.
419    pub commit_id: CommitId,
420    /// Delta position that disambiguates the revision within `committed_seq`.
421    pub delta_index: u32,
422    /// Actor that updated the attributes.
423    pub updated_by: crate::ActorId,
424    /// Time of the attribute update, in Unix milliseconds.
425    pub updated_at_ms: u64,
426    /// The inode's complete attribute map at this revision.
427    pub attributes: Attributes,
428}
429
430/// One inode's complete access state at one revision.
431#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
432#[serde(deny_unknown_fields)]
433pub struct AccessRevisionRecord {
434    /// Inode whose access this revision states.
435    pub inode_id: InodeId,
436    /// Monotonic per-inode access revision.
437    pub access_revision_no: AccessRevisionNo,
438    /// Namespace sequence that published the revision.
439    pub committed_seq: ChangeSeq,
440    /// Commit ID associated with this row.
441    pub commit_id: CommitId,
442    /// Delta position that disambiguates the revision within `committed_seq`.
443    pub delta_index: u32,
444    /// Actor that updated the access state.
445    pub updated_by: crate::ActorId,
446    /// Time of the update, in Unix milliseconds.
447    pub updated_at_ms: u64,
448    /// Whether this directory stops inheritance from its ancestors.
449    pub boundary: bool,
450    /// The inode's complete direct grants at this revision.
451    pub grants: AccessGrants,
452}
453
454/// Names one deletion generation: the commit that recorded a tombstone
455/// event and the position that disambiguates it inside that commit.
456///
457/// Shared by the tombstone row and the WAL delta that revokes one, so a
458/// revoke names its target in the same spelling everywhere.
459#[derive(
460    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
461)]
462#[serde(deny_unknown_fields)]
463pub struct TombstoneGeneration {
464    /// Commit sequence that published the event.
465    pub seq: ChangeSeq,
466    /// Position that disambiguates the event within `seq`.
467    pub delta_index: u32,
468}
469
470/// Directory binding removed by a path deletion.
471///
472/// Tombstones retain this binding after the corresponding unbind row may be
473/// collected. Undelete uses it to restore the original parent and name.
474#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
475#[serde(deny_unknown_fields)]
476pub struct DeletedDirentry {
477    /// Directory that held the binding.
478    pub parent_inode_id: InodeId,
479    /// Canonical key the binding was reachable under.
480    pub name_key: NameKey,
481    /// User-facing spelling the binding carried.
482    pub display_name: DisplayName,
483}
484
485/// Tombstone-row event vocabulary (format spec, "Tombstones and deletion").
486#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
487#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
488pub enum TombstoneRowAction {
489    /// The subtree rooted at the row's inode is deleted.
490    Set {
491        /// The binding the delete removed.
492        deleted_direntry: DeletedDirentry,
493    },
494    /// The deletion recorded at `target` is revoked. Only a `set` carries a
495    /// binding, so the revoke has no place to put one.
496    Revoke {
497        /// The exact `set` event being compensated.
498        target: TombstoneGeneration,
499    },
500}
501
502/// Current-state rows for recoverable deletions.
503///
504/// `Listed` exposes a deletion in trash; `Removed` hides it after undelete.
505/// Both rows share a key prefix, with `Removed` sorting first, so scans can
506/// suppress restored entries. Reorganization later removes the cancelled
507/// pair.
508#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
509#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
510pub enum ActiveDeletionRowAction {
511    /// The deletion is recoverable; these are the fields the trash entry
512    /// renders, denormalized so a page needs no per-entry join.
513    Listed {
514        /// Whether the deleted root is a file or a directory.
515        inode_kind: InodeKind,
516        /// Wall-clock stamp of the deleting commit. Observational, like every
517        /// `committed_at_ms`.
518        deleted_at_ms: u64,
519        /// Actor responsible for the deletion.
520        deleted_by: crate::ActorId,
521        /// The binding the deletion removed, copied from the tombstone event
522        /// this row derives from.
523        deleted_direntry: DeletedDirentry,
524    },
525    /// The deletion was cancelled by an undelete at `revocation_seq`.
526    Removed {
527        /// Commit sequence of the undelete that cancelled the deletion.
528        revocation_seq: ChangeSeq,
529    },
530}
531
532impl ActiveDeletionRowAction {
533    /// The row-key component that orders a removal ahead of the row it
534    /// removes.
535    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    /// Returns the snake-case name used in durable keys and serialized values.
545    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    /// Fixed prefix before the first variable component in this family's row
562    /// keys. Compaction uses the remaining components to group rows for
563    /// retention.
564    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    /// Builds this row's canonical durable key in its primary row family.
583    ///
584    /// See [metadata rows and row keys](../../../docs/specs/format.md#a6-metadata-rows-and-row-keys).
585    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    /// Builds this row's durable key using the selected primary or secondary ordering.
601    ///
602    /// See [metadata rows and row keys](../../../docs/specs/format.md#a6-metadata-rows-and-row-keys).
603    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    /// Returns the Bloom filter key for this row in `family`.
677    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            // The family is only ever range-scanned in key order, never
705            // probed for one deletion, so the filter key is the row key.
706            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
719/// Encodes an arbitrary string so it can occupy one component of a durable row key.
720///
721/// See [metadata rows and row keys](../../../docs/specs/format.md#a6-metadata-rows-and-row-keys).
722pub fn hex_encode_row_key_component(value: &str) -> String {
723    crate::hex::hex_encode_bytes(value.as_bytes())
724}
725
726/// Builders for metadata row keys, lookup prefixes, and Bloom filter probes.
727///
728/// See [metadata rows and row keys](../../../docs/specs/format.md#a6-metadata-rows-and-row-keys).
729pub mod lookup_keys {
730    use super::{hex_encode_row_key_component, TombstoneGeneration};
731    use crate::{AccessRevisionNo, AttributeRevisionNo, ChangeSeq, ContentId, InodeId, RevisionNo};
732
733    /// Prefix for inode row keys.
734    pub const INODE_ROW_PREFIX: &str = "inode-";
735
736    /// Prefix for revision row keys.
737    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    /// Builds the exclusive lower bound after `row_key`.
749    pub fn after_row_key(row_key: &str) -> String {
750        format!("{row_key}\0")
751    }
752
753    /// Builds an inode row key.
754    pub fn inode_key(inode_id: InodeId) -> String {
755        format!("{INODE_ROW_PREFIX}{:020}", inode_id.0)
756    }
757
758    /// Builds a scan bound immediately after an inode row.
759    pub fn inode_key_after(inode_id: InodeId) -> String {
760        after_row_key(&inode_key(inode_id))
761    }
762
763    /// Builds the prefix for directory bindings under one parent.
764    pub fn direntry_parent_prefix(parent_inode_id: InodeId) -> String {
765        format!("{DIRENTRY_BIND_ROW_PREFIX}{:020}-", parent_inode_id.0)
766    }
767
768    /// Builds the Bloom filter probe for a parent/name binding.
769    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    /// Builds the prefix for every generation of a parent/name binding.
778    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    /// Builds a row key for one generation of a parent/name binding.
783    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    /// Builds the Bloom filter probe for bindings to one child inode.
797    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    /// Builds the reverse-index prefix for bindings to one child inode.
802    pub fn direntry_child_prefix(child_inode_id: InodeId) -> String {
803        format!("{}-", direntry_child_probe(child_inode_id))
804    }
805
806    /// Builds a reverse-index row key for one binding generation.
807    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    /// Builds the Bloom filter probe for unbinds of one parent/name pair.
824    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    /// Builds the prefix for unbinds of one binding generation.
833    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    /// Builds a row key for one unbind event.
847    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    /// Builds the prefix for unbinds below one parent directory.
863    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    /// Builds the prefix for unbinds of one parent/name pair.
868    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    /// Builds the Bloom filter probe for one tombstone root.
873    pub fn tombstone_probe(root_inode_id: InodeId) -> String {
874        format!("{TOMBSTONE_ROW_PREFIX}{:020}", root_inode_id.0)
875    }
876
877    /// Builds the prefix for a root inode's tombstone history.
878    pub fn tombstone_prefix(root_inode_id: InodeId) -> String {
879        format!("{}-", tombstone_probe(root_inode_id))
880    }
881
882    /// Builds a row key for one tombstone event.
883    ///
884    /// The action is stored in the value, so delete and revoke rows for one
885    /// generation share a key.
886    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    /// Prefix for active-deletion row keys.
899    pub const ACTIVE_DELETION_ROW_PREFIX: &str = "active-deletion-";
900
901    /// Rank of an undelete's removal marker within one deletion generation.
902    /// It is the lowest rank on purpose: an ascending scan sees the removal
903    /// before the row it removes, so a page never lists a deletion whose
904    /// marker was going to arrive one page later.
905    pub(super) const ACTIVE_DELETION_RANK_REMOVED: u32 = 0;
906
907    /// Rank of the listed row within one deletion generation, and the highest
908    /// rank the family defines.
909    pub(super) const ACTIVE_DELETION_RANK_LISTED: u32 = 1;
910
911    /// Builds an active-deletion row key.
912    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    /// Builds a trash scan bound after one deletion generation.
924    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    /// Selects all publications of one content identity in the Bloom filter.
933    pub fn content_publication_probe(content_id: &ContentId) -> String {
934        format!("{CONTENT_PUBLICATION_ROW_PREFIX}{content_id}")
935    }
936
937    /// Selects the rows for one content identity.
938    pub fn content_publication_prefix(content_id: &ContentId) -> String {
939        format!("{}-", content_publication_probe(content_id))
940    }
941
942    /// Orders publications by content identity and commit sequence.
943    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    /// Builds the Bloom filter probe for one commit ID.
955    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    /// Builds the prefix for receipts with one commit ID.
963    pub fn commit_receipt_prefix(commit_id: &str) -> String {
964        format!("{}-", commit_receipt_probe(commit_id))
965    }
966
967    /// Builds a commit receipt row key.
968    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    /// Builds the Bloom filter probe for an inode's revisions.
977    pub fn revision_probe(inode_id: InodeId) -> String {
978        format!("{REVISION_ROW_PREFIX}{:020}", inode_id.0)
979    }
980
981    /// Builds the prefix for an inode's newest-first revisions.
982    pub fn revision_prefix(inode_id: InodeId) -> String {
983        format!("{}-", revision_probe(inode_id))
984    }
985
986    /// Builds a prefix for one revision number within an inode.
987    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    /// Builds a newest-first revision row key.
996    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    /// Builds the Bloom filter probe for an inode's attribute revisions.
1011    pub fn attributes_probe(inode_id: InodeId) -> String {
1012        format!("{ATTRIBUTE_ROW_PREFIX}{:020}", inode_id.0)
1013    }
1014
1015    /// Builds the prefix for an inode's newest-first attribute revisions.
1016    pub fn attributes_prefix(inode_id: InodeId) -> String {
1017        format!("{}-", attributes_probe(inode_id))
1018    }
1019
1020    /// Builds a row key for an attribute revision.
1021    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    /// Builds the Bloom filter probe for an inode's access revisions.
1037    pub fn access_probe(inode_id: InodeId) -> String {
1038        format!("{ACCESS_ROW_PREFIX}{:020}", inode_id.0)
1039    }
1040
1041    /// Builds the prefix for an inode's newest-first access revisions.
1042    pub fn access_prefix(inode_id: InodeId) -> String {
1043        format!("{}-", access_probe(inode_id))
1044    }
1045
1046    /// Builds a row key for an access revision.
1047    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/// A namespace's access mode, fixed at creation.
1064#[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    /// Every caller holding the deployment credential may do everything.
1069    Unrestricted {},
1070    /// Access rows govern every operation.
1071    Acl {
1072        /// Identity domain the namespace's principal ids belong to.
1073        principal_scope: PrincipalScope,
1074        /// The root inode's grants at genesis, normally `admin` for each
1075        /// initial administrator.
1076        root_grants: AccessGrants,
1077    },
1078}
1079
1080impl NamespaceAccess {
1081    /// The unrestricted access mode.
1082    pub fn unrestricted() -> Self {
1083        Self::Unrestricted {}
1084    }
1085
1086    /// Whether every caller holding the deployment credential may do everything.
1087    pub const fn is_unrestricted(&self) -> bool {
1088        matches!(self, Self::Unrestricted {})
1089    }
1090}
1091
1092/// Carries one complete namespace file-set description inside a manifest envelope.
1093///
1094/// See [manifest publication](../../../docs/specs/format.md#72-publishing-a-materialized-file-set).
1095#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1096#[serde(deny_unknown_fields)]
1097pub struct NamespaceManifestPayload {
1098    /// Namespace whose materialized state this manifest describes.
1099    pub namespace_id: NamespaceId,
1100    /// Content domain shared by this namespace and its forks.
1101    pub content_store_id: ContentStoreId,
1102    /// Namespace creation stamp in Unix milliseconds.
1103    pub created_at_ms: u64,
1104    /// Actor that created the namespace, as supplied by the application.
1105    pub created_by: ActorId,
1106    /// Access mode, fixed at creation.
1107    pub access: NamespaceAccess,
1108    /// Permanent fork provenance and source checkpoint identity.
1109    #[serde(default, skip_serializing_if = "Option::is_none")]
1110    pub fork_basis: Option<ForkBasis>,
1111    /// Terminal deletion and retirement state.
1112    pub status: NamespaceStatus,
1113    /// Writer that acquired the current epoch.
1114    #[serde(default, skip_serializing_if = "Option::is_none")]
1115    pub writer: Option<WriterBlock>,
1116    /// Highest WAL number represented by the runs.
1117    pub last_folded_wal_no: WalNo,
1118    /// WAL number covered by the manifest whose head established the floor.
1119    pub retention_floor_wal_no: WalNo,
1120    /// Positive publication number matching the manifest object key.
1121    pub manifest_no: ManifestNo,
1122    /// Stops a stale streaming compactor at its next check when a newer runtime claims it.
1123    /// Streaming compaction rebuilds a whole family group and publishes once at the end.
1124    /// Grep publishes each bounded step, so a lost race costs only one step.
1125    pub compactor_epoch: u64,
1126    /// Materialized head sequence, or the final namespace sequence on deletion.
1127    pub head_seq: ChangeSeq,
1128    /// Commit identity used when no newer data segment exists.
1129    pub head_commit_id: CommitId,
1130    /// Oldest run sequence still represented by `runs`.
1131    pub base_seq: ChangeSeq,
1132    /// Current writer fencing epoch.
1133    pub writer_epoch: WriterEpoch,
1134    /// First inode identity available after replaying the manifest snapshot.
1135    pub next_inode_id: InodeId,
1136    /// Run number the next producer allocates. Every run's `run_no` is below it.
1137    pub next_run_no: RunNo,
1138    /// Earliest sequence for which retained history remains readable.
1139    pub retention_floor_seq: ChangeSeq,
1140    /// Complete set of metadata runs required to reconstruct the snapshot.
1141    pub runs: Vec<MetadataRunRef>,
1142}
1143
1144/// A successor manifest changed one of the namespace's immutable identity
1145/// fields. Every manifest a namespace ever publishes carries them forward
1146/// verbatim from the manifest that created it.
1147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1148pub struct ManifestIdentityDrift {
1149    /// Which field the successor changed.
1150    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    /// Constructs manifest 1 with the root inode reserved.
1167    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    /// Rejects changes to permanent identity and terminal lifecycle state.
1199    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
1238/// A manifest decoded through its checked durable codec.
1239pub type NamespaceManifestEnvelope = crate::envelope::VerifiedEnvelope<NamespaceManifestPayload>;
1240
1241/// Encodes a manifest once and returns its immutable framing and durable bytes.
1242pub 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
1252/// Decodes and verifies a durable namespace-manifest JSON envelope.
1253///
1254/// Decoding fails for invalid JSON, the wrong kind or version, a checksum
1255/// mismatch, or an invalid payload. See
1256/// [manifest publication](../../../docs/specs/format.md#72-publishing-a-materialized-file-set).
1257pub 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        // The inode family's durable order IS ascending inode id, which is
1379        // what lets a whole-namespace file walk resume from one bound.
1380        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        // A point lookup probes the filter with the inode's shared key, and
1614        // the writer stores exactly that key.
1615        assert_eq!(
1616            newest.filter_key_for_family(MetadataRowFamily::Attributes),
1617            super::lookup_keys::attributes_probe(InodeId(42))
1618        );
1619        // Another inode's rows sort outside the prefix.
1620        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}