chronix_engine/storage/
backend.rs1use std::fmt;
8use std::path::{Path, PathBuf};
9
10use chronix_core::{NamespaceId, ShardId};
11
12use crate::storage::error::Result;
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub struct SegmentPath {
27 namespace: NamespaceId,
29 shard_id: ShardId,
31 segment_name: String,
33}
34
35impl SegmentPath {
36 pub fn new(
42 namespace: NamespaceId,
43 shard_id: ShardId,
44 segment_name: impl Into<String>,
45 ) -> crate::storage::error::Result<Self> {
46 let name = segment_name.into();
47 if name.is_empty()
48 || name.contains('/')
49 || name.contains('\\')
50 || name.contains("..")
51 || name == "."
52 {
53 return Err(crate::storage::error::StorageError::InvalidPath {
54 detail: format!(
55 "invalid segment name '{name}': must not be empty or contain path separators / '..' components"
56 ),
57 });
58 }
59 Ok(Self {
60 namespace,
61 shard_id,
62 segment_name: name,
63 })
64 }
65
66 #[must_use]
68 pub fn namespace(&self) -> &NamespaceId {
69 &self.namespace
70 }
71
72 #[must_use]
74 pub fn shard_id(&self) -> ShardId {
75 self.shard_id
76 }
77
78 #[must_use]
80 pub fn segment_name(&self) -> &str {
81 &self.segment_name
82 }
83
84 #[must_use]
88 pub fn to_fs_path(&self, data_dir: &Path) -> PathBuf {
89 data_dir
90 .join(format!("ns_{}", self.namespace.as_str()))
91 .join(format!("shard_{}", self.shard_id.0))
92 .join(&self.segment_name)
93 }
94}
95
96impl fmt::Display for SegmentPath {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 write!(
99 f,
100 "ns_{}/shard_{}/{}",
101 self.namespace.as_str(),
102 self.shard_id.0,
103 self.segment_name
104 )
105 }
106}
107
108pub trait StorageBackend: Send + Sync + 'static {
122 fn put_segment(
126 &self,
127 path: &SegmentPath,
128 data: &[u8],
129 ) -> impl std::future::Future<Output = Result<()>> + Send;
130
131 fn get_segment(
133 &self,
134 path: &SegmentPath,
135 ) -> impl std::future::Future<Output = Result<Vec<u8>>> + Send;
136
137 fn get_range(
157 &self,
158 path: &SegmentPath,
159 offset: u64,
160 length: usize,
161 ) -> impl std::future::Future<Output = Result<Vec<u8>>> + Send;
162
163 fn delete_segment(
165 &self,
166 path: &SegmentPath,
167 ) -> impl std::future::Future<Output = Result<()>> + Send;
168
169 fn list_segments(
171 &self,
172 namespace: &NamespaceId,
173 shard_id: &ShardId,
174 ) -> impl std::future::Future<Output = Result<Vec<SegmentPath>>> + Send;
175
176 fn exists(&self, path: &SegmentPath) -> impl std::future::Future<Output = Result<bool>> + Send;
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 fn default_ns() -> NamespaceId {
185 NamespaceId::default_namespace()
186 }
187
188 #[test]
189 fn segment_path_display() {
190 let path = SegmentPath::new(default_ns(), ShardId(42), "segment_001.csx").unwrap();
191 assert_eq!(path.to_string(), "ns_default/shard_42/segment_001.csx");
192 }
193
194 #[test]
195 fn segment_path_to_fs_path() {
196 let path = SegmentPath::new(default_ns(), ShardId(7), "seg.csx").unwrap();
197 let fs_path = path.to_fs_path(Path::new("/data"));
198 assert_eq!(fs_path, PathBuf::from("/data/ns_default/shard_7/seg.csx"));
199 }
200
201 #[test]
202 fn segment_path_accessors() {
203 let path = SegmentPath::new(default_ns(), ShardId(3), "test.csx").unwrap();
204 assert_eq!(path.namespace(), &default_ns());
205 assert_eq!(path.shard_id(), ShardId(3));
206 assert_eq!(path.segment_name(), "test.csx");
207 }
208
209 #[test]
210 fn segment_path_equality() {
211 let a = SegmentPath::new(default_ns(), ShardId(1), "seg.csx").unwrap();
212 let b = SegmentPath::new(default_ns(), ShardId(1), "seg.csx").unwrap();
213 let c = SegmentPath::new(default_ns(), ShardId(2), "seg.csx").unwrap();
214 assert_eq!(a, b);
215 assert_ne!(a, c);
216 }
217
218 #[test]
219 fn segment_path_different_namespaces_differ() {
220 let ns_a = NamespaceId::new("tenant-a").unwrap();
221 let ns_b = NamespaceId::new("tenant-b").unwrap();
222 let a = SegmentPath::new(ns_a, ShardId(1), "seg.csx").unwrap();
223 let b = SegmentPath::new(ns_b, ShardId(1), "seg.csx").unwrap();
224 assert_ne!(a, b);
225 assert_ne!(
226 a.to_fs_path(Path::new("/data")),
227 b.to_fs_path(Path::new("/data"))
228 );
229 }
230
231 #[test]
232 fn segment_path_rejects_invalid_names() {
233 assert!(SegmentPath::new(default_ns(), ShardId(1), "").is_err());
234 assert!(SegmentPath::new(default_ns(), ShardId(1), "a/b").is_err());
235 assert!(SegmentPath::new(default_ns(), ShardId(1), "a\\b").is_err());
236 assert!(SegmentPath::new(default_ns(), ShardId(1), "a..b").is_err());
237 assert!(SegmentPath::new(default_ns(), ShardId(1), ".").is_err());
238 assert!(SegmentPath::new(default_ns(), ShardId(1), "segment_001.csx").is_ok());
240 }
241}