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