Skip to main content

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}