use std::hash::{BuildHasher, Hash, Hasher};
use ahash::RandomState;
use serde_json::Value as JsonValue;
use crate::{
backend::{
ComposedEmbed, ComposedKeyset, ComposedLevel, EmbedShape, EmbedSource, LevelKeys,
NativeCorrelation, OrderByClause, ProjectionRequest, RelayCursor, WhereOperator,
where_clause::WhereClause,
},
schema::QueryDefinition,
security::SecurityContext,
};
const SEED_K0: u64 = 0x5241_4953_454F_4E31; const SEED_K1: u64 = 0x4652_4149_5345_514C; const SEED_K2: u64 = 0x4341_4348_454B_4559; const SEED_K3: u64 = 0x5632_5F43_4143_4845;
fn new_hasher() -> impl Hasher {
RandomState::with_seeds(SEED_K0, SEED_K1, SEED_K2, SEED_K3).build_hasher()
}
#[must_use]
pub fn generate_cache_key(
query: &str,
variables: &JsonValue,
where_clause: Option<&WhereClause>,
schema_version: &str,
) -> u64 {
let mut h = new_hasher();
h.write(b"q:");
h.write(query.as_bytes());
h.write(b"\0v:");
hash_json_value(&mut h, variables);
h.write(b"\0w:");
if let Some(wc) = where_clause {
h.write_u8(1);
hash_where_clause(&mut h, wc);
} else {
h.write_u8(0);
}
h.write(b"\0s:");
h.write(schema_version.as_bytes());
h.finish()
}
#[must_use]
#[allow(clippy::too_many_arguments)] pub fn generate_view_query_key(
view: &str,
where_clause: Option<&WhereClause>,
limit: Option<u32>,
offset: Option<u32>,
order_by: Option<&[OrderByClause]>,
session_vars: &[(&str, &str)],
locale: Option<&str>,
schema_version: &str,
) -> u64 {
let mut h = new_hasher();
h.write(b"v:");
h.write(view.as_bytes());
h.write(b"\0w:");
if let Some(wc) = where_clause {
h.write_u8(1);
hash_where_clause(&mut h, wc);
} else {
h.write_u8(0);
}
h.write(b"\0l:");
match limit {
Some(l) => {
h.write_u8(1);
h.write_u32(l);
},
None => h.write_u8(0),
}
h.write(b"\0o:");
match offset {
Some(o) => {
h.write_u8(1);
h.write_u32(o);
},
None => h.write_u8(0),
}
h.write(b"\0b:");
hash_order_by(&mut h, order_by);
h.write(b"\0sv:");
hash_session_vars(&mut h, session_vars);
hash_locale(&mut h, locale);
h.write(b"\0s:");
h.write(schema_version.as_bytes());
h.finish()
}
#[must_use]
pub fn generate_projection_query_key(
request: &ProjectionRequest<'_>,
session_vars: &[(&str, &str)],
locale: Option<&str>,
schema_version: &str,
) -> u64 {
let ProjectionRequest {
view,
projection,
where_clause,
order_by,
limit,
offset,
matched_up_to,
} = *request;
let mut h = new_hasher();
h.write(b"p:");
h.write(view.as_bytes());
h.write(b"\0j:");
match projection {
Some(p) => {
h.write_u8(1);
h.write(p.projection_template.as_bytes());
},
None => h.write_u8(0),
}
h.write(b"\0w:");
if let Some(wc) = where_clause {
h.write_u8(1);
hash_where_clause(&mut h, wc);
} else {
h.write_u8(0);
}
h.write(b"\0l:");
match limit {
Some(l) => {
h.write_u8(1);
h.write_u32(l);
},
None => h.write_u8(0),
}
h.write(b"\0o:");
match offset {
Some(o) => {
h.write_u8(1);
h.write_u32(o);
},
None => h.write_u8(0),
}
h.write(b"\0b:");
hash_order_by(&mut h, order_by);
h.write(b"\0m:");
match matched_up_to {
Some(bound) => {
h.write_u8(1);
h.write_u32(bound);
},
None => h.write_u8(0),
}
h.write(b"\0sv:");
hash_session_vars(&mut h, session_vars);
hash_locale(&mut h, locale);
h.write(b"\0s:");
h.write(schema_version.as_bytes());
h.finish()
}
#[must_use]
pub fn generate_composed_query_key(
read: &ComposedLevel,
session_vars: &[(&str, &str)],
locale: Option<&str>,
schema_version: &str,
) -> u64 {
let mut h = new_hasher();
h.write(b"c:");
hash_composed_level(&mut h, read);
h.write(b"\0sv:");
hash_session_vars(&mut h, session_vars);
hash_locale(&mut h, locale);
h.write(b"\0s:");
h.write(schema_version.as_bytes());
h.finish()
}
fn hash_composed_level(h: &mut impl Hasher, level: &ComposedLevel) {
let ComposedLevel {
view,
projection,
where_clause,
order_by,
limit,
offset,
keyset,
keys,
embeds,
} = level;
h.write(view.as_bytes());
h.write(b"\0j:");
hash_optional_str(h, projection.as_deref());
h.write(b"\0w:");
match where_clause {
Some(clause) => {
h.write_u8(1);
hash_where_clause(h, clause);
},
None => h.write_u8(0),
}
h.write(b"\0b:");
hash_order_by(h, order_by.as_deref());
h.write(b"\0lo:");
for bound in [limit, offset] {
match bound {
Some(n) => {
h.write_u8(1);
h.write_u32(*n);
},
None => h.write_u8(0),
}
}
h.write(b"\0k:");
match keyset {
Some(ComposedKeyset {
cursor_column,
cursor,
forward,
}) => {
h.write_u8(1);
h.write(cursor_column.as_bytes());
h.write_u8(0);
h.write_u8(u8::from(*forward));
match cursor {
Some(RelayCursor {
position,
sort_keys,
}) => {
h.write_u8(1);
h.write(format!("{position:?}").as_bytes());
h.write_u8(0);
h.write_usize(sort_keys.len());
for key in sort_keys {
hash_optional_str(h, key.as_deref());
}
},
None => h.write_u8(0),
}
},
None => h.write_u8(0),
}
h.write(b"\0d:");
match keys {
LevelKeys::Whole => h.write_u8(b'W'),
LevelKeys::Without(dropped) => {
h.write_u8(b'X');
hash_strings(h, dropped);
},
LevelKeys::Only { kept, masked } => {
h.write_u8(b'O');
hash_strings(h, kept);
hash_strings(h, masked);
},
}
h.write(b"\0e:");
h.write_usize(embeds.len());
for ComposedEmbed {
output_key,
shape,
source,
level,
} in embeds
{
h.write(output_key.as_bytes());
h.write_u8(0);
h.write_u8(match shape {
EmbedShape::Many => b'M',
EmbedShape::One => b'1',
EmbedShape::Count => b'C',
});
match source {
EmbedSource::Correlated {
target_key,
parent_key,
key_type,
native,
} => {
h.write_u8(b'R');
hash_strings(h, target_key);
hash_strings(h, parent_key);
h.write(format!("{key_type:?}").as_bytes());
h.write_u8(0);
match native {
Some(NativeCorrelation {
target_column,
parent_column,
}) => {
h.write_u8(1);
hash_strings(h, &[target_column.clone(), parent_column.clone()]);
},
None => h.write_u8(0),
}
},
EmbedSource::Materialised { keys } => {
h.write_u8(b'P');
hash_strings(h, keys);
},
}
hash_composed_level(h, level);
}
}
fn hash_optional_str(h: &mut impl Hasher, value: Option<&str>) {
match value {
Some(v) => {
h.write_u8(1);
h.write(v.as_bytes());
h.write_u8(0);
},
None => h.write_u8(0),
}
}
fn hash_strings(h: &mut impl Hasher, values: &[String]) {
h.write_usize(values.len());
for value in values {
h.write(value.as_bytes());
h.write_u8(0);
}
}
#[must_use]
pub fn hash_security_context(ctx: Option<&SecurityContext>) -> u64 {
let Some(ctx) = ctx else {
return 0;
};
let mut h = new_hasher();
h.write(b"u:");
ctx.user_id.hash(&mut h);
let mut roles: Vec<&String> = ctx.roles.iter().collect();
roles.sort_unstable();
h.write(b"r:");
h.write_usize(roles.len());
for role in roles {
role.hash(&mut h);
}
h.write(b"t:");
ctx.tenant_id.hash(&mut h);
let mut scopes: Vec<&String> = ctx.scopes.iter().collect();
scopes.sort_unstable();
h.write(b"s:");
h.write_usize(scopes.len());
for scope in scopes {
scope.hash(&mut h);
}
let mut attribute_keys: Vec<&String> = ctx.attributes.keys().collect();
attribute_keys.sort_unstable();
h.write(b"a:");
h.write_usize(attribute_keys.len());
for key in attribute_keys {
key.hash(&mut h);
hash_json_value(&mut h, &ctx.attributes[key]);
}
h.finish()
}
fn hash_json_value(h: &mut impl Hasher, value: &JsonValue) {
match value {
JsonValue::Null => h.write_u8(0),
JsonValue::Bool(b) => {
h.write_u8(1);
b.hash(h);
},
JsonValue::Number(n) => {
h.write_u8(2);
h.write(n.to_string().as_bytes());
},
JsonValue::String(s) => {
h.write_u8(3);
h.write(s.as_bytes());
},
JsonValue::Array(arr) => {
h.write_u8(4);
h.write_usize(arr.len());
for item in arr {
hash_json_value(h, item);
}
},
JsonValue::Object(map) => {
h.write_u8(5);
h.write_usize(map.len());
let mut keys: Vec<&String> = map.keys().collect();
keys.sort_unstable();
for key in keys {
h.write(key.as_bytes());
hash_json_value(h, &map[key]);
}
},
}
}
fn hash_where_clause(h: &mut impl Hasher, clause: &WhereClause) {
match clause {
WhereClause::Field {
path,
operator,
value,
} => {
h.write_u8(b'F');
h.write_usize(path.len());
for segment in path {
h.write(segment.as_bytes());
h.write_u8(0); }
hash_where_operator(h, operator);
hash_json_value(h, value);
},
WhereClause::And(clauses) => {
h.write_u8(b'A');
h.write_usize(clauses.len());
for c in clauses {
hash_where_clause(h, c);
}
},
WhereClause::Or(clauses) => {
h.write_u8(b'O');
h.write_usize(clauses.len());
for c in clauses {
hash_where_clause(h, c);
}
},
WhereClause::Not(inner) => {
h.write_u8(b'N');
hash_where_clause(h, inner);
},
WhereClause::Typed { inner, .. } => {
h.write_u8(b'T');
hash_where_clause(h, inner);
},
WhereClause::Guarded {
under,
guard,
inner,
} => {
h.write_u8(b'G');
h.write_usize(under.len());
for segment in under {
h.write(segment.as_bytes());
h.write_u8(0);
}
hash_where_clause(h, guard);
hash_where_clause(h, inner);
},
WhereClause::KeyIn {
path,
key_type,
view,
target_key,
predicate,
} => {
h.write_u8(b'K');
for segments in [path, target_key] {
h.write_usize(segments.len());
for segment in segments {
h.write(segment.as_bytes());
h.write_u8(0);
}
}
h.write(format!("{key_type:?}").as_bytes());
h.write_u8(0);
h.write(view.as_bytes());
h.write_u8(0);
hash_where_clause(h, predicate);
},
_ => {
h.write_u8(b'?');
h.write(format!("{clause:?}").as_bytes());
},
}
}
fn hash_where_operator(h: &mut impl Hasher, op: &WhereOperator) {
std::mem::discriminant(op).hash(h);
}
fn hash_order_by(h: &mut impl Hasher, order_by: Option<&[OrderByClause]>) {
match order_by.filter(|c| !c.is_empty()) {
Some(clauses) => {
h.write_u8(1);
h.write_usize(clauses.len());
for clause in clauses {
let key = clause.storage_key();
h.write(key.as_bytes());
h.write_u8(clause.direction as u8);
}
},
None => h.write_u8(0),
}
}
#[must_use]
pub fn extract_accessed_views(query_def: &QueryDefinition) -> Vec<String> {
let mut views = Vec::new();
if let Some(sql_source) = &query_def.sql_source {
views.push(sql_source.clone());
}
views.extend(query_def.additional_views.iter().cloned());
views
}
#[cfg(test)]
#[must_use]
pub fn verify_deterministic(query: &str, variables: &JsonValue, schema_version: &str) -> bool {
let key1 = generate_cache_key(query, variables, None, schema_version);
let key2 = generate_cache_key(query, variables, None, schema_version);
key1 == key2
}
fn hash_locale(h: &mut impl std::hash::Hasher, locale: Option<&str>) {
h.write(b"\0loc:");
match locale {
Some(tag) => {
h.write_u8(1);
h.write(tag.as_bytes());
},
None => h.write_u8(0),
}
}
fn hash_session_vars(h: &mut impl std::hash::Hasher, session_vars: &[(&str, &str)]) {
let mut sorted: Vec<&(&str, &str)> = session_vars.iter().collect();
sorted.sort_unstable_by(|a, b| a.0.cmp(b.0).then_with(|| a.1.cmp(b.1)));
h.write_usize(sorted.len());
for (name, value) in sorted {
h.write(name.as_bytes());
h.write_u8(0);
h.write(value.as_bytes());
h.write_u8(0);
}
}
#[cfg(test)]
#[path = "key_composed_tests.rs"]
mod key_composed_tests;