Skip to main content

loonfs_core/commit/
prepared.rs

1//! [`PreparedCommit`]: a validated request paired with its plan and
2//! semantic identity, ready for publication.
3
4use 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}