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}