1use crate::error::InvalidError;
2use crate::limits::MAX_NAMESPACE_BYTES;
3use crate::mutation::MutationPosition;
4use serde::{Deserialize, Serialize};
5
6#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
10pub struct MemoryRowScope {
11 #[serde(default, skip_serializing_if = "Option::is_none")]
13 pub kind: Option<String>,
14 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub agent: Option<String>,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub user: Option<String>,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub app: Option<String>,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub conversation: Option<String>,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub source: Option<crate::graph::SourceRef>,
31}
32
33#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
37pub struct KvEntry {
38 #[serde(with = "crate::encoding::bin_bytes")]
39 pub key: Vec<u8>,
40 #[serde(with = "crate::encoding::bin_bytes")]
41 pub value: Vec<u8>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub expires_at_micros: Option<u64>,
46 #[serde(default, skip_serializing_if = "is_zero_u64")]
54 pub version: u64,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub scope: Option<Box<MemoryRowScope>>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub source: Option<Box<crate::graph::SourceRef>>,
68}
69
70fn is_zero_u64(value: &u64) -> bool {
71 *value == 0
72}
73
74impl KvEntry {
75 pub fn key_str(&self) -> Option<&str> {
78 std::str::from_utf8(&self.key).ok()
79 }
80}
81
82#[derive(Clone, Debug, Default, Serialize, Deserialize)]
85pub struct KvPage {
86 pub entries: Vec<KvEntry>,
87 #[serde(
88 default,
89 skip_serializing_if = "Option::is_none",
90 with = "crate::encoding::opt_bin_bytes"
91 )]
92 pub cursor: Option<Vec<u8>>,
93}
94
95#[derive(Clone, Debug, Serialize, Deserialize)]
109pub struct KvGet {
110 pub v: u32,
111 pub namespace: String,
112 #[serde(with = "crate::encoding::bin_bytes")]
113 pub key: Vec<u8>,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub if_none_match: Option<u64>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub min_position: Option<MutationPosition>,
118}
119
120#[derive(Clone, Debug, Serialize, Deserialize)]
122pub struct KvSet {
123 pub v: u32,
124 pub namespace: String,
125 #[serde(with = "crate::encoding::bin_bytes")]
126 pub key: Vec<u8>,
127 #[serde(with = "crate::encoding::bin_bytes")]
128 pub value: Vec<u8>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub expires_at_micros: Option<u64>,
131}
132
133#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
137pub enum CasExpect {
138 Match(u64),
140 Absent,
142}
143
144#[derive(Clone, Debug, Serialize, Deserialize)]
149pub struct KvCas {
150 pub v: u32,
151 pub namespace: String,
152 #[serde(with = "crate::encoding::bin_bytes")]
153 pub key: Vec<u8>,
154 #[serde(with = "crate::encoding::bin_bytes")]
155 pub value: Vec<u8>,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub expires_at_micros: Option<u64>,
158 pub expect: CasExpect,
159}
160
161#[derive(Clone, Debug, Serialize, Deserialize)]
171pub struct KvCasFenced {
172 pub v: u32,
173 pub namespace: String,
174 #[serde(with = "crate::encoding::bin_bytes")]
175 pub key: Vec<u8>,
176 #[serde(with = "crate::encoding::bin_bytes")]
177 pub value: Vec<u8>,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub expires_at_micros: Option<u64>,
180 pub expect: CasExpect,
181 pub fence_namespace: String,
185 #[serde(with = "crate::encoding::bin_bytes")]
187 pub fence_key: Vec<u8>,
188 pub fence_token: u64,
191}
192
193#[derive(Clone, Debug, Serialize, Deserialize)]
197pub struct KvDelete {
198 pub v: u32,
199 pub namespace: String,
200 #[serde(with = "crate::encoding::bin_bytes")]
201 pub key: Vec<u8>,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub if_match: Option<u64>,
204}
205
206#[derive(Clone, Debug, Serialize, Deserialize)]
210pub struct KvExists {
211 pub v: u32,
212 pub namespace: String,
213 #[serde(with = "crate::encoding::bin_bytes")]
214 pub key: Vec<u8>,
215}
216
217#[derive(Clone, Debug, Serialize, Deserialize)]
221pub struct KvExpire {
222 pub v: u32,
223 pub namespace: String,
224 #[serde(with = "crate::encoding::bin_bytes")]
225 pub key: Vec<u8>,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub expires_at_micros: Option<u64>,
228}
229
230#[derive(Clone, Debug, Serialize, Deserialize)]
235pub struct KvPatch {
236 pub v: u32,
237 pub namespace: String,
238 #[serde(with = "crate::encoding::bin_bytes")]
239 pub key: Vec<u8>,
240 #[serde(with = "crate::encoding::bin_bytes")]
241 pub patch: Vec<u8>,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
243 pub if_match: Option<u64>,
244}
245
246#[derive(Clone, Debug, Serialize, Deserialize)]
251pub struct KvCopy {
252 pub v: u32,
253 pub namespace: String,
254 #[serde(with = "crate::encoding::bin_bytes")]
255 pub key: Vec<u8>,
256 #[serde(default, skip_serializing_if = "Option::is_none")]
258 pub to_namespace: Option<String>,
259 #[serde(with = "crate::encoding::bin_bytes")]
260 pub to_key: Vec<u8>,
261}
262
263#[derive(Clone, Debug, Serialize, Deserialize)]
266pub struct KvMove {
267 pub v: u32,
268 pub namespace: String,
269 #[serde(with = "crate::encoding::bin_bytes")]
270 pub key: Vec<u8>,
271 #[serde(default, skip_serializing_if = "Option::is_none")]
273 pub to_namespace: Option<String>,
274 #[serde(with = "crate::encoding::bin_bytes")]
275 pub to_key: Vec<u8>,
276}
277
278#[derive(Clone, Debug, Serialize, Deserialize)]
286pub struct KvLease {
287 pub v: u32,
288 pub namespace: String,
289 #[serde(with = "crate::encoding::bin_bytes")]
290 pub key: Vec<u8>,
291 pub lease_ttl_micros: u64,
298 pub holder_id: String,
303 #[serde(default, skip_serializing_if = "Option::is_none")]
308 pub subject_user_id: Option<u32>,
309}
310
311#[derive(Clone, Debug, Serialize, Deserialize)]
318pub struct KvLeaseRenew {
319 pub v: u32,
320 pub namespace: String,
321 #[serde(with = "crate::encoding::bin_bytes")]
322 pub key: Vec<u8>,
323 pub holder_id: String,
324 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub subject_user_id: Option<u32>,
326 pub lease_token: u64,
327 pub lease_ttl_micros: u64,
332}
333
334#[derive(Clone, Debug, Serialize, Deserialize)]
342pub struct KvRelease {
343 pub v: u32,
344 pub namespace: String,
345 #[serde(with = "crate::encoding::bin_bytes")]
346 pub key: Vec<u8>,
347 pub lease_token: u64,
348 pub holder_id: String,
349}
350
351#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
354pub struct KvMetadata {
355 pub version: u64,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
357 pub expires_at_micros: Option<u64>,
358 pub size_bytes: usize,
359}
360
361#[derive(Clone, Debug, Serialize, Deserialize)]
363pub struct KvNamespaces {
364 pub v: u32,
365}
366
367#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
369pub struct KvNamespaceInfo {
370 pub namespace: String,
371 pub entries: usize,
372}
373
374#[derive(Clone, Debug, Serialize, Deserialize)]
380pub struct KvScan {
381 pub v: u32,
382 pub namespace: String,
383 #[serde(
384 default,
385 skip_serializing_if = "Option::is_none",
386 with = "crate::encoding::opt_bin_bytes"
387 )]
388 pub prefix: Option<Vec<u8>>,
389 #[serde(
390 default,
391 skip_serializing_if = "Option::is_none",
392 with = "crate::encoding::opt_bin_bytes"
393 )]
394 pub start: Option<Vec<u8>>,
395 #[serde(
396 default,
397 skip_serializing_if = "Option::is_none",
398 with = "crate::encoding::opt_bin_bytes"
399 )]
400 pub end: Option<Vec<u8>>,
401 #[serde(default, skip_serializing_if = "Option::is_none")]
402 pub key_contains: Option<String>,
403 #[serde(default, skip_serializing_if = "Option::is_none")]
409 pub conversation: Option<String>,
410 pub limit: usize,
411 #[serde(
412 default,
413 skip_serializing_if = "Option::is_none",
414 with = "crate::encoding::opt_bin_bytes"
415 )]
416 pub cursor: Option<Vec<u8>>,
417}
418
419#[derive(Clone, Debug, Serialize, Deserialize)]
424pub struct KvDeleteMany {
425 pub v: u32,
426 pub namespace: String,
427 #[serde(
428 default,
429 skip_serializing_if = "Option::is_none",
430 with = "crate::encoding::opt_bin_bytes"
431 )]
432 pub prefix: Option<Vec<u8>>,
433 #[serde(
434 default,
435 skip_serializing_if = "Option::is_none",
436 with = "crate::encoding::opt_bin_bytes"
437 )]
438 pub start: Option<Vec<u8>>,
439 #[serde(
440 default,
441 skip_serializing_if = "Option::is_none",
442 with = "crate::encoding::opt_bin_bytes"
443 )]
444 pub end: Option<Vec<u8>>,
445 #[serde(default, skip_serializing_if = "Option::is_none")]
446 pub key_contains: Option<String>,
447 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub conversation: Option<String>,
451}
452
453#[derive(Clone, Debug, Serialize, Deserialize)]
456#[non_exhaustive]
457pub enum KvReply {
458 Ok(KvOutcome),
459 Err(KvError),
460}
461
462#[derive(Clone, Debug, Serialize, Deserialize)]
464#[non_exhaustive]
465pub enum KvOutcome {
466 Value(Option<KvEntry>),
468 Written,
470 Committed { version: u64 },
473 Deleted(bool),
475 DeletedMany(usize),
477 Page(KvPage),
479 Namespaces(Vec<KvNamespaceInfo>),
482 NotModified,
485 Metadata(Option<KvMetadata>),
487 Versioned { version: u64 },
490 Leased {
496 lease_token: u64,
497 granted_ttl_micros: u64,
498 position: MutationPosition,
499 },
500 Renewed {
505 lease_token: u64,
506 granted_ttl_micros: u64,
507 position: MutationPosition,
508 },
509 Released(bool),
512}
513
514#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
516#[non_exhaustive]
517pub enum KvError {
518 #[error("kv not supported: {0}")]
519 Unsupported(String),
520 #[error("invalid key: {0}")]
521 InvalidKey(String),
522 #[error("invalid namespace: {0}")]
524 InvalidNamespace(String),
525 #[error("{what} is {size}B, exceeds cap {cap}B")]
526 TooLarge {
527 what: String,
528 size: usize,
529 cap: usize,
530 },
531 #[error("kv backend error: {0}")]
532 Backend(String),
533 #[error("temporarily unavailable: {0}")]
538 Unavailable(String),
539 #[error("unsupported kv op version (expected {expected}, got {got})")]
540 Version { expected: u32, got: u32 },
541 #[error("kv version conflict (current: {current:?})")]
545 VersionConflict { current: Option<u64> },
546 #[error("kv lease lost")]
550 LeaseLost,
551 #[error("kv read barrier not reached (required: {required:?})")]
556 Stale { required: MutationPosition },
557 #[error("kv key not found")]
560 NotFound,
561 #[error("not the partition leader for this key")]
564 NotLeader,
565}
566
567pub fn validate_namespace(namespace: &str) -> Result<(), InvalidError> {
572 if namespace.is_empty() {
573 return Err(InvalidError::new("namespace must not be empty"));
574 }
575 if namespace.len() > MAX_NAMESPACE_BYTES {
576 return Err(InvalidError::new(format!(
577 "namespace is {}B, exceeds cap {MAX_NAMESPACE_BYTES}B",
578 namespace.len()
579 )));
580 }
581 if namespace.bytes().any(|byte| byte.is_ascii_control()) {
582 return Err(InvalidError::new(
583 "namespace must not contain ASCII control characters",
584 ));
585 }
586 Ok(())
587}
588
589#[cfg(all(test, feature = "cbor"))]
590mod tests {
591 use super::*;
592 use crate::codes::KV_OP_VERSION;
593 use crate::framing::{decode_named, encode_named};
594
595 #[test]
596 fn given_kv_delete_many_when_round_tripped_then_should_preserve_bounds() {
597 let request = KvDeleteMany {
598 v: KV_OP_VERSION,
599 namespace: "sessions".to_owned(),
600 prefix: Some(b"user:".to_vec()),
601 start: None,
602 end: None,
603 key_contains: Some("stale".to_owned()),
604 conversation: None,
605 };
606 let bytes = encode_named(&request).expect("serializes");
607 let back: KvDeleteMany = decode_named(&bytes).expect("deserializes");
608 assert_eq!(back.prefix, Some(b"user:".to_vec()));
609 assert_eq!(back.key_contains.as_deref(), Some("stale"));
610 }
611
612 #[test]
613 fn given_kv_deleted_many_reply_when_round_tripped_then_should_preserve_count() {
614 let reply = KvReply::Ok(KvOutcome::DeletedMany(7));
615 let bytes = encode_named(&reply).expect("serializes");
616 let back: KvReply = decode_named(&bytes).expect("deserializes");
617 match back {
618 KvReply::Ok(KvOutcome::DeletedMany(n)) => assert_eq!(n, 7),
619 other => panic!("expected DeletedMany, got {other:?}"),
620 }
621 }
622
623 #[test]
624 fn given_a_set_request_when_round_tripped_then_should_preserve_value_and_expiry() {
625 let request = KvSet {
626 v: KV_OP_VERSION,
627 namespace: "sessions".to_owned(),
628 key: b"user:42".to_vec(),
629 value: b"online".to_vec(),
630 expires_at_micros: Some(1_700_000_000_000_000),
631 };
632 let bytes = encode_named(&request).expect("the request serializes");
633 let back: KvSet = decode_named(&bytes).expect("the request deserializes");
634 assert_eq!(back.key, b"user:42");
635 assert_eq!(back.value, b"online");
636 assert_eq!(back.expires_at_micros, Some(1_700_000_000_000_000));
637 }
638
639 #[test]
640 fn given_a_binary_key_entry_when_round_tripped_then_should_preserve_raw_bytes() {
641 let reply = KvReply::Ok(KvOutcome::Value(Some(KvEntry {
642 key: vec![0xff, 0x00, 0xfe],
643 value: vec![0x00, 0x01, 0x02],
644 expires_at_micros: None,
645 version: 0,
646 scope: None,
647 source: None,
648 })));
649 let bytes = encode_named(&reply).expect("the reply serializes");
650 let back: KvReply = decode_named(&bytes).expect("the reply deserializes");
651 let KvReply::Ok(KvOutcome::Value(Some(entry))) = back else {
652 panic!("expected an Ok(Value(Some)) reply");
653 };
654 assert_eq!(entry.key, vec![0xff, 0x00, 0xfe]);
655 assert_eq!(entry.key_str(), None, "non-UTF-8 key has no string form");
656 assert_eq!(entry.value, vec![0x00, 0x01, 0x02]);
657 }
658
659 #[test]
660 fn given_a_scan_page_when_round_tripped_then_should_preserve_cursor() {
661 let reply = KvReply::Ok(KvOutcome::Page(KvPage {
662 entries: vec![KvEntry {
663 key: b"a".to_vec(),
664 value: b"1".to_vec(),
665 expires_at_micros: None,
666 version: 0,
667 scope: None,
668 source: None,
669 }],
670 cursor: Some(b"a".to_vec()),
671 }));
672 let bytes = encode_named(&reply).expect("serializes");
673 let back: KvReply = decode_named(&bytes).expect("deserializes");
674 let KvReply::Ok(KvOutcome::Page(page)) = back else {
675 panic!("expected an Ok(Page) reply");
676 };
677 assert_eq!(page.entries.len(), 1);
678 assert_eq!(page.entries[0].key_str(), Some("a"));
679 assert_eq!(page.cursor.as_deref(), Some(b"a".as_ref()));
680 }
681
682 #[test]
683 fn given_a_cas_request_when_round_tripped_then_should_preserve_the_precondition() {
684 for expect in [CasExpect::Match(7), CasExpect::Absent] {
685 let request = KvCas {
686 v: KV_OP_VERSION,
687 namespace: "counters".to_owned(),
688 key: b"hits".to_vec(),
689 value: b"42".to_vec(),
690 expires_at_micros: None,
691 expect,
692 };
693 let bytes = encode_named(&request).expect("serializes");
694 let back: KvCas = decode_named(&bytes).expect("deserializes");
695 assert_eq!(back.expect, expect);
696 assert_eq!(back.key, b"hits");
697 }
698 }
699
700 #[test]
701 fn given_a_committed_reply_when_round_tripped_then_should_preserve_the_version() {
702 let reply = KvReply::Ok(KvOutcome::Committed { version: 9 });
703 let bytes = encode_named(&reply).expect("serializes");
704 let back: KvReply = decode_named(&bytes).expect("deserializes");
705 match back {
706 KvReply::Ok(KvOutcome::Committed { version }) => assert_eq!(version, 9),
707 other => panic!("expected Committed, got {other:?}"),
708 }
709 }
710
711 #[test]
712 fn given_an_exists_metadata_reply_when_round_tripped_then_should_preserve_metadata() {
713 let reply = KvReply::Ok(KvOutcome::Metadata(Some(KvMetadata {
714 version: 4,
715 expires_at_micros: Some(1_700_000_000_000_000),
716 size_bytes: 128,
717 })));
718 let bytes = encode_named(&reply).expect("serializes");
719 let back: KvReply = decode_named(&bytes).expect("deserializes");
720 let KvReply::Ok(KvOutcome::Metadata(Some(meta))) = back else {
721 panic!("expected Ok(Metadata(Some))");
722 };
723 assert_eq!(meta.version, 4);
724 assert_eq!(meta.size_bytes, 128);
725 }
726
727 #[test]
728 fn given_a_patch_request_when_round_tripped_then_should_preserve_patch_and_precondition() {
729 let request = KvPatch {
730 v: KV_OP_VERSION,
731 namespace: "docs".to_owned(),
732 key: b"doc:1".to_vec(),
733 patch: br#"{"status":"closed"}"#.to_vec(),
734 if_match: Some(3),
735 };
736 let bytes = encode_named(&request).expect("serializes");
737 let back: KvPatch = decode_named(&bytes).expect("deserializes");
738 assert_eq!(back.patch, br#"{"status":"closed"}"#);
739 assert_eq!(back.if_match, Some(3));
740 }
741
742 #[test]
743 fn given_a_lease_reply_when_round_tripped_then_should_preserve_token_ttl_and_position() {
744 let position = MutationPosition {
745 topic_generation: 3,
746 partition: 1,
747 offset: 4_200,
748 };
749 for reply in [
750 KvReply::Ok(KvOutcome::Leased {
751 lease_token: 77,
752 granted_ttl_micros: 30_000_000,
753 position,
754 }),
755 KvReply::Ok(KvOutcome::Renewed {
756 lease_token: 77,
757 granted_ttl_micros: 30_000_000,
758 position,
759 }),
760 ] {
761 let bytes = encode_named(&reply).expect("serializes");
762 let back: KvReply = decode_named(&bytes).expect("deserializes");
763 match back {
764 KvReply::Ok(
765 KvOutcome::Leased {
766 lease_token,
767 granted_ttl_micros,
768 position: got,
769 }
770 | KvOutcome::Renewed {
771 lease_token,
772 granted_ttl_micros,
773 position: got,
774 },
775 ) => {
776 assert_eq!(lease_token, 77);
777 assert_eq!(granted_ttl_micros, 30_000_000);
778 assert_eq!(got, position);
779 }
780 other => panic!("expected Leased or Renewed, got {other:?}"),
781 }
782 }
783 }
784
785 #[test]
786 fn given_a_reshaped_lease_request_when_round_tripped_then_should_preserve_holder_and_subject() {
787 let request = KvLease {
788 v: crate::codes::KV_LEASE_OP_VERSION,
789 namespace: "connectors.coordination".to_owned(),
790 key: b"source-owner".to_vec(),
791 lease_ttl_micros: 30_000_000,
792 holder_id: "warden-node-1".to_owned(),
793 subject_user_id: Some(42),
794 };
795 let bytes = encode_named(&request).expect("serializes");
796 let back: KvLease = decode_named(&bytes).expect("deserializes");
797 assert_eq!(back.holder_id, "warden-node-1");
798 assert_eq!(back.subject_user_id, Some(42));
799 let own = KvLease {
801 subject_user_id: None,
802 ..request
803 };
804 let json = serde_json::to_string(&own).expect("json");
805 assert!(!json.contains("subject_user_id"), "absent subject omitted");
806 }
807
808 #[test]
809 fn given_a_renew_request_when_round_tripped_then_should_preserve_token_and_holder() {
810 let request = KvLeaseRenew {
811 v: crate::codes::KV_LEASE_OP_VERSION,
812 namespace: "connectors.coordination".to_owned(),
813 key: b"source-owner".to_vec(),
814 holder_id: "warden-node-1".to_owned(),
815 subject_user_id: None,
816 lease_token: 7,
817 lease_ttl_micros: 30_000_000,
818 };
819 let bytes = encode_named(&request).expect("serializes");
820 let back: KvLeaseRenew = decode_named(&bytes).expect("deserializes");
821 assert_eq!(back.lease_token, 7);
822 assert_eq!(back.holder_id, "warden-node-1");
823 }
824
825 #[test]
826 fn given_a_pre_fencing_lease_payload_when_decoded_then_should_fail_closed() {
827 #[derive(Serialize)]
832 struct LegacyLease<'a> {
833 v: u32,
834 namespace: &'a str,
835 #[serde(with = "crate::encoding::bin_bytes")]
836 key: &'a [u8],
837 lease_ttl_micros: u64,
838 }
839 let legacy = encode_named(&LegacyLease {
840 v: KV_OP_VERSION,
841 namespace: "sessions",
842 key: b"job",
843 lease_ttl_micros: 30_000_000,
844 })
845 .expect("legacy shape serializes");
846 assert!(
847 decode_named::<KvLease>(&legacy).is_err(),
848 "a holder-less lease payload must not decode"
849 );
850
851 #[derive(Serialize)]
852 struct LegacyRelease<'a> {
853 v: u32,
854 namespace: &'a str,
855 #[serde(with = "crate::encoding::bin_bytes")]
856 key: &'a [u8],
857 lease_token: u64,
858 }
859 let legacy = encode_named(&LegacyRelease {
860 v: KV_OP_VERSION,
861 namespace: "sessions",
862 key: b"job",
863 lease_token: 7,
864 })
865 .expect("legacy shape serializes");
866 assert!(
867 decode_named::<KvRelease>(&legacy).is_err(),
868 "a holder-less release payload must not decode"
869 );
870
871 #[derive(Serialize)]
872 struct LegacyCasFenced<'a> {
873 v: u32,
874 namespace: &'a str,
875 #[serde(with = "crate::encoding::bin_bytes")]
876 key: &'a [u8],
877 #[serde(with = "crate::encoding::bin_bytes")]
878 value: &'a [u8],
879 expect: CasExpect,
880 #[serde(with = "crate::encoding::bin_bytes")]
881 fence_key: &'a [u8],
882 fence_token: u64,
883 }
884 let legacy = encode_named(&LegacyCasFenced {
885 v: KV_OP_VERSION,
886 namespace: "sessions",
887 key: b"cursor",
888 value: b"42",
889 expect: CasExpect::Absent,
890 fence_key: b"job",
891 fence_token: 7,
892 })
893 .expect("legacy shape serializes");
894 assert!(
895 decode_named::<KvCasFenced>(&legacy).is_err(),
896 "a fence-only CAS payload must not decode"
897 );
898 }
899
900 #[test]
901 fn given_a_stale_reply_when_round_tripped_then_should_preserve_the_required_position() {
902 let required = MutationPosition {
903 topic_generation: 2,
904 partition: 0,
905 offset: 99,
906 };
907 let reply = KvReply::Err(KvError::Stale { required });
908 let bytes = encode_named(&reply).expect("serializes");
909 let back: KvReply = decode_named(&bytes).expect("deserializes");
910 match back {
911 KvReply::Err(KvError::Stale { required: got }) => assert_eq!(got, required),
912 other => panic!("expected Stale, got {other:?}"),
913 }
914 }
915
916 #[test]
917 fn given_a_conditional_get_when_round_tripped_then_should_preserve_if_none_match() {
918 let request = KvGet {
919 v: KV_OP_VERSION,
920 namespace: "sessions".to_owned(),
921 key: b"user:1".to_vec(),
922 if_none_match: Some(5),
923 min_position: None,
924 };
925 let bytes = encode_named(&request).expect("serializes");
926 let back: KvGet = decode_named(&bytes).expect("deserializes");
927 assert_eq!(back.if_none_match, Some(5));
928 let plain = KvGet {
931 if_none_match: None,
932 ..request
933 };
934 let json = serde_json::to_string(&plain).expect("json");
935 assert!(
936 !json.contains("if_none_match"),
937 "absent precondition omitted"
938 );
939 assert!(!json.contains("min_position"), "absent barrier omitted");
940 }
941
942 #[test]
943 fn given_a_barriered_get_when_round_tripped_then_should_preserve_the_minimum_position() {
944 let request = KvGet {
945 v: KV_OP_VERSION,
946 namespace: "connectors.state".to_owned(),
947 key: b"source_state/pg".to_vec(),
948 if_none_match: None,
949 min_position: Some(MutationPosition {
950 topic_generation: 1,
951 partition: 0,
952 offset: 512,
953 }),
954 };
955 let bytes = encode_named(&request).expect("serializes");
956 let back: KvGet = decode_named(&bytes).expect("deserializes");
957 assert_eq!(
958 back.min_position,
959 Some(MutationPosition {
960 topic_generation: 1,
961 partition: 0,
962 offset: 512,
963 })
964 );
965 }
966
967 #[test]
968 fn given_a_version_conflict_when_round_tripped_then_should_preserve_the_current_version() {
969 for current in [Some(3u64), None] {
970 let reply = KvReply::Err(KvError::VersionConflict { current });
971 let bytes = encode_named(&reply).expect("serializes");
972 let back: KvReply = decode_named(&bytes).expect("deserializes");
973 match back {
974 KvReply::Err(KvError::VersionConflict { current: got }) => assert_eq!(got, current),
975 other => panic!("expected VersionConflict, got {other:?}"),
976 }
977 }
978 }
979
980 #[test]
981 fn given_a_versioned_entry_when_round_tripped_then_should_preserve_version_and_skip_zero() {
982 let entry = KvEntry {
983 key: b"k".to_vec(),
984 value: b"v".to_vec(),
985 expires_at_micros: None,
986 version: 5,
987 scope: None,
988 source: None,
989 };
990 let bytes = encode_named(&entry).expect("serializes");
991 let back: KvEntry = decode_named(&bytes).expect("deserializes");
992 assert_eq!(back.version, 5);
993 let unversioned = KvEntry {
996 version: 0,
997 ..entry
998 };
999 let json = serde_json::to_string(&unversioned).expect("json");
1000 assert!(
1001 !json.contains("version"),
1002 "version 0 must be omitted: {json}"
1003 );
1004 }
1005
1006 #[test]
1007 fn given_a_scan_with_bounds_when_round_tripped_then_should_preserve_filters() {
1008 let scan = KvScan {
1009 v: KV_OP_VERSION,
1010 namespace: "sessions".to_owned(),
1011 prefix: Some(b"user:".to_vec()),
1012 start: None,
1013 end: None,
1014 key_contains: Some("admin".to_owned()),
1015 conversation: None,
1016 limit: 50,
1017 cursor: Some(b"user:9".to_vec()),
1018 };
1019 let bytes = encode_named(&scan).expect("serializes");
1020 let back: KvScan = decode_named(&bytes).expect("deserializes");
1021 assert_eq!(back.prefix.as_deref(), Some(b"user:".as_ref()));
1022 assert_eq!(back.key_contains.as_deref(), Some("admin"));
1023 assert_eq!(back.cursor.as_deref(), Some(b"user:9".as_ref()));
1024 }
1025}