chronix_core/
wal_codec.rs1use crate::types::{Point, WalEntry};
22use std::fmt;
23
24const WAL_BINARY_V1: u8 = 0x01;
26
27const DISC_WRITE: u8 = 0x00;
29const DISC_DELETE: u8 = 0x01;
30
31#[derive(Debug)]
33pub enum CodecError {
34 Truncated,
36 UnknownVersion(u8),
38 UnknownDiscriminant(u8),
40 Postcard(postcard::Error),
42}
43
44impl fmt::Display for CodecError {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 Self::Truncated => write!(f, "WAL record truncated"),
48 Self::UnknownVersion(v) => write!(f, "unknown WAL version prefix: 0x{v:02x}"),
49 Self::UnknownDiscriminant(d) => write!(f, "unknown WAL binary discriminant: 0x{d:02x}"),
50 Self::Postcard(e) => write!(f, "WAL postcard error: {e}"),
51 }
52 }
53}
54
55impl std::error::Error for CodecError {
56 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
57 match self {
58 Self::Postcard(e) => Some(e),
59 _ => None,
60 }
61 }
62}
63
64impl From<postcard::Error> for CodecError {
65 fn from(e: postcard::Error) -> Self {
66 Self::Postcard(e)
67 }
68}
69
70pub fn encode(entry: &WalEntry) -> Result<Vec<u8>, CodecError> {
78 let mut buf = Vec::with_capacity(256);
80 buf.push(WAL_BINARY_V1);
81
82 match entry {
83 WalEntry::Write { point } => {
84 buf.push(DISC_WRITE);
85 let data = postcard::to_stdvec(point)?;
86 buf.extend_from_slice(&data);
87 }
88 WalEntry::Delete { tombstones } => {
89 buf.push(DISC_DELETE);
90 let data = postcard::to_stdvec(tombstones)?;
91 buf.extend_from_slice(&data);
92 }
93 }
94
95 Ok(buf)
96}
97
98pub fn encode_write_point(point: &Point) -> Result<Vec<u8>, CodecError> {
108 let mut buf = Vec::with_capacity(256);
109 buf.push(WAL_BINARY_V1);
110 buf.push(DISC_WRITE);
111 let data = postcard::to_stdvec(point)?;
112 buf.extend_from_slice(&data);
113 Ok(buf)
114}
115
116#[allow(clippy::indexing_slicing)] pub fn decode(data: &[u8]) -> Result<WalEntry, CodecError> {
127 if data.is_empty() {
128 return Err(CodecError::Truncated);
129 }
130
131 if data[0] != WAL_BINARY_V1 {
132 return Err(CodecError::UnknownVersion(data[0]));
133 }
134
135 if data.len() < 2 {
136 return Err(CodecError::Truncated);
137 }
138
139 let payload = &data[2..];
140 match data[1] {
141 DISC_WRITE => {
142 let point: Point = postcard::from_bytes(payload)?;
143 Ok(WalEntry::Write { point })
144 }
145 DISC_DELETE => {
146 let tombstones: Vec<crate::types::Tombstone> = postcard::from_bytes(payload)?;
147 Ok(WalEntry::Delete { tombstones })
148 }
149 d => Err(CodecError::UnknownDiscriminant(d)),
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::types::{FieldValue, SeriesKey, Tombstone};
157 use std::collections::BTreeMap;
158
159 fn sample_point() -> Point {
160 let key = SeriesKey::new("cpu", BTreeMap::from([("host".into(), "srv-1".into())])).unwrap();
161 let fields = BTreeMap::from([("value".to_string(), FieldValue::F64(42.5))]);
162 Point::new(key, fields, 1_000_000_000).unwrap()
163 }
164
165 #[test]
166 fn write_roundtrip() {
167 let entry = WalEntry::Write {
168 point: sample_point(),
169 };
170 let encoded = encode(&entry).unwrap();
171 assert_eq!(encoded[0], WAL_BINARY_V1);
172 assert_eq!(encoded[1], DISC_WRITE);
173 let decoded = decode(&encoded).unwrap();
174 assert_eq!(entry, decoded);
175 }
176
177 #[test]
178 fn delete_roundtrip_preserves_the_time_range() {
179 let entry = WalEntry::Delete {
183 tombstones: vec![
184 Tombstone::ranged("cpu\0host=srv-1", 100, 200),
185 Tombstone::ranged("cpu\0host=srv-2", i64::MIN, 5_000).with_segments([1, 2, 3]),
186 ],
187 };
188 let encoded = encode(&entry).unwrap();
189 assert_eq!(encoded[1], DISC_DELETE);
190 let decoded = decode(&encoded).unwrap();
191 assert_eq!(entry, decoded);
192 }
193
194 #[test]
195 fn decode_json_rejected_with_unknown_version() {
196 let json = b"{\"Write\":{}}";
198 let err = decode(json).unwrap_err();
199 assert!(
200 matches!(err, CodecError::UnknownVersion(0x7B)),
201 "expected UnknownVersion(0x7B), got {err:?}"
202 );
203 }
204
205 #[test]
206 fn decode_empty_payload_returns_error() {
207 assert!(decode(&[]).is_err());
208 }
209
210 #[test]
211 fn decode_truncated_binary_returns_error() {
212 assert!(decode(&[WAL_BINARY_V1]).is_err());
213 }
214
215 #[test]
216 fn decode_unknown_discriminant_returns_error() {
217 assert!(decode(&[WAL_BINARY_V1, 0xFF, 0x00]).is_err());
218 }
219}