1use crate::error::InvalidError;
2use crate::limits::MAX_NAMESPACE_BYTES;
3use serde::{Deserialize, Serialize};
4
5pub fn validate_namespace(namespace: &str) -> Result<(), InvalidError> {
10 if namespace.is_empty() {
11 return Err(InvalidError::new("namespace must not be empty"));
12 }
13 if namespace.len() > MAX_NAMESPACE_BYTES {
14 return Err(InvalidError::new(format!(
15 "namespace is {}B, exceeds cap {MAX_NAMESPACE_BYTES}B",
16 namespace.len()
17 )));
18 }
19 if namespace.bytes().any(|byte| byte.is_ascii_control()) {
20 return Err(InvalidError::new(
21 "namespace must not contain ASCII control characters",
22 ));
23 }
24 Ok(())
25}
26
27#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
31pub struct MemoryRowScope {
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub kind: Option<String>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub agent: Option<String>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub user: Option<String>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub app: Option<String>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub conversation: Option<String>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub source: Option<crate::graph::SourceRef>,
52}
53
54#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
58pub struct KvEntry {
59 #[serde(with = "crate::encoding::bin_bytes")]
60 pub key: Vec<u8>,
61 #[serde(with = "crate::encoding::bin_bytes")]
62 pub value: Vec<u8>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub expires_at_micros: Option<u64>,
67 #[serde(default, skip_serializing_if = "is_zero_u64")]
75 pub version: u64,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub scope: Option<Box<MemoryRowScope>>,
81}
82
83fn is_zero_u64(value: &u64) -> bool {
84 *value == 0
85}
86
87impl KvEntry {
88 pub fn key_str(&self) -> Option<&str> {
91 std::str::from_utf8(&self.key).ok()
92 }
93}
94
95#[derive(Clone, Debug, Default, Serialize, Deserialize)]
98pub struct KvPage {
99 pub entries: Vec<KvEntry>,
100 #[serde(
101 default,
102 skip_serializing_if = "Option::is_none",
103 with = "crate::encoding::opt_bin_bytes"
104 )]
105 pub cursor: Option<Vec<u8>>,
106}
107
108#[derive(Clone, Debug, Serialize, Deserialize)]
113pub struct KvGet {
114 pub v: u32,
115 pub namespace: String,
116 #[serde(with = "crate::encoding::bin_bytes")]
117 pub key: Vec<u8>,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub if_none_match: Option<u64>,
120}
121
122#[derive(Clone, Debug, Serialize, Deserialize)]
124pub struct KvSet {
125 pub v: u32,
126 pub namespace: String,
127 #[serde(with = "crate::encoding::bin_bytes")]
128 pub key: Vec<u8>,
129 #[serde(with = "crate::encoding::bin_bytes")]
130 pub value: Vec<u8>,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub expires_at_micros: Option<u64>,
133}
134
135#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
139pub enum CasExpect {
140 Match(u64),
142 Absent,
144}
145
146#[derive(Clone, Debug, Serialize, Deserialize)]
151pub struct KvCas {
152 pub v: u32,
153 pub namespace: String,
154 #[serde(with = "crate::encoding::bin_bytes")]
155 pub key: Vec<u8>,
156 #[serde(with = "crate::encoding::bin_bytes")]
157 pub value: Vec<u8>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub expires_at_micros: Option<u64>,
160 pub expect: CasExpect,
161}
162
163#[derive(Clone, Debug, Serialize, Deserialize)]
169pub struct KvCasFenced {
170 pub v: u32,
171 pub namespace: String,
172 #[serde(with = "crate::encoding::bin_bytes")]
173 pub key: Vec<u8>,
174 #[serde(with = "crate::encoding::bin_bytes")]
175 pub value: Vec<u8>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub expires_at_micros: Option<u64>,
178 pub expect: CasExpect,
179 #[serde(with = "crate::encoding::bin_bytes")]
182 pub fence_key: Vec<u8>,
183 pub fence_token: u64,
186}
187
188#[derive(Clone, Debug, Serialize, Deserialize)]
192pub struct KvDelete {
193 pub v: u32,
194 pub namespace: String,
195 #[serde(with = "crate::encoding::bin_bytes")]
196 pub key: Vec<u8>,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub if_match: Option<u64>,
199}
200
201#[derive(Clone, Debug, Serialize, Deserialize)]
205pub struct KvExists {
206 pub v: u32,
207 pub namespace: String,
208 #[serde(with = "crate::encoding::bin_bytes")]
209 pub key: Vec<u8>,
210}
211
212#[derive(Clone, Debug, Serialize, Deserialize)]
216pub struct KvExpire {
217 pub v: u32,
218 pub namespace: String,
219 #[serde(with = "crate::encoding::bin_bytes")]
220 pub key: Vec<u8>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub expires_at_micros: Option<u64>,
223}
224
225#[derive(Clone, Debug, Serialize, Deserialize)]
230pub struct KvPatch {
231 pub v: u32,
232 pub namespace: String,
233 #[serde(with = "crate::encoding::bin_bytes")]
234 pub key: Vec<u8>,
235 #[serde(with = "crate::encoding::bin_bytes")]
236 pub patch: Vec<u8>,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub if_match: Option<u64>,
239}
240
241#[derive(Clone, Debug, Serialize, Deserialize)]
246pub struct KvCopy {
247 pub v: u32,
248 pub namespace: String,
249 #[serde(with = "crate::encoding::bin_bytes")]
250 pub key: Vec<u8>,
251 #[serde(default, skip_serializing_if = "Option::is_none")]
253 pub to_namespace: Option<String>,
254 #[serde(with = "crate::encoding::bin_bytes")]
255 pub to_key: Vec<u8>,
256}
257
258#[derive(Clone, Debug, Serialize, Deserialize)]
261pub struct KvMove {
262 pub v: u32,
263 pub namespace: String,
264 #[serde(with = "crate::encoding::bin_bytes")]
265 pub key: Vec<u8>,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
268 pub to_namespace: Option<String>,
269 #[serde(with = "crate::encoding::bin_bytes")]
270 pub to_key: Vec<u8>,
271}
272
273#[derive(Clone, Debug, Serialize, Deserialize)]
277pub struct KvLease {
278 pub v: u32,
279 pub namespace: String,
280 #[serde(with = "crate::encoding::bin_bytes")]
281 pub key: Vec<u8>,
282 pub lease_ttl_micros: u64,
283}
284
285#[derive(Clone, Debug, Serialize, Deserialize)]
288pub struct KvRelease {
289 pub v: u32,
290 pub namespace: String,
291 #[serde(with = "crate::encoding::bin_bytes")]
292 pub key: Vec<u8>,
293 pub lease_token: u64,
294}
295
296#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
299pub struct KvMetadata {
300 pub version: u64,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub expires_at_micros: Option<u64>,
303 pub size_bytes: usize,
304}
305
306#[derive(Clone, Debug, Serialize, Deserialize)]
308pub struct KvNamespaces {
309 pub v: u32,
310}
311
312#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
314pub struct KvNamespaceInfo {
315 pub namespace: String,
316 pub entries: usize,
317}
318
319#[derive(Clone, Debug, Serialize, Deserialize)]
325pub struct KvScan {
326 pub v: u32,
327 pub namespace: String,
328 #[serde(
329 default,
330 skip_serializing_if = "Option::is_none",
331 with = "crate::encoding::opt_bin_bytes"
332 )]
333 pub prefix: Option<Vec<u8>>,
334 #[serde(
335 default,
336 skip_serializing_if = "Option::is_none",
337 with = "crate::encoding::opt_bin_bytes"
338 )]
339 pub start: Option<Vec<u8>>,
340 #[serde(
341 default,
342 skip_serializing_if = "Option::is_none",
343 with = "crate::encoding::opt_bin_bytes"
344 )]
345 pub end: Option<Vec<u8>>,
346 #[serde(default, skip_serializing_if = "Option::is_none")]
347 pub key_contains: Option<String>,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
354 pub conversation: Option<String>,
355 pub limit: usize,
356 #[serde(
357 default,
358 skip_serializing_if = "Option::is_none",
359 with = "crate::encoding::opt_bin_bytes"
360 )]
361 pub cursor: Option<Vec<u8>>,
362}
363
364#[derive(Clone, Debug, Serialize, Deserialize)]
369pub struct KvDeleteMany {
370 pub v: u32,
371 pub namespace: String,
372 #[serde(
373 default,
374 skip_serializing_if = "Option::is_none",
375 with = "crate::encoding::opt_bin_bytes"
376 )]
377 pub prefix: Option<Vec<u8>>,
378 #[serde(
379 default,
380 skip_serializing_if = "Option::is_none",
381 with = "crate::encoding::opt_bin_bytes"
382 )]
383 pub start: Option<Vec<u8>>,
384 #[serde(
385 default,
386 skip_serializing_if = "Option::is_none",
387 with = "crate::encoding::opt_bin_bytes"
388 )]
389 pub end: Option<Vec<u8>>,
390 #[serde(default, skip_serializing_if = "Option::is_none")]
391 pub key_contains: Option<String>,
392 #[serde(default, skip_serializing_if = "Option::is_none")]
395 pub conversation: Option<String>,
396}
397
398#[derive(Clone, Debug, Serialize, Deserialize)]
401#[non_exhaustive]
402pub enum KvReply {
403 Ok(KvOutcome),
404 Err(KvError),
405}
406
407#[derive(Clone, Debug, Serialize, Deserialize)]
409#[non_exhaustive]
410pub enum KvOutcome {
411 Value(Option<KvEntry>),
413 Written,
415 Committed { version: u64 },
418 Deleted(bool),
420 DeletedMany(usize),
422 Page(KvPage),
424 Namespaces(Vec<KvNamespaceInfo>),
427 NotModified,
430 Metadata(Option<KvMetadata>),
432 Versioned { version: u64 },
435 Leased {
438 lease_token: u64,
439 granted_ttl_micros: u64,
440 },
441 Released(bool),
444}
445
446#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
448#[non_exhaustive]
449pub enum KvError {
450 #[error("kv not supported: {0}")]
451 Unsupported(String),
452 #[error("invalid key: {0}")]
453 InvalidKey(String),
454 #[error("invalid namespace: {0}")]
456 InvalidNamespace(String),
457 #[error("{what} is {size}B, exceeds cap {cap}B")]
458 TooLarge {
459 what: String,
460 size: usize,
461 cap: usize,
462 },
463 #[error("kv backend error: {0}")]
464 Backend(String),
465 #[error("unsupported kv op version (expected {expected}, got {got})")]
466 Version { expected: u32, got: u32 },
467 #[error("kv version conflict (current: {current:?})")]
471 VersionConflict { current: Option<u64> },
472 #[error("kv lease lost")]
475 LeaseLost,
476 #[error("kv key not found")]
479 NotFound,
480}
481
482#[cfg(all(test, feature = "cbor"))]
483mod tests {
484 use super::*;
485 use crate::codes::KV_OP_VERSION;
486 use crate::framing::{decode_named, encode_named};
487
488 #[test]
489 fn given_kv_delete_many_when_round_tripped_then_should_preserve_bounds() {
490 let request = KvDeleteMany {
491 v: KV_OP_VERSION,
492 namespace: "sessions".to_owned(),
493 prefix: Some(b"user:".to_vec()),
494 start: None,
495 end: None,
496 key_contains: Some("stale".to_owned()),
497 conversation: None,
498 };
499 let bytes = encode_named(&request).expect("serializes");
500 let back: KvDeleteMany = decode_named(&bytes).expect("deserializes");
501 assert_eq!(back.prefix, Some(b"user:".to_vec()));
502 assert_eq!(back.key_contains.as_deref(), Some("stale"));
503 }
504
505 #[test]
506 fn given_kv_deleted_many_reply_when_round_tripped_then_should_preserve_count() {
507 let reply = KvReply::Ok(KvOutcome::DeletedMany(7));
508 let bytes = encode_named(&reply).expect("serializes");
509 let back: KvReply = decode_named(&bytes).expect("deserializes");
510 match back {
511 KvReply::Ok(KvOutcome::DeletedMany(n)) => assert_eq!(n, 7),
512 other => panic!("expected DeletedMany, got {other:?}"),
513 }
514 }
515
516 #[test]
517 fn given_a_set_request_when_round_tripped_then_should_preserve_value_and_expiry() {
518 let request = KvSet {
519 v: KV_OP_VERSION,
520 namespace: "sessions".to_owned(),
521 key: b"user:42".to_vec(),
522 value: b"online".to_vec(),
523 expires_at_micros: Some(1_700_000_000_000_000),
524 };
525 let bytes = encode_named(&request).expect("the request serializes");
526 let back: KvSet = decode_named(&bytes).expect("the request deserializes");
527 assert_eq!(back.key, b"user:42");
528 assert_eq!(back.value, b"online");
529 assert_eq!(back.expires_at_micros, Some(1_700_000_000_000_000));
530 }
531
532 #[test]
533 fn given_a_binary_key_entry_when_round_tripped_then_should_preserve_raw_bytes() {
534 let reply = KvReply::Ok(KvOutcome::Value(Some(KvEntry {
535 key: vec![0xff, 0x00, 0xfe],
536 value: vec![0x00, 0x01, 0x02],
537 expires_at_micros: None,
538 version: 0,
539 scope: None,
540 })));
541 let bytes = encode_named(&reply).expect("the reply serializes");
542 let back: KvReply = decode_named(&bytes).expect("the reply deserializes");
543 let KvReply::Ok(KvOutcome::Value(Some(entry))) = back else {
544 panic!("expected an Ok(Value(Some)) reply");
545 };
546 assert_eq!(entry.key, vec![0xff, 0x00, 0xfe]);
547 assert_eq!(entry.key_str(), None, "non-UTF-8 key has no string form");
548 assert_eq!(entry.value, vec![0x00, 0x01, 0x02]);
549 }
550
551 #[test]
552 fn given_a_scan_page_when_round_tripped_then_should_preserve_cursor() {
553 let reply = KvReply::Ok(KvOutcome::Page(KvPage {
554 entries: vec![KvEntry {
555 key: b"a".to_vec(),
556 value: b"1".to_vec(),
557 expires_at_micros: None,
558 version: 0,
559 scope: None,
560 }],
561 cursor: Some(b"a".to_vec()),
562 }));
563 let bytes = encode_named(&reply).expect("serializes");
564 let back: KvReply = decode_named(&bytes).expect("deserializes");
565 let KvReply::Ok(KvOutcome::Page(page)) = back else {
566 panic!("expected an Ok(Page) reply");
567 };
568 assert_eq!(page.entries.len(), 1);
569 assert_eq!(page.entries[0].key_str(), Some("a"));
570 assert_eq!(page.cursor.as_deref(), Some(b"a".as_ref()));
571 }
572
573 #[test]
574 fn given_a_cas_request_when_round_tripped_then_should_preserve_the_precondition() {
575 for expect in [CasExpect::Match(7), CasExpect::Absent] {
576 let request = KvCas {
577 v: KV_OP_VERSION,
578 namespace: "counters".to_owned(),
579 key: b"hits".to_vec(),
580 value: b"42".to_vec(),
581 expires_at_micros: None,
582 expect,
583 };
584 let bytes = encode_named(&request).expect("serializes");
585 let back: KvCas = decode_named(&bytes).expect("deserializes");
586 assert_eq!(back.expect, expect);
587 assert_eq!(back.key, b"hits");
588 }
589 }
590
591 #[test]
592 fn given_a_committed_reply_when_round_tripped_then_should_preserve_the_version() {
593 let reply = KvReply::Ok(KvOutcome::Committed { version: 9 });
594 let bytes = encode_named(&reply).expect("serializes");
595 let back: KvReply = decode_named(&bytes).expect("deserializes");
596 match back {
597 KvReply::Ok(KvOutcome::Committed { version }) => assert_eq!(version, 9),
598 other => panic!("expected Committed, got {other:?}"),
599 }
600 }
601
602 #[test]
603 fn given_an_exists_metadata_reply_when_round_tripped_then_should_preserve_metadata() {
604 let reply = KvReply::Ok(KvOutcome::Metadata(Some(KvMetadata {
605 version: 4,
606 expires_at_micros: Some(1_700_000_000_000_000),
607 size_bytes: 128,
608 })));
609 let bytes = encode_named(&reply).expect("serializes");
610 let back: KvReply = decode_named(&bytes).expect("deserializes");
611 let KvReply::Ok(KvOutcome::Metadata(Some(meta))) = back else {
612 panic!("expected Ok(Metadata(Some))");
613 };
614 assert_eq!(meta.version, 4);
615 assert_eq!(meta.size_bytes, 128);
616 }
617
618 #[test]
619 fn given_a_patch_request_when_round_tripped_then_should_preserve_patch_and_precondition() {
620 let request = KvPatch {
621 v: KV_OP_VERSION,
622 namespace: "docs".to_owned(),
623 key: b"doc:1".to_vec(),
624 patch: br#"{"status":"closed"}"#.to_vec(),
625 if_match: Some(3),
626 };
627 let bytes = encode_named(&request).expect("serializes");
628 let back: KvPatch = decode_named(&bytes).expect("deserializes");
629 assert_eq!(back.patch, br#"{"status":"closed"}"#);
630 assert_eq!(back.if_match, Some(3));
631 }
632
633 #[test]
634 fn given_a_lease_reply_when_round_tripped_then_should_preserve_token_and_ttl() {
635 let reply = KvReply::Ok(KvOutcome::Leased {
636 lease_token: 77,
637 granted_ttl_micros: 30_000_000,
638 });
639 let bytes = encode_named(&reply).expect("serializes");
640 let back: KvReply = decode_named(&bytes).expect("deserializes");
641 match back {
642 KvReply::Ok(KvOutcome::Leased {
643 lease_token,
644 granted_ttl_micros,
645 }) => {
646 assert_eq!(lease_token, 77);
647 assert_eq!(granted_ttl_micros, 30_000_000);
648 }
649 other => panic!("expected Leased, got {other:?}"),
650 }
651 }
652
653 #[test]
654 fn given_a_conditional_get_when_round_tripped_then_should_preserve_if_none_match() {
655 let request = KvGet {
656 v: KV_OP_VERSION,
657 namespace: "sessions".to_owned(),
658 key: b"user:1".to_vec(),
659 if_none_match: Some(5),
660 };
661 let bytes = encode_named(&request).expect("serializes");
662 let back: KvGet = decode_named(&bytes).expect("deserializes");
663 assert_eq!(back.if_none_match, Some(5));
664 let plain = KvGet {
667 if_none_match: None,
668 ..request
669 };
670 let json = serde_json::to_string(&plain).expect("json");
671 assert!(
672 !json.contains("if_none_match"),
673 "absent precondition omitted"
674 );
675 }
676
677 #[test]
678 fn given_a_version_conflict_when_round_tripped_then_should_preserve_the_current_version() {
679 for current in [Some(3u64), None] {
680 let reply = KvReply::Err(KvError::VersionConflict { current });
681 let bytes = encode_named(&reply).expect("serializes");
682 let back: KvReply = decode_named(&bytes).expect("deserializes");
683 match back {
684 KvReply::Err(KvError::VersionConflict { current: got }) => assert_eq!(got, current),
685 other => panic!("expected VersionConflict, got {other:?}"),
686 }
687 }
688 }
689
690 #[test]
691 fn given_a_versioned_entry_when_round_tripped_then_should_preserve_version_and_skip_zero() {
692 let entry = KvEntry {
693 key: b"k".to_vec(),
694 value: b"v".to_vec(),
695 expires_at_micros: None,
696 version: 5,
697 scope: None,
698 };
699 let bytes = encode_named(&entry).expect("serializes");
700 let back: KvEntry = decode_named(&bytes).expect("deserializes");
701 assert_eq!(back.version, 5);
702 let unversioned = KvEntry {
705 version: 0,
706 ..entry
707 };
708 let json = serde_json::to_string(&unversioned).expect("json");
709 assert!(
710 !json.contains("version"),
711 "version 0 must be omitted: {json}"
712 );
713 }
714
715 #[test]
716 fn given_a_scan_with_bounds_when_round_tripped_then_should_preserve_filters() {
717 let scan = KvScan {
718 v: KV_OP_VERSION,
719 namespace: "sessions".to_owned(),
720 prefix: Some(b"user:".to_vec()),
721 start: None,
722 end: None,
723 key_contains: Some("admin".to_owned()),
724 conversation: None,
725 limit: 50,
726 cursor: Some(b"user:9".to_vec()),
727 };
728 let bytes = encode_named(&scan).expect("serializes");
729 let back: KvScan = decode_named(&bytes).expect("deserializes");
730 assert_eq!(back.prefix.as_deref(), Some(b"user:".as_ref()));
731 assert_eq!(back.key_contains.as_deref(), Some("admin"));
732 assert_eq!(back.cursor.as_deref(), Some(b"user:9".as_ref()));
733 }
734}