Skip to main content

loonfs_api/
manifest.rs

1//! The namespace manifest format: the durable document naming the
2//! metadata SST runs that materialize one namespace file-set version
3//! (format spec, "Namespace manifests").
4
5use crate::envelope::EnvelopeCodecError;
6use crate::sst_blocks::BlockHandle;
7use crate::WriterEpoch;
8use crate::{
9    AttributeRevisionNo, Attributes, ChangeSeq, CommitId, ContentRef, DisplayName, InodeId,
10    InodeKind, ManifestId, ManifestObjectId, MetadataTableId, NameKey, NamespaceId, RevisionNo,
11};
12use serde::{Deserialize, Serialize};
13
14/// Version 1: an uncompressed JSON envelope document carrying the payload as
15/// a raw JSON fragment. `payload_checksum` covers the fragment's exact bytes.
16pub const NAMESPACE_MANIFEST_FORMAT_VERSION: u32 = 1;
17
18/// Identifies the durable payload family carried by a namespace-manifest envelope.
19///
20/// See [durable object families](../../../docs/specs/format.md#12-durable-object-families).
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum NamespaceManifestKind {
24    /// Marks the file-set descriptor used to materialize a namespace snapshot.
25    NamespaceManifest,
26}
27
28impl NamespaceManifestKind {
29    /// Returns the frozen envelope discriminator written to durable storage.
30    pub const fn as_str(self) -> &'static str {
31        match self {
32            Self::NamespaceManifest => "namespace_manifest",
33        }
34    }
35}
36
37/// Selects a metadata row family and its durable lookup ordering.
38///
39/// See [metadata segments](../../../docs/specs/format.md#421-metadata-segments).
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum MetadataTableFamily {
43    /// Stores inode identity, kind, and creation position.
44    Inodes,
45    /// Orders directory bindings for parent-and-name visibility lookups.
46    DirentryBinds,
47    /// Re-indexes directory bindings by child for parent discovery.
48    DirentryChildBinds,
49    /// Stores immutable events that retire exact historical bindings.
50    DirentryUnbinds,
51    /// Stores file revisions in their canonical durable ordering.
52    Revisions,
53    /// Re-indexes file revisions for newest-first per-inode reads.
54    RevisionsByInodeDesc,
55    /// Stores set and revoke events used to determine active subtree tombstones.
56    Tombstones,
57    /// Names the deletions that are recoverable right now, derived from the
58    /// tombstone family and ordered by deletion time.
59    ActiveDeletions,
60    /// Preserves commit idempotency evidence independently of retained WAL history.
61    CommitReceipts,
62    /// Stores inode attribute revisions newest-first.
63    ///
64    /// Attributes are read only in this order, so the family has no secondary
65    /// index and requires no cross-family parity check.
66    Attributes,
67}
68
69/// Describes one immutable metadata SST object referenced by a namespace manifest.
70///
71/// See [metadata segments](../../../docs/specs/format.md#421-metadata-segments).
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub struct MetadataFileRef {
74    /// Namespace whose keyspace owns the object, which may be a fork source rather than the reader.
75    pub owner_namespace_id: NamespaceId,
76    /// Immutable table identity incorporated into the object's durable key.
77    pub table_id: MetadataTableId,
78    /// Fully resolved object-store key trusted only after descriptor validation.
79    pub object_key: String,
80    /// Namespace sequence at which this run was produced.
81    pub run_seq: ChangeSeq,
82    /// Compaction tier used to order overlapping runs during reads and reorganization.
83    pub level: u32,
84    /// Row schema and lookup ordering encoded in this segment.
85    pub family: MetadataTableFamily,
86    /// Zero-based shard position among segments emitted for the same family and run.
87    pub segment_index: u32,
88    /// Number of row payloads in the segment, used for validation and planning.
89    pub row_count: u64,
90    /// Inclusive least durable row key; the segment is corrupt if decoded rows disagree.
91    pub min_key: String,
92    /// Inclusive greatest durable row key; range planning skips disjoint segments.
93    pub max_key: String,
94    /// Location and verification data for the segment index block.
95    ///
96    /// Segments have no footer, so readers begin with this handle.
97    pub index_block: BlockHandle,
98    /// Where the segment's bloom filter block lives and how to verify it.
99    pub filter_block: BlockHandle,
100    /// The filter block's stored bytes inlined as hex, present when the
101    /// filter is small (small delta runs). Point lookups consult it to skip
102    /// the segment without any object fetch; `filter_block` still names and
103    /// verifies the same bytes, so the inline copy must decode byte-for-byte
104    /// identical (same length and CRC32C) or the manifest is corrupt.
105    #[serde(default, skip_serializing_if = "Option::is_none")]
106    pub filter_inline: Option<String>,
107    /// Checksum of the segment object's full bytes, in `sha256:<hex>` form.
108    /// The ranged read path verifies per-block CRCs instead; this digest is
109    /// the segment's identity in the decoded-block cache.
110    pub payload_checksum: String,
111}
112
113/// Stores one materialized metadata event in an SST segment.
114///
115/// See [metadata segments](../../../docs/specs/format.md#421-metadata-segments).
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(tag = "kind", rename_all = "snake_case")]
118pub enum MetadataRow {
119    /// Establishes one inode's immutable identity and kind.
120    Inode {
121        /// Namespace-scoped inode identity allocated by the publishing writer.
122        inode_id: InodeId,
123        /// Classification fixed when the inode was created.
124        inode_kind: InodeKind,
125        /// Commit sequence from which the inode can become visible.
126        created_seq: ChangeSeq,
127        /// Actor that created the inode, as supplied by the application.
128        created_by: crate::ActorRef,
129        /// Time the inode was created, in Unix milliseconds.
130        created_at_ms: u64,
131    },
132    /// Records one generation of a directory name binding.
133    DirentryBind {
134        /// Directory in which the name was bound.
135        parent_inode_id: InodeId,
136        /// Policy-derived key used for uniqueness and lookup.
137        name_key: NameKey,
138        /// User-facing component spelling retained for directory responses.
139        display_name: DisplayName,
140        /// Inode reached while this binding generation remains active.
141        child_inode_id: InodeId,
142        /// Commit sequence that created this binding generation.
143        bind_seq: ChangeSeq,
144        /// Position that disambiguates the binding within `bind_seq`.
145        bind_delta_index: u32,
146    },
147    /// Retires one exact directory-binding generation.
148    DirentryUnbind {
149        /// Directory that held the targeted binding.
150        parent_inode_id: InodeId,
151        /// Canonical name key of the targeted binding.
152        name_key: NameKey,
153        /// User-facing spelling the retired binding carried.
154        display_name: DisplayName,
155        /// Child identity recorded by the targeted binding.
156        child_inode_id: InodeId,
157        /// Commit sequence that created the binding being retired.
158        bind_seq: ChangeSeq,
159        /// Delta position of the binding being retired.
160        bind_delta_index: u32,
161        /// Commit sequence from which this unbind takes effect.
162        unbind_seq: ChangeSeq,
163        /// Position that disambiguates the unbind within `unbind_seq`.
164        unbind_delta_index: u32,
165    },
166    /// Publishes one immutable content revision for a file inode.
167    Revision {
168        /// File inode whose history contains the revision.
169        inode_id: InodeId,
170        /// Monotonic revision number within that file's history.
171        revision_no: RevisionNo,
172        /// Namespace sequence that published the revision.
173        committed_seq: ChangeSeq,
174        /// The owning commit's observational wall-clock stamp, denormalized
175        /// onto the row so revision reads answer times without a receipt
176        /// join. Never a validity input; `committed_seq` is the order.
177        committed_at_ms: u64,
178        /// Actor responsible for this revision, as supplied by the application.
179        actor: crate::ActorRef,
180        /// Delta position that disambiguates the revision within `committed_seq`.
181        revision_delta_index: u32,
182        /// Immutable bytes published by the revision.
183        content_ref: ContentRef,
184    },
185    /// Changes whether one root inode has an active subtree tombstone.
186    Tombstone {
187        /// Inode whose rooted subtree the event governs.
188        root_inode_id: InodeId,
189        /// Where this event sits in the namespace's history, and the
190        /// generation a later `revoke` names.
191        generation: TombstoneGeneration,
192        /// What this event did; readers take the newest row per root and
193        /// treat a `revoke` newest row as "no active tombstone".
194        action: TombstoneRowAction,
195        /// Wall-clock stamp of the recording commit. Observational, like
196        /// every `committed_at_ms`.
197        deleted_at_ms: u64,
198        /// Actor responsible for this tombstone event.
199        actor: crate::ActorRef,
200    },
201    /// Derived row used to list currently recoverable deletions.
202    ///
203    /// Materialization writes `listed` for each tombstone set and `removed` for
204    /// each revoke. This lets trash listing use an ordered range scan instead of
205    /// replaying all historical deletion events.
206    ActiveDeletion {
207        /// Subtree root the deletion covers. With `deleted_at_seq` this is
208        /// exactly the handle `undelete` addresses.
209        root_inode_id: InodeId,
210        /// Commit sequence of the deletion this row speaks for. A `removed`
211        /// row repeats its target's sequence, not the undelete's, so the two
212        /// rows sort together.
213        deleted_at_seq: ChangeSeq,
214        /// Whether the deletion is still recoverable, and the listing detail
215        /// it carries while it is.
216        action: ActiveDeletionRowAction,
217    },
218    /// Preserves the evidence needed to answer a retried logical commit.
219    CommitReceipt {
220        /// Caller idempotency key whose later reuse is checked against this row.
221        commit_id: CommitId,
222        /// Actor responsible for the commit, as supplied by the application.
223        actor: crate::ActorRef,
224        /// Digest used to distinguish a safe retry from conflicting id reuse.
225        semantic_commit_fingerprint: String,
226        /// Namespace sequence assigned to the accepted commit.
227        committed_seq: ChangeSeq,
228        /// The commit's observational wall-clock stamp. Receipts are the
229        /// durable per-commit record once WAL history drops below the
230        /// retention floor, so the stamp lives here for every commit,
231        /// revision-bearing or not.
232        committed_at_ms: u64,
233        /// Caller annotation preserved for idempotent response reconstruction.
234        #[serde(default, skip_serializing_if = "Option::is_none")]
235        message: Option<String>,
236    },
237    /// Publishes one inode's complete attribute map at one revision.
238    ///
239    /// The row is whole state, not a change: a reader takes the newest row
240    /// for an inode and needs nothing older. An inode with no row anywhere is
241    /// at revision 0 with an empty map, so nothing is written until a caller
242    /// writes an attribute.
243    AttributesRevision {
244        /// Inode whose attributes this revision states.
245        inode_id: InodeId,
246        /// Monotonic per-inode attribute revision.
247        attributes_revision_no: AttributeRevisionNo,
248        /// Namespace sequence that published the revision.
249        committed_seq: ChangeSeq,
250        /// Delta position that disambiguates the revision within `committed_seq`.
251        delta_index: u32,
252        /// Actor responsible for this attribute update.
253        actor: crate::ActorRef,
254        /// Time of the attribute update, in Unix milliseconds.
255        updated_at_ms: u64,
256        /// The inode's complete attribute map at this revision. An empty map
257        /// is the cleared state.
258        attributes: Attributes,
259    },
260}
261
262/// Names one deletion generation: the commit that recorded a tombstone
263/// event and the position that disambiguates it inside that commit.
264///
265/// Shared by the tombstone row and the WAL delta that revokes one, so a
266/// revoke names its target in the same spelling everywhere.
267#[derive(
268    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
269)]
270#[serde(deny_unknown_fields)]
271pub struct TombstoneGeneration {
272    /// Commit sequence that published the event.
273    pub seq: ChangeSeq,
274    /// Position that disambiguates the event within `seq`.
275    pub delta_index: u32,
276}
277
278/// Directory binding removed by a path deletion.
279///
280/// Tombstones retain this binding after the corresponding unbind row may be
281/// collected. Undelete uses it to restore the original parent and name.
282#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
283#[serde(deny_unknown_fields)]
284pub struct DeletedDirentry {
285    /// Directory that held the binding.
286    pub parent_inode_id: InodeId,
287    /// Canonical key the binding was reachable under.
288    pub name_key: NameKey,
289    /// User-facing spelling the binding carried.
290    pub display_name: DisplayName,
291}
292
293/// Reads an optional field that must still be written.
294///
295/// Serde reads a missing `Option` field as `None`, which would make an
296/// encoding that never had the field indistinguishable from one that stated
297/// its absence. A durable optional that distinguishes those two reads
298/// through here instead.
299pub(crate) fn required_option<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
300where
301    T: Deserialize<'de>,
302    D: serde::Deserializer<'de>,
303{
304    Option::deserialize(deserializer)
305}
306
307/// Tombstone-row event vocabulary (format spec, "Tombstones and deletion").
308#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
309#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
310pub enum TombstoneRowAction {
311    /// The subtree rooted at the row's inode is deleted.
312    Set {
313        /// The binding the delete removed, or `null` for a delete addressed
314        /// by inode, which had no name to record. Stated either way and
315        /// never defaulted, so bytes without the field are the pre-grouping
316        /// layout — which spelled the binding as three optional row fields —
317        /// rather than a deletion that recorded no name.
318        #[serde(deserialize_with = "required_option")]
319        deleted_direntry: Option<DeletedDirentry>,
320    },
321    /// The deletion recorded at `target` is revoked. Only a `set` carries a
322    /// binding, so the revoke has no place to put one.
323    Revoke {
324        /// The exact `set` event being compensated.
325        target: TombstoneGeneration,
326    },
327}
328
329/// Current-state rows for recoverable deletions.
330///
331/// `Listed` exposes a deletion in trash; `Removed` hides it after undelete.
332/// Both rows share a key prefix, with `Removed` sorting first, so scans can
333/// suppress restored entries. Reorganization later removes the cancelled
334/// pair.
335#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
336#[serde(tag = "kind", rename_all = "snake_case")]
337pub enum ActiveDeletionRowAction {
338    /// The deletion is recoverable; these are the fields the trash entry
339    /// renders, denormalized so a page needs no per-entry join.
340    Listed {
341        /// Wall-clock stamp of the deleting commit. Observational, like every
342        /// `committed_at_ms`.
343        deleted_at_ms: u64,
344        /// Actor responsible for the deletion.
345        deleted_by: crate::ActorRef,
346        /// The binding the deletion removed, copied from the tombstone event
347        /// this row derives from, or `null` when it recorded none. Stated
348        /// either way, like the event's own.
349        #[serde(deserialize_with = "required_option")]
350        deleted_direntry: Option<DeletedDirentry>,
351    },
352    /// An undelete at `revoked_at_seq` cancelled the deletion this row's key
353    /// names, so the listing skips the key.
354    Removed {
355        /// Commit sequence of the undelete that cancelled the deletion.
356        revoked_at_seq: ChangeSeq,
357    },
358}
359
360impl ActiveDeletionRowAction {
361    /// The row-key component that orders a removal ahead of the row it
362    /// removes.
363    fn sort_rank(&self) -> u8 {
364        match self {
365            Self::Removed { .. } => lookup_keys::ACTIVE_DELETION_RANK_REMOVED,
366            Self::Listed { .. } => lookup_keys::ACTIVE_DELETION_RANK_LISTED,
367        }
368    }
369}
370
371impl MetadataTableFamily {
372    /// The fixed text every row key in this family starts with, up to but
373    /// not including the family's first variable component.
374    ///
375    /// Defined here because it is the front of [`MetadataRow::row_key_for_family`]
376    /// and must change with it; `row_key_prefixes_match_the_row_keys_they_front`
377    /// holds the two together. A streaming compaction reads its retention
378    /// groupings out of the components that follow this prefix, so the
379    /// boundary between two groupings is this prefix followed by whole
380    /// component values (format spec, "Compaction").
381    pub const fn row_key_prefix(self) -> &'static str {
382        match self {
383            Self::Inodes => "inode-",
384            Self::DirentryBinds => "direntry-",
385            Self::DirentryChildBinds => "direntry-child-",
386            Self::DirentryUnbinds => "direntry-unbind-",
387            Self::Revisions => "revision-",
388            Self::RevisionsByInodeDesc => "revision-by-inode-desc-",
389            Self::Tombstones => "tombstone-",
390            Self::ActiveDeletions => "active-deletion-",
391            Self::CommitReceipts => "commit-receipt-",
392            Self::Attributes => "attributes-",
393        }
394    }
395}
396
397impl MetadataRow {
398    /// Builds this row's canonical durable key in its primary table family.
399    ///
400    /// See [metadata segments](../../../docs/specs/format.md#421-metadata-segments).
401    pub fn row_key(&self) -> String {
402        self.row_key_for_family(match self {
403            Self::Inode { .. } => MetadataTableFamily::Inodes,
404            Self::DirentryBind { .. } => MetadataTableFamily::DirentryBinds,
405            Self::DirentryUnbind { .. } => MetadataTableFamily::DirentryUnbinds,
406            Self::Revision { .. } => MetadataTableFamily::Revisions,
407            Self::Tombstone { .. } => MetadataTableFamily::Tombstones,
408            Self::ActiveDeletion { .. } => MetadataTableFamily::ActiveDeletions,
409            Self::CommitReceipt { .. } => MetadataTableFamily::CommitReceipts,
410            Self::AttributesRevision { .. } => MetadataTableFamily::Attributes,
411        })
412    }
413
414    /// Builds this row's durable key using the selected primary or secondary ordering.
415    ///
416    /// See [metadata segments](../../../docs/specs/format.md#421-metadata-segments).
417    pub fn row_key_for_family(&self, family: MetadataTableFamily) -> String {
418        match self {
419            Self::Inode { inode_id, .. } => format!("inode-{:020}", inode_id.0),
420            Self::DirentryBind {
421                parent_inode_id,
422                name_key,
423                child_inode_id,
424                bind_seq,
425                bind_delta_index,
426                ..
427            } => match family {
428                MetadataTableFamily::DirentryChildBinds => {
429                    let name_key = hex_encode_row_key_component(name_key.as_str());
430                    format!(
431                        "direntry-child-{:020}-{:020}-{:010}-{:020}-{name_key}",
432                        child_inode_id.0, bind_seq.0, bind_delta_index, parent_inode_id.0
433                    )
434                }
435                _ => {
436                    let name_key = hex_encode_row_key_component(name_key.as_str());
437                    format!(
438                        "direntry-{:020}-{name_key}-{:020}-{:010}",
439                        parent_inode_id.0, bind_seq.0, bind_delta_index
440                    )
441                }
442            },
443            Self::DirentryUnbind {
444                parent_inode_id,
445                name_key,
446                bind_seq,
447                bind_delta_index,
448                unbind_seq,
449                unbind_delta_index,
450                ..
451            } => {
452                let name_key = hex_encode_row_key_component(name_key.as_str());
453                format!(
454                    "direntry-unbind-{:020}-{name_key}-{:020}-{:010}-{:020}-{:010}",
455                    parent_inode_id.0,
456                    bind_seq.0,
457                    bind_delta_index,
458                    unbind_seq.0,
459                    unbind_delta_index
460                )
461            }
462            Self::Revision {
463                inode_id,
464                revision_no,
465                committed_seq,
466                revision_delta_index,
467                ..
468            } => match family {
469                MetadataTableFamily::RevisionsByInodeDesc => {
470                    let reverse_revision_no = u64::MAX - revision_no.0;
471                    let reverse_committed_seq = u64::MAX - committed_seq.0;
472                    let reverse_delta_index = u32::MAX - revision_delta_index;
473                    format!(
474                        "revision-by-inode-desc-{:020}-{:020}-{:020}-{:010}",
475                        inode_id.0, reverse_revision_no, reverse_committed_seq, reverse_delta_index
476                    )
477                }
478                _ => {
479                    format!(
480                        "revision-{:020}-{:020}-{:010}",
481                        inode_id.0, revision_no.0, revision_delta_index
482                    )
483                }
484            },
485            Self::Tombstone {
486                root_inode_id,
487                generation,
488                // The action and binding context live in the value: revoke
489                // rows sort exactly like the deletions they cancel, so
490                // newest-per-root scans see them in one prefix pass.
491                ..
492            } => {
493                format!(
494                    "tombstone-{:020}-{:020}-{:010}",
495                    root_inode_id.0, generation.seq.0, generation.delta_index
496                )
497            }
498            Self::ActiveDeletion {
499                root_inode_id,
500                deleted_at_seq,
501                action,
502            } => lookup_keys::active_deletion_row_key(
503                *deleted_at_seq,
504                *root_inode_id,
505                action.sort_rank(),
506            ),
507            Self::CommitReceipt {
508                committed_seq,
509                commit_id,
510                ..
511            } => {
512                let commit_id = hex_encode_row_key_component(commit_id.as_str());
513                format!("commit-receipt-{commit_id}-{:020}", committed_seq.0)
514            }
515            Self::AttributesRevision {
516                inode_id,
517                attributes_revision_no,
518                committed_seq,
519                delta_index,
520                ..
521            } => lookup_keys::attributes_row_key(
522                *inode_id,
523                *attributes_revision_no,
524                *committed_seq,
525                *delta_index,
526            ),
527        }
528    }
529
530    /// The exact lookup prefix a point read probes for this row in `family`,
531    /// and therefore the key inserted into the segment's bloom filter. The
532    /// two sides must agree byte-for-byte — a filter is an exact-match
533    /// structure — so both are defined here, next to the row keys they
534    /// shorten. Range scans at coarser granularity (a whole directory, a
535    /// wave of names) do not consult filters.
536    pub fn filter_key_for_family(&self, family: MetadataTableFamily) -> String {
537        match self {
538            Self::Inode { .. } => self.row_key_for_family(family),
539            Self::DirentryBind {
540                parent_inode_id,
541                name_key,
542                child_inode_id,
543                ..
544            } => match family {
545                MetadataTableFamily::DirentryChildBinds => {
546                    format!("direntry-child-{:020}", child_inode_id.0)
547                }
548                _ => {
549                    let name_key = hex_encode_row_key_component(name_key.as_str());
550                    format!("direntry-{:020}-{name_key}", parent_inode_id.0)
551                }
552            },
553            Self::DirentryUnbind {
554                parent_inode_id,
555                name_key,
556                ..
557            } => {
558                let name_key = hex_encode_row_key_component(name_key.as_str());
559                format!("direntry-unbind-{:020}-{name_key}", parent_inode_id.0)
560            }
561            Self::Revision { inode_id, .. } => match family {
562                MetadataTableFamily::RevisionsByInodeDesc => {
563                    format!("revision-by-inode-desc-{:020}", inode_id.0)
564                }
565                _ => format!("revision-{:020}", inode_id.0),
566            },
567            Self::Tombstone { root_inode_id, .. } => {
568                format!("tombstone-{:020}", root_inode_id.0)
569            }
570            // The family is only ever range-scanned in key order, never
571            // probed for one deletion, so the filter key is the row key.
572            Self::ActiveDeletion { .. } => self.row_key_for_family(family),
573            Self::CommitReceipt { commit_id, .. } => {
574                let commit_id = hex_encode_row_key_component(commit_id.as_str());
575                format!("commit-receipt-{commit_id}")
576            }
577            Self::AttributesRevision { inode_id, .. } => lookup_keys::attributes_probe(*inode_id),
578        }
579    }
580}
581
582/// Encodes an arbitrary string so it can occupy one component of a durable row key.
583///
584/// See [metadata segments](../../../docs/specs/format.md#421-metadata-segments).
585pub fn hex_encode_row_key_component(value: &str) -> String {
586    crate::hex::hex_encode_bytes(value.as_bytes())
587}
588
589/// Reader-side lookup grammar: the probes, prefixes, and resume keys that
590/// point lookups and scans build per family. Defined beside
591/// `row_key_for_family` and `filter_key_for_family` because the pairing is
592/// byte-for-byte — a probe must equal the filter key the writer stored, and
593/// a prefix must be a prefix of the row keys it selects. Change a key
594/// format and its lookup grammar together, here.
595pub mod lookup_keys {
596    use super::hex_encode_row_key_component;
597    use crate::{AttributeRevisionNo, ChangeSeq, InodeId, RevisionNo};
598
599    /// The prefix every inode row key starts with. Inode ids are
600    /// zero-padded to a fixed width after it, so a range scan over this
601    /// prefix walks the inode family in ascending inode-id order.
602    ///
603    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
604    pub const INODE_ROW_PREFIX: &str = "inode-";
605
606    /// The prefix every canonical revision row key starts with. A range scan
607    /// over it walks every revision the manifest records, superseded ones
608    /// included, which is what a reachability question about content needs.
609    ///
610    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
611    pub const REVISION_ROW_PREFIX: &str = "revision-";
612
613    /// Builds the exact point-lookup key for an inode row.
614    ///
615    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
616    pub fn inode_key(inode_id: InodeId) -> String {
617        format!("{INODE_ROW_PREFIX}{:020}", inode_id.0)
618    }
619
620    /// Builds the resume bound for a scan that must continue strictly after
621    /// `inode_id`'s row.
622    ///
623    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
624    pub fn inode_key_after(inode_id: InodeId) -> String {
625        format!("{}\0", inode_key(inode_id))
626    }
627
628    /// Builds the range prefix selecting directory bindings under one parent.
629    ///
630    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
631    pub fn direntry_parent_prefix(parent_inode_id: InodeId) -> String {
632        format!("direntry-{:020}-", parent_inode_id.0)
633    }
634
635    /// Builds the bloom-filter probe shared by all generations of one parent/name binding.
636    ///
637    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
638    pub fn direntry_bind_probe(parent_inode_id: InodeId, name_key: &str) -> String {
639        format!(
640            "direntry-{:020}-{}",
641            parent_inode_id.0,
642            hex_encode_row_key_component(name_key)
643        )
644    }
645
646    /// Builds the range prefix selecting every generation of one parent/name binding.
647    ///
648    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
649    pub fn direntry_bind_prefix(parent_inode_id: InodeId, name_key: &str) -> String {
650        format!("{}-", direntry_bind_probe(parent_inode_id, name_key))
651    }
652
653    /// Builds the bloom-filter probe shared by bindings that target one child inode.
654    ///
655    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
656    pub fn direntry_child_probe(child_inode_id: InodeId) -> String {
657        format!("direntry-child-{:020}", child_inode_id.0)
658    }
659
660    /// Builds the reverse-index range prefix selecting bindings to one child inode.
661    ///
662    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
663    pub fn direntry_child_prefix(child_inode_id: InodeId) -> String {
664        format!("{}-", direntry_child_probe(child_inode_id))
665    }
666
667    /// Builds the bloom-filter probe shared by unbinds for one parent/name pair.
668    ///
669    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
670    pub fn direntry_unbind_probe(parent_inode_id: InodeId, name_key: &str) -> String {
671        format!(
672            "direntry-unbind-{:020}-{}",
673            parent_inode_id.0,
674            hex_encode_row_key_component(name_key)
675        )
676    }
677
678    /// Rows for one specific binding generation under the unbind probe.
679    ///
680    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
681    pub fn direntry_unbind_binding_prefix(
682        parent_inode_id: InodeId,
683        name_key: &str,
684        bind_seq: ChangeSeq,
685        bind_delta_index: u32,
686    ) -> String {
687        format!(
688            "{}-{:020}-{:010}-",
689            direntry_unbind_probe(parent_inode_id, name_key),
690            bind_seq.0,
691            bind_delta_index
692        )
693    }
694
695    /// Builds the range prefix selecting every unbind below one parent directory.
696    ///
697    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
698    pub fn direntry_unbind_parent_prefix(parent_inode_id: InodeId) -> String {
699        format!("direntry-unbind-{:020}-", parent_inode_id.0)
700    }
701
702    /// Builds the range prefix selecting unbinds for one parent/name pair.
703    ///
704    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
705    pub fn direntry_unbind_name_prefix(parent_inode_id: InodeId, name_key: &str) -> String {
706        format!(
707            "{}{}-",
708            direntry_unbind_parent_prefix(parent_inode_id),
709            hex_encode_row_key_component(name_key)
710        )
711    }
712
713    /// Builds the bloom-filter probe shared by tombstone events for one root inode.
714    ///
715    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
716    pub fn tombstone_probe(root_inode_id: InodeId) -> String {
717        format!("tombstone-{:020}", root_inode_id.0)
718    }
719
720    /// Builds the range prefix selecting the tombstone history of one root inode.
721    ///
722    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
723    pub fn tombstone_prefix(root_inode_id: InodeId) -> String {
724        format!("{}-", tombstone_probe(root_inode_id))
725    }
726
727    /// The prefix every active-deletion row key starts with. Deletion
728    /// sequence and root inode are zero-padded to fixed widths after it, so a
729    /// range scan over this prefix walks the namespace's recoverable
730    /// deletions oldest deletion first — the trash listing's whole read.
731    ///
732    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
733    pub const ACTIVE_DELETION_ROW_PREFIX: &str = "active-deletion-";
734
735    /// Rank of an undelete's removal marker within one deletion generation.
736    /// It is the lowest rank on purpose: an ascending scan sees the removal
737    /// before the row it removes, so a page never lists a deletion whose
738    /// marker was going to arrive one page later.
739    pub const ACTIVE_DELETION_RANK_REMOVED: u8 = 0;
740
741    /// Rank of the listed row within one deletion generation, and the highest
742    /// rank the family defines.
743    pub const ACTIVE_DELETION_RANK_LISTED: u8 = 1;
744
745    /// Builds an active-deletion row key from its generation and rank.
746    ///
747    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
748    pub fn active_deletion_row_key(
749        deleted_at_seq: ChangeSeq,
750        root_inode_id: InodeId,
751        sort_rank: u8,
752    ) -> String {
753        format!(
754            "{ACTIVE_DELETION_ROW_PREFIX}{:020}-{:020}-{sort_rank}",
755            deleted_at_seq.0, root_inode_id.0
756        )
757    }
758
759    /// Builds the resume bound for a trash page that must continue strictly
760    /// after the deletion generation it last returned. The listed row is the
761    /// generation's highest-ranked row, so resuming past it skips the whole
762    /// generation and nothing else.
763    ///
764    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
765    pub fn active_deletion_key_after(deleted_at_seq: ChangeSeq, root_inode_id: InodeId) -> String {
766        format!(
767            "{}\0",
768            active_deletion_row_key(deleted_at_seq, root_inode_id, ACTIVE_DELETION_RANK_LISTED)
769        )
770    }
771
772    /// Builds the bloom-filter probe shared by receipts for one commit id.
773    ///
774    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
775    pub fn commit_receipt_probe(commit_id: &str) -> String {
776        format!("commit-receipt-{}", hex_encode_row_key_component(commit_id))
777    }
778
779    /// Builds the range prefix selecting durable receipts for one commit id.
780    ///
781    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
782    pub fn commit_receipt_prefix(commit_id: &str) -> String {
783        format!("{}-", commit_receipt_probe(commit_id))
784    }
785
786    /// Builds the bloom-filter probe shared by newest-first revisions of one inode.
787    ///
788    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
789    pub fn revision_by_inode_desc_probe(inode_id: InodeId) -> String {
790        format!("revision-by-inode-desc-{:020}", inode_id.0)
791    }
792
793    /// Builds the range prefix selecting newest-first revisions of one inode.
794    ///
795    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
796    pub fn revision_by_inode_desc_prefix(inode_id: InodeId) -> String {
797        format!("{}-", revision_by_inode_desc_probe(inode_id))
798    }
799
800    /// Revision numbers are stored inverted so newest sorts first.
801    ///
802    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
803    pub fn revision_by_inode_desc_revision_prefix(
804        inode_id: InodeId,
805        revision_no: RevisionNo,
806    ) -> String {
807        format!(
808            "{}{:020}-",
809            revision_by_inode_desc_prefix(inode_id),
810            u64::MAX - revision_no.0
811        )
812    }
813
814    /// The full descending-index row key: revision number, commit seq, and
815    /// delta index all inverted so newest sorts first.
816    ///
817    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
818    pub fn revision_by_inode_desc_row_key(
819        inode_id: InodeId,
820        revision_no: RevisionNo,
821        committed_seq: ChangeSeq,
822        revision_delta_index: u32,
823    ) -> String {
824        format!(
825            "{}{:020}-{:020}-{:010}",
826            revision_by_inode_desc_prefix(inode_id),
827            u64::MAX - revision_no.0,
828            u64::MAX - committed_seq.0,
829            u32::MAX - revision_delta_index
830        )
831    }
832
833    /// Builds the bloom-filter probe shared by every attribute revision of one
834    /// inode.
835    ///
836    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
837    pub fn attributes_probe(inode_id: InodeId) -> String {
838        format!("attributes-{:020}", inode_id.0)
839    }
840
841    /// Builds the range prefix selecting one inode's attribute revisions,
842    /// newest first.
843    ///
844    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
845    pub fn attributes_prefix(inode_id: InodeId) -> String {
846        format!("{}-", attributes_probe(inode_id))
847    }
848
849    /// The full attribute row key: revision number, commit seq, and delta
850    /// index all inverted so an ascending scan of one inode's prefix reads
851    /// its newest attribute state first.
852    ///
853    /// See [metadata segments](../../../../docs/specs/format.md#421-metadata-segments).
854    pub fn attributes_row_key(
855        inode_id: InodeId,
856        attributes_revision_no: AttributeRevisionNo,
857        committed_seq: ChangeSeq,
858        delta_index: u32,
859    ) -> String {
860        format!(
861            "{}{:020}-{:020}-{:010}",
862            attributes_prefix(inode_id),
863            u64::MAX - attributes_revision_no.0,
864            u64::MAX - committed_seq.0,
865            u32::MAX - delta_index
866        )
867    }
868}
869
870/// Carries one complete namespace file-set description inside a manifest envelope.
871///
872/// See [manifest publication](../../../docs/specs/format.md#61-manifest-publication-and-checkpoint-verification).
873#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
874pub struct NamespaceManifestPayload {
875    /// Namespace whose materialized state this manifest describes.
876    pub namespace_id: NamespaceId,
877    /// Monotonic logical manifest position selected by the namespace root.
878    pub manifest_id: ManifestId,
879    /// Immutable object identity that distinguishes speculative candidates at `manifest_id`.
880    pub manifest_object_id: ManifestObjectId,
881    /// Greatest namespace sequence materialized by the referenced file set.
882    pub head_seq: ChangeSeq,
883    /// Commit id assigned to `head_seq`, used to validate agreement with the head.
884    pub head_commit_id: CommitId,
885    /// Oldest run sequence still represented by `metadata_files`.
886    pub base_seq: ChangeSeq,
887    /// Fencing epoch of the writer that produced this candidate.
888    pub writer_epoch: WriterEpoch,
889    /// First inode identity available after replaying the manifest snapshot.
890    pub next_inode_id: InodeId,
891    /// Earliest sequence for which retained history remains readable.
892    pub retention_floor_seq: ChangeSeq,
893    /// Complete ordered set of metadata segments required to reconstruct the snapshot.
894    pub metadata_files: Vec<MetadataFileRef>,
895}
896
897/// In-memory view of a namespace manifest envelope.
898///
899/// This struct is not the durable layout; durable bytes are produced only by
900/// [`encode_namespace_manifest_json`] and validated only by
901/// [`decode_namespace_manifest_json`].
902#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
903pub struct NamespaceManifestEnvelope {
904    /// Durable-family discriminator checked before payload decoding.
905    pub kind: NamespaceManifestKind,
906    /// Family-local format version, which must equal [`NAMESPACE_MANIFEST_FORMAT_VERSION`].
907    pub format_version: u32,
908    /// Digest of the payload JSON exactly as stored in the durable document,
909    /// in `sha256:<hex>` form.
910    pub payload_checksum: String,
911    /// Decoded file-set description protected by `payload_checksum`.
912    pub payload: NamespaceManifestPayload,
913}
914
915impl NamespaceManifestEnvelope {
916    /// Builds a versioned envelope and computes its checksum from canonical payload JSON.
917    ///
918    /// Construction fails when the payload cannot be encoded.
919    pub fn from_payload(payload: NamespaceManifestPayload) -> Result<Self, EnvelopeCodecError> {
920        Ok(Self {
921            kind: NamespaceManifestKind::NamespaceManifest,
922            format_version: NAMESPACE_MANIFEST_FORMAT_VERSION,
923            payload_checksum: namespace_manifest_payload_checksum(&payload)?,
924            payload,
925        })
926    }
927}
928
929fn namespace_manifest_payload_checksum(
930    payload: &NamespaceManifestPayload,
931) -> Result<String, EnvelopeCodecError> {
932    crate::envelope::json_payload_checksum(payload)
933}
934
935/// Encodes a namespace-manifest envelope as its durable JSON representation.
936///
937/// Encoding fails when the version is unsupported, the in-memory checksum is
938/// stale, or JSON serialization fails. See
939/// [manifest publication](../../../docs/specs/format.md#61-manifest-publication-and-checkpoint-verification).
940pub fn encode_namespace_manifest_json(
941    envelope: &NamespaceManifestEnvelope,
942) -> Result<Vec<u8>, EnvelopeCodecError> {
943    crate::envelope::encode_json_envelope(
944        envelope.kind.as_str(),
945        envelope.format_version,
946        NAMESPACE_MANIFEST_FORMAT_VERSION,
947        &envelope.payload_checksum,
948        &envelope.payload,
949    )
950}
951
952/// Decodes and verifies a durable namespace-manifest JSON envelope.
953///
954/// Decoding fails for invalid JSON, the wrong kind or version, a checksum
955/// mismatch, or an invalid payload. See
956/// [manifest publication](../../../docs/specs/format.md#61-manifest-publication-and-checkpoint-verification).
957pub fn decode_namespace_manifest_json(
958    bytes: &[u8],
959) -> Result<NamespaceManifestEnvelope, EnvelopeCodecError> {
960    let expected_kind = NamespaceManifestKind::NamespaceManifest;
961    let decoded =
962        crate::envelope::decode_json_envelope(bytes, NAMESPACE_MANIFEST_FORMAT_VERSION, |found| {
963            crate::envelope::verify_kind(expected_kind.as_str(), found)
964        })?;
965
966    Ok(NamespaceManifestEnvelope {
967        kind: expected_kind,
968        format_version: decoded.format_version,
969        payload_checksum: decoded.payload_checksum,
970        payload: decoded.payload,
971    })
972}
973
974#[cfg(test)]
975mod tests {
976    use super::{
977        decode_namespace_manifest_json, encode_namespace_manifest_json, BlockHandle,
978        MetadataFileRef, MetadataTableFamily, NamespaceManifestEnvelope, NamespaceManifestPayload,
979    };
980    use crate::{
981        ChangeSeq, CommitId, InodeId, ManifestId, ManifestObjectId, MetadataTableId, NameKey,
982        NamespaceId, WriterEpoch,
983    };
984
985    #[test]
986    fn inode_row_keys_sort_by_ascending_inode_id() {
987        // The inode family's durable order IS ascending inode id, which is
988        // what lets a whole-namespace file walk resume from one bound.
989        let ids = [9_u64, 1, 100, 10, 2];
990        let key_of = |id: u64| super::lookup_keys::inode_key(InodeId(id));
991        let mut keys: Vec<String> = ids.iter().copied().map(key_of).collect();
992        keys.sort();
993
994        let mut ascending_ids = ids;
995        ascending_ids.sort_unstable();
996        assert_eq!(
997            keys,
998            ascending_ids
999                .iter()
1000                .copied()
1001                .map(key_of)
1002                .collect::<Vec<_>>(),
1003            "row-key order must agree with inode-id order"
1004        );
1005        assert!(keys
1006            .iter()
1007            .all(|key| key.starts_with(super::lookup_keys::INODE_ROW_PREFIX)));
1008    }
1009
1010    #[test]
1011    fn the_inode_resume_bound_skips_its_own_row_and_nothing_after_it() {
1012        let resume = super::lookup_keys::inode_key_after(InodeId(7));
1013        assert!(resume > super::lookup_keys::inode_key(InodeId(7)));
1014        assert!(resume < super::lookup_keys::inode_key(InodeId(8)));
1015    }
1016
1017    #[test]
1018    fn namespace_manifest_kind_string_matches_serde() {
1019        let kind = super::NamespaceManifestKind::NamespaceManifest;
1020        let serialized = serde_json::to_value(kind).expect("serialize kind");
1021        assert_eq!(serialized, serde_json::Value::from(kind.as_str()));
1022    }
1023
1024    #[test]
1025    fn namespace_manifest_codec_round_trips_base_only_materialization() {
1026        let envelope = NamespaceManifestEnvelope::from_payload(NamespaceManifestPayload {
1027            namespace_id: NamespaceId::parse("demo").expect("valid namespace id"),
1028            manifest_id: ManifestId(10),
1029            manifest_object_id: ManifestObjectId::parse("00000000000000000010-0123456789abcdef")
1030                .expect("valid manifest object id"),
1031            head_seq: ChangeSeq(10),
1032            head_commit_id: CommitId::parse("c_00000000000000000000000000000001")
1033                .expect("commit id"),
1034            base_seq: ChangeSeq(10),
1035            writer_epoch: WriterEpoch(2),
1036            next_inode_id: InodeId(42),
1037            retention_floor_seq: ChangeSeq(0),
1038            metadata_files: vec![metadata_file_ref(
1039                "demo",
1040                "tbl_00000000000000000000000000000001",
1041                ChangeSeq(10),
1042                1,
1043                "namespaces/demo/metadata/tables/tbl_00000000000000000000000000000001.sst.zst",
1044            )],
1045        })
1046        .expect("manifest");
1047
1048        let encoded = encode_namespace_manifest_json(&envelope).expect("encode manifest");
1049        let decoded = decode_namespace_manifest_json(&encoded).expect("decode manifest");
1050
1051        assert_eq!(decoded, envelope);
1052        assert_eq!(decoded.payload.base_seq, ChangeSeq(10));
1053        assert_eq!(decoded.payload.metadata_files.len(), 1);
1054        assert_eq!(decoded.payload.metadata_files[0].run_seq, ChangeSeq(10));
1055    }
1056
1057    /// A fork target's first own manifest keeps referencing the source's
1058    /// metadata objects: ownership travels with each file reference, not
1059    /// with the manifest.
1060    #[test]
1061    fn namespace_manifest_codec_round_trips_inherited_source_tables() {
1062        let envelope = NamespaceManifestEnvelope::from_payload(
1063            NamespaceManifestPayload {
1064                namespace_id: NamespaceId::parse("demo").expect("valid namespace id"),
1065                manifest_id: ManifestId(12),
1066                manifest_object_id: ManifestObjectId::parse(
1067                    "00000000000000000012-0123456789abcdef",
1068                )
1069                .expect("valid manifest object id"),
1070                head_seq: ChangeSeq(12),
1071                head_commit_id: CommitId::parse("c_00000000000000000000000000000002")
1072                    .expect("commit id"),
1073                base_seq: ChangeSeq(10),
1074                writer_epoch: WriterEpoch(2),
1075                next_inode_id: InodeId(42),
1076                retention_floor_seq: ChangeSeq(0),
1077                metadata_files: vec![
1078                    metadata_file_ref(
1079                        "source",
1080                        "tbl_00000000000000000000000000000001",
1081                        ChangeSeq(10),
1082                        1,
1083                        "namespaces/source/tables/metadata/tbl_00000000000000000000000000000001.sst.zst",
1084                    ),
1085                    metadata_file_ref(
1086                        "demo",
1087                        "tbl_00000000000000000000000000000002",
1088                        ChangeSeq(12),
1089                        0,
1090                        "namespaces/demo/metadata/tables/tbl_00000000000000000000000000000002.sst.zst",
1091                    ),
1092                ],
1093            },
1094        )
1095        .expect("manifest");
1096
1097        let encoded = encode_namespace_manifest_json(&envelope).expect("encode manifest");
1098        let decoded = decode_namespace_manifest_json(&encoded).expect("decode manifest");
1099
1100        assert_eq!(decoded, envelope);
1101        assert_eq!(decoded.payload.metadata_files[0].level, 1);
1102        assert_eq!(decoded.payload.metadata_files[1].level, 0);
1103        assert_eq!(decoded.payload.metadata_files[1].run_seq, ChangeSeq(12));
1104        assert_eq!(
1105            decoded.payload.metadata_files[0].owner_namespace_id,
1106            NamespaceId::parse("source").expect("valid namespace id")
1107        );
1108    }
1109
1110    #[test]
1111    fn direntry_bind_row_key_supports_parent_and_child_indexes() {
1112        let row = super::MetadataRow::DirentryBind {
1113            parent_inode_id: InodeId(9),
1114            name_key: NameKey::parse("report.txt").expect("valid name key"),
1115            display_name: crate::DisplayName::parse("Report.txt").expect("valid display name"),
1116            child_inode_id: InodeId(42),
1117            bind_seq: ChangeSeq(17),
1118            bind_delta_index: 3,
1119        };
1120
1121        assert_eq!(
1122            row.row_key_for_family(MetadataTableFamily::DirentryBinds),
1123            "direntry-00000000000000000009-7265706f72742e747874-00000000000000000017-0000000003"
1124        );
1125        assert_eq!(
1126            row.row_key_for_family(MetadataTableFamily::DirentryChildBinds),
1127            "direntry-child-00000000000000000042-00000000000000000017-0000000003-00000000000000000009-7265706f72742e747874"
1128        );
1129    }
1130
1131    #[test]
1132    fn row_keys_hex_encode_dash_containing_variable_components() {
1133        let row = super::MetadataRow::DirentryBind {
1134            parent_inode_id: InodeId(9),
1135            name_key: NameKey::parse("report-2024").expect("valid name key"),
1136            display_name: crate::DisplayName::parse("report-2024").expect("valid display name"),
1137            child_inode_id: InodeId(42),
1138            bind_seq: ChangeSeq(17),
1139            bind_delta_index: 3,
1140        };
1141
1142        assert_eq!(
1143            row.row_key_for_family(MetadataTableFamily::DirentryBinds),
1144            "direntry-00000000000000000009-7265706f72742d32303234-00000000000000000017-0000000003"
1145        );
1146    }
1147
1148    #[test]
1149    fn revision_row_key_supports_newest_first_inode_index() {
1150        let row = super::MetadataRow::Revision {
1151            inode_id: InodeId(42),
1152            revision_no: crate::RevisionNo(7),
1153            committed_seq: ChangeSeq(12),
1154            committed_at_ms: 12_000,
1155            actor: crate::ActorRef::loonfs_system(),
1156            revision_delta_index: 3,
1157            content_ref: crate::ContentRef::blob_v1(
1158                crate::ContentId::parse("con_0123456789abcdef0123456789abcdef")
1159                    .expect("valid content id"),
1160                b"row key sample",
1161            ),
1162        };
1163
1164        assert_eq!(
1165            row.row_key_for_family(MetadataTableFamily::Revisions),
1166            "revision-00000000000000000042-00000000000000000007-0000000003"
1167        );
1168        assert_eq!(
1169            row.row_key_for_family(MetadataTableFamily::RevisionsByInodeDesc),
1170            "revision-by-inode-desc-00000000000000000042-18446744073709551608-18446744073709551603-4294967292"
1171        );
1172    }
1173
1174    /// The attribute family's durable order is newest revision first within
1175    /// one inode, and the row key a writer stores must be the exact key a
1176    /// reader's prefix selects.
1177    #[test]
1178    fn attributes_row_keys_sort_newest_revision_first_under_the_inode_prefix() {
1179        let row_of =
1180            |revision: u64, seq: u64, delta_index: u32| super::MetadataRow::AttributesRevision {
1181                inode_id: InodeId(42),
1182                attributes_revision_no: crate::AttributeRevisionNo(revision),
1183                committed_seq: ChangeSeq(seq),
1184                delta_index,
1185                actor: crate::ActorRef::loonfs_system(),
1186                updated_at_ms: 12_000 + seq,
1187                attributes: crate::Attributes::default(),
1188            };
1189        let newest = row_of(3, 12, 1);
1190        let older = row_of(2, 11, 0);
1191
1192        assert_eq!(
1193            newest.row_key_for_family(MetadataTableFamily::Attributes),
1194            "attributes-00000000000000000042-18446744073709551612-18446744073709551603-4294967294"
1195        );
1196        assert_eq!(
1197            newest.row_key(),
1198            newest.row_key_for_family(MetadataTableFamily::Attributes)
1199        );
1200        assert!(
1201            newest.row_key() < older.row_key(),
1202            "an ascending scan must reach the newest revision first"
1203        );
1204        let prefix = super::lookup_keys::attributes_prefix(InodeId(42));
1205        assert!(newest.row_key().starts_with(&prefix));
1206        assert!(older.row_key().starts_with(&prefix));
1207        // A point lookup probes the filter with the inode's shared key, and
1208        // the writer stores exactly that key.
1209        assert_eq!(
1210            newest.filter_key_for_family(MetadataTableFamily::Attributes),
1211            super::lookup_keys::attributes_probe(InodeId(42))
1212        );
1213        // Another inode's rows sort outside the prefix.
1214        assert!(!row_of(3, 12, 1)
1215            .row_key()
1216            .starts_with(&super::lookup_keys::attributes_prefix(InodeId(43))));
1217    }
1218
1219    /// [`MetadataTableFamily::row_key_prefix`] states the front of every row
1220    /// key in a family, and a streaming compaction reads its retention
1221    /// groupings out of the components that follow it. A prefix that drifted
1222    /// from the row-key format would put grouping boundaries in the wrong
1223    /// place, so the two are pinned against each other for every family.
1224    #[test]
1225    fn row_key_prefixes_match_the_row_keys_they_front() {
1226        let name_key = NameKey::parse("report.txt").expect("valid name key");
1227        let display_name = crate::DisplayName::parse("report.txt").expect("valid display name");
1228        let bind = super::MetadataRow::DirentryBind {
1229            parent_inode_id: InodeId(9),
1230            name_key: name_key.clone(),
1231            display_name: display_name.clone(),
1232            child_inode_id: InodeId(42),
1233            bind_seq: ChangeSeq(17),
1234            bind_delta_index: 3,
1235        };
1236        let revision = super::MetadataRow::Revision {
1237            inode_id: InodeId(42),
1238            revision_no: crate::RevisionNo(7),
1239            committed_seq: ChangeSeq(12),
1240            committed_at_ms: 12_000,
1241            actor: crate::ActorRef::loonfs_system(),
1242            revision_delta_index: 3,
1243            content_ref: crate::ContentRef::blob_v1(
1244                crate::ContentId::parse("con_0123456789abcdef0123456789abcdef")
1245                    .expect("valid content id"),
1246                b"row key prefix sample",
1247            ),
1248        };
1249        let rows: [(MetadataTableFamily, super::MetadataRow); 10] = [
1250            (
1251                MetadataTableFamily::Inodes,
1252                super::MetadataRow::Inode {
1253                    inode_id: InodeId(42),
1254                    inode_kind: crate::InodeKind::File,
1255                    created_seq: ChangeSeq(3),
1256                    created_by: crate::ActorRef::loonfs_system(),
1257                    created_at_ms: 3_000,
1258                },
1259            ),
1260            (MetadataTableFamily::DirentryBinds, bind.clone()),
1261            (MetadataTableFamily::DirentryChildBinds, bind),
1262            (
1263                MetadataTableFamily::DirentryUnbinds,
1264                super::MetadataRow::DirentryUnbind {
1265                    parent_inode_id: InodeId(9),
1266                    name_key,
1267                    display_name,
1268                    child_inode_id: InodeId(42),
1269                    bind_seq: ChangeSeq(17),
1270                    bind_delta_index: 3,
1271                    unbind_seq: ChangeSeq(19),
1272                    unbind_delta_index: 0,
1273                },
1274            ),
1275            (MetadataTableFamily::Revisions, revision.clone()),
1276            (MetadataTableFamily::RevisionsByInodeDesc, revision),
1277            (
1278                MetadataTableFamily::Tombstones,
1279                super::MetadataRow::Tombstone {
1280                    root_inode_id: InodeId(42),
1281                    generation: super::TombstoneGeneration {
1282                        seq: ChangeSeq(12),
1283                        delta_index: 0,
1284                    },
1285                    action: super::TombstoneRowAction::Set {
1286                        deleted_direntry: None,
1287                    },
1288                    deleted_at_ms: 12_000,
1289                    actor: crate::ActorRef::loonfs_system(),
1290                },
1291            ),
1292            (
1293                MetadataTableFamily::ActiveDeletions,
1294                super::MetadataRow::ActiveDeletion {
1295                    root_inode_id: InodeId(42),
1296                    deleted_at_seq: ChangeSeq(12),
1297                    action: super::ActiveDeletionRowAction::Removed {
1298                        revoked_at_seq: ChangeSeq(15),
1299                    },
1300                },
1301            ),
1302            (
1303                MetadataTableFamily::CommitReceipts,
1304                super::MetadataRow::CommitReceipt {
1305                    commit_id: CommitId::parse("c_00000000000000000000000000000001")
1306                        .expect("commit id"),
1307                    actor: crate::ActorRef::loonfs_system(),
1308                    semantic_commit_fingerprint: "sha256:unused".to_owned(),
1309                    committed_seq: ChangeSeq(12),
1310                    committed_at_ms: 12_000,
1311                    message: None,
1312                },
1313            ),
1314            (
1315                MetadataTableFamily::Attributes,
1316                super::MetadataRow::AttributesRevision {
1317                    inode_id: InodeId(42),
1318                    attributes_revision_no: crate::AttributeRevisionNo(3),
1319                    committed_seq: ChangeSeq(12),
1320                    delta_index: 0,
1321                    actor: crate::ActorRef::loonfs_system(),
1322                    updated_at_ms: 12_000,
1323                    attributes: crate::Attributes::default(),
1324                },
1325            ),
1326        ];
1327
1328        for (family, row) in rows {
1329            let row_key = row.row_key_for_family(family);
1330            let prefix = family.row_key_prefix();
1331            assert!(
1332                !prefix.is_empty(),
1333                "`{family:?}` declares no row-key prefix"
1334            );
1335            assert!(
1336                row_key.starts_with(prefix),
1337                "row key `{row_key}` for `{family:?}` does not start with `{prefix}`"
1338            );
1339        }
1340    }
1341
1342    #[test]
1343    fn attribution_values_never_change_row_or_index_keys() {
1344        fn rows(actor: crate::ActorRef) -> Vec<(MetadataTableFamily, super::MetadataRow)> {
1345            vec![
1346                (
1347                    MetadataTableFamily::Inodes,
1348                    super::MetadataRow::Inode {
1349                        inode_id: InodeId(42),
1350                        inode_kind: crate::InodeKind::File,
1351                        created_seq: ChangeSeq(3),
1352                        created_by: actor.clone(),
1353                        created_at_ms: 3_000,
1354                    },
1355                ),
1356                (
1357                    MetadataTableFamily::RevisionsByInodeDesc,
1358                    super::MetadataRow::Revision {
1359                        inode_id: InodeId(42),
1360                        revision_no: crate::RevisionNo(7),
1361                        committed_seq: ChangeSeq(12),
1362                        committed_at_ms: 12_000,
1363                        actor: actor.clone(),
1364                        revision_delta_index: 3,
1365                        content_ref: crate::ContentRef::blob_v1(
1366                            crate::ContentId::parse("con_0123456789abcdef0123456789abcdef")
1367                                .expect("content id"),
1368                            b"attribution key test",
1369                        ),
1370                    },
1371                ),
1372                (
1373                    MetadataTableFamily::Tombstones,
1374                    super::MetadataRow::Tombstone {
1375                        root_inode_id: InodeId(42),
1376                        generation: super::TombstoneGeneration {
1377                            seq: ChangeSeq(12),
1378                            delta_index: 3,
1379                        },
1380                        action: super::TombstoneRowAction::Set {
1381                            deleted_direntry: None,
1382                        },
1383                        deleted_at_ms: 12_000,
1384                        actor: actor.clone(),
1385                    },
1386                ),
1387                (
1388                    MetadataTableFamily::ActiveDeletions,
1389                    super::MetadataRow::ActiveDeletion {
1390                        root_inode_id: InodeId(42),
1391                        deleted_at_seq: ChangeSeq(12),
1392                        action: super::ActiveDeletionRowAction::Listed {
1393                            deleted_at_ms: 12_000,
1394                            deleted_by: actor.clone(),
1395                            deleted_direntry: None,
1396                        },
1397                    },
1398                ),
1399                (
1400                    MetadataTableFamily::Attributes,
1401                    super::MetadataRow::AttributesRevision {
1402                        inode_id: InodeId(42),
1403                        attributes_revision_no: crate::AttributeRevisionNo(2),
1404                        committed_seq: ChangeSeq(12),
1405                        delta_index: 3,
1406                        actor,
1407                        updated_at_ms: 12_000,
1408                        attributes: crate::Attributes::default(),
1409                    },
1410                ),
1411            ]
1412        }
1413
1414        let actors = [
1415            crate::ActorRef::user(crate::ActorId::parse("auth0|x").expect("actor id")),
1416            crate::ActorRef::service(
1417                crate::ActorId::parse("x".repeat(256)).expect("256-byte actor id"),
1418            ),
1419            crate::ActorRef::system(crate::ActorId::parse("雪-actor").expect("unicode actor id")),
1420        ];
1421        let baseline = rows(actors[0].clone());
1422        for actor in actors.into_iter().skip(1) {
1423            let changed = rows(actor);
1424            for ((family, baseline), (changed_family, changed)) in baseline.iter().zip(&changed) {
1425                assert_eq!(family, changed_family);
1426                assert_eq!(
1427                    baseline.row_key_for_family(*family),
1428                    changed.row_key_for_family(*family)
1429                );
1430                assert_eq!(
1431                    baseline.filter_key_for_family(*family),
1432                    changed.filter_key_for_family(*family)
1433                );
1434            }
1435        }
1436    }
1437
1438    fn metadata_file_ref(
1439        owner_namespace_id: &str,
1440        table_id: &str,
1441        run_seq: ChangeSeq,
1442        level: u32,
1443        object_key: &str,
1444    ) -> MetadataFileRef {
1445        MetadataFileRef {
1446            owner_namespace_id: NamespaceId::parse(owner_namespace_id).expect("valid namespace id"),
1447            table_id: MetadataTableId::parse(table_id).expect("valid table id"),
1448            object_key: object_key.to_owned(),
1449            run_seq,
1450            level,
1451            family: MetadataTableFamily::Inodes,
1452            segment_index: 0,
1453            row_count: 0,
1454            min_key: String::new(),
1455            max_key: String::new(),
1456            index_block: BlockHandle {
1457                offset: 0,
1458                stored_len: 0,
1459                decoded_len: 0,
1460                crc32c: 0,
1461            },
1462            filter_block: BlockHandle {
1463                offset: 0,
1464                stored_len: 0,
1465                decoded_len: 0,
1466                crc32c: 0,
1467            },
1468            filter_inline: None,
1469            payload_checksum: "sha256:unused".to_owned(),
1470        }
1471    }
1472}