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, 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 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub weight: Option<f32>,
40}
41
42#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
46pub struct KvEntry {
47 #[serde(with = "crate::encoding::bin_bytes")]
48 pub key: Vec<u8>,
49 #[serde(with = "crate::encoding::bin_bytes")]
50 pub value: Vec<u8>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub expires_at_micros: Option<u64>,
55 #[serde(default, skip_serializing_if = "is_zero_u64")]
63 pub version: u64,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub scope: Option<Box<MemoryRowScope>>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub source: Option<Box<crate::graph::SourceRef>>,
77}
78
79fn is_zero_u64(value: &u64) -> bool {
80 *value == 0
81}
82
83impl KvEntry {
84 pub fn key_str(&self) -> Option<&str> {
87 std::str::from_utf8(&self.key).ok()
88 }
89}
90
91#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
95pub struct KvPage {
96 pub items: Vec<KvEntry>,
97 pub page: u32,
98 pub total_pages: u32,
99 pub total_results: u32,
100}
101
102#[derive(Clone, Debug, Serialize, Deserialize)]
116pub struct KvGet {
117 pub v: u32,
118 pub namespace: String,
119 #[serde(with = "crate::encoding::bin_bytes")]
120 pub key: Vec<u8>,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub if_none_match: Option<u64>,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub min_position: Option<MutationPosition>,
125}
126
127#[derive(Clone, Debug, Serialize, Deserialize)]
129pub struct KvSet {
130 pub v: u32,
131 pub namespace: String,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub session: Option<crate::agent::SessionRef>,
134 #[serde(with = "crate::encoding::bin_bytes")]
135 pub key: Vec<u8>,
136 #[serde(with = "crate::encoding::bin_bytes")]
137 pub value: Vec<u8>,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub expires_at_micros: Option<u64>,
140 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub ttl_micros: Option<u64>,
146}
147
148#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
152pub enum CasExpect {
153 Match(u64),
155 Absent,
157}
158
159#[derive(Clone, Debug, Serialize, Deserialize)]
164pub struct KvCas {
165 pub v: u32,
166 pub namespace: String,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub session: Option<crate::agent::SessionRef>,
169 #[serde(with = "crate::encoding::bin_bytes")]
170 pub key: Vec<u8>,
171 #[serde(with = "crate::encoding::bin_bytes")]
172 pub value: Vec<u8>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub expires_at_micros: Option<u64>,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub ttl_micros: Option<u64>,
181 pub expect: CasExpect,
182}
183
184#[derive(Clone, Debug, Serialize, Deserialize)]
193pub struct KvCasFenced {
194 pub v: u32,
195 pub namespace: String,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub session: Option<crate::agent::SessionRef>,
199 #[serde(with = "crate::encoding::bin_bytes")]
200 pub key: Vec<u8>,
201 #[serde(with = "crate::encoding::bin_bytes")]
202 pub value: Vec<u8>,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub expires_at_micros: Option<u64>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub ttl_micros: Option<u64>,
211 pub expect: CasExpect,
212 pub fence_namespace: String,
216 #[serde(with = "crate::encoding::bin_bytes")]
218 pub fence_key: Vec<u8>,
219 pub fence_token: u64,
222}
223
224#[derive(Clone, Debug, Serialize, Deserialize)]
228pub struct KvDelete {
229 pub v: u32,
230 pub namespace: String,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub session: Option<crate::agent::SessionRef>,
233 #[serde(with = "crate::encoding::bin_bytes")]
234 pub key: Vec<u8>,
235 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub if_match: Option<u64>,
237}
238
239#[derive(Clone, Debug, Serialize, Deserialize)]
243pub struct KvExists {
244 pub v: u32,
245 pub namespace: String,
246 #[serde(with = "crate::encoding::bin_bytes")]
247 pub key: Vec<u8>,
248}
249
250#[derive(Clone, Debug, Serialize, Deserialize)]
254pub struct KvExpire {
255 pub v: u32,
256 pub namespace: String,
257 #[serde(with = "crate::encoding::bin_bytes")]
258 pub key: Vec<u8>,
259 #[serde(default, skip_serializing_if = "Option::is_none")]
260 pub expires_at_micros: Option<u64>,
261 #[serde(default, skip_serializing_if = "Option::is_none")]
266 pub ttl_micros: Option<u64>,
267}
268
269#[derive(Clone, Debug, Serialize, Deserialize)]
274pub struct KvPatch {
275 pub v: u32,
276 pub namespace: String,
277 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub session: Option<crate::agent::SessionRef>,
279 #[serde(with = "crate::encoding::bin_bytes")]
280 pub key: Vec<u8>,
281 #[serde(with = "crate::encoding::bin_bytes")]
282 pub patch: Vec<u8>,
283 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub if_match: Option<u64>,
285}
286
287#[derive(Clone, Debug, Serialize, Deserialize)]
292pub struct KvCopy {
293 pub v: u32,
294 pub namespace: String,
295 #[serde(with = "crate::encoding::bin_bytes")]
296 pub key: Vec<u8>,
297 #[serde(default, skip_serializing_if = "Option::is_none")]
299 pub to_namespace: Option<String>,
300 #[serde(with = "crate::encoding::bin_bytes")]
301 pub to_key: Vec<u8>,
302}
303
304#[derive(Clone, Debug, Serialize, Deserialize)]
307pub struct KvMove {
308 pub v: u32,
309 pub namespace: String,
310 #[serde(with = "crate::encoding::bin_bytes")]
311 pub key: Vec<u8>,
312 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub to_namespace: Option<String>,
315 #[serde(with = "crate::encoding::bin_bytes")]
316 pub to_key: Vec<u8>,
317}
318
319#[derive(Clone, Debug, Serialize, Deserialize)]
326pub struct KvLease {
327 pub v: u32,
328 pub namespace: String,
329 #[serde(with = "crate::encoding::bin_bytes")]
330 pub key: Vec<u8>,
331 pub lease_ttl_micros: u64,
338 pub holder_id: String,
343 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub subject_user_id: Option<u32>,
349}
350
351#[derive(Clone, Debug, Serialize, Deserialize)]
358pub struct KvLeaseRenew {
359 pub v: u32,
360 pub namespace: String,
361 #[serde(with = "crate::encoding::bin_bytes")]
362 pub key: Vec<u8>,
363 pub holder_id: String,
364 #[serde(default, skip_serializing_if = "Option::is_none")]
365 pub subject_user_id: Option<u32>,
366 pub lease_token: u64,
367 pub lease_ttl_micros: u64,
372}
373
374#[derive(Clone, Debug, Serialize, Deserialize)]
382pub struct KvRelease {
383 pub v: u32,
384 pub namespace: String,
385 #[serde(with = "crate::encoding::bin_bytes")]
386 pub key: Vec<u8>,
387 pub lease_token: u64,
388 pub holder_id: String,
389}
390
391#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
394pub struct KvMetadata {
395 pub version: u64,
396 #[serde(default, skip_serializing_if = "Option::is_none")]
397 pub expires_at_micros: Option<u64>,
398 pub size_bytes: usize,
399}
400
401#[derive(Clone, Debug, Serialize, Deserialize)]
403pub struct KvNamespaces {
404 pub v: u32,
405 #[serde(default, skip_serializing_if = "Option::is_none")]
409 pub stream: Option<String>,
410}
411
412#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
414pub struct KvNamespaceInfo {
415 pub namespace: String,
416 pub entries: usize,
417}
418
419#[derive(Clone, Debug, Serialize, Deserialize)]
425pub struct KvScan {
426 pub v: u32,
427 pub namespace: String,
428 #[serde(
429 default,
430 skip_serializing_if = "Option::is_none",
431 with = "crate::encoding::opt_bin_bytes"
432 )]
433 pub prefix: Option<Vec<u8>>,
434 #[serde(
435 default,
436 skip_serializing_if = "Option::is_none",
437 with = "crate::encoding::opt_bin_bytes"
438 )]
439 pub start: Option<Vec<u8>>,
440 #[serde(
441 default,
442 skip_serializing_if = "Option::is_none",
443 with = "crate::encoding::opt_bin_bytes"
444 )]
445 pub end: Option<Vec<u8>>,
446 #[serde(default, skip_serializing_if = "Option::is_none")]
447 pub key_contains: Option<String>,
448 #[serde(default, skip_serializing_if = "Option::is_none")]
454 pub conversation: Option<String>,
455 #[serde(default, skip_serializing_if = "Option::is_none")]
459 pub stream: Option<String>,
460 #[serde(default, skip_serializing_if = "Option::is_none")]
465 pub user: Option<String>,
466 #[serde(default, skip_serializing_if = "Option::is_none")]
467 pub agent: Option<String>,
468 #[serde(default, skip_serializing_if = "Option::is_none")]
469 pub app: Option<String>,
470 #[serde(default)]
473 pub page: u32,
474 pub results: u32,
477}
478
479#[derive(Clone, Debug, Serialize, Deserialize)]
484pub struct KvDeleteMany {
485 pub v: u32,
486 pub namespace: String,
487 #[serde(
488 default,
489 skip_serializing_if = "Option::is_none",
490 with = "crate::encoding::opt_bin_bytes"
491 )]
492 pub prefix: Option<Vec<u8>>,
493 #[serde(
494 default,
495 skip_serializing_if = "Option::is_none",
496 with = "crate::encoding::opt_bin_bytes"
497 )]
498 pub start: Option<Vec<u8>>,
499 #[serde(
500 default,
501 skip_serializing_if = "Option::is_none",
502 with = "crate::encoding::opt_bin_bytes"
503 )]
504 pub end: Option<Vec<u8>>,
505 #[serde(default, skip_serializing_if = "Option::is_none")]
506 pub key_contains: Option<String>,
507 #[serde(default, skip_serializing_if = "Option::is_none")]
510 pub conversation: Option<String>,
511 #[serde(default, skip_serializing_if = "Option::is_none")]
513 pub stream: Option<String>,
514}
515
516#[derive(Clone, Debug, Serialize, Deserialize)]
519#[non_exhaustive]
520pub enum KvReply {
521 Ok(KvOutcome),
522 Err(KvError),
523}
524
525#[derive(Clone, Debug, Serialize, Deserialize)]
527#[non_exhaustive]
528pub enum KvOutcome {
529 Value(Option<KvEntry>),
531 Written,
533 Committed { version: u64 },
536 Deleted(bool),
538 DeletedMany(usize),
540 Page(KvPage),
542 Namespaces(Vec<KvNamespaceInfo>),
545 NotModified,
548 Metadata(Option<KvMetadata>),
550 Versioned { version: u64 },
553 Leased {
559 lease_token: u64,
560 granted_ttl_micros: u64,
561 position: MutationPosition,
562 },
563 Renewed {
568 lease_token: u64,
569 granted_ttl_micros: u64,
570 position: MutationPosition,
571 },
572 Released(bool),
575}
576
577#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
579#[non_exhaustive]
580pub enum KvError {
581 #[error("kv not supported: {0}")]
582 Unsupported(String),
583 #[error("invalid key: {0}")]
584 InvalidKey(String),
585 #[error("invalid namespace: {0}")]
587 InvalidNamespace(String),
588 #[error("{what} is {size}B, exceeds cap {cap}B")]
589 TooLarge {
590 what: String,
591 size: usize,
592 cap: usize,
593 },
594 #[error("kv backend error: {0}")]
595 Backend(String),
596 #[error("temporarily unavailable: {0}")]
601 Unavailable(String),
602 #[error("unsupported kv op version (expected {expected}, got {got})")]
603 Version { expected: u32, got: u32 },
604 #[error("kv version conflict (current: {current:?})")]
608 VersionConflict { current: Option<u64> },
609 #[error("kv lease lost")]
613 LeaseLost,
614 #[error("kv lease held (expires at: {expires_at_micros:?})")]
618 LeaseHeld { expires_at_micros: Option<u64> },
619 #[error("kv read barrier not reached (required: {required:?})")]
623 Stale { required: MutationPosition },
624 #[error("kv read barrier unreachable (required: {required:?})")]
629 BarrierUnreachable { required: MutationPosition },
630 #[error("kv key not found")]
633 NotFound,
634 #[error("not the partition leader for this key")]
637 NotLeader,
638 #[error("kv operation not permitted: {0}")]
641 Forbidden(String),
642 #[error("invalid kv expiry: {0}")]
644 InvalidExpiry(String),
645}
646
647pub fn resolve_expiry(
652 expires_at_micros: Option<u64>,
653 ttl_micros: Option<u64>,
654 now_micros: u64,
655) -> Result<Option<u64>, InvalidError> {
656 match (expires_at_micros, ttl_micros) {
657 (Some(_), Some(_)) => Err(InvalidError::new(
658 "a write names either expires_at_micros or ttl_micros, not both",
659 )),
660 (Some(expires_at_micros), None) => Ok(Some(expires_at_micros)),
661 (None, ttl_micros) => Ok(ttl_micros.map(|ttl| now_micros.saturating_add(ttl))),
662 }
663}
664
665pub fn validate_namespace(namespace: &str) -> Result<(), InvalidError> {
670 if namespace.is_empty() {
671 return Err(InvalidError::new("namespace must not be empty"));
672 }
673 if namespace.len() > MAX_NAMESPACE_BYTES {
674 return Err(InvalidError::new(format!(
675 "namespace is {}B, exceeds cap {MAX_NAMESPACE_BYTES}B",
676 namespace.len()
677 )));
678 }
679 if namespace.bytes().any(|byte| byte.is_ascii_control()) {
680 return Err(InvalidError::new(
681 "namespace must not contain ASCII control characters",
682 ));
683 }
684 Ok(())
685}
686
687#[cfg(all(test, feature = "cbor"))]
688mod tests {
689 use super::*;
690 use crate::codes::KV_OP_VERSION;
691 use crate::framing::{decode_named, encode_named};
692 use crate::result::ResultCode;
693
694 #[test]
695 fn given_a_held_lease_or_an_unreachable_barrier_when_round_tripped_then_should_keep_fields_and_codes()
696 {
697 let required = MutationPosition {
698 topic_generation: 7,
699 partition: 0,
700 offset: 3,
701 };
702 for (error, code) in [
703 (
704 KvError::LeaseHeld {
705 expires_at_micros: Some(1_700_000_000_000_000),
706 },
707 ResultCode::Conflict,
708 ),
709 (
710 KvError::BarrierUnreachable { required },
711 ResultCode::StaleGeneration,
712 ),
713 ] {
714 let bytes = encode_named(&KvReply::Err(error.clone())).expect("serializes");
715 let back: KvReply = decode_named(&bytes).expect("deserializes");
716 assert!(matches!(back, KvReply::Err(ref decoded) if *decoded == error));
717 assert_eq!(ResultCode::from(&error), code);
718 }
719 assert!(
720 !ResultCode::StaleGeneration.is_retryable(),
721 "an unreachable barrier is never retried"
722 );
723 }
724
725 #[test]
726 fn given_an_absolute_expiry_or_a_ttl_when_resolved_then_should_store_one_expiry_and_refuse_both()
727 {
728 assert_eq!(resolve_expiry(Some(9), None, 100), Ok(Some(9)));
729 assert_eq!(
730 resolve_expiry(None, Some(30), 100),
731 Ok(Some(130)),
732 "a TTL counts from the server timestamp of the write"
733 );
734 assert_eq!(resolve_expiry(None, None, 100), Ok(None));
735 assert_eq!(
736 resolve_expiry(None, Some(u64::MAX), 100),
737 Ok(Some(u64::MAX))
738 );
739 assert!(resolve_expiry(Some(9), Some(30), 100).is_err());
740 }
741
742 #[test]
743 fn given_kv_delete_many_when_round_tripped_then_should_preserve_bounds() {
744 let request = KvDeleteMany {
745 v: KV_OP_VERSION,
746 namespace: "sessions".to_owned(),
747 prefix: Some(b"user:".to_vec()),
748 start: None,
749 end: None,
750 key_contains: Some("stale".to_owned()),
751 conversation: None,
752 stream: None,
753 };
754 let bytes = encode_named(&request).expect("serializes");
755 let back: KvDeleteMany = decode_named(&bytes).expect("deserializes");
756 assert_eq!(back.prefix, Some(b"user:".to_vec()));
757 assert_eq!(back.key_contains.as_deref(), Some("stale"));
758 }
759
760 #[test]
761 fn given_kv_deleted_many_reply_when_round_tripped_then_should_preserve_count() {
762 let reply = KvReply::Ok(KvOutcome::DeletedMany(7));
763 let bytes = encode_named(&reply).expect("serializes");
764 let back: KvReply = decode_named(&bytes).expect("deserializes");
765 match back {
766 KvReply::Ok(KvOutcome::DeletedMany(n)) => assert_eq!(n, 7),
767 other => panic!("expected DeletedMany, got {other:?}"),
768 }
769 }
770
771 #[test]
772 fn given_a_set_request_when_round_tripped_then_should_preserve_value_and_expiry() {
773 let request = KvSet {
774 v: KV_OP_VERSION,
775 namespace: "sessions".to_owned(),
776 session: None,
777 key: b"user:42".to_vec(),
778 value: b"online".to_vec(),
779 expires_at_micros: Some(1_700_000_000_000_000),
780 ttl_micros: None,
781 };
782 let bytes = encode_named(&request).expect("the request serializes");
783 let back: KvSet = decode_named(&bytes).expect("the request deserializes");
784 assert_eq!(back.key, b"user:42");
785 assert_eq!(back.value, b"online");
786 assert_eq!(back.expires_at_micros, Some(1_700_000_000_000_000));
787 }
788
789 #[test]
790 fn given_a_binary_key_entry_when_round_tripped_then_should_preserve_raw_bytes() {
791 let reply = KvReply::Ok(KvOutcome::Value(Some(KvEntry {
792 key: vec![0xff, 0x00, 0xfe],
793 value: vec![0x00, 0x01, 0x02],
794 expires_at_micros: None,
795 version: 0,
796 scope: None,
797 source: None,
798 })));
799 let bytes = encode_named(&reply).expect("the reply serializes");
800 let back: KvReply = decode_named(&bytes).expect("the reply deserializes");
801 let KvReply::Ok(KvOutcome::Value(Some(entry))) = back else {
802 panic!("expected an Ok(Value(Some)) reply");
803 };
804 assert_eq!(entry.key, vec![0xff, 0x00, 0xfe]);
805 assert_eq!(entry.key_str(), None, "non-UTF-8 key has no string form");
806 assert_eq!(entry.value, vec![0x00, 0x01, 0x02]);
807 }
808
809 #[test]
810 fn given_a_scan_page_when_round_tripped_then_should_preserve_the_page_and_totals() {
811 let page = KvPage {
812 items: vec![KvEntry {
813 key: b"a".to_vec(),
814 value: b"1".to_vec(),
815 expires_at_micros: None,
816 version: 0,
817 scope: None,
818 source: None,
819 }],
820 page: 2,
821 total_pages: 3,
822 total_results: 21,
823 };
824 let bytes = encode_named(&KvReply::Ok(KvOutcome::Page(page.clone()))).expect("serializes");
825 let back: KvReply = decode_named(&bytes).expect("deserializes");
826 let KvReply::Ok(KvOutcome::Page(decoded)) = back else {
827 panic!("expected an Ok(Page) reply");
828 };
829 assert_eq!(decoded, page);
830 }
831
832 #[test]
833 fn given_a_cas_request_when_round_tripped_then_should_preserve_the_precondition() {
834 for expect in [CasExpect::Match(7), CasExpect::Absent] {
835 let request = KvCas {
836 v: KV_OP_VERSION,
837 namespace: "counters".to_owned(),
838 session: None,
839 key: b"hits".to_vec(),
840 value: b"42".to_vec(),
841 expires_at_micros: None,
842 ttl_micros: None,
843 expect,
844 };
845 let bytes = encode_named(&request).expect("serializes");
846 let back: KvCas = decode_named(&bytes).expect("deserializes");
847 assert_eq!(back.expect, expect);
848 assert_eq!(back.key, b"hits");
849 }
850 }
851
852 #[test]
853 fn given_a_committed_reply_when_round_tripped_then_should_preserve_the_version() {
854 let reply = KvReply::Ok(KvOutcome::Committed { version: 9 });
855 let bytes = encode_named(&reply).expect("serializes");
856 let back: KvReply = decode_named(&bytes).expect("deserializes");
857 match back {
858 KvReply::Ok(KvOutcome::Committed { version }) => assert_eq!(version, 9),
859 other => panic!("expected Committed, got {other:?}"),
860 }
861 }
862
863 #[test]
864 fn given_an_exists_metadata_reply_when_round_tripped_then_should_preserve_metadata() {
865 let reply = KvReply::Ok(KvOutcome::Metadata(Some(KvMetadata {
866 version: 4,
867 expires_at_micros: Some(1_700_000_000_000_000),
868 size_bytes: 128,
869 })));
870 let bytes = encode_named(&reply).expect("serializes");
871 let back: KvReply = decode_named(&bytes).expect("deserializes");
872 let KvReply::Ok(KvOutcome::Metadata(Some(meta))) = back else {
873 panic!("expected Ok(Metadata(Some))");
874 };
875 assert_eq!(meta.version, 4);
876 assert_eq!(meta.size_bytes, 128);
877 }
878
879 #[test]
880 fn given_a_patch_request_when_round_tripped_then_should_preserve_patch_and_precondition() {
881 let request = KvPatch {
882 v: KV_OP_VERSION,
883 namespace: "docs".to_owned(),
884 session: None,
885 key: b"doc:1".to_vec(),
886 patch: br#"{"status":"closed"}"#.to_vec(),
887 if_match: Some(3),
888 };
889 let bytes = encode_named(&request).expect("serializes");
890 let back: KvPatch = decode_named(&bytes).expect("deserializes");
891 assert_eq!(back.patch, br#"{"status":"closed"}"#);
892 assert_eq!(back.if_match, Some(3));
893 }
894
895 #[test]
896 fn given_a_lease_reply_when_round_tripped_then_should_preserve_token_ttl_and_position() {
897 let position = MutationPosition {
898 topic_generation: 3,
899 partition: 1,
900 offset: 4_200,
901 };
902 for reply in [
903 KvReply::Ok(KvOutcome::Leased {
904 lease_token: 77,
905 granted_ttl_micros: 30_000_000,
906 position,
907 }),
908 KvReply::Ok(KvOutcome::Renewed {
909 lease_token: 77,
910 granted_ttl_micros: 30_000_000,
911 position,
912 }),
913 ] {
914 let bytes = encode_named(&reply).expect("serializes");
915 let back: KvReply = decode_named(&bytes).expect("deserializes");
916 match back {
917 KvReply::Ok(
918 KvOutcome::Leased {
919 lease_token,
920 granted_ttl_micros,
921 position: got,
922 }
923 | KvOutcome::Renewed {
924 lease_token,
925 granted_ttl_micros,
926 position: got,
927 },
928 ) => {
929 assert_eq!(lease_token, 77);
930 assert_eq!(granted_ttl_micros, 30_000_000);
931 assert_eq!(got, position);
932 }
933 other => panic!("expected Leased or Renewed, got {other:?}"),
934 }
935 }
936 }
937
938 #[test]
939 fn given_a_reshaped_lease_request_when_round_tripped_then_should_preserve_holder_and_subject() {
940 let request = KvLease {
941 v: crate::codes::KV_LEASE_OP_VERSION,
942 namespace: "connectors.coordination".to_owned(),
943 key: b"source-owner".to_vec(),
944 lease_ttl_micros: 30_000_000,
945 holder_id: "node-1".to_owned(),
946 subject_user_id: Some(42),
947 };
948 let bytes = encode_named(&request).expect("serializes");
949 let back: KvLease = decode_named(&bytes).expect("deserializes");
950 assert_eq!(back.holder_id, "node-1");
951 assert_eq!(back.subject_user_id, Some(42));
952 let own = KvLease {
954 subject_user_id: None,
955 ..request
956 };
957 let json = serde_json::to_string(&own).expect("json");
958 assert!(!json.contains("subject_user_id"), "absent subject omitted");
959 }
960
961 #[test]
962 fn given_a_renew_request_when_round_tripped_then_should_preserve_token_and_holder() {
963 let request = KvLeaseRenew {
964 v: crate::codes::KV_LEASE_OP_VERSION,
965 namespace: "connectors.coordination".to_owned(),
966 key: b"source-owner".to_vec(),
967 holder_id: "node-1".to_owned(),
968 subject_user_id: None,
969 lease_token: 7,
970 lease_ttl_micros: 30_000_000,
971 };
972 let bytes = encode_named(&request).expect("serializes");
973 let back: KvLeaseRenew = decode_named(&bytes).expect("deserializes");
974 assert_eq!(back.lease_token, 7);
975 assert_eq!(back.holder_id, "node-1");
976 }
977
978 #[test]
979 fn given_a_pre_fencing_lease_payload_when_decoded_then_should_fail_closed() {
980 #[derive(Serialize)]
985 struct LegacyLease<'a> {
986 v: u32,
987 namespace: &'a str,
988 #[serde(with = "crate::encoding::bin_bytes")]
989 key: &'a [u8],
990 lease_ttl_micros: u64,
991 }
992 let legacy = encode_named(&LegacyLease {
993 v: KV_OP_VERSION,
994 namespace: "sessions",
995 key: b"job",
996 lease_ttl_micros: 30_000_000,
997 })
998 .expect("legacy shape serializes");
999 assert!(
1000 decode_named::<KvLease>(&legacy).is_err(),
1001 "a holder-less lease payload must not decode"
1002 );
1003
1004 #[derive(Serialize)]
1005 struct LegacyRelease<'a> {
1006 v: u32,
1007 namespace: &'a str,
1008 #[serde(with = "crate::encoding::bin_bytes")]
1009 key: &'a [u8],
1010 lease_token: u64,
1011 }
1012 let legacy = encode_named(&LegacyRelease {
1013 v: KV_OP_VERSION,
1014 namespace: "sessions",
1015 key: b"job",
1016 lease_token: 7,
1017 })
1018 .expect("legacy shape serializes");
1019 assert!(
1020 decode_named::<KvRelease>(&legacy).is_err(),
1021 "a holder-less release payload must not decode"
1022 );
1023
1024 #[derive(Serialize)]
1025 struct LegacyCasFenced<'a> {
1026 v: u32,
1027 namespace: &'a str,
1028 #[serde(with = "crate::encoding::bin_bytes")]
1029 key: &'a [u8],
1030 #[serde(with = "crate::encoding::bin_bytes")]
1031 value: &'a [u8],
1032 expect: CasExpect,
1033 #[serde(with = "crate::encoding::bin_bytes")]
1034 fence_key: &'a [u8],
1035 fence_token: u64,
1036 }
1037 let legacy = encode_named(&LegacyCasFenced {
1038 v: KV_OP_VERSION,
1039 namespace: "sessions",
1040 key: b"cursor",
1041 value: b"42",
1042 expect: CasExpect::Absent,
1043 fence_key: b"job",
1044 fence_token: 7,
1045 })
1046 .expect("legacy shape serializes");
1047 assert!(
1048 decode_named::<KvCasFenced>(&legacy).is_err(),
1049 "a fence-only CAS payload must not decode"
1050 );
1051 }
1052
1053 #[test]
1054 fn given_a_stale_reply_when_round_tripped_then_should_preserve_the_required_position() {
1055 let required = MutationPosition {
1056 topic_generation: 2,
1057 partition: 0,
1058 offset: 99,
1059 };
1060 let reply = KvReply::Err(KvError::Stale { required });
1061 let bytes = encode_named(&reply).expect("serializes");
1062 let back: KvReply = decode_named(&bytes).expect("deserializes");
1063 match back {
1064 KvReply::Err(KvError::Stale { required: got }) => assert_eq!(got, required),
1065 other => panic!("expected Stale, got {other:?}"),
1066 }
1067 }
1068
1069 #[test]
1070 fn given_a_conditional_get_when_round_tripped_then_should_preserve_if_none_match() {
1071 let request = KvGet {
1072 v: KV_OP_VERSION,
1073 namespace: "sessions".to_owned(),
1074 key: b"user:1".to_vec(),
1075 if_none_match: Some(5),
1076 min_position: None,
1077 };
1078 let bytes = encode_named(&request).expect("serializes");
1079 let back: KvGet = decode_named(&bytes).expect("deserializes");
1080 assert_eq!(back.if_none_match, Some(5));
1081 let plain = KvGet {
1084 if_none_match: None,
1085 ..request
1086 };
1087 let json = serde_json::to_string(&plain).expect("json");
1088 assert!(
1089 !json.contains("if_none_match"),
1090 "absent precondition omitted"
1091 );
1092 assert!(!json.contains("min_position"), "absent barrier omitted");
1093 }
1094
1095 #[test]
1096 fn given_a_barriered_get_when_round_tripped_then_should_preserve_the_minimum_position() {
1097 let request = KvGet {
1098 v: KV_OP_VERSION,
1099 namespace: "connectors.state".to_owned(),
1100 key: b"source_state/pg".to_vec(),
1101 if_none_match: None,
1102 min_position: Some(MutationPosition {
1103 topic_generation: 1,
1104 partition: 0,
1105 offset: 512,
1106 }),
1107 };
1108 let bytes = encode_named(&request).expect("serializes");
1109 let back: KvGet = decode_named(&bytes).expect("deserializes");
1110 assert_eq!(
1111 back.min_position,
1112 Some(MutationPosition {
1113 topic_generation: 1,
1114 partition: 0,
1115 offset: 512,
1116 })
1117 );
1118 }
1119
1120 #[test]
1121 fn given_a_version_conflict_when_round_tripped_then_should_preserve_the_current_version() {
1122 for current in [Some(3u64), None] {
1123 let reply = KvReply::Err(KvError::VersionConflict { current });
1124 let bytes = encode_named(&reply).expect("serializes");
1125 let back: KvReply = decode_named(&bytes).expect("deserializes");
1126 match back {
1127 KvReply::Err(KvError::VersionConflict { current: got }) => assert_eq!(got, current),
1128 other => panic!("expected VersionConflict, got {other:?}"),
1129 }
1130 }
1131 }
1132
1133 #[test]
1134 fn given_a_versioned_entry_when_round_tripped_then_should_preserve_version_and_skip_zero() {
1135 let entry = KvEntry {
1136 key: b"k".to_vec(),
1137 value: b"v".to_vec(),
1138 expires_at_micros: None,
1139 version: 5,
1140 scope: None,
1141 source: None,
1142 };
1143 let bytes = encode_named(&entry).expect("serializes");
1144 let back: KvEntry = decode_named(&bytes).expect("deserializes");
1145 assert_eq!(back.version, 5);
1146 let unversioned = KvEntry {
1149 version: 0,
1150 ..entry
1151 };
1152 let json = serde_json::to_string(&unversioned).expect("json");
1153 assert!(
1154 !json.contains("version"),
1155 "version 0 must be omitted: {json}"
1156 );
1157 }
1158
1159 #[test]
1160 fn given_a_scan_with_bounds_when_round_tripped_then_should_preserve_filters() {
1161 let scan = KvScan {
1162 v: KV_OP_VERSION,
1163 namespace: "sessions".to_owned(),
1164 prefix: Some(b"user:".to_vec()),
1165 start: None,
1166 end: None,
1167 key_contains: Some("admin".to_owned()),
1168 conversation: None,
1169 stream: None,
1170 user: None,
1171 agent: None,
1172 app: None,
1173 page: 3,
1174 results: 50,
1175 };
1176 let bytes = encode_named(&scan).expect("serializes");
1177 let back: KvScan = decode_named(&bytes).expect("deserializes");
1178 assert_eq!(back.prefix.as_deref(), Some(b"user:".as_ref()));
1179 assert_eq!(back.key_contains.as_deref(), Some("admin"));
1180 assert_eq!((back.page, back.results), (3, 50));
1181 assert_eq!(back.stream, None);
1182 }
1183
1184 #[test]
1185 fn given_a_lens_with_a_stream_when_round_tripped_then_should_preserve_it_and_omit_it_when_unset()
1186 {
1187 let scan = KvScan {
1188 v: KV_OP_VERSION,
1189 namespace: "stream:acme/memory".to_owned(),
1190 prefix: None,
1191 start: None,
1192 end: None,
1193 key_contains: None,
1194 conversation: Some("01KWM3K3XEP3NP5TN850J17YBP".to_owned()),
1195 stream: Some("acme".to_owned()),
1196 user: Some("reader".to_owned()),
1197 agent: Some("planner".to_owned()),
1198 app: Some("desk".to_owned()),
1199 page: 0,
1200 results: 10,
1201 };
1202 let back: KvScan =
1203 decode_named(&encode_named(&scan).expect("serializes")).expect("deserializes");
1204 assert_eq!(back.stream.as_deref(), Some("acme"));
1205 assert_eq!(
1206 (
1207 back.user.as_deref(),
1208 back.agent.as_deref(),
1209 back.app.as_deref()
1210 ),
1211 (Some("reader"), Some("planner"), Some("desk"))
1212 );
1213 let delete = KvDeleteMany {
1214 v: KV_OP_VERSION,
1215 namespace: "stream:acme/memory".to_owned(),
1216 prefix: None,
1217 start: None,
1218 end: None,
1219 key_contains: None,
1220 conversation: Some("01KWM3K3XEP3NP5TN850J17YBP".to_owned()),
1221 stream: Some("acme".to_owned()),
1222 };
1223 let back: KvDeleteMany =
1224 decode_named(&encode_named(&delete).expect("serializes")).expect("deserializes");
1225 assert_eq!(back.stream.as_deref(), Some("acme"));
1226 let unset = KvDeleteMany {
1227 stream: None,
1228 ..delete
1229 };
1230 assert!(
1231 !serde_json::to_string(&unset)
1232 .expect("serializes")
1233 .contains("\"stream\":"),
1234 "an unset stream is omitted"
1235 );
1236 }
1237}