1use serde_json::Value as JsonValue;
2
3use super::util::{hex_decode, hex_encode};
4
5pub const DEFAULT_REPLICATION_TERM: u64 = 1;
6pub type ChangeRecordJsonValue = JsonValue;
7
8pub fn parse_change_record_json_value(text: &str) -> Result<ChangeRecordJsonValue, String> {
9 serde_json::from_str(text).map_err(|err| err.to_string())
10}
11
12pub fn change_record_json_value_to_string(value: &ChangeRecordJsonValue) -> String {
13 serde_json::to_string(value).unwrap_or_else(|_| "null".to_string())
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ChangeOperation {
18 Insert,
19 Update,
20 Delete,
21 Refresh,
22 Transaction,
24}
25
26impl ChangeOperation {
27 #[allow(clippy::should_implement_trait)]
28 pub fn from_str(value: &str) -> Option<Self> {
29 Self::from_wire_str(value)
30 }
31
32 pub fn from_wire_str(value: &str) -> Option<Self> {
33 match value {
34 "insert" => Some(Self::Insert),
35 "update" => Some(Self::Update),
36 "delete" => Some(Self::Delete),
37 "refresh" => Some(Self::Refresh),
38 "transaction" => Some(Self::Transaction),
39 _ => None,
40 }
41 }
42
43 pub fn as_wire_str(&self) -> &'static str {
44 match self {
45 Self::Insert => "insert",
46 Self::Update => "update",
47 Self::Delete => "delete",
48 Self::Refresh => "refresh",
49 Self::Transaction => "transaction",
50 }
51 }
52
53 pub fn as_str(&self) -> &'static str {
54 self.as_wire_str()
55 }
56}
57
58#[derive(Debug, Clone)]
59pub struct ChangeRecord {
60 pub term: u64,
61 pub lsn: u64,
62 pub timestamp: u64,
63 pub operation: ChangeOperation,
64 pub collection: String,
65 pub entity_id: u64,
66 pub entity_kind: String,
67 pub entity_bytes: Option<Vec<u8>>,
68 pub metadata: Option<JsonValue>,
69 pub refresh_records: Option<Vec<Vec<u8>>>,
70 pub range_id: Option<u64>,
76 pub ownership_epoch: Option<u64>,
82}
83
84impl ChangeRecord {
85 pub fn for_transaction(lsn: u64, timestamp: u64, records: Vec<ChangeRecord>) -> Self {
87 let mut record = Self::for_refresh(
88 lsn,
89 timestamp,
90 "",
91 records.iter().map(Self::encode).collect(),
92 );
93 record.operation = ChangeOperation::Transaction;
94 record.entity_kind = "transaction".into();
95 record
96 }
97
98 pub fn for_refresh(
99 lsn: u64,
100 timestamp: u64,
101 collection: impl Into<String>,
102 records: Vec<Vec<u8>>,
103 ) -> Self {
104 Self {
105 term: DEFAULT_REPLICATION_TERM,
106 lsn,
107 timestamp,
108 operation: ChangeOperation::Refresh,
109 collection: collection.into(),
110 entity_id: 0,
111 entity_kind: "refresh".to_string(),
112 entity_bytes: None,
113 metadata: None,
114 refresh_records: Some(records),
115 range_id: None,
116 ownership_epoch: None,
117 }
118 }
119
120 pub fn with_range_authority(mut self, range_id: u64, ownership_epoch: u64) -> Self {
124 self.range_id = Some(range_id);
125 self.ownership_epoch = Some(ownership_epoch);
126 self
127 }
128
129 pub fn to_json_value(&self) -> JsonValue {
130 let mut object = serde_json::Map::new();
131 object.insert("term".to_string(), JsonValue::Number(self.term.into()));
132 object.insert("lsn".to_string(), JsonValue::Number(self.lsn.into()));
133 object.insert(
134 "timestamp".to_string(),
135 JsonValue::Number(self.timestamp.into()),
136 );
137 object.insert(
138 "operation".to_string(),
139 JsonValue::String(self.operation.as_wire_str().to_string()),
140 );
141 object.insert(
142 "collection".to_string(),
143 JsonValue::String(self.collection.clone()),
144 );
145 object.insert("rid".to_string(), JsonValue::Number(self.entity_id.into()));
146 object.insert(
147 "kind".to_string(),
148 JsonValue::String(public_item_kind(&self.entity_kind).to_string()),
149 );
150 if let Some(bytes) = &self.entity_bytes {
151 object.insert(
152 "entity_bytes_hex".to_string(),
153 JsonValue::String(hex_encode(bytes)),
154 );
155 }
156 if let Some(metadata) = &self.metadata {
157 object.insert("metadata".to_string(), metadata.clone());
158 }
159 if let Some(records) = &self.refresh_records {
160 let arr = records
161 .iter()
162 .map(|bytes| JsonValue::String(hex_encode(bytes)))
163 .collect();
164 object.insert("refresh_records_hex".to_string(), JsonValue::Array(arr));
165 }
166 if let Some(range_id) = self.range_id {
169 object.insert("range_id".to_string(), JsonValue::Number(range_id.into()));
170 }
171 if let Some(epoch) = self.ownership_epoch {
172 object.insert(
173 "ownership_epoch".to_string(),
174 JsonValue::Number(epoch.into()),
175 );
176 }
177 JsonValue::Object(object)
178 }
179
180 pub fn encode(&self) -> Vec<u8> {
181 serde_json::to_string(&self.to_json_value())
182 .unwrap_or_else(|_| "{}".to_string())
183 .into_bytes()
184 }
185
186 pub fn with_term(mut self, term: u64) -> Self {
187 self.term = term;
188 self
189 }
190
191 pub fn decode(bytes: &[u8]) -> Result<Self, String> {
192 let text = std::str::from_utf8(bytes).map_err(|err| err.to_string())?;
193 let value = serde_json::from_str::<JsonValue>(text).map_err(|err| err.to_string())?;
194 let operation = value
195 .get("operation")
196 .and_then(JsonValue::as_str)
197 .and_then(ChangeOperation::from_wire_str)
198 .ok_or_else(|| "invalid replication operation".to_string())?;
199 let entity_bytes = value
200 .get("entity_bytes_hex")
201 .and_then(JsonValue::as_str)
202 .map(|value| hex_decode_string("entity_bytes_hex", value))
203 .transpose()?;
204
205 Ok(Self {
206 term: value
207 .get("term")
208 .and_then(JsonValue::as_u64)
209 .unwrap_or(DEFAULT_REPLICATION_TERM),
210 lsn: value.get("lsn").and_then(JsonValue::as_u64).unwrap_or(0),
211 timestamp: value
212 .get("timestamp")
213 .and_then(JsonValue::as_u64)
214 .unwrap_or(0),
215 operation,
216 collection: value
217 .get("collection")
218 .and_then(JsonValue::as_str)
219 .unwrap_or_default()
220 .to_string(),
221 entity_id: value
222 .get("rid")
223 .or_else(|| value.get("entity_id"))
224 .and_then(JsonValue::as_u64)
225 .unwrap_or(0),
226 entity_kind: value
227 .get("kind")
228 .or_else(|| value.get("entity_kind"))
229 .and_then(JsonValue::as_str)
230 .unwrap_or("entity")
231 .to_string(),
232 entity_bytes,
233 metadata: value.get("metadata").cloned(),
234 refresh_records: match value.get("refresh_records_hex") {
235 Some(JsonValue::Array(items)) => {
236 let mut out = Vec::with_capacity(items.len());
237 for item in items {
238 let hex_str = item
239 .as_str()
240 .ok_or_else(|| "refresh_records_hex entry not a string".to_string())?;
241 out.push(hex_decode_string("refresh_records_hex", hex_str)?);
242 }
243 Some(out)
244 }
245 None | Some(JsonValue::Null) => None,
246 _ => return Err("refresh_records_hex is not an array".to_string()),
247 },
248 range_id: value.get("range_id").and_then(JsonValue::as_u64),
251 ownership_epoch: value.get("ownership_epoch").and_then(JsonValue::as_u64),
252 })
253 }
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
268pub struct RangeAuthority {
269 pub range_id: u64,
270 pub min_term: u64,
271 pub min_ownership_epoch: u64,
272}
273
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
276pub enum RangeAdmitError {
277 StaleTerm {
280 record_term: u64,
281 accepted_term: u64,
282 },
283 StaleOwnershipEpoch {
286 record_epoch: u64,
287 accepted_epoch: u64,
288 },
289}
290
291impl RangeAuthority {
292 pub fn admit(&self, record: &ChangeRecord) -> Result<(), RangeAdmitError> {
297 if record.range_id != Some(self.range_id) {
298 return Ok(());
299 }
300 if record.term < self.min_term {
301 return Err(RangeAdmitError::StaleTerm {
302 record_term: record.term,
303 accepted_term: self.min_term,
304 });
305 }
306 if let Some(epoch) = record.ownership_epoch {
309 if epoch < self.min_ownership_epoch {
310 return Err(RangeAdmitError::StaleOwnershipEpoch {
311 record_epoch: epoch,
312 accepted_epoch: self.min_ownership_epoch,
313 });
314 }
315 }
316 Ok(())
317 }
318}
319
320pub fn public_item_kind(entity_kind: &str) -> &'static str {
321 match entity_kind {
322 "table" | "entity" | "row" => "row",
323 "graph_node" | "node" => "node",
324 "graph_edge" | "edge" => "edge",
325 "kv" => "kv",
326 "document" => "document",
327 "vector" => "vector",
328 other if other.contains("kv") => "kv",
329 other if other.contains("document") => "document",
330 other if other.contains("vector") => "vector",
331 _ => "item",
332 }
333}
334
335fn hex_decode_string(field: &'static str, value: &str) -> Result<Vec<u8>, String> {
336 hex_decode(field, value).map_err(|err| err.to_string())
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn change_record_round_trips_json_wire_payload() {
345 let record = ChangeRecord {
346 term: 3,
347 lsn: 7,
348 timestamp: 1234,
349 operation: ChangeOperation::Update,
350 collection: "users".to_string(),
351 entity_id: 42,
352 entity_kind: "row".to_string(),
353 entity_bytes: Some(vec![1, 2, 3]),
354 metadata: Some(serde_json::json!({"role": "admin"})),
355 refresh_records: None,
356 range_id: None,
357 ownership_epoch: None,
358 };
359
360 let decoded = ChangeRecord::decode(&record.encode()).expect("decode");
361
362 assert_eq!(decoded.term, record.term);
363 assert_eq!(decoded.lsn, record.lsn);
364 assert_eq!(decoded.collection, record.collection);
365 assert_eq!(decoded.entity_id, record.entity_id);
366 assert_eq!(decoded.entity_bytes, record.entity_bytes);
367 assert_eq!(decoded.metadata, record.metadata);
368 }
369
370 #[test]
371 fn range_authority_round_trips_on_the_json_wire() {
372 let record = ChangeRecord {
373 term: 5,
374 lsn: 12,
375 timestamp: 999,
376 operation: ChangeOperation::Insert,
377 collection: "orders".to_string(),
378 entity_id: 8,
379 entity_kind: "row".to_string(),
380 entity_bytes: Some(vec![9, 9]),
381 metadata: None,
382 refresh_records: None,
383 range_id: None,
384 ownership_epoch: None,
385 }
386 .with_range_authority(7, 3);
387
388 let decoded = ChangeRecord::decode(&record.encode()).expect("decode");
389
390 assert_eq!(decoded.range_id, Some(7));
391 assert_eq!(decoded.ownership_epoch, Some(3));
392 assert_eq!(decoded.term, 5);
393 }
394
395 #[test]
396 fn legacy_record_without_range_authority_decodes_to_none() {
397 let legacy =
401 br#"{"term":2,"lsn":4,"timestamp":1,"operation":"insert","collection":"users","rid":1,"kind":"row"}"#;
402
403 let decoded = ChangeRecord::decode(legacy).expect("decode legacy");
404
405 assert_eq!(decoded.range_id, None);
406 assert_eq!(decoded.ownership_epoch, None);
407 }
408
409 #[test]
410 fn unstamped_record_omits_range_keys_from_the_wire() {
411 let record = ChangeRecord::for_refresh(1, 1, "mv", Vec::new());
412 let text = String::from_utf8(record.encode()).expect("utf8");
413 assert!(!text.contains("range_id"), "got {text}");
414 assert!(!text.contains("ownership_epoch"), "got {text}");
415 }
416
417 fn stamped(term: u64, range_id: u64, epoch: u64) -> ChangeRecord {
418 ChangeRecord {
419 term,
420 lsn: 1,
421 timestamp: 1,
422 operation: ChangeOperation::Insert,
423 collection: "c".to_string(),
424 entity_id: 1,
425 entity_kind: "row".to_string(),
426 entity_bytes: Some(vec![1]),
427 metadata: None,
428 refresh_records: None,
429 range_id: None,
430 ownership_epoch: None,
431 }
432 .with_range_authority(range_id, epoch)
433 }
434
435 #[test]
436 fn range_authority_admits_current_term_and_epoch() {
437 let fence = RangeAuthority {
438 range_id: 7,
439 min_term: 3,
440 min_ownership_epoch: 4,
441 };
442 assert_eq!(fence.admit(&stamped(3, 7, 4)), Ok(()));
443 assert_eq!(fence.admit(&stamped(9, 7, 9)), Ok(()));
444 }
445
446 #[test]
447 fn range_authority_rejects_stale_epoch_and_term() {
448 let fence = RangeAuthority {
449 range_id: 7,
450 min_term: 3,
451 min_ownership_epoch: 4,
452 };
453 assert_eq!(
454 fence.admit(&stamped(3, 7, 2)),
455 Err(RangeAdmitError::StaleOwnershipEpoch {
456 record_epoch: 2,
457 accepted_epoch: 4,
458 })
459 );
460 assert_eq!(
462 fence.admit(&stamped(1, 7, 1)),
463 Err(RangeAdmitError::StaleTerm {
464 record_term: 1,
465 accepted_term: 3,
466 })
467 );
468 }
469
470 #[test]
471 fn range_authority_ignores_other_ranges_and_unstamped_records() {
472 let fence = RangeAuthority {
473 range_id: 7,
474 min_term: 5,
475 min_ownership_epoch: 5,
476 };
477 assert_eq!(fence.admit(&stamped(1, 99, 1)), Ok(()));
479 let mut legacy = stamped(1, 7, 1);
481 legacy.range_id = None;
482 legacy.ownership_epoch = None;
483 assert_eq!(fence.admit(&legacy), Ok(()));
484 }
485
486 #[test]
487 fn refresh_records_round_trip_without_reordering() {
488 let records = vec![vec![0x10, 0x20, 0x30], vec![0xAA, 0xBB], Vec::new()];
489 let record =
490 ChangeRecord::for_refresh(11, 99, "mv_orders_summary", records.clone()).with_term(4);
491
492 let decoded = ChangeRecord::decode(&record.encode()).expect("decode");
493
494 assert_eq!(decoded.term, 4);
495 assert_eq!(decoded.operation, ChangeOperation::Refresh);
496 assert_eq!(decoded.collection, "mv_orders_summary");
497 assert_eq!(decoded.refresh_records.as_deref(), Some(&records[..]));
498 }
499
500 #[test]
501 fn legacy_change_record_defaults_term() {
502 let legacy =
503 br#"{"lsn":9,"timestamp":1,"operation":"delete","collection":"users","rid":5,"kind":"row"}"#;
504
505 let decoded = ChangeRecord::decode(legacy).expect("decode legacy record");
506
507 assert_eq!(decoded.term, DEFAULT_REPLICATION_TERM);
508 assert_eq!(decoded.lsn, 9);
509 }
510}