yo_kv/cond.rs
1//! The conditional write comparisons Redis 8.4 added.
2//!
3//! `SET` grew `IFEQ`, `IFNE`, `IFDEQ` and `IFDNE`, and `DELEX` is the same four
4//! conditions in front of a delete. They exist for the read modify write that
5//! nobody was doing correctly: a client reads a value, decides something about
6//! it, and writes back, and another client can get to the key in between. The
7//! alternative was `WATCH` and `MULTI`, which costs a round trip.
8//!
9//! The digest forms are the same conditions with the value replaced by its
10//! [`DIGEST`](yo_common::xxh3), so a client comparing against a megabyte value
11//! sends sixteen bytes instead of a megabyte.
12//!
13//! Whether a missing key satisfies a condition is not symmetric and it is not
14//! arbitrary. A key that is not there is not equal to anything, so `IFEQ` fails
15//! on it, and it is not equal to anything, so `IFNE` passes on it. That is what
16//! a real 8.8 does: `SET k v IFNE other` on a key that does not exist stores.
17
18use crate::value::Str;
19
20/// What a conditional write compares the current value against.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Compare<'a> {
23 /// `IFEQ`: the value is exactly these bytes.
24 Equal(&'a [u8]),
25 /// `IFNE`: the value is not exactly these bytes, or there is no value.
26 NotEqual(&'a [u8]),
27 /// `IFDEQ`: the value's digest is this number.
28 DigestEqual(u64),
29 /// `IFDNE`: the value's digest is not this number, or there is no value.
30 DigestNotEqual(u64),
31}
32
33impl Compare<'_> {
34 /// Whether the condition holds against what is stored, where `None` is a
35 /// key that is not there.
36 #[must_use]
37 pub fn holds(&self, current: Option<Str<'_>>) -> bool {
38 match *self {
39 Compare::Equal(want) => current.is_some_and(|v| v.eq_bytes(want)),
40 Compare::NotEqual(want) => !current.is_some_and(|v| v.eq_bytes(want)),
41 Compare::DigestEqual(want) => current.is_some_and(|v| v.digest() == want),
42 Compare::DigestNotEqual(want) => !current.is_some_and(|v| v.digest() == want),
43 }
44 }
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50
51 /// The digest of `hello`, which is the number a real Redis replies to
52 /// `DIGEST` with and the number a client would send back as `IFDEQ`.
53 const HELLO: u64 = 0x9555_e855_5c62_dcfd;
54
55 #[test]
56 fn equality_is_against_the_string_the_client_would_have_read() {
57 let int = Some(Str::Int(42));
58 assert!(Compare::Equal(b"42").holds(int));
59 // Not `042`, which parses to the same number and is a different string.
60 assert!(!Compare::Equal(b"042").holds(int));
61 assert!(Compare::NotEqual(b"042").holds(int));
62 }
63
64 #[test]
65 fn a_missing_key_fails_the_equal_forms_and_passes_the_not_equal_forms() {
66 assert!(!Compare::Equal(b"").holds(None));
67 assert!(!Compare::DigestEqual(HELLO).holds(None));
68 assert!(Compare::NotEqual(b"anything").holds(None));
69 assert!(Compare::DigestNotEqual(HELLO).holds(None));
70 }
71
72 #[test]
73 fn the_digest_forms_agree_with_the_value_forms() {
74 let v = Some(Str::Bytes(b"hello"));
75 assert!(Compare::Equal(b"hello").holds(v));
76 assert!(Compare::DigestEqual(HELLO).holds(v));
77 assert!(!Compare::NotEqual(b"hello").holds(v));
78 assert!(!Compare::DigestNotEqual(HELLO).holds(v));
79 }
80
81 #[test]
82 fn an_int_encoded_value_digests_its_digits() {
83 // A real 8.8 answers this for `SET n 42` followed by `DIGEST n`, so the
84 // digits and not the eight bytes the record actually holds.
85 assert_eq!(Str::Int(42).digest(), 0x1217_cb28_c0ef_2191);
86 assert!(Compare::DigestEqual(0x1217_cb28_c0ef_2191).holds(Some(Str::Int(42))));
87 }
88}