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)]
170pub struct KvCasFenced {
171 pub v: u32,
172 pub namespace: String,
173 #[serde(with = "crate::encoding::bin_bytes")]
174 pub key: Vec<u8>,
175 #[serde(with = "crate::encoding::bin_bytes")]
176 pub value: Vec<u8>,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub expires_at_micros: Option<u64>,
179 pub expect: CasExpect,
180 pub fence_namespace: String,
184 #[serde(with = "crate::encoding::bin_bytes")]
186 pub fence_key: Vec<u8>,
187 pub fence_token: u64,
190}
191
192#[derive(Clone, Debug, Serialize, Deserialize)]
196pub struct KvDelete {
197 pub v: u32,
198 pub namespace: String,
199 #[serde(with = "crate::encoding::bin_bytes")]
200 pub key: Vec<u8>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub if_match: Option<u64>,
203}
204
205#[derive(Clone, Debug, Serialize, Deserialize)]
209pub struct KvExists {
210 pub v: u32,
211 pub namespace: String,
212 #[serde(with = "crate::encoding::bin_bytes")]
213 pub key: Vec<u8>,
214}
215
216#[derive(Clone, Debug, Serialize, Deserialize)]
220pub struct KvExpire {
221 pub v: u32,
222 pub namespace: String,
223 #[serde(with = "crate::encoding::bin_bytes")]
224 pub key: Vec<u8>,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub expires_at_micros: Option<u64>,
227}
228
229#[derive(Clone, Debug, Serialize, Deserialize)]
234pub struct KvPatch {
235 pub v: u32,
236 pub namespace: String,
237 #[serde(with = "crate::encoding::bin_bytes")]
238 pub key: Vec<u8>,
239 #[serde(with = "crate::encoding::bin_bytes")]
240 pub patch: Vec<u8>,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub if_match: Option<u64>,
243}
244
245#[derive(Clone, Debug, Serialize, Deserialize)]
250pub struct KvCopy {
251 pub v: u32,
252 pub namespace: String,
253 #[serde(with = "crate::encoding::bin_bytes")]
254 pub key: Vec<u8>,
255 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub to_namespace: Option<String>,
258 #[serde(with = "crate::encoding::bin_bytes")]
259 pub to_key: Vec<u8>,
260}
261
262#[derive(Clone, Debug, Serialize, Deserialize)]
265pub struct KvMove {
266 pub v: u32,
267 pub namespace: String,
268 #[serde(with = "crate::encoding::bin_bytes")]
269 pub key: Vec<u8>,
270 #[serde(default, skip_serializing_if = "Option::is_none")]
272 pub to_namespace: Option<String>,
273 #[serde(with = "crate::encoding::bin_bytes")]
274 pub to_key: Vec<u8>,
275}
276
277#[derive(Clone, Debug, Serialize, Deserialize)]
284pub struct KvLease {
285 pub v: u32,
286 pub namespace: String,
287 #[serde(with = "crate::encoding::bin_bytes")]
288 pub key: Vec<u8>,
289 pub lease_ttl_micros: u64,
296 pub holder_id: String,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub subject_user_id: Option<u32>,
307}
308
309#[derive(Clone, Debug, Serialize, Deserialize)]
316pub struct KvLeaseRenew {
317 pub v: u32,
318 pub namespace: String,
319 #[serde(with = "crate::encoding::bin_bytes")]
320 pub key: Vec<u8>,
321 pub holder_id: String,
322 #[serde(default, skip_serializing_if = "Option::is_none")]
323 pub subject_user_id: Option<u32>,
324 pub lease_token: u64,
325 pub lease_ttl_micros: u64,
330}
331
332#[derive(Clone, Debug, Serialize, Deserialize)]
340pub struct KvRelease {
341 pub v: u32,
342 pub namespace: String,
343 #[serde(with = "crate::encoding::bin_bytes")]
344 pub key: Vec<u8>,
345 pub lease_token: u64,
346 pub holder_id: String,
347}
348
349#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
352pub struct KvMetadata {
353 pub version: u64,
354 #[serde(default, skip_serializing_if = "Option::is_none")]
355 pub expires_at_micros: Option<u64>,
356 pub size_bytes: usize,
357}
358
359#[derive(Clone, Debug, Serialize, Deserialize)]
361pub struct KvNamespaces {
362 pub v: u32,
363}
364
365#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
367pub struct KvNamespaceInfo {
368 pub namespace: String,
369 pub entries: usize,
370}
371
372#[derive(Clone, Debug, Serialize, Deserialize)]
378pub struct KvScan {
379 pub v: u32,
380 pub namespace: String,
381 #[serde(
382 default,
383 skip_serializing_if = "Option::is_none",
384 with = "crate::encoding::opt_bin_bytes"
385 )]
386 pub prefix: Option<Vec<u8>>,
387 #[serde(
388 default,
389 skip_serializing_if = "Option::is_none",
390 with = "crate::encoding::opt_bin_bytes"
391 )]
392 pub start: Option<Vec<u8>>,
393 #[serde(
394 default,
395 skip_serializing_if = "Option::is_none",
396 with = "crate::encoding::opt_bin_bytes"
397 )]
398 pub end: Option<Vec<u8>>,
399 #[serde(default, skip_serializing_if = "Option::is_none")]
400 pub key_contains: Option<String>,
401 #[serde(default, skip_serializing_if = "Option::is_none")]
407 pub conversation: Option<String>,
408 pub limit: usize,
409 #[serde(
410 default,
411 skip_serializing_if = "Option::is_none",
412 with = "crate::encoding::opt_bin_bytes"
413 )]
414 pub cursor: Option<Vec<u8>>,
415}
416
417#[derive(Clone, Debug, Serialize, Deserialize)]
422pub struct KvDeleteMany {
423 pub v: u32,
424 pub namespace: String,
425 #[serde(
426 default,
427 skip_serializing_if = "Option::is_none",
428 with = "crate::encoding::opt_bin_bytes"
429 )]
430 pub prefix: Option<Vec<u8>>,
431 #[serde(
432 default,
433 skip_serializing_if = "Option::is_none",
434 with = "crate::encoding::opt_bin_bytes"
435 )]
436 pub start: Option<Vec<u8>>,
437 #[serde(
438 default,
439 skip_serializing_if = "Option::is_none",
440 with = "crate::encoding::opt_bin_bytes"
441 )]
442 pub end: Option<Vec<u8>>,
443 #[serde(default, skip_serializing_if = "Option::is_none")]
444 pub key_contains: Option<String>,
445 #[serde(default, skip_serializing_if = "Option::is_none")]
448 pub conversation: Option<String>,
449}
450
451#[derive(Clone, Debug, Serialize, Deserialize)]
454#[non_exhaustive]
455pub enum KvReply {
456 Ok(KvOutcome),
457 Err(KvError),
458}
459
460#[derive(Clone, Debug, Serialize, Deserialize)]
462#[non_exhaustive]
463pub enum KvOutcome {
464 Value(Option<KvEntry>),
466 Written,
468 Committed { version: u64 },
471 Deleted(bool),
473 DeletedMany(usize),
475 Page(KvPage),
477 Namespaces(Vec<KvNamespaceInfo>),
480 NotModified,
483 Metadata(Option<KvMetadata>),
485 Versioned { version: u64 },
488 Leased {
494 lease_token: u64,
495 granted_ttl_micros: u64,
496 position: MutationPosition,
497 },
498 Renewed {
503 lease_token: u64,
504 granted_ttl_micros: u64,
505 position: MutationPosition,
506 },
507 Released(bool),
510}
511
512#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
514#[non_exhaustive]
515pub enum KvError {
516 #[error("kv not supported: {0}")]
517 Unsupported(String),
518 #[error("invalid key: {0}")]
519 InvalidKey(String),
520 #[error("invalid namespace: {0}")]
522 InvalidNamespace(String),
523 #[error("{what} is {size}B, exceeds cap {cap}B")]
524 TooLarge {
525 what: String,
526 size: usize,
527 cap: usize,
528 },
529 #[error("kv backend error: {0}")]
530 Backend(String),
531 #[error("temporarily unavailable: {0}")]
536 Unavailable(String),
537 #[error("unsupported kv op version (expected {expected}, got {got})")]
538 Version { expected: u32, got: u32 },
539 #[error("kv version conflict (current: {current:?})")]
543 VersionConflict { current: Option<u64> },
544 #[error("kv lease lost")]
548 LeaseLost,
549 #[error("kv read barrier not reached (required: {required:?})")]
554 Stale { required: MutationPosition },
555 #[error("kv key not found")]
558 NotFound,
559 #[error("not the partition leader for this key")]
562 NotLeader,
563}
564
565pub fn validate_namespace(namespace: &str) -> Result<(), InvalidError> {
570 if namespace.is_empty() {
571 return Err(InvalidError::new("namespace must not be empty"));
572 }
573 if namespace.len() > MAX_NAMESPACE_BYTES {
574 return Err(InvalidError::new(format!(
575 "namespace is {}B, exceeds cap {MAX_NAMESPACE_BYTES}B",
576 namespace.len()
577 )));
578 }
579 if namespace.bytes().any(|byte| byte.is_ascii_control()) {
580 return Err(InvalidError::new(
581 "namespace must not contain ASCII control characters",
582 ));
583 }
584 Ok(())
585}
586
587#[cfg(all(test, feature = "cbor"))]
588mod tests {
589 use super::*;
590 use crate::codes::KV_OP_VERSION;
591 use crate::framing::{decode_named, encode_named};
592
593 #[test]
594 fn given_kv_delete_many_when_round_tripped_then_should_preserve_bounds() {
595 let request = KvDeleteMany {
596 v: KV_OP_VERSION,
597 namespace: "sessions".to_owned(),
598 prefix: Some(b"user:".to_vec()),
599 start: None,
600 end: None,
601 key_contains: Some("stale".to_owned()),
602 conversation: None,
603 };
604 let bytes = encode_named(&request).expect("serializes");
605 let back: KvDeleteMany = decode_named(&bytes).expect("deserializes");
606 assert_eq!(back.prefix, Some(b"user:".to_vec()));
607 assert_eq!(back.key_contains.as_deref(), Some("stale"));
608 }
609
610 #[test]
611 fn given_kv_deleted_many_reply_when_round_tripped_then_should_preserve_count() {
612 let reply = KvReply::Ok(KvOutcome::DeletedMany(7));
613 let bytes = encode_named(&reply).expect("serializes");
614 let back: KvReply = decode_named(&bytes).expect("deserializes");
615 match back {
616 KvReply::Ok(KvOutcome::DeletedMany(n)) => assert_eq!(n, 7),
617 other => panic!("expected DeletedMany, got {other:?}"),
618 }
619 }
620
621 #[test]
622 fn given_a_set_request_when_round_tripped_then_should_preserve_value_and_expiry() {
623 let request = KvSet {
624 v: KV_OP_VERSION,
625 namespace: "sessions".to_owned(),
626 key: b"user:42".to_vec(),
627 value: b"online".to_vec(),
628 expires_at_micros: Some(1_700_000_000_000_000),
629 };
630 let bytes = encode_named(&request).expect("the request serializes");
631 let back: KvSet = decode_named(&bytes).expect("the request deserializes");
632 assert_eq!(back.key, b"user:42");
633 assert_eq!(back.value, b"online");
634 assert_eq!(back.expires_at_micros, Some(1_700_000_000_000_000));
635 }
636
637 #[test]
638 fn given_a_binary_key_entry_when_round_tripped_then_should_preserve_raw_bytes() {
639 let reply = KvReply::Ok(KvOutcome::Value(Some(KvEntry {
640 key: vec![0xff, 0x00, 0xfe],
641 value: vec![0x00, 0x01, 0x02],
642 expires_at_micros: None,
643 version: 0,
644 scope: None,
645 source: None,
646 })));
647 let bytes = encode_named(&reply).expect("the reply serializes");
648 let back: KvReply = decode_named(&bytes).expect("the reply deserializes");
649 let KvReply::Ok(KvOutcome::Value(Some(entry))) = back else {
650 panic!("expected an Ok(Value(Some)) reply");
651 };
652 assert_eq!(entry.key, vec![0xff, 0x00, 0xfe]);
653 assert_eq!(entry.key_str(), None, "non-UTF-8 key has no string form");
654 assert_eq!(entry.value, vec![0x00, 0x01, 0x02]);
655 }
656
657 #[test]
658 fn given_a_scan_page_when_round_tripped_then_should_preserve_cursor() {
659 let reply = KvReply::Ok(KvOutcome::Page(KvPage {
660 entries: vec![KvEntry {
661 key: b"a".to_vec(),
662 value: b"1".to_vec(),
663 expires_at_micros: None,
664 version: 0,
665 scope: None,
666 source: None,
667 }],
668 cursor: Some(b"a".to_vec()),
669 }));
670 let bytes = encode_named(&reply).expect("serializes");
671 let back: KvReply = decode_named(&bytes).expect("deserializes");
672 let KvReply::Ok(KvOutcome::Page(page)) = back else {
673 panic!("expected an Ok(Page) reply");
674 };
675 assert_eq!(page.entries.len(), 1);
676 assert_eq!(page.entries[0].key_str(), Some("a"));
677 assert_eq!(page.cursor.as_deref(), Some(b"a".as_ref()));
678 }
679
680 #[test]
681 fn given_a_cas_request_when_round_tripped_then_should_preserve_the_precondition() {
682 for expect in [CasExpect::Match(7), CasExpect::Absent] {
683 let request = KvCas {
684 v: KV_OP_VERSION,
685 namespace: "counters".to_owned(),
686 key: b"hits".to_vec(),
687 value: b"42".to_vec(),
688 expires_at_micros: None,
689 expect,
690 };
691 let bytes = encode_named(&request).expect("serializes");
692 let back: KvCas = decode_named(&bytes).expect("deserializes");
693 assert_eq!(back.expect, expect);
694 assert_eq!(back.key, b"hits");
695 }
696 }
697
698 #[test]
699 fn given_a_committed_reply_when_round_tripped_then_should_preserve_the_version() {
700 let reply = KvReply::Ok(KvOutcome::Committed { version: 9 });
701 let bytes = encode_named(&reply).expect("serializes");
702 let back: KvReply = decode_named(&bytes).expect("deserializes");
703 match back {
704 KvReply::Ok(KvOutcome::Committed { version }) => assert_eq!(version, 9),
705 other => panic!("expected Committed, got {other:?}"),
706 }
707 }
708
709 #[test]
710 fn given_an_exists_metadata_reply_when_round_tripped_then_should_preserve_metadata() {
711 let reply = KvReply::Ok(KvOutcome::Metadata(Some(KvMetadata {
712 version: 4,
713 expires_at_micros: Some(1_700_000_000_000_000),
714 size_bytes: 128,
715 })));
716 let bytes = encode_named(&reply).expect("serializes");
717 let back: KvReply = decode_named(&bytes).expect("deserializes");
718 let KvReply::Ok(KvOutcome::Metadata(Some(meta))) = back else {
719 panic!("expected Ok(Metadata(Some))");
720 };
721 assert_eq!(meta.version, 4);
722 assert_eq!(meta.size_bytes, 128);
723 }
724
725 #[test]
726 fn given_a_patch_request_when_round_tripped_then_should_preserve_patch_and_precondition() {
727 let request = KvPatch {
728 v: KV_OP_VERSION,
729 namespace: "docs".to_owned(),
730 key: b"doc:1".to_vec(),
731 patch: br#"{"status":"closed"}"#.to_vec(),
732 if_match: Some(3),
733 };
734 let bytes = encode_named(&request).expect("serializes");
735 let back: KvPatch = decode_named(&bytes).expect("deserializes");
736 assert_eq!(back.patch, br#"{"status":"closed"}"#);
737 assert_eq!(back.if_match, Some(3));
738 }
739
740 #[test]
741 fn given_a_lease_reply_when_round_tripped_then_should_preserve_token_ttl_and_position() {
742 let position = MutationPosition {
743 topic_generation: 3,
744 partition: 1,
745 offset: 4_200,
746 };
747 for reply in [
748 KvReply::Ok(KvOutcome::Leased {
749 lease_token: 77,
750 granted_ttl_micros: 30_000_000,
751 position,
752 }),
753 KvReply::Ok(KvOutcome::Renewed {
754 lease_token: 77,
755 granted_ttl_micros: 30_000_000,
756 position,
757 }),
758 ] {
759 let bytes = encode_named(&reply).expect("serializes");
760 let back: KvReply = decode_named(&bytes).expect("deserializes");
761 match back {
762 KvReply::Ok(
763 KvOutcome::Leased {
764 lease_token,
765 granted_ttl_micros,
766 position: got,
767 }
768 | KvOutcome::Renewed {
769 lease_token,
770 granted_ttl_micros,
771 position: got,
772 },
773 ) => {
774 assert_eq!(lease_token, 77);
775 assert_eq!(granted_ttl_micros, 30_000_000);
776 assert_eq!(got, position);
777 }
778 other => panic!("expected Leased or Renewed, got {other:?}"),
779 }
780 }
781 }
782
783 #[test]
784 fn given_a_reshaped_lease_request_when_round_tripped_then_should_preserve_holder_and_subject() {
785 let request = KvLease {
786 v: crate::codes::KV_LEASE_OP_VERSION,
787 namespace: "connectors.coordination".to_owned(),
788 key: b"source-owner".to_vec(),
789 lease_ttl_micros: 30_000_000,
790 holder_id: "warden-node-1".to_owned(),
791 subject_user_id: Some(42),
792 };
793 let bytes = encode_named(&request).expect("serializes");
794 let back: KvLease = decode_named(&bytes).expect("deserializes");
795 assert_eq!(back.holder_id, "warden-node-1");
796 assert_eq!(back.subject_user_id, Some(42));
797 let own = KvLease {
799 subject_user_id: None,
800 ..request
801 };
802 let json = serde_json::to_string(&own).expect("json");
803 assert!(!json.contains("subject_user_id"), "absent subject omitted");
804 }
805
806 #[test]
807 fn given_a_renew_request_when_round_tripped_then_should_preserve_token_and_holder() {
808 let request = KvLeaseRenew {
809 v: crate::codes::KV_LEASE_OP_VERSION,
810 namespace: "connectors.coordination".to_owned(),
811 key: b"source-owner".to_vec(),
812 holder_id: "warden-node-1".to_owned(),
813 subject_user_id: None,
814 lease_token: 7,
815 lease_ttl_micros: 30_000_000,
816 };
817 let bytes = encode_named(&request).expect("serializes");
818 let back: KvLeaseRenew = decode_named(&bytes).expect("deserializes");
819 assert_eq!(back.lease_token, 7);
820 assert_eq!(back.holder_id, "warden-node-1");
821 }
822
823 #[test]
824 fn given_a_pre_fencing_lease_payload_when_decoded_then_should_fail_closed() {
825 #[derive(Serialize)]
830 struct LegacyLease<'a> {
831 v: u32,
832 namespace: &'a str,
833 #[serde(with = "crate::encoding::bin_bytes")]
834 key: &'a [u8],
835 lease_ttl_micros: u64,
836 }
837 let legacy = encode_named(&LegacyLease {
838 v: KV_OP_VERSION,
839 namespace: "sessions",
840 key: b"job",
841 lease_ttl_micros: 30_000_000,
842 })
843 .expect("legacy shape serializes");
844 assert!(
845 decode_named::<KvLease>(&legacy).is_err(),
846 "a holder-less lease payload must not decode"
847 );
848
849 #[derive(Serialize)]
850 struct LegacyRelease<'a> {
851 v: u32,
852 namespace: &'a str,
853 #[serde(with = "crate::encoding::bin_bytes")]
854 key: &'a [u8],
855 lease_token: u64,
856 }
857 let legacy = encode_named(&LegacyRelease {
858 v: KV_OP_VERSION,
859 namespace: "sessions",
860 key: b"job",
861 lease_token: 7,
862 })
863 .expect("legacy shape serializes");
864 assert!(
865 decode_named::<KvRelease>(&legacy).is_err(),
866 "a holder-less release payload must not decode"
867 );
868
869 #[derive(Serialize)]
870 struct LegacyCasFenced<'a> {
871 v: u32,
872 namespace: &'a str,
873 #[serde(with = "crate::encoding::bin_bytes")]
874 key: &'a [u8],
875 #[serde(with = "crate::encoding::bin_bytes")]
876 value: &'a [u8],
877 expect: CasExpect,
878 #[serde(with = "crate::encoding::bin_bytes")]
879 fence_key: &'a [u8],
880 fence_token: u64,
881 }
882 let legacy = encode_named(&LegacyCasFenced {
883 v: KV_OP_VERSION,
884 namespace: "sessions",
885 key: b"cursor",
886 value: b"42",
887 expect: CasExpect::Absent,
888 fence_key: b"job",
889 fence_token: 7,
890 })
891 .expect("legacy shape serializes");
892 assert!(
893 decode_named::<KvCasFenced>(&legacy).is_err(),
894 "a fence-only CAS payload must not decode"
895 );
896 }
897
898 #[test]
899 fn given_a_stale_reply_when_round_tripped_then_should_preserve_the_required_position() {
900 let required = MutationPosition {
901 topic_generation: 2,
902 partition: 0,
903 offset: 99,
904 };
905 let reply = KvReply::Err(KvError::Stale { required });
906 let bytes = encode_named(&reply).expect("serializes");
907 let back: KvReply = decode_named(&bytes).expect("deserializes");
908 match back {
909 KvReply::Err(KvError::Stale { required: got }) => assert_eq!(got, required),
910 other => panic!("expected Stale, got {other:?}"),
911 }
912 }
913
914 #[test]
915 fn given_a_conditional_get_when_round_tripped_then_should_preserve_if_none_match() {
916 let request = KvGet {
917 v: KV_OP_VERSION,
918 namespace: "sessions".to_owned(),
919 key: b"user:1".to_vec(),
920 if_none_match: Some(5),
921 min_position: None,
922 };
923 let bytes = encode_named(&request).expect("serializes");
924 let back: KvGet = decode_named(&bytes).expect("deserializes");
925 assert_eq!(back.if_none_match, Some(5));
926 let plain = KvGet {
929 if_none_match: None,
930 ..request
931 };
932 let json = serde_json::to_string(&plain).expect("json");
933 assert!(
934 !json.contains("if_none_match"),
935 "absent precondition omitted"
936 );
937 assert!(!json.contains("min_position"), "absent barrier omitted");
938 }
939
940 #[test]
941 fn given_a_barriered_get_when_round_tripped_then_should_preserve_the_minimum_position() {
942 let request = KvGet {
943 v: KV_OP_VERSION,
944 namespace: "connectors.state".to_owned(),
945 key: b"source_state/pg".to_vec(),
946 if_none_match: None,
947 min_position: Some(MutationPosition {
948 topic_generation: 1,
949 partition: 0,
950 offset: 512,
951 }),
952 };
953 let bytes = encode_named(&request).expect("serializes");
954 let back: KvGet = decode_named(&bytes).expect("deserializes");
955 assert_eq!(
956 back.min_position,
957 Some(MutationPosition {
958 topic_generation: 1,
959 partition: 0,
960 offset: 512,
961 })
962 );
963 }
964
965 #[test]
966 fn given_a_version_conflict_when_round_tripped_then_should_preserve_the_current_version() {
967 for current in [Some(3u64), None] {
968 let reply = KvReply::Err(KvError::VersionConflict { current });
969 let bytes = encode_named(&reply).expect("serializes");
970 let back: KvReply = decode_named(&bytes).expect("deserializes");
971 match back {
972 KvReply::Err(KvError::VersionConflict { current: got }) => assert_eq!(got, current),
973 other => panic!("expected VersionConflict, got {other:?}"),
974 }
975 }
976 }
977
978 #[test]
979 fn given_a_versioned_entry_when_round_tripped_then_should_preserve_version_and_skip_zero() {
980 let entry = KvEntry {
981 key: b"k".to_vec(),
982 value: b"v".to_vec(),
983 expires_at_micros: None,
984 version: 5,
985 scope: None,
986 source: None,
987 };
988 let bytes = encode_named(&entry).expect("serializes");
989 let back: KvEntry = decode_named(&bytes).expect("deserializes");
990 assert_eq!(back.version, 5);
991 let unversioned = KvEntry {
994 version: 0,
995 ..entry
996 };
997 let json = serde_json::to_string(&unversioned).expect("json");
998 assert!(
999 !json.contains("version"),
1000 "version 0 must be omitted: {json}"
1001 );
1002 }
1003
1004 #[test]
1005 fn given_a_scan_with_bounds_when_round_tripped_then_should_preserve_filters() {
1006 let scan = KvScan {
1007 v: KV_OP_VERSION,
1008 namespace: "sessions".to_owned(),
1009 prefix: Some(b"user:".to_vec()),
1010 start: None,
1011 end: None,
1012 key_contains: Some("admin".to_owned()),
1013 conversation: None,
1014 limit: 50,
1015 cursor: Some(b"user:9".to_vec()),
1016 };
1017 let bytes = encode_named(&scan).expect("serializes");
1018 let back: KvScan = decode_named(&bytes).expect("deserializes");
1019 assert_eq!(back.prefix.as_deref(), Some(b"user:".as_ref()));
1020 assert_eq!(back.key_contains.as_deref(), Some("admin"));
1021 assert_eq!(back.cursor.as_deref(), Some(b"user:9".as_ref()));
1022 }
1023}