loonfs_core/commit/
prepared.rs1use super::{CommitFingerprint, CommitIr, CommitPlan};
5use loonfs_api::NamespaceId;
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub(crate) struct PreparedCommit {
11 pub(crate) request: CommitIr,
12 pub(crate) plan: CommitPlan,
13 pub(crate) semantic_identity: CommitFingerprint,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, Error)]
17pub enum CommitPrepareError {
18 #[error("prepared commit namespace mismatch: request `{request}`, plan `{plan}`")]
19 NamespaceMismatch {
20 request: NamespaceId,
21 plan: NamespaceId,
22 },
23 #[error("prepared commit id mismatch")]
24 CommitIdMismatch,
25}
26
27impl PreparedCommit {
28 pub(crate) fn new(
29 request: CommitIr,
30 plan: CommitPlan,
31 semantic_identity: CommitFingerprint,
32 ) -> Result<Self, CommitPrepareError> {
33 if request.namespace_id != plan.namespace_id {
34 return Err(CommitPrepareError::NamespaceMismatch {
35 request: request.namespace_id.clone(),
36 plan: plan.namespace_id.clone(),
37 });
38 }
39 if request.commit_id != plan.commit_id {
40 return Err(CommitPrepareError::CommitIdMismatch);
41 }
42
43 Ok(Self {
44 request,
45 plan,
46 semantic_identity,
47 })
48 }
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54 use crate::commit::{materialize_commit, CommitOpResult, PlannedOp, ValidatedOp};
55 use crate::commit::{CommitFingerprint, CommitOp};
56 use loonfs_api::wire::wal::WalDelta;
57 use loonfs_api::NameKey;
58 use loonfs_api::{ChangeSeq, CommitId, InodeId, WriterEpoch};
59
60 fn fingerprint() -> CommitFingerprint {
61 CommitFingerprint::new_unchecked("v0:sha256:test".to_owned())
62 }
63
64 fn request() -> CommitIr {
65 CommitIr {
66 namespace_id: NamespaceId::parse("demo").expect("valid namespace id"),
67 commit_id: CommitId::parse("commit-a").expect("valid commit id"),
68 writer_epoch: WriterEpoch(1),
69 ops: vec![PlannedOp::unchecked(CommitOp::CreateDirectory {
70 parent_inode_id: InodeId(1),
71 display_name: loonfs_api::DisplayName::parse("docs").expect("valid display name"),
72 })],
73 message: None,
74 }
75 }
76
77 fn plan() -> CommitPlan {
78 CommitPlan {
79 namespace_id: NamespaceId::parse("demo").expect("valid namespace id"),
80 commit_id: CommitId::parse("commit-a").expect("valid commit id"),
81 apply_after_seq: ChangeSeq(0),
82 assigned_seq: ChangeSeq(1),
83 validated_ops: vec![ValidatedOp::CreateDir {
84 op_index: 0,
85 parent_inode_id: InodeId(1),
86 display_name: loonfs_api::DisplayName::parse("docs").expect("valid display name"),
87 name_key: NameKey::parse("docs").expect("valid name key"),
88 child_inode_id: InodeId(2),
89 create_inode_delta_index: 0,
90 bind_delta_index: 1,
91 }],
92 resulting_next_inode_id: InodeId(3),
93 }
94 }
95
96 #[test]
97 fn prepared_commit_rejects_namespace_mismatch() {
98 let mut plan = plan();
99 plan.namespace_id = NamespaceId::parse("other").expect("valid namespace id");
100
101 assert!(matches!(
102 PreparedCommit::new(request(), plan, fingerprint()),
103 Err(CommitPrepareError::NamespaceMismatch { .. })
104 ));
105 }
106
107 #[test]
108 fn prepared_commit_rejects_commit_id_mismatch() {
109 let mut plan = plan();
110 plan.commit_id = CommitId::parse("commit-b").expect("valid commit id");
111
112 assert!(matches!(
113 PreparedCommit::new(request(), plan, fingerprint()),
114 Err(CommitPrepareError::CommitIdMismatch)
115 ));
116 }
117
118 #[test]
119 fn prepared_commit_allows_ephemeral_batch_apply_after_seq() {
120 let mut plan = plan();
121 plan.apply_after_seq = ChangeSeq(9);
122
123 PreparedCommit::new(request(), plan, fingerprint()).expect("prepare commit");
124 }
125
126 #[test]
127 fn prepared_commit_carries_the_request_fingerprint() {
128 let prepared =
129 PreparedCommit::new(request(), plan(), fingerprint()).expect("prepare commit");
130
131 assert_eq!(prepared.semantic_identity, fingerprint());
132 }
133
134 #[test]
135 fn materialize_commit_outputs_wal_ops_and_results_once() {
136 let materialized = materialize_commit(
137 PreparedCommit::new(request(), plan(), fingerprint()).expect("prepare commit"),
138 4_200,
139 );
140
141 assert_eq!(materialized.deltas.len(), 2);
142 assert!(matches!(
143 materialized.deltas[0].wal_delta,
144 WalDelta::CreateInode { .. }
145 ));
146 assert!(matches!(
147 materialized.deltas[1].wal_delta,
148 WalDelta::BindDirentry { .. }
149 ));
150 assert!(matches!(
151 materialized.results[0],
152 CommitOpResult::CreateDirectory { .. }
153 ));
154 }
155}