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("temporarily unavailable: {0}")]
456 Unavailable(String),
457 #[error("unsupported kv op version (expected {expected}, got {got})")]
458 Version { expected: u32, got: u32 },
459 #[error("kv version conflict (current: {current:?})")]
463 VersionConflict { current: Option<u64> },
464 #[error("kv lease lost")]
467 LeaseLost,
468 #[error("kv key not found")]
471 NotFound,
472 #[error("not the partition leader for this key")]
475 NotLeader,
476}
477
478pub fn validate_namespace(namespace: &str) -> Result<(), InvalidError> {
483 if namespace.is_empty() {
484 return Err(InvalidError::new("namespace must not be empty"));
485 }
486 if namespace.len() > MAX_NAMESPACE_BYTES {
487 return Err(InvalidError::new(format!(
488 "namespace is {}B, exceeds cap {MAX_NAMESPACE_BYTES}B",
489 namespace.len()
490 )));
491 }
492 if namespace.bytes().any(|byte| byte.is_ascii_control()) {
493 return Err(InvalidError::new(
494 "namespace must not contain ASCII control characters",
495 ));
496 }
497 Ok(())
498}
499
500#[cfg(all(test, feature = "cbor"))]
501mod tests {
502 use super::*;
503 use crate::codes::KV_OP_VERSION;
504 use crate::framing::{decode_named, encode_named};
505
506 #[test]
507 fn given_kv_delete_many_when_round_tripped_then_should_preserve_bounds() {
508 let request = KvDeleteMany {
509 v: KV_OP_VERSION,
510 namespace: "sessions".to_owned(),
511 prefix: Some(b"user:".to_vec()),
512 start: None,
513 end: None,
514 key_contains: Some("stale".to_owned()),
515 conversation: None,
516 };
517 let bytes = encode_named(&request).expect("serializes");
518 let back: KvDeleteMany = decode_named(&bytes).expect("deserializes");
519 assert_eq!(back.prefix, Some(b"user:".to_vec()));
520 assert_eq!(back.key_contains.as_deref(), Some("stale"));
521 }
522
523 #[test]
524 fn given_kv_deleted_many_reply_when_round_tripped_then_should_preserve_count() {
525 let reply = KvReply::Ok(KvOutcome::DeletedMany(7));
526 let bytes = encode_named(&reply).expect("serializes");
527 let back: KvReply = decode_named(&bytes).expect("deserializes");
528 match back {
529 KvReply::Ok(KvOutcome::DeletedMany(n)) => assert_eq!(n, 7),
530 other => panic!("expected DeletedMany, got {other:?}"),
531 }
532 }
533
534 #[test]
535 fn given_a_set_request_when_round_tripped_then_should_preserve_value_and_expiry() {
536 let request = KvSet {
537 v: KV_OP_VERSION,
538 namespace: "sessions".to_owned(),
539 key: b"user:42".to_vec(),
540 value: b"online".to_vec(),
541 expires_at_micros: Some(1_700_000_000_000_000),
542 };
543 let bytes = encode_named(&request).expect("the request serializes");
544 let back: KvSet = decode_named(&bytes).expect("the request deserializes");
545 assert_eq!(back.key, b"user:42");
546 assert_eq!(back.value, b"online");
547 assert_eq!(back.expires_at_micros, Some(1_700_000_000_000_000));
548 }
549
550 #[test]
551 fn given_a_binary_key_entry_when_round_tripped_then_should_preserve_raw_bytes() {
552 let reply = KvReply::Ok(KvOutcome::Value(Some(KvEntry {
553 key: vec![0xff, 0x00, 0xfe],
554 value: vec![0x00, 0x01, 0x02],
555 expires_at_micros: None,
556 version: 0,
557 scope: None,
558 source: None,
559 })));
560 let bytes = encode_named(&reply).expect("the reply serializes");
561 let back: KvReply = decode_named(&bytes).expect("the reply deserializes");
562 let KvReply::Ok(KvOutcome::Value(Some(entry))) = back else {
563 panic!("expected an Ok(Value(Some)) reply");
564 };
565 assert_eq!(entry.key, vec![0xff, 0x00, 0xfe]);
566 assert_eq!(entry.key_str(), None, "non-UTF-8 key has no string form");
567 assert_eq!(entry.value, vec![0x00, 0x01, 0x02]);
568 }
569
570 #[test]
571 fn given_a_scan_page_when_round_tripped_then_should_preserve_cursor() {
572 let reply = KvReply::Ok(KvOutcome::Page(KvPage {
573 entries: vec![KvEntry {
574 key: b"a".to_vec(),
575 value: b"1".to_vec(),
576 expires_at_micros: None,
577 version: 0,
578 scope: None,
579 source: None,
580 }],
581 cursor: Some(b"a".to_vec()),
582 }));
583 let bytes = encode_named(&reply).expect("serializes");
584 let back: KvReply = decode_named(&bytes).expect("deserializes");
585 let KvReply::Ok(KvOutcome::Page(page)) = back else {
586 panic!("expected an Ok(Page) reply");
587 };
588 assert_eq!(page.entries.len(), 1);
589 assert_eq!(page.entries[0].key_str(), Some("a"));
590 assert_eq!(page.cursor.as_deref(), Some(b"a".as_ref()));
591 }
592
593 #[test]
594 fn given_a_cas_request_when_round_tripped_then_should_preserve_the_precondition() {
595 for expect in [CasExpect::Match(7), CasExpect::Absent] {
596 let request = KvCas {
597 v: KV_OP_VERSION,
598 namespace: "counters".to_owned(),
599 key: b"hits".to_vec(),
600 value: b"42".to_vec(),
601 expires_at_micros: None,
602 expect,
603 };
604 let bytes = encode_named(&request).expect("serializes");
605 let back: KvCas = decode_named(&bytes).expect("deserializes");
606 assert_eq!(back.expect, expect);
607 assert_eq!(back.key, b"hits");
608 }
609 }
610
611 #[test]
612 fn given_a_committed_reply_when_round_tripped_then_should_preserve_the_version() {
613 let reply = KvReply::Ok(KvOutcome::Committed { version: 9 });
614 let bytes = encode_named(&reply).expect("serializes");
615 let back: KvReply = decode_named(&bytes).expect("deserializes");
616 match back {
617 KvReply::Ok(KvOutcome::Committed { version }) => assert_eq!(version, 9),
618 other => panic!("expected Committed, got {other:?}"),
619 }
620 }
621
622 #[test]
623 fn given_an_exists_metadata_reply_when_round_tripped_then_should_preserve_metadata() {
624 let reply = KvReply::Ok(KvOutcome::Metadata(Some(KvMetadata {
625 version: 4,
626 expires_at_micros: Some(1_700_000_000_000_000),
627 size_bytes: 128,
628 })));
629 let bytes = encode_named(&reply).expect("serializes");
630 let back: KvReply = decode_named(&bytes).expect("deserializes");
631 let KvReply::Ok(KvOutcome::Metadata(Some(meta))) = back else {
632 panic!("expected Ok(Metadata(Some))");
633 };
634 assert_eq!(meta.version, 4);
635 assert_eq!(meta.size_bytes, 128);
636 }
637
638 #[test]
639 fn given_a_patch_request_when_round_tripped_then_should_preserve_patch_and_precondition() {
640 let request = KvPatch {
641 v: KV_OP_VERSION,
642 namespace: "docs".to_owned(),
643 key: b"doc:1".to_vec(),
644 patch: br#"{"status":"closed"}"#.to_vec(),
645 if_match: Some(3),
646 };
647 let bytes = encode_named(&request).expect("serializes");
648 let back: KvPatch = decode_named(&bytes).expect("deserializes");
649 assert_eq!(back.patch, br#"{"status":"closed"}"#);
650 assert_eq!(back.if_match, Some(3));
651 }
652
653 #[test]
654 fn given_a_lease_reply_when_round_tripped_then_should_preserve_token_and_ttl() {
655 let reply = KvReply::Ok(KvOutcome::Leased {
656 lease_token: 77,
657 granted_ttl_micros: 30_000_000,
658 });
659 let bytes = encode_named(&reply).expect("serializes");
660 let back: KvReply = decode_named(&bytes).expect("deserializes");
661 match back {
662 KvReply::Ok(KvOutcome::Leased {
663 lease_token,
664 granted_ttl_micros,
665 }) => {
666 assert_eq!(lease_token, 77);
667 assert_eq!(granted_ttl_micros, 30_000_000);
668 }
669 other => panic!("expected Leased, got {other:?}"),
670 }
671 }
672
673 #[test]
674 fn given_a_conditional_get_when_round_tripped_then_should_preserve_if_none_match() {
675 let request = KvGet {
676 v: KV_OP_VERSION,
677 namespace: "sessions".to_owned(),
678 key: b"user:1".to_vec(),
679 if_none_match: Some(5),
680 };
681 let bytes = encode_named(&request).expect("serializes");
682 let back: KvGet = decode_named(&bytes).expect("deserializes");
683 assert_eq!(back.if_none_match, Some(5));
684 let plain = KvGet {
687 if_none_match: None,
688 ..request
689 };
690 let json = serde_json::to_string(&plain).expect("json");
691 assert!(
692 !json.contains("if_none_match"),
693 "absent precondition omitted"
694 );
695 }
696
697 #[test]
698 fn given_a_version_conflict_when_round_tripped_then_should_preserve_the_current_version() {
699 for current in [Some(3u64), None] {
700 let reply = KvReply::Err(KvError::VersionConflict { current });
701 let bytes = encode_named(&reply).expect("serializes");
702 let back: KvReply = decode_named(&bytes).expect("deserializes");
703 match back {
704 KvReply::Err(KvError::VersionConflict { current: got }) => assert_eq!(got, current),
705 other => panic!("expected VersionConflict, got {other:?}"),
706 }
707 }
708 }
709
710 #[test]
711 fn given_a_versioned_entry_when_round_tripped_then_should_preserve_version_and_skip_zero() {
712 let entry = KvEntry {
713 key: b"k".to_vec(),
714 value: b"v".to_vec(),
715 expires_at_micros: None,
716 version: 5,
717 scope: None,
718 source: None,
719 };
720 let bytes = encode_named(&entry).expect("serializes");
721 let back: KvEntry = decode_named(&bytes).expect("deserializes");
722 assert_eq!(back.version, 5);
723 let unversioned = KvEntry {
726 version: 0,
727 ..entry
728 };
729 let json = serde_json::to_string(&unversioned).expect("json");
730 assert!(
731 !json.contains("version"),
732 "version 0 must be omitted: {json}"
733 );
734 }
735
736 #[test]
737 fn given_a_scan_with_bounds_when_round_tripped_then_should_preserve_filters() {
738 let scan = KvScan {
739 v: KV_OP_VERSION,
740 namespace: "sessions".to_owned(),
741 prefix: Some(b"user:".to_vec()),
742 start: None,
743 end: None,
744 key_contains: Some("admin".to_owned()),
745 conversation: None,
746 limit: 50,
747 cursor: Some(b"user:9".to_vec()),
748 };
749 let bytes = encode_named(&scan).expect("serializes");
750 let back: KvScan = decode_named(&bytes).expect("deserializes");
751 assert_eq!(back.prefix.as_deref(), Some(b"user:".as_ref()));
752 assert_eq!(back.key_contains.as_deref(), Some("admin"));
753 assert_eq!(back.cursor.as_deref(), Some(b"user:9".as_ref()));
754 }
755}