Skip to main content

rustlavel_model_cache/
keys.rs

1//! What a cached thing is stored under.
2//!
3//! Three shapes, all prefixed so a `flush` of the application's own cache does
4//! not have to know about this one:
5//!
6//! - `rl:mc:e:{table}:{key}` — one entity.
7//! - `rl:mc:q:{table}:{fingerprint}` — one query's result set.
8//! - `rl:mc:g:{table}` — the table's generation counter.
9
10use rustlavel_core::Json;
11use rustlavel_db::Value;
12use std::hash::{Hash, Hasher};
13
14pub const PREFIX: &str = "rl:mc";
15
16pub fn entity(table: &str, key: &str) -> String {
17    format!("{PREFIX}:e:{table}:{key}")
18}
19
20pub fn query(table: &str, fingerprint: u64) -> String {
21    format!("{PREFIX}:q:{table}:{fingerprint:016x}")
22}
23
24pub fn generation(table: &str) -> String {
25    format!("{PREFIX}:g:{table}")
26}
27
28/// A 64-bit fingerprint of a statement and its bindings.
29///
30/// **A collision here would serve one query's rows as another's**, which is
31/// the worst thing a cache can do, so the fingerprint is not trusted on its
32/// own: the statement it was taken from is stored alongside the rows and
33/// compared on the way out — see [`crate::cache::Entry`]. A collision is then
34/// a miss rather than a wrong answer, which turns a correctness problem into a
35/// performance one and costs a string comparison per hit.
36///
37/// That is also why this is not a cryptographic hash. There is nothing to
38/// forge — the input is SQL this process generated — and the verification
39/// above makes the collision rate a matter of cost rather than of safety.
40pub fn fingerprint(sql: &str, bindings: &[Value]) -> u64 {
41    let mut hasher = std::collections::hash_map::DefaultHasher::new();
42    sql.hash(&mut hasher);
43    for value in bindings {
44        // Through `Json` because `Value` is not `Hash` — a float is in there —
45        // and its text form is exactly what distinguishes two bindings.
46        Json::from(value.clone()).to_string().hash(&mut hasher);
47    }
48    hasher.finish()
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[test]
56    fn the_three_shapes_do_not_collide_with_each_other() {
57        assert_eq!(entity("users", "7"), "rl:mc:e:users:7");
58        assert_eq!(generation("users"), "rl:mc:g:users");
59        assert!(query("users", 1).starts_with("rl:mc:q:users:"));
60
61        // A table called `e:users` must not be able to spell another table's
62        // entity key. The prefixes are distinct segments, so it cannot.
63        assert_ne!(entity("users", "7"), query("users", 7));
64    }
65
66    #[test]
67    fn the_bindings_are_part_of_the_fingerprint() {
68        let sql = "select * from users where role = ?";
69
70        let admin = fingerprint(sql, &[Value::from("admin")]);
71        let support = fingerprint(sql, &[Value::from("support")]);
72
73        assert_ne!(admin, support, "two different filters shared a cache key");
74        assert_eq!(admin, fingerprint(sql, &[Value::from("admin")]), "not stable");
75
76        // And so is the statement, for the same bindings.
77        assert_ne!(admin, fingerprint("select 1", &[Value::from("admin")]));
78    }
79
80    /// `7` the number and `"7"` the string are different bindings, and a
81    /// fingerprint that flattened both to `7` would serve one for the other.
82    #[test]
83    fn a_number_and_its_text_are_different_bindings() {
84        let sql = "select * from users where id = ?";
85
86        assert_ne!(
87            fingerprint(sql, &[Value::from(7i64)]),
88            fingerprint(sql, &[Value::from("7")]),
89        );
90    }
91
92    #[test]
93    fn the_order_of_the_bindings_matters() {
94        let sql = "select * from users where a = ? and b = ?";
95
96        assert_ne!(
97            fingerprint(sql, &[Value::from("x"), Value::from("y")]),
98            fingerprint(sql, &[Value::from("y"), Value::from("x")]),
99        );
100    }
101}