1use crate::error::InvalidError;
2use crate::limits::MAX_NAMESPACE_BYTES;
3use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
9pub struct MemoryRowScope {
10 #[serde(default, skip_serializing_if = "Option::is_none")]
12 pub kind: Option<String>,
13 #[serde(default, skip_serializing_if = "Option::is_none")]
15 pub agent: Option<String>,
16 #[serde(default, skip_serializing_if = "Option::is_none")]
18 pub user: Option<String>,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub app: Option<String>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub conversation: Option<String>,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub source: Option<crate::graph::SourceRef>,
30}
31
32#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
36pub struct KvEntry {
37 #[serde(with = "crate::encoding::bin_bytes")]
38 pub key: Vec<u8>,
39 #[serde(with = "crate::encoding::bin_bytes")]
40 pub value: Vec<u8>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub expires_at_micros: Option<u64>,
45 #[serde(default, skip_serializing_if = "is_zero_u64")]
53 pub version: u64,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub scope: Option<Box<MemoryRowScope>>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub source: Option<Box<crate::graph::SourceRef>>,
67}
68
69fn is_zero_u64(value: &u64) -> bool {
70 *value == 0
71}
72
73impl KvEntry {
74 pub fn key_str(&self) -> Option<&str> {
77 std::str::from_utf8(&self.key).ok()
78 }
79}
80
81#[derive(Clone, Debug, Default, Serialize, Deserialize)]
84pub struct KvPage {
85 pub entries: Vec<KvEntry>,
86 #[serde(
87 default,
88 skip_serializing_if = "Option::is_none",
89 with = "crate::encoding::opt_bin_bytes"
90 )]
91 pub cursor: Option<Vec<u8>>,
92}
93
94#[derive(Clone, Debug, Serialize, Deserialize)]
99pub struct KvGet {
100 pub v: u32,
101 pub namespace: String,
102 #[serde(with = "crate::encoding::bin_bytes")]
103 pub key: Vec<u8>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub if_none_match: Option<u64>,
106}
107
108#[derive(Clone, Debug, Serialize, Deserialize)]
110pub struct KvSet {
111 pub v: u32,
112 pub namespace: String,
113 #[serde(with = "crate::encoding::bin_bytes")]
114 pub key: Vec<u8>,
115 #[serde(with = "crate::encoding::bin_bytes")]
116 pub value: Vec<u8>,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub expires_at_micros: Option<u64>,
119}
120
121#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
125pub enum CasExpect {
126 Match(u64),
128 Absent,
130}
131
132#[derive(Clone, Debug, Serialize, Deserialize)]
137pub struct KvCas {
138 pub v: u32,
139 pub namespace: String,
140 #[serde(with = "crate::encoding::bin_bytes")]
141 pub key: Vec<u8>,
142 #[serde(with = "crate::encoding::bin_bytes")]
143 pub value: Vec<u8>,
144 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub expires_at_micros: Option<u64>,
146 pub expect: CasExpect,
147}
148
149#[derive(Clone, Debug, Serialize, Deserialize)]
155pub struct KvCasFenced {
156 pub v: u32,
157 pub namespace: String,
158 #[serde(with = "crate::encoding::bin_bytes")]
159 pub key: Vec<u8>,
160 #[serde(with = "crate::encoding::bin_bytes")]
161 pub value: Vec<u8>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub expires_at_micros: Option<u64>,
164 pub expect: CasExpect,
165 #[serde(with = "crate::encoding::bin_bytes")]
168 pub fence_key: Vec<u8>,
169 pub fence_token: u64,
172}
173
174#[derive(Clone, Debug, Serialize, Deserialize)]
178pub struct KvDelete {
179 pub v: u32,
180 pub namespace: String,
181 #[serde(with = "crate::encoding::bin_bytes")]
182 pub key: Vec<u8>,
183 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub if_match: Option<u64>,
185}
186
187#[derive(Clone, Debug, Serialize, Deserialize)]
191pub struct KvExists {
192 pub v: u32,
193 pub namespace: String,
194 #[serde(with = "crate::encoding::bin_bytes")]
195 pub key: Vec<u8>,
196}
197
198#[derive(Clone, Debug, Serialize, Deserialize)]
202pub struct KvExpire {
203 pub v: u32,
204 pub namespace: String,
205 #[serde(with = "crate::encoding::bin_bytes")]
206 pub key: Vec<u8>,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub expires_at_micros: Option<u64>,
209}
210
211#[derive(Clone, Debug, Serialize, Deserialize)]
216pub struct KvPatch {
217 pub v: u32,
218 pub namespace: String,
219 #[serde(with = "crate::encoding::bin_bytes")]
220 pub key: Vec<u8>,
221 #[serde(with = "crate::encoding::bin_bytes")]
222 pub patch: Vec<u8>,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub if_match: Option<u64>,
225}
226
227#[derive(Clone, Debug, Serialize, Deserialize)]
232pub struct KvCopy {
233 pub v: u32,
234 pub namespace: String,
235 #[serde(with = "crate::encoding::bin_bytes")]
236 pub key: Vec<u8>,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub to_namespace: Option<String>,
240 #[serde(with = "crate::encoding::bin_bytes")]
241 pub to_key: Vec<u8>,
242}
243
244#[derive(Clone, Debug, Serialize, Deserialize)]
247pub struct KvMove {
248 pub v: u32,
249 pub namespace: String,
250 #[serde(with = "crate::encoding::bin_bytes")]
251 pub key: Vec<u8>,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub to_namespace: Option<String>,
255 #[serde(with = "crate::encoding::bin_bytes")]
256 pub to_key: Vec<u8>,
257}
258
259#[derive(Clone, Debug, Serialize, Deserialize)]
263pub struct KvLease {
264 pub v: u32,
265 pub namespace: String,
266 #[serde(with = "crate::encoding::bin_bytes")]
267 pub key: Vec<u8>,
268 pub lease_ttl_micros: u64,
269}
270
271#[derive(Clone, Debug, Serialize, Deserialize)]
274pub struct KvRelease {
275 pub v: u32,
276 pub namespace: String,
277 #[serde(with = "crate::encoding::bin_bytes")]
278 pub key: Vec<u8>,
279 pub lease_token: u64,
280}
281
282#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
285pub struct KvMetadata {
286 pub version: u64,
287 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub expires_at_micros: Option<u64>,
289 pub size_bytes: usize,
290}
291
292#[derive(Clone, Debug, Serialize, Deserialize)]
294pub struct KvNamespaces {
295 pub v: u32,
296}
297
298#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
300pub struct KvNamespaceInfo {
301 pub namespace: String,
302 pub entries: usize,
303}
304
305#[derive(Clone, Debug, Serialize, Deserialize)]
311pub struct KvScan {
312 pub v: u32,
313 pub namespace: String,
314 #[serde(
315 default,
316 skip_serializing_if = "Option::is_none",
317 with = "crate::encoding::opt_bin_bytes"
318 )]
319 pub prefix: Option<Vec<u8>>,
320 #[serde(
321 default,
322 skip_serializing_if = "Option::is_none",
323 with = "crate::encoding::opt_bin_bytes"
324 )]
325 pub start: Option<Vec<u8>>,
326 #[serde(
327 default,
328 skip_serializing_if = "Option::is_none",
329 with = "crate::encoding::opt_bin_bytes"
330 )]
331 pub end: Option<Vec<u8>>,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
333 pub key_contains: Option<String>,
334 #[serde(default, skip_serializing_if = "Option::is_none")]
340 pub conversation: Option<String>,
341 pub limit: usize,
342 #[serde(
343 default,
344 skip_serializing_if = "Option::is_none",
345 with = "crate::encoding::opt_bin_bytes"
346 )]
347 pub cursor: Option<Vec<u8>>,
348}
349
350#[derive(Clone, Debug, Serialize, Deserialize)]
355pub struct KvDeleteMany {
356 pub v: u32,
357 pub namespace: String,
358 #[serde(
359 default,
360 skip_serializing_if = "Option::is_none",
361 with = "crate::encoding::opt_bin_bytes"
362 )]
363 pub prefix: Option<Vec<u8>>,
364 #[serde(
365 default,
366 skip_serializing_if = "Option::is_none",
367 with = "crate::encoding::opt_bin_bytes"
368 )]
369 pub start: Option<Vec<u8>>,
370 #[serde(
371 default,
372 skip_serializing_if = "Option::is_none",
373 with = "crate::encoding::opt_bin_bytes"
374 )]
375 pub end: Option<Vec<u8>>,
376 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub key_contains: Option<String>,
378 #[serde(default, skip_serializing_if = "Option::is_none")]
381 pub conversation: Option<String>,
382}
383
384#[derive(Clone, Debug, Serialize, Deserialize)]
387#[non_exhaustive]
388pub enum KvReply {
389 Ok(KvOutcome),
390 Err(KvError),
391}
392
393#[derive(Clone, Debug, Serialize, Deserialize)]
395#[non_exhaustive]
396pub enum KvOutcome {
397 Value(Option<KvEntry>),
399 Written,
401 Committed { version: u64 },
404 Deleted(bool),
406 DeletedMany(usize),
408 Page(KvPage),
410 Namespaces(Vec<KvNamespaceInfo>),
413 NotModified,
416 Metadata(Option<KvMetadata>),
418 Versioned { version: u64 },
421 Leased {
424 lease_token: u64,
425 granted_ttl_micros: u64,
426 },
427 Released(bool),
430}
431
432#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
434#[non_exhaustive]
435pub enum KvError {
436 #[error("kv not supported: {0}")]
437 Unsupported(String),
438 #[error("invalid key: {0}")]
439 InvalidKey(String),
440 #[error("invalid namespace: {0}")]
442 InvalidNamespace(String),
443 #[error("{what} is {size}B, exceeds cap {cap}B")]
444 TooLarge {
445 what: String,
446 size: usize,
447 cap: usize,
448 },
449 #[error("kv backend error: {0}")]
450 Backend(String),
451 #[error("unsupported kv op version (expected {expected}, got {got})")]
452 Version { expected: u32, got: u32 },
453 #[error("kv version conflict (current: {current:?})")]
457 VersionConflict { current: Option<u64> },
458 #[error("kv lease lost")]
461 LeaseLost,
462 #[error("kv key not found")]
465 NotFound,
466 #[error("not the partition leader for this key")]
469 NotLeader,
470}
471
472pub fn validate_namespace(namespace: &str) -> Result<(), InvalidError> {
477 if namespace.is_empty() {
478 return Err(InvalidError::new("namespace must not be empty"));
479 }
480 if namespace.len() > MAX_NAMESPACE_BYTES {
481 return Err(InvalidError::new(format!(
482 "namespace is {}B, exceeds cap {MAX_NAMESPACE_BYTES}B",
483 namespace.len()
484 )));
485 }
486 if namespace.bytes().any(|byte| byte.is_ascii_control()) {
487 return Err(InvalidError::new(
488 "namespace must not contain ASCII control characters",
489 ));
490 }
491 Ok(())
492}
493
494#[cfg(all(test, feature = "cbor"))]
495mod tests {
496 use super::*;
497 use crate::codes::KV_OP_VERSION;
498 use crate::framing::{decode_named, encode_named};
499
500 #[test]
501 fn given_kv_delete_many_when_round_tripped_then_should_preserve_bounds() {
502 let request = KvDeleteMany {
503 v: KV_OP_VERSION,
504 namespace: "sessions".to_owned(),
505 prefix: Some(b"user:".to_vec()),
506 start: None,
507 end: None,
508 key_contains: Some("stale".to_owned()),
509 conversation: None,
510 };
511 let bytes = encode_named(&request).expect("serializes");
512 let back: KvDeleteMany = decode_named(&bytes).expect("deserializes");
513 assert_eq!(back.prefix, Some(b"user:".to_vec()));
514 assert_eq!(back.key_contains.as_deref(), Some("stale"));
515 }
516
517 #[test]
518 fn given_kv_deleted_many_reply_when_round_tripped_then_should_preserve_count() {
519 let reply = KvReply::Ok(KvOutcome::DeletedMany(7));
520 let bytes = encode_named(&reply).expect("serializes");
521 let back: KvReply = decode_named(&bytes).expect("deserializes");
522 match back {
523 KvReply::Ok(KvOutcome::DeletedMany(n)) => assert_eq!(n, 7),
524 other => panic!("expected DeletedMany, got {other:?}"),
525 }
526 }
527
528 #[test]
529 fn given_a_set_request_when_round_tripped_then_should_preserve_value_and_expiry() {
530 let request = KvSet {
531 v: KV_OP_VERSION,
532 namespace: "sessions".to_owned(),
533 key: b"user:42".to_vec(),
534 value: b"online".to_vec(),
535 expires_at_micros: Some(1_700_000_000_000_000),
536 };
537 let bytes = encode_named(&request).expect("the request serializes");
538 let back: KvSet = decode_named(&bytes).expect("the request deserializes");
539 assert_eq!(back.key, b"user:42");
540 assert_eq!(back.value, b"online");
541 assert_eq!(back.expires_at_micros, Some(1_700_000_000_000_000));
542 }
543
544 #[test]
545 fn given_a_binary_key_entry_when_round_tripped_then_should_preserve_raw_bytes() {
546 let reply = KvReply::Ok(KvOutcome::Value(Some(KvEntry {
547 key: vec![0xff, 0x00, 0xfe],
548 value: vec![0x00, 0x01, 0x02],
549 expires_at_micros: None,
550 version: 0,
551 scope: None,
552 source: None,
553 })));
554 let bytes = encode_named(&reply).expect("the reply serializes");
555 let back: KvReply = decode_named(&bytes).expect("the reply deserializes");
556 let KvReply::Ok(KvOutcome::Value(Some(entry))) = back else {
557 panic!("expected an Ok(Value(Some)) reply");
558 };
559 assert_eq!(entry.key, vec![0xff, 0x00, 0xfe]);
560 assert_eq!(entry.key_str(), None, "non-UTF-8 key has no string form");
561 assert_eq!(entry.value, vec![0x00, 0x01, 0x02]);
562 }
563
564 #[test]
565 fn given_a_scan_page_when_round_tripped_then_should_preserve_cursor() {
566 let reply = KvReply::Ok(KvOutcome::Page(KvPage {
567 entries: vec![KvEntry {
568 key: b"a".to_vec(),
569 value: b"1".to_vec(),
570 expires_at_micros: None,
571 version: 0,
572 scope: None,
573 source: None,
574 }],
575 cursor: Some(b"a".to_vec()),
576 }));
577 let bytes = encode_named(&reply).expect("serializes");
578 let back: KvReply = decode_named(&bytes).expect("deserializes");
579 let KvReply::Ok(KvOutcome::Page(page)) = back else {
580 panic!("expected an Ok(Page) reply");
581 };
582 assert_eq!(page.entries.len(), 1);
583 assert_eq!(page.entries[0].key_str(), Some("a"));
584 assert_eq!(page.cursor.as_deref(), Some(b"a".as_ref()));
585 }
586
587 #[test]
588 fn given_a_cas_request_when_round_tripped_then_should_preserve_the_precondition() {
589 for expect in [CasExpect::Match(7), CasExpect::Absent] {
590 let request = KvCas {
591 v: KV_OP_VERSION,
592 namespace: "counters".to_owned(),
593 key: b"hits".to_vec(),
594 value: b"42".to_vec(),
595 expires_at_micros: None,
596 expect,
597 };
598 let bytes = encode_named(&request).expect("serializes");
599 let back: KvCas = decode_named(&bytes).expect("deserializes");
600 assert_eq!(back.expect, expect);
601 assert_eq!(back.key, b"hits");
602 }
603 }
604
605 #[test]
606 fn given_a_committed_reply_when_round_tripped_then_should_preserve_the_version() {
607 let reply = KvReply::Ok(KvOutcome::Committed { version: 9 });
608 let bytes = encode_named(&reply).expect("serializes");
609 let back: KvReply = decode_named(&bytes).expect("deserializes");
610 match back {
611 KvReply::Ok(KvOutcome::Committed { version }) => assert_eq!(version, 9),
612 other => panic!("expected Committed, got {other:?}"),
613 }
614 }
615
616 #[test]
617 fn given_an_exists_metadata_reply_when_round_tripped_then_should_preserve_metadata() {
618 let reply = KvReply::Ok(KvOutcome::Metadata(Some(KvMetadata {
619 version: 4,
620 expires_at_micros: Some(1_700_000_000_000_000),
621 size_bytes: 128,
622 })));
623 let bytes = encode_named(&reply).expect("serializes");
624 let back: KvReply = decode_named(&bytes).expect("deserializes");
625 let KvReply::Ok(KvOutcome::Metadata(Some(meta))) = back else {
626 panic!("expected Ok(Metadata(Some))");
627 };
628 assert_eq!(meta.version, 4);
629 assert_eq!(meta.size_bytes, 128);
630 }
631
632 #[test]
633 fn given_a_patch_request_when_round_tripped_then_should_preserve_patch_and_precondition() {
634 let request = KvPatch {
635 v: KV_OP_VERSION,
636 namespace: "docs".to_owned(),
637 key: b"doc:1".to_vec(),
638 patch: br#"{"status":"closed"}"#.to_vec(),
639 if_match: Some(3),
640 };
641 let bytes = encode_named(&request).expect("serializes");
642 let back: KvPatch = decode_named(&bytes).expect("deserializes");
643 assert_eq!(back.patch, br#"{"status":"closed"}"#);
644 assert_eq!(back.if_match, Some(3));
645 }
646
647 #[test]
648 fn given_a_lease_reply_when_round_tripped_then_should_preserve_token_and_ttl() {
649 let reply = KvReply::Ok(KvOutcome::Leased {
650 lease_token: 77,
651 granted_ttl_micros: 30_000_000,
652 });
653 let bytes = encode_named(&reply).expect("serializes");
654 let back: KvReply = decode_named(&bytes).expect("deserializes");
655 match back {
656 KvReply::Ok(KvOutcome::Leased {
657 lease_token,
658 granted_ttl_micros,
659 }) => {
660 assert_eq!(lease_token, 77);
661 assert_eq!(granted_ttl_micros, 30_000_000);
662 }
663 other => panic!("expected Leased, got {other:?}"),
664 }
665 }
666
667 #[test]
668 fn given_a_conditional_get_when_round_tripped_then_should_preserve_if_none_match() {
669 let request = KvGet {
670 v: KV_OP_VERSION,
671 namespace: "sessions".to_owned(),
672 key: b"user:1".to_vec(),
673 if_none_match: Some(5),
674 };
675 let bytes = encode_named(&request).expect("serializes");
676 let back: KvGet = decode_named(&bytes).expect("deserializes");
677 assert_eq!(back.if_none_match, Some(5));
678 let plain = KvGet {
681 if_none_match: None,
682 ..request
683 };
684 let json = serde_json::to_string(&plain).expect("json");
685 assert!(
686 !json.contains("if_none_match"),
687 "absent precondition omitted"
688 );
689 }
690
691 #[test]
692 fn given_a_version_conflict_when_round_tripped_then_should_preserve_the_current_version() {
693 for current in [Some(3u64), None] {
694 let reply = KvReply::Err(KvError::VersionConflict { current });
695 let bytes = encode_named(&reply).expect("serializes");
696 let back: KvReply = decode_named(&bytes).expect("deserializes");
697 match back {
698 KvReply::Err(KvError::VersionConflict { current: got }) => assert_eq!(got, current),
699 other => panic!("expected VersionConflict, got {other:?}"),
700 }
701 }
702 }
703
704 #[test]
705 fn given_a_versioned_entry_when_round_tripped_then_should_preserve_version_and_skip_zero() {
706 let entry = KvEntry {
707 key: b"k".to_vec(),
708 value: b"v".to_vec(),
709 expires_at_micros: None,
710 version: 5,
711 scope: None,
712 source: None,
713 };
714 let bytes = encode_named(&entry).expect("serializes");
715 let back: KvEntry = decode_named(&bytes).expect("deserializes");
716 assert_eq!(back.version, 5);
717 let unversioned = KvEntry {
720 version: 0,
721 ..entry
722 };
723 let json = serde_json::to_string(&unversioned).expect("json");
724 assert!(
725 !json.contains("version"),
726 "version 0 must be omitted: {json}"
727 );
728 }
729
730 #[test]
731 fn given_a_scan_with_bounds_when_round_tripped_then_should_preserve_filters() {
732 let scan = KvScan {
733 v: KV_OP_VERSION,
734 namespace: "sessions".to_owned(),
735 prefix: Some(b"user:".to_vec()),
736 start: None,
737 end: None,
738 key_contains: Some("admin".to_owned()),
739 conversation: None,
740 limit: 50,
741 cursor: Some(b"user:9".to_vec()),
742 };
743 let bytes = encode_named(&scan).expect("serializes");
744 let back: KvScan = decode_named(&bytes).expect("deserializes");
745 assert_eq!(back.prefix.as_deref(), Some(b"user:".as_ref()));
746 assert_eq!(back.key_contains.as_deref(), Some("admin"));
747 assert_eq!(back.cursor.as_deref(), Some(b"user:9".as_ref()));
748 }
749}