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 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub timestamp_micros: Option<u64>,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
41pub struct KvEntry {
42 #[serde(with = "crate::encoding::bin_bytes")]
43 pub key: Vec<u8>,
44 #[serde(with = "crate::encoding::bin_bytes")]
45 pub value: Vec<u8>,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub expires_at_micros: Option<u64>,
50 #[serde(default, skip_serializing_if = "is_zero_u64")]
58 pub version: u64,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub scope: Option<Box<MemoryRowScope>>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub source: Option<Box<crate::graph::SourceRef>>,
72}
73
74fn is_zero_u64(value: &u64) -> bool {
75 *value == 0
76}
77
78impl KvEntry {
79 pub fn key_str(&self) -> Option<&str> {
82 std::str::from_utf8(&self.key).ok()
83 }
84}
85
86#[derive(Clone, Debug, Default, Serialize, Deserialize)]
89pub struct KvPage {
90 pub entries: Vec<KvEntry>,
91 #[serde(
92 default,
93 skip_serializing_if = "Option::is_none",
94 with = "crate::encoding::opt_bin_bytes"
95 )]
96 pub cursor: Option<Vec<u8>>,
97}
98
99#[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 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub min_position: Option<MutationPosition>,
122}
123
124#[derive(Clone, Debug, Serialize, Deserialize)]
126pub struct KvSet {
127 pub v: u32,
128 pub namespace: String,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub session: Option<crate::agent::SessionRef>,
131 #[serde(with = "crate::encoding::bin_bytes")]
132 pub key: Vec<u8>,
133 #[serde(with = "crate::encoding::bin_bytes")]
134 pub value: Vec<u8>,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub expires_at_micros: Option<u64>,
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
143pub enum CasExpect {
144 Match(u64),
146 Absent,
148}
149
150#[derive(Clone, Debug, Serialize, Deserialize)]
155pub struct KvCas {
156 pub v: u32,
157 pub namespace: String,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub session: Option<crate::agent::SessionRef>,
160 #[serde(with = "crate::encoding::bin_bytes")]
161 pub key: Vec<u8>,
162 #[serde(with = "crate::encoding::bin_bytes")]
163 pub value: Vec<u8>,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub expires_at_micros: Option<u64>,
166 pub expect: CasExpect,
167}
168
169#[derive(Clone, Debug, Serialize, Deserialize)]
178pub struct KvCasFenced {
179 pub v: u32,
180 pub namespace: String,
181 #[serde(with = "crate::encoding::bin_bytes")]
182 pub key: Vec<u8>,
183 #[serde(with = "crate::encoding::bin_bytes")]
184 pub value: Vec<u8>,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub expires_at_micros: Option<u64>,
187 pub expect: CasExpect,
188 pub fence_namespace: String,
192 #[serde(with = "crate::encoding::bin_bytes")]
194 pub fence_key: Vec<u8>,
195 pub fence_token: u64,
198}
199
200#[derive(Clone, Debug, Serialize, Deserialize)]
204pub struct KvDelete {
205 pub v: u32,
206 pub namespace: String,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub session: Option<crate::agent::SessionRef>,
209 #[serde(with = "crate::encoding::bin_bytes")]
210 pub key: Vec<u8>,
211 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub if_match: Option<u64>,
213}
214
215#[derive(Clone, Debug, Serialize, Deserialize)]
219pub struct KvExists {
220 pub v: u32,
221 pub namespace: String,
222 #[serde(with = "crate::encoding::bin_bytes")]
223 pub key: Vec<u8>,
224}
225
226#[derive(Clone, Debug, Serialize, Deserialize)]
230pub struct KvExpire {
231 pub v: u32,
232 pub namespace: String,
233 #[serde(with = "crate::encoding::bin_bytes")]
234 pub key: Vec<u8>,
235 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub expires_at_micros: Option<u64>,
237}
238
239#[derive(Clone, Debug, Serialize, Deserialize)]
244pub struct KvPatch {
245 pub v: u32,
246 pub namespace: String,
247 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub session: Option<crate::agent::SessionRef>,
249 #[serde(with = "crate::encoding::bin_bytes")]
250 pub key: Vec<u8>,
251 #[serde(with = "crate::encoding::bin_bytes")]
252 pub patch: Vec<u8>,
253 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub if_match: Option<u64>,
255}
256
257#[derive(Clone, Debug, Serialize, Deserialize)]
262pub struct KvCopy {
263 pub v: u32,
264 pub namespace: String,
265 #[serde(with = "crate::encoding::bin_bytes")]
266 pub key: Vec<u8>,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub to_namespace: Option<String>,
270 #[serde(with = "crate::encoding::bin_bytes")]
271 pub to_key: Vec<u8>,
272}
273
274#[derive(Clone, Debug, Serialize, Deserialize)]
277pub struct KvMove {
278 pub v: u32,
279 pub namespace: String,
280 #[serde(with = "crate::encoding::bin_bytes")]
281 pub key: Vec<u8>,
282 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub to_namespace: Option<String>,
285 #[serde(with = "crate::encoding::bin_bytes")]
286 pub to_key: Vec<u8>,
287}
288
289#[derive(Clone, Debug, Serialize, Deserialize)]
296pub struct KvLease {
297 pub v: u32,
298 pub namespace: String,
299 #[serde(with = "crate::encoding::bin_bytes")]
300 pub key: Vec<u8>,
301 pub lease_ttl_micros: u64,
308 pub holder_id: String,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub subject_user_id: Option<u32>,
319}
320
321#[derive(Clone, Debug, Serialize, Deserialize)]
328pub struct KvLeaseRenew {
329 pub v: u32,
330 pub namespace: String,
331 #[serde(with = "crate::encoding::bin_bytes")]
332 pub key: Vec<u8>,
333 pub holder_id: String,
334 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub subject_user_id: Option<u32>,
336 pub lease_token: u64,
337 pub lease_ttl_micros: u64,
342}
343
344#[derive(Clone, Debug, Serialize, Deserialize)]
352pub struct KvRelease {
353 pub v: u32,
354 pub namespace: String,
355 #[serde(with = "crate::encoding::bin_bytes")]
356 pub key: Vec<u8>,
357 pub lease_token: u64,
358 pub holder_id: String,
359}
360
361#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
364pub struct KvMetadata {
365 pub version: u64,
366 #[serde(default, skip_serializing_if = "Option::is_none")]
367 pub expires_at_micros: Option<u64>,
368 pub size_bytes: usize,
369}
370
371#[derive(Clone, Debug, Serialize, Deserialize)]
373pub struct KvNamespaces {
374 pub v: u32,
375}
376
377#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
379pub struct KvNamespaceInfo {
380 pub namespace: String,
381 pub entries: usize,
382}
383
384#[derive(Clone, Debug, Serialize, Deserialize)]
390pub struct KvScan {
391 pub v: u32,
392 pub namespace: String,
393 #[serde(
394 default,
395 skip_serializing_if = "Option::is_none",
396 with = "crate::encoding::opt_bin_bytes"
397 )]
398 pub prefix: Option<Vec<u8>>,
399 #[serde(
400 default,
401 skip_serializing_if = "Option::is_none",
402 with = "crate::encoding::opt_bin_bytes"
403 )]
404 pub start: Option<Vec<u8>>,
405 #[serde(
406 default,
407 skip_serializing_if = "Option::is_none",
408 with = "crate::encoding::opt_bin_bytes"
409 )]
410 pub end: Option<Vec<u8>>,
411 #[serde(default, skip_serializing_if = "Option::is_none")]
412 pub key_contains: Option<String>,
413 #[serde(default, skip_serializing_if = "Option::is_none")]
419 pub conversation: Option<String>,
420 #[serde(default, skip_serializing_if = "Option::is_none")]
424 pub stream: Option<String>,
425 pub limit: usize,
426 #[serde(
427 default,
428 skip_serializing_if = "Option::is_none",
429 with = "crate::encoding::opt_bin_bytes"
430 )]
431 pub cursor: Option<Vec<u8>>,
432}
433
434#[derive(Clone, Debug, Serialize, Deserialize)]
439pub struct KvDeleteMany {
440 pub v: u32,
441 pub namespace: String,
442 #[serde(
443 default,
444 skip_serializing_if = "Option::is_none",
445 with = "crate::encoding::opt_bin_bytes"
446 )]
447 pub prefix: Option<Vec<u8>>,
448 #[serde(
449 default,
450 skip_serializing_if = "Option::is_none",
451 with = "crate::encoding::opt_bin_bytes"
452 )]
453 pub start: Option<Vec<u8>>,
454 #[serde(
455 default,
456 skip_serializing_if = "Option::is_none",
457 with = "crate::encoding::opt_bin_bytes"
458 )]
459 pub end: Option<Vec<u8>>,
460 #[serde(default, skip_serializing_if = "Option::is_none")]
461 pub key_contains: Option<String>,
462 #[serde(default, skip_serializing_if = "Option::is_none")]
465 pub conversation: Option<String>,
466 #[serde(default, skip_serializing_if = "Option::is_none")]
468 pub stream: Option<String>,
469}
470
471#[derive(Clone, Debug, Serialize, Deserialize)]
474#[non_exhaustive]
475pub enum KvReply {
476 Ok(KvOutcome),
477 Err(KvError),
478}
479
480#[derive(Clone, Debug, Serialize, Deserialize)]
482#[non_exhaustive]
483pub enum KvOutcome {
484 Value(Option<KvEntry>),
486 Written,
488 Committed { version: u64 },
491 Deleted(bool),
493 DeletedMany(usize),
495 Page(KvPage),
497 Namespaces(Vec<KvNamespaceInfo>),
500 NotModified,
503 Metadata(Option<KvMetadata>),
505 Versioned { version: u64 },
508 Leased {
514 lease_token: u64,
515 granted_ttl_micros: u64,
516 position: MutationPosition,
517 },
518 Renewed {
523 lease_token: u64,
524 granted_ttl_micros: u64,
525 position: MutationPosition,
526 },
527 Released(bool),
530}
531
532#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
534#[non_exhaustive]
535pub enum KvError {
536 #[error("kv not supported: {0}")]
537 Unsupported(String),
538 #[error("invalid key: {0}")]
539 InvalidKey(String),
540 #[error("invalid namespace: {0}")]
542 InvalidNamespace(String),
543 #[error("{what} is {size}B, exceeds cap {cap}B")]
544 TooLarge {
545 what: String,
546 size: usize,
547 cap: usize,
548 },
549 #[error("kv backend error: {0}")]
550 Backend(String),
551 #[error("temporarily unavailable: {0}")]
556 Unavailable(String),
557 #[error("unsupported kv op version (expected {expected}, got {got})")]
558 Version { expected: u32, got: u32 },
559 #[error("kv version conflict (current: {current:?})")]
563 VersionConflict { current: Option<u64> },
564 #[error("kv lease lost")]
568 LeaseLost,
569 #[error("kv read barrier not reached (required: {required:?})")]
574 Stale { required: MutationPosition },
575 #[error("kv key not found")]
578 NotFound,
579 #[error("not the partition leader for this key")]
582 NotLeader,
583}
584
585pub fn validate_namespace(namespace: &str) -> Result<(), InvalidError> {
590 if namespace.is_empty() {
591 return Err(InvalidError::new("namespace must not be empty"));
592 }
593 if namespace.len() > MAX_NAMESPACE_BYTES {
594 return Err(InvalidError::new(format!(
595 "namespace is {}B, exceeds cap {MAX_NAMESPACE_BYTES}B",
596 namespace.len()
597 )));
598 }
599 if namespace.bytes().any(|byte| byte.is_ascii_control()) {
600 return Err(InvalidError::new(
601 "namespace must not contain ASCII control characters",
602 ));
603 }
604 Ok(())
605}
606
607#[cfg(all(test, feature = "cbor"))]
608mod tests {
609 use super::*;
610 use crate::codes::KV_OP_VERSION;
611 use crate::framing::{decode_named, encode_named};
612
613 #[test]
614 fn given_kv_delete_many_when_round_tripped_then_should_preserve_bounds() {
615 let request = KvDeleteMany {
616 v: KV_OP_VERSION,
617 namespace: "sessions".to_owned(),
618 prefix: Some(b"user:".to_vec()),
619 start: None,
620 end: None,
621 key_contains: Some("stale".to_owned()),
622 conversation: None,
623 stream: None,
624 };
625 let bytes = encode_named(&request).expect("serializes");
626 let back: KvDeleteMany = decode_named(&bytes).expect("deserializes");
627 assert_eq!(back.prefix, Some(b"user:".to_vec()));
628 assert_eq!(back.key_contains.as_deref(), Some("stale"));
629 }
630
631 #[test]
632 fn given_kv_deleted_many_reply_when_round_tripped_then_should_preserve_count() {
633 let reply = KvReply::Ok(KvOutcome::DeletedMany(7));
634 let bytes = encode_named(&reply).expect("serializes");
635 let back: KvReply = decode_named(&bytes).expect("deserializes");
636 match back {
637 KvReply::Ok(KvOutcome::DeletedMany(n)) => assert_eq!(n, 7),
638 other => panic!("expected DeletedMany, got {other:?}"),
639 }
640 }
641
642 #[test]
643 fn given_a_set_request_when_round_tripped_then_should_preserve_value_and_expiry() {
644 let request = KvSet {
645 v: KV_OP_VERSION,
646 namespace: "sessions".to_owned(),
647 session: None,
648 key: b"user:42".to_vec(),
649 value: b"online".to_vec(),
650 expires_at_micros: Some(1_700_000_000_000_000),
651 };
652 let bytes = encode_named(&request).expect("the request serializes");
653 let back: KvSet = decode_named(&bytes).expect("the request deserializes");
654 assert_eq!(back.key, b"user:42");
655 assert_eq!(back.value, b"online");
656 assert_eq!(back.expires_at_micros, Some(1_700_000_000_000_000));
657 }
658
659 #[test]
660 fn given_a_binary_key_entry_when_round_tripped_then_should_preserve_raw_bytes() {
661 let reply = KvReply::Ok(KvOutcome::Value(Some(KvEntry {
662 key: vec![0xff, 0x00, 0xfe],
663 value: vec![0x00, 0x01, 0x02],
664 expires_at_micros: None,
665 version: 0,
666 scope: None,
667 source: None,
668 })));
669 let bytes = encode_named(&reply).expect("the reply serializes");
670 let back: KvReply = decode_named(&bytes).expect("the reply deserializes");
671 let KvReply::Ok(KvOutcome::Value(Some(entry))) = back else {
672 panic!("expected an Ok(Value(Some)) reply");
673 };
674 assert_eq!(entry.key, vec![0xff, 0x00, 0xfe]);
675 assert_eq!(entry.key_str(), None, "non-UTF-8 key has no string form");
676 assert_eq!(entry.value, vec![0x00, 0x01, 0x02]);
677 }
678
679 #[test]
680 fn given_a_scan_page_when_round_tripped_then_should_preserve_cursor() {
681 let reply = KvReply::Ok(KvOutcome::Page(KvPage {
682 entries: vec![KvEntry {
683 key: b"a".to_vec(),
684 value: b"1".to_vec(),
685 expires_at_micros: None,
686 version: 0,
687 scope: None,
688 source: None,
689 }],
690 cursor: Some(b"a".to_vec()),
691 }));
692 let bytes = encode_named(&reply).expect("serializes");
693 let back: KvReply = decode_named(&bytes).expect("deserializes");
694 let KvReply::Ok(KvOutcome::Page(page)) = back else {
695 panic!("expected an Ok(Page) reply");
696 };
697 assert_eq!(page.entries.len(), 1);
698 assert_eq!(page.entries[0].key_str(), Some("a"));
699 assert_eq!(page.cursor.as_deref(), Some(b"a".as_ref()));
700 }
701
702 #[test]
703 fn given_a_cas_request_when_round_tripped_then_should_preserve_the_precondition() {
704 for expect in [CasExpect::Match(7), CasExpect::Absent] {
705 let request = KvCas {
706 v: KV_OP_VERSION,
707 namespace: "counters".to_owned(),
708 session: None,
709 key: b"hits".to_vec(),
710 value: b"42".to_vec(),
711 expires_at_micros: None,
712 expect,
713 };
714 let bytes = encode_named(&request).expect("serializes");
715 let back: KvCas = decode_named(&bytes).expect("deserializes");
716 assert_eq!(back.expect, expect);
717 assert_eq!(back.key, b"hits");
718 }
719 }
720
721 #[test]
722 fn given_a_committed_reply_when_round_tripped_then_should_preserve_the_version() {
723 let reply = KvReply::Ok(KvOutcome::Committed { version: 9 });
724 let bytes = encode_named(&reply).expect("serializes");
725 let back: KvReply = decode_named(&bytes).expect("deserializes");
726 match back {
727 KvReply::Ok(KvOutcome::Committed { version }) => assert_eq!(version, 9),
728 other => panic!("expected Committed, got {other:?}"),
729 }
730 }
731
732 #[test]
733 fn given_an_exists_metadata_reply_when_round_tripped_then_should_preserve_metadata() {
734 let reply = KvReply::Ok(KvOutcome::Metadata(Some(KvMetadata {
735 version: 4,
736 expires_at_micros: Some(1_700_000_000_000_000),
737 size_bytes: 128,
738 })));
739 let bytes = encode_named(&reply).expect("serializes");
740 let back: KvReply = decode_named(&bytes).expect("deserializes");
741 let KvReply::Ok(KvOutcome::Metadata(Some(meta))) = back else {
742 panic!("expected Ok(Metadata(Some))");
743 };
744 assert_eq!(meta.version, 4);
745 assert_eq!(meta.size_bytes, 128);
746 }
747
748 #[test]
749 fn given_a_patch_request_when_round_tripped_then_should_preserve_patch_and_precondition() {
750 let request = KvPatch {
751 v: KV_OP_VERSION,
752 namespace: "docs".to_owned(),
753 session: None,
754 key: b"doc:1".to_vec(),
755 patch: br#"{"status":"closed"}"#.to_vec(),
756 if_match: Some(3),
757 };
758 let bytes = encode_named(&request).expect("serializes");
759 let back: KvPatch = decode_named(&bytes).expect("deserializes");
760 assert_eq!(back.patch, br#"{"status":"closed"}"#);
761 assert_eq!(back.if_match, Some(3));
762 }
763
764 #[test]
765 fn given_a_lease_reply_when_round_tripped_then_should_preserve_token_ttl_and_position() {
766 let position = MutationPosition {
767 topic_generation: 3,
768 partition: 1,
769 offset: 4_200,
770 };
771 for reply in [
772 KvReply::Ok(KvOutcome::Leased {
773 lease_token: 77,
774 granted_ttl_micros: 30_000_000,
775 position,
776 }),
777 KvReply::Ok(KvOutcome::Renewed {
778 lease_token: 77,
779 granted_ttl_micros: 30_000_000,
780 position,
781 }),
782 ] {
783 let bytes = encode_named(&reply).expect("serializes");
784 let back: KvReply = decode_named(&bytes).expect("deserializes");
785 match back {
786 KvReply::Ok(
787 KvOutcome::Leased {
788 lease_token,
789 granted_ttl_micros,
790 position: got,
791 }
792 | KvOutcome::Renewed {
793 lease_token,
794 granted_ttl_micros,
795 position: got,
796 },
797 ) => {
798 assert_eq!(lease_token, 77);
799 assert_eq!(granted_ttl_micros, 30_000_000);
800 assert_eq!(got, position);
801 }
802 other => panic!("expected Leased or Renewed, got {other:?}"),
803 }
804 }
805 }
806
807 #[test]
808 fn given_a_reshaped_lease_request_when_round_tripped_then_should_preserve_holder_and_subject() {
809 let request = KvLease {
810 v: crate::codes::KV_LEASE_OP_VERSION,
811 namespace: "connectors.coordination".to_owned(),
812 key: b"source-owner".to_vec(),
813 lease_ttl_micros: 30_000_000,
814 holder_id: "node-1".to_owned(),
815 subject_user_id: Some(42),
816 };
817 let bytes = encode_named(&request).expect("serializes");
818 let back: KvLease = decode_named(&bytes).expect("deserializes");
819 assert_eq!(back.holder_id, "node-1");
820 assert_eq!(back.subject_user_id, Some(42));
821 let own = KvLease {
823 subject_user_id: None,
824 ..request
825 };
826 let json = serde_json::to_string(&own).expect("json");
827 assert!(!json.contains("subject_user_id"), "absent subject omitted");
828 }
829
830 #[test]
831 fn given_a_renew_request_when_round_tripped_then_should_preserve_token_and_holder() {
832 let request = KvLeaseRenew {
833 v: crate::codes::KV_LEASE_OP_VERSION,
834 namespace: "connectors.coordination".to_owned(),
835 key: b"source-owner".to_vec(),
836 holder_id: "node-1".to_owned(),
837 subject_user_id: None,
838 lease_token: 7,
839 lease_ttl_micros: 30_000_000,
840 };
841 let bytes = encode_named(&request).expect("serializes");
842 let back: KvLeaseRenew = decode_named(&bytes).expect("deserializes");
843 assert_eq!(back.lease_token, 7);
844 assert_eq!(back.holder_id, "node-1");
845 }
846
847 #[test]
848 fn given_a_pre_fencing_lease_payload_when_decoded_then_should_fail_closed() {
849 #[derive(Serialize)]
854 struct LegacyLease<'a> {
855 v: u32,
856 namespace: &'a str,
857 #[serde(with = "crate::encoding::bin_bytes")]
858 key: &'a [u8],
859 lease_ttl_micros: u64,
860 }
861 let legacy = encode_named(&LegacyLease {
862 v: KV_OP_VERSION,
863 namespace: "sessions",
864 key: b"job",
865 lease_ttl_micros: 30_000_000,
866 })
867 .expect("legacy shape serializes");
868 assert!(
869 decode_named::<KvLease>(&legacy).is_err(),
870 "a holder-less lease payload must not decode"
871 );
872
873 #[derive(Serialize)]
874 struct LegacyRelease<'a> {
875 v: u32,
876 namespace: &'a str,
877 #[serde(with = "crate::encoding::bin_bytes")]
878 key: &'a [u8],
879 lease_token: u64,
880 }
881 let legacy = encode_named(&LegacyRelease {
882 v: KV_OP_VERSION,
883 namespace: "sessions",
884 key: b"job",
885 lease_token: 7,
886 })
887 .expect("legacy shape serializes");
888 assert!(
889 decode_named::<KvRelease>(&legacy).is_err(),
890 "a holder-less release payload must not decode"
891 );
892
893 #[derive(Serialize)]
894 struct LegacyCasFenced<'a> {
895 v: u32,
896 namespace: &'a str,
897 #[serde(with = "crate::encoding::bin_bytes")]
898 key: &'a [u8],
899 #[serde(with = "crate::encoding::bin_bytes")]
900 value: &'a [u8],
901 expect: CasExpect,
902 #[serde(with = "crate::encoding::bin_bytes")]
903 fence_key: &'a [u8],
904 fence_token: u64,
905 }
906 let legacy = encode_named(&LegacyCasFenced {
907 v: KV_OP_VERSION,
908 namespace: "sessions",
909 key: b"cursor",
910 value: b"42",
911 expect: CasExpect::Absent,
912 fence_key: b"job",
913 fence_token: 7,
914 })
915 .expect("legacy shape serializes");
916 assert!(
917 decode_named::<KvCasFenced>(&legacy).is_err(),
918 "a fence-only CAS payload must not decode"
919 );
920 }
921
922 #[test]
923 fn given_a_stale_reply_when_round_tripped_then_should_preserve_the_required_position() {
924 let required = MutationPosition {
925 topic_generation: 2,
926 partition: 0,
927 offset: 99,
928 };
929 let reply = KvReply::Err(KvError::Stale { required });
930 let bytes = encode_named(&reply).expect("serializes");
931 let back: KvReply = decode_named(&bytes).expect("deserializes");
932 match back {
933 KvReply::Err(KvError::Stale { required: got }) => assert_eq!(got, required),
934 other => panic!("expected Stale, got {other:?}"),
935 }
936 }
937
938 #[test]
939 fn given_a_conditional_get_when_round_tripped_then_should_preserve_if_none_match() {
940 let request = KvGet {
941 v: KV_OP_VERSION,
942 namespace: "sessions".to_owned(),
943 key: b"user:1".to_vec(),
944 if_none_match: Some(5),
945 min_position: None,
946 };
947 let bytes = encode_named(&request).expect("serializes");
948 let back: KvGet = decode_named(&bytes).expect("deserializes");
949 assert_eq!(back.if_none_match, Some(5));
950 let plain = KvGet {
953 if_none_match: None,
954 ..request
955 };
956 let json = serde_json::to_string(&plain).expect("json");
957 assert!(
958 !json.contains("if_none_match"),
959 "absent precondition omitted"
960 );
961 assert!(!json.contains("min_position"), "absent barrier omitted");
962 }
963
964 #[test]
965 fn given_a_barriered_get_when_round_tripped_then_should_preserve_the_minimum_position() {
966 let request = KvGet {
967 v: KV_OP_VERSION,
968 namespace: "connectors.state".to_owned(),
969 key: b"source_state/pg".to_vec(),
970 if_none_match: None,
971 min_position: Some(MutationPosition {
972 topic_generation: 1,
973 partition: 0,
974 offset: 512,
975 }),
976 };
977 let bytes = encode_named(&request).expect("serializes");
978 let back: KvGet = decode_named(&bytes).expect("deserializes");
979 assert_eq!(
980 back.min_position,
981 Some(MutationPosition {
982 topic_generation: 1,
983 partition: 0,
984 offset: 512,
985 })
986 );
987 }
988
989 #[test]
990 fn given_a_version_conflict_when_round_tripped_then_should_preserve_the_current_version() {
991 for current in [Some(3u64), None] {
992 let reply = KvReply::Err(KvError::VersionConflict { current });
993 let bytes = encode_named(&reply).expect("serializes");
994 let back: KvReply = decode_named(&bytes).expect("deserializes");
995 match back {
996 KvReply::Err(KvError::VersionConflict { current: got }) => assert_eq!(got, current),
997 other => panic!("expected VersionConflict, got {other:?}"),
998 }
999 }
1000 }
1001
1002 #[test]
1003 fn given_a_versioned_entry_when_round_tripped_then_should_preserve_version_and_skip_zero() {
1004 let entry = KvEntry {
1005 key: b"k".to_vec(),
1006 value: b"v".to_vec(),
1007 expires_at_micros: None,
1008 version: 5,
1009 scope: None,
1010 source: None,
1011 };
1012 let bytes = encode_named(&entry).expect("serializes");
1013 let back: KvEntry = decode_named(&bytes).expect("deserializes");
1014 assert_eq!(back.version, 5);
1015 let unversioned = KvEntry {
1018 version: 0,
1019 ..entry
1020 };
1021 let json = serde_json::to_string(&unversioned).expect("json");
1022 assert!(
1023 !json.contains("version"),
1024 "version 0 must be omitted: {json}"
1025 );
1026 }
1027
1028 #[test]
1029 fn given_a_scan_with_bounds_when_round_tripped_then_should_preserve_filters() {
1030 let scan = KvScan {
1031 v: KV_OP_VERSION,
1032 namespace: "sessions".to_owned(),
1033 prefix: Some(b"user:".to_vec()),
1034 start: None,
1035 end: None,
1036 key_contains: Some("admin".to_owned()),
1037 conversation: None,
1038 stream: None,
1039 limit: 50,
1040 cursor: Some(b"user:9".to_vec()),
1041 };
1042 let bytes = encode_named(&scan).expect("serializes");
1043 let back: KvScan = decode_named(&bytes).expect("deserializes");
1044 assert_eq!(back.prefix.as_deref(), Some(b"user:".as_ref()));
1045 assert_eq!(back.key_contains.as_deref(), Some("admin"));
1046 assert_eq!(back.cursor.as_deref(), Some(b"user:9".as_ref()));
1047 assert_eq!(back.stream, None);
1048 }
1049
1050 #[test]
1051 fn given_a_lens_with_a_stream_when_round_tripped_then_should_preserve_it_and_omit_it_when_unset()
1052 {
1053 let scan = KvScan {
1054 v: KV_OP_VERSION,
1055 namespace: "stream:acme/memory".to_owned(),
1056 prefix: None,
1057 start: None,
1058 end: None,
1059 key_contains: None,
1060 conversation: Some("01KWM3K3XEP3NP5TN850J17YBP".to_owned()),
1061 stream: Some("acme".to_owned()),
1062 limit: 10,
1063 cursor: None,
1064 };
1065 let back: KvScan =
1066 decode_named(&encode_named(&scan).expect("serializes")).expect("deserializes");
1067 assert_eq!(back.stream.as_deref(), Some("acme"));
1068 let delete = KvDeleteMany {
1069 v: KV_OP_VERSION,
1070 namespace: "stream:acme/memory".to_owned(),
1071 prefix: None,
1072 start: None,
1073 end: None,
1074 key_contains: None,
1075 conversation: Some("01KWM3K3XEP3NP5TN850J17YBP".to_owned()),
1076 stream: Some("acme".to_owned()),
1077 };
1078 let back: KvDeleteMany =
1079 decode_named(&encode_named(&delete).expect("serializes")).expect("deserializes");
1080 assert_eq!(back.stream.as_deref(), Some("acme"));
1081 let unset = KvDeleteMany {
1082 stream: None,
1083 ..delete
1084 };
1085 assert!(
1086 !serde_json::to_string(&unset)
1087 .expect("serializes")
1088 .contains("\"stream\":"),
1089 "an unset stream is omitted"
1090 );
1091 }
1092}