1use std::hash::{BuildHasher, Hash, Hasher};
46
47use ahash::RandomState;
48use serde_json::Value as JsonValue;
49
50use crate::{
51 db::{OrderByClause, WhereOperator, where_clause::WhereClause},
52 schema::{QueryDefinition, SqlProjectionHint},
53};
54
55const 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 {
64 RandomState::with_seeds(SEED_K0, SEED_K1, SEED_K2, SEED_K3).build_hasher()
65}
66
67#[must_use]
123pub fn generate_cache_key(
124 query: &str,
125 variables: &JsonValue,
126 where_clause: Option<&WhereClause>,
127 schema_version: &str,
128) -> u64 {
129 let mut h = new_hasher();
130
131 h.write(b"q:");
135 h.write(query.as_bytes());
136
137 h.write(b"\0v:");
138 hash_json_value(&mut h, variables);
139
140 h.write(b"\0w:");
141 if let Some(wc) = where_clause {
142 h.write_u8(1);
143 hash_where_clause(&mut h, wc);
144 } else {
145 h.write_u8(0);
146 }
147
148 h.write(b"\0s:");
149 h.write(schema_version.as_bytes());
150
151 h.finish()
152}
153
154#[must_use]
171pub fn generate_view_query_key(
172 view: &str,
173 where_clause: Option<&WhereClause>,
174 limit: Option<u32>,
175 offset: Option<u32>,
176 order_by: Option<&[OrderByClause]>,
177 schema_version: &str,
178) -> u64 {
179 let mut h = new_hasher();
180 h.write(b"v:");
181 h.write(view.as_bytes());
182 h.write(b"\0w:");
183 if let Some(wc) = where_clause {
184 h.write_u8(1);
185 hash_where_clause(&mut h, wc);
186 } else {
187 h.write_u8(0);
188 }
189 h.write(b"\0l:");
190 match limit {
191 Some(l) => {
192 h.write_u8(1);
193 h.write_u32(l);
194 },
195 None => h.write_u8(0),
196 }
197 h.write(b"\0o:");
198 match offset {
199 Some(o) => {
200 h.write_u8(1);
201 h.write_u32(o);
202 },
203 None => h.write_u8(0),
204 }
205 h.write(b"\0b:");
206 hash_order_by(&mut h, order_by);
207 h.write(b"\0s:");
208 h.write(schema_version.as_bytes());
209 h.finish()
210}
211
212#[must_use]
226pub fn generate_projection_query_key(
227 view: &str,
228 projection: Option<&SqlProjectionHint>,
229 where_clause: Option<&WhereClause>,
230 limit: Option<u32>,
231 offset: Option<u32>,
232 order_by: Option<&[OrderByClause]>,
233 schema_version: &str,
234) -> u64 {
235 let mut h = new_hasher();
236 h.write(b"p:");
237 h.write(view.as_bytes());
238 h.write(b"\0j:");
239 match projection {
240 Some(p) => {
241 h.write_u8(1);
242 h.write(p.projection_template.as_bytes());
243 },
244 None => h.write_u8(0),
245 }
246 h.write(b"\0w:");
247 if let Some(wc) = where_clause {
248 h.write_u8(1);
249 hash_where_clause(&mut h, wc);
250 } else {
251 h.write_u8(0);
252 }
253 h.write(b"\0l:");
254 match limit {
255 Some(l) => {
256 h.write_u8(1);
257 h.write_u32(l);
258 },
259 None => h.write_u8(0),
260 }
261 h.write(b"\0o:");
262 match offset {
263 Some(o) => {
264 h.write_u8(1);
265 h.write_u32(o);
266 },
267 None => h.write_u8(0),
268 }
269 h.write(b"\0b:");
270 hash_order_by(&mut h, order_by);
271 h.write(b"\0s:");
272 h.write(schema_version.as_bytes());
273 h.finish()
274}
275
276fn hash_json_value(h: &mut impl Hasher, value: &JsonValue) {
281 match value {
284 JsonValue::Null => h.write_u8(0),
285 JsonValue::Bool(b) => {
286 h.write_u8(1);
287 b.hash(h);
288 },
289 JsonValue::Number(n) => {
290 h.write_u8(2);
291 h.write(n.to_string().as_bytes());
294 },
295 JsonValue::String(s) => {
296 h.write_u8(3);
297 h.write(s.as_bytes());
298 },
299 JsonValue::Array(arr) => {
300 h.write_u8(4);
301 h.write_usize(arr.len());
302 for item in arr {
303 hash_json_value(h, item);
304 }
305 },
306 JsonValue::Object(map) => {
307 h.write_u8(5);
308 h.write_usize(map.len());
309 let mut keys: Vec<&String> = map.keys().collect();
311 keys.sort_unstable();
312 for key in keys {
313 h.write(key.as_bytes());
314 hash_json_value(h, &map[key]);
315 }
316 },
317 }
318}
319
320fn hash_where_clause(h: &mut impl Hasher, clause: &WhereClause) {
325 match clause {
326 WhereClause::Field {
327 path,
328 operator,
329 value,
330 } => {
331 h.write_u8(b'F');
332 h.write_usize(path.len());
333 for segment in path {
334 h.write(segment.as_bytes());
335 h.write_u8(0); }
337 hash_where_operator(h, operator);
338 hash_json_value(h, value);
339 },
340 WhereClause::And(clauses) => {
341 h.write_u8(b'A');
342 h.write_usize(clauses.len());
343 for c in clauses {
344 hash_where_clause(h, c);
345 }
346 },
347 WhereClause::Or(clauses) => {
348 h.write_u8(b'O');
349 h.write_usize(clauses.len());
350 for c in clauses {
351 hash_where_clause(h, c);
352 }
353 },
354 WhereClause::Not(inner) => {
355 h.write_u8(b'N');
356 hash_where_clause(h, inner);
357 },
358 _ => {
361 h.write_u8(b'?');
362 h.write(format!("{clause:?}").as_bytes());
363 },
364 }
365}
366
367fn hash_where_operator(h: &mut impl Hasher, op: &WhereOperator) {
378 std::mem::discriminant(op).hash(h);
380
381 if let WhereOperator::Extended(inner) = op {
384 let inner_str = format!("{inner:?}");
387 h.write(inner_str.as_bytes());
388 }
389}
390
391fn hash_order_by(h: &mut impl Hasher, order_by: Option<&[OrderByClause]>) {
396 match order_by.filter(|c| !c.is_empty()) {
397 Some(clauses) => {
398 h.write_u8(1);
399 h.write_usize(clauses.len());
400 for clause in clauses {
401 let key = clause.storage_key();
402 h.write(key.as_bytes());
403 h.write_u8(clause.direction as u8);
404 }
405 },
406 None => h.write_u8(0),
407 }
408}
409
410#[must_use]
453pub fn extract_accessed_views(query_def: &QueryDefinition) -> Vec<String> {
454 let mut views = Vec::new();
455
456 if let Some(sql_source) = &query_def.sql_source {
458 views.push(sql_source.clone());
459 }
460
461 views.extend(query_def.additional_views.iter().cloned());
464
465 views
466}
467
468#[cfg(test)]
483#[must_use]
484pub fn verify_deterministic(query: &str, variables: &JsonValue, schema_version: &str) -> bool {
485 let key1 = generate_cache_key(query, variables, None, schema_version);
486 let key2 = generate_cache_key(query, variables, None, schema_version);
487 key1 == key2
488}
489
490#[cfg(test)]
491mod tests {
492 use std::collections::{HashMap, HashSet};
493
494 use indexmap::IndexMap;
495 use serde_json::json;
496
497 use super::*;
498 use crate::schema::CursorType;
499
500 #[test]
505 fn test_different_variables_produce_different_keys() {
506 let query = "query getUser($id: ID!) { user(id: $id) { name email } }";
509
510 let key_alice = generate_cache_key(query, &json!({"id": "alice"}), None, "v1");
511 let key_bob = generate_cache_key(query, &json!({"id": "bob"}), None, "v1");
512
513 assert_ne!(
514 key_alice, key_bob,
515 "SECURITY: Different variables MUST produce different cache keys"
516 );
517 }
518
519 #[test]
520 fn test_different_variable_values_produce_different_keys() {
521 let query = "query getUsers($limit: Int!) { users(limit: $limit) { id } }";
522
523 let key_10 = generate_cache_key(query, &json!({"limit": 10}), None, "v1");
524 let key_20 = generate_cache_key(query, &json!({"limit": 20}), None, "v1");
525
526 assert_ne!(
527 key_10, key_20,
528 "SECURITY: Different variable values MUST produce different keys"
529 );
530 }
531
532 #[test]
533 fn test_empty_vs_non_empty_variables() {
534 let query = "query { users { id } }";
535
536 let key_empty = generate_cache_key(query, &json!({}), None, "v1");
537 let key_with_vars = generate_cache_key(query, &json!({"limit": 10}), None, "v1");
538
539 assert_ne!(
540 key_empty, key_with_vars,
541 "Empty variables must produce different key than non-empty"
542 );
543 }
544
545 #[test]
546 fn test_variable_order_independence() {
547 let query = "query($a: Int, $b: Int) { users { id } }";
551
552 let key1 = generate_cache_key(query, &json!({"a": 1, "b": 2}), None, "v1");
553 let key2 = generate_cache_key(query, &json!({"a": 1, "b": 2}), None, "v1");
554
555 assert_eq!(key1, key2, "Same variables must produce same key");
556 }
557
558 #[test]
563 fn test_cache_key_deterministic() {
564 let query = "query { users { id } }";
566 let vars = json!({"limit": 10});
567
568 let key1 = generate_cache_key(query, &vars, None, "v1");
569 let key2 = generate_cache_key(query, &vars, None, "v1");
570
571 assert_eq!(key1, key2, "Cache keys must be deterministic");
572 }
573
574 #[test]
575 fn test_verify_deterministic_helper() {
576 assert!(
577 verify_deterministic("query { users }", &json!({}), "v1"),
578 "Helper should verify determinism"
579 );
580 }
581
582 #[test]
587 fn test_different_where_clauses_produce_different_keys() {
588 let query = "query { users { id } }";
589
590 let where1 = WhereClause::Field {
591 path: vec!["email".to_string()],
592 operator: WhereOperator::Eq,
593 value: json!("alice@example.com"),
594 };
595
596 let where2 = WhereClause::Field {
597 path: vec!["email".to_string()],
598 operator: WhereOperator::Eq,
599 value: json!("bob@example.com"),
600 };
601
602 let key1 = generate_cache_key(query, &json!({}), Some(&where1), "v1");
603 let key2 = generate_cache_key(query, &json!({}), Some(&where2), "v1");
604
605 assert_ne!(key1, key2, "Different WHERE clauses must produce different keys");
606 }
607
608 #[test]
609 fn test_different_where_operators_produce_different_keys() {
610 let query = "query { users { id } }";
611
612 let where_eq = WhereClause::Field {
613 path: vec!["age".to_string()],
614 operator: WhereOperator::Eq,
615 value: json!(30),
616 };
617
618 let where_gt = WhereClause::Field {
619 path: vec!["age".to_string()],
620 operator: WhereOperator::Gt,
621 value: json!(30),
622 };
623
624 let key_eq = generate_cache_key(query, &json!({}), Some(&where_eq), "v1");
625 let key_gt = generate_cache_key(query, &json!({}), Some(&where_gt), "v1");
626
627 assert_ne!(key_eq, key_gt, "Different operators must produce different keys");
628 }
629
630 #[test]
631 fn test_with_and_without_where_clause() {
632 let query = "query { users { id } }";
633
634 let where_clause = WhereClause::Field {
635 path: vec!["active".to_string()],
636 operator: WhereOperator::Eq,
637 value: json!(true),
638 };
639
640 let key_without = generate_cache_key(query, &json!({}), None, "v1");
641 let key_with = generate_cache_key(query, &json!({}), Some(&where_clause), "v1");
642
643 assert_ne!(key_without, key_with, "Presence of WHERE clause must change key");
644 }
645
646 #[test]
647 fn test_complex_where_clause() {
648 let query = "query { users { id } }";
649
650 let where_clause = WhereClause::And(vec![
651 WhereClause::Field {
652 path: vec!["age".to_string()],
653 operator: WhereOperator::Gte,
654 value: json!(18),
655 },
656 WhereClause::Field {
657 path: vec!["active".to_string()],
658 operator: WhereOperator::Eq,
659 value: json!(true),
660 },
661 ]);
662
663 let _key = generate_cache_key(query, &json!({}), Some(&where_clause), "v1");
665 }
666
667 #[test]
672 fn test_different_schema_versions_produce_different_keys() {
673 let query = "query { users { id } }";
674
675 let key_v1 = generate_cache_key(query, &json!({}), None, "v1");
676 let key_v2 = generate_cache_key(query, &json!({}), None, "v2");
677
678 assert_ne!(key_v1, key_v2, "Different schema versions must produce different keys");
679 }
680
681 #[test]
682 fn test_schema_version_invalidation() {
683 let query = "query { users { id } }";
685
686 let old_schema = "abc123";
687 let new_schema = "def456";
688
689 let key_old = generate_cache_key(query, &json!({}), None, old_schema);
690 let key_new = generate_cache_key(query, &json!({}), None, new_schema);
691
692 assert_ne!(key_old, key_new, "Schema changes should invalidate cache");
693 }
694
695 #[test]
700 fn test_no_collisions_in_sample() {
701 let mut keys = HashSet::new();
704 let mut count = 0u32;
705
706 let queries = [
707 "query { users { id } }",
708 "query { posts { id } }",
709 "query { users { id name } }",
710 "query getUser($id: ID!) { user(id: $id) { name } }",
711 "",
712 ];
713 let variable_sets: &[JsonValue] = &[
714 json!({}),
715 json!(null),
716 json!({"id": 1}),
717 json!({"id": 2}),
718 json!({"id": "alice"}),
719 json!({"limit": 10, "offset": 0}),
720 json!({"filter": {"active": true}}),
721 ];
722 let schema_versions = ["v1", "v2", "abc123"];
723
724 for query in &queries {
725 for vars in variable_sets {
726 for sv in &schema_versions {
727 let key = generate_cache_key(query, vars, None, sv);
728 keys.insert(key);
729 count += 1;
730 }
731 }
732 }
733
734 assert_eq!(
735 keys.len(),
736 count as usize,
737 "Collision detected among {count} sample cache keys"
738 );
739 }
740
741 #[test]
746 fn test_extract_accessed_views_with_sql_source() {
747 use crate::schema::AutoParams;
748
749 let query_def = QueryDefinition {
750 name: "users".to_string(),
751 return_type: "User".to_string(),
752 returns_list: true,
753 nullable: false,
754 arguments: vec![],
755 sql_source: Some("v_user".to_string()),
756 description: None,
757 auto_params: AutoParams {
758 has_where: true,
759 has_order_by: false,
760 has_limit: true,
761 has_offset: false,
762 },
763 deprecation: None,
764 jsonb_column: "data".to_string(),
765 relay: false,
766 relay_cursor_column: None,
767 relay_cursor_type: CursorType::default(),
768 inject_params: IndexMap::default(),
769 cache_ttl_seconds: None,
770 additional_views: vec![],
771 requires_role: None,
772 rest_path: None,
773 rest_method: None,
774 native_columns: HashMap::new(),
775 };
776
777 let views = extract_accessed_views(&query_def);
778 assert_eq!(views, vec!["v_user"]);
779 }
780
781 #[test]
782 fn test_extract_accessed_views_without_sql_source() {
783 use crate::schema::AutoParams;
784
785 let query_def = QueryDefinition {
786 name: "customQuery".to_string(),
787 return_type: "Custom".to_string(),
788 returns_list: false,
789 nullable: false,
790 arguments: vec![],
791 sql_source: None, description: None,
793 auto_params: AutoParams {
794 has_where: false,
795 has_order_by: false,
796 has_limit: false,
797 has_offset: false,
798 },
799 deprecation: None,
800 jsonb_column: "data".to_string(),
801 relay: false,
802 relay_cursor_column: None,
803 relay_cursor_type: CursorType::default(),
804 inject_params: IndexMap::default(),
805 cache_ttl_seconds: None,
806 additional_views: vec![],
807 requires_role: None,
808 rest_path: None,
809 rest_method: None,
810 native_columns: HashMap::new(),
811 };
812
813 let views = extract_accessed_views(&query_def);
814 assert_eq!(views, Vec::<String>::new());
815 }
816
817 #[test]
818 fn test_extract_accessed_views_with_additional_views() {
819 use crate::schema::AutoParams;
820
821 let query_def = QueryDefinition {
822 name: "usersWithPosts".to_string(),
823 return_type: "UserWithPosts".to_string(),
824 returns_list: true,
825 nullable: false,
826 arguments: vec![],
827 sql_source: Some("v_user_with_posts".to_string()),
828 description: None,
829 auto_params: AutoParams::default(),
830 deprecation: None,
831 jsonb_column: "data".to_string(),
832 relay: false,
833 relay_cursor_column: None,
834 relay_cursor_type: CursorType::default(),
835 inject_params: IndexMap::default(),
836 cache_ttl_seconds: None,
837 additional_views: vec!["v_post".to_string(), "v_tag".to_string()],
838 requires_role: None,
839 rest_path: None,
840 rest_method: None,
841 native_columns: HashMap::new(),
842 };
843
844 let views = extract_accessed_views(&query_def);
845 assert_eq!(views, vec!["v_user_with_posts", "v_post", "v_tag"]);
846 }
847
848 #[test]
853 fn test_empty_query_string() {
854 let _key = generate_cache_key("", &json!({}), None, "v1");
856 }
857
858 #[test]
859 fn test_null_variables() {
860 let _key = generate_cache_key("query { users }", &json!(null), None, "v1");
862 }
863
864 #[test]
865 fn test_large_variable_object() {
866 let large_vars = json!({
867 "filter": {
868 "age": 30,
869 "active": true,
870 "tags": ["rust", "graphql", "database"],
871 "metadata": {
872 "created_after": "2024-01-01",
873 "updated_before": "2024-12-31"
874 }
875 }
876 });
877
878 let _key = generate_cache_key("query { users }", &large_vars, None, "v1");
880 }
881
882 #[test]
883 fn test_special_characters_in_query() {
884 let query = r#"query { user(email: "test@example.com") { name } }"#;
885 let _key = generate_cache_key(query, &json!({}), None, "v1");
887 }
888
889 #[test]
894 fn test_view_key_different_order_by_produces_different_keys() {
895 use crate::db::{OrderByClause, OrderDirection};
896
897 let asc = [OrderByClause::new("name".into(), OrderDirection::Asc)];
898 let desc = [OrderByClause::new("name".into(), OrderDirection::Desc)];
899
900 let key_asc = generate_view_query_key("v_user", None, None, None, Some(&asc), "v1");
901 let key_desc = generate_view_query_key("v_user", None, None, None, Some(&desc), "v1");
902
903 assert_ne!(key_asc, key_desc, "Different order directions must produce different keys");
904 }
905
906 #[test]
907 fn test_view_key_same_order_by_produces_same_key() {
908 use crate::db::{OrderByClause, OrderDirection};
909
910 let clauses = [OrderByClause::new("createdAt".into(), OrderDirection::Desc)];
911
912 let key1 = generate_view_query_key("v_user", None, None, None, Some(&clauses), "v1");
913 let key2 = generate_view_query_key("v_user", None, None, None, Some(&clauses), "v1");
914
915 assert_eq!(key1, key2, "Same order_by must produce identical keys");
916 }
917
918 #[test]
919 fn test_view_key_with_and_without_order_by() {
920 use crate::db::{OrderByClause, OrderDirection};
921
922 let clauses = [OrderByClause::new("name".into(), OrderDirection::Asc)];
923
924 let key_with = generate_view_query_key("v_user", None, None, None, Some(&clauses), "v1");
925 let key_without = generate_view_query_key("v_user", None, None, None, None, "v1");
926
927 assert_ne!(key_with, key_without, "Presence of order_by must change key");
928 }
929
930 #[test]
931 fn test_view_key_different_fields_produce_different_keys() {
932 use crate::db::{OrderByClause, OrderDirection};
933
934 let by_name = [OrderByClause::new("name".into(), OrderDirection::Asc)];
935 let by_date = [OrderByClause::new("createdAt".into(), OrderDirection::Asc)];
936
937 let key_name = generate_view_query_key("v_user", None, None, None, Some(&by_name), "v1");
938 let key_date = generate_view_query_key("v_user", None, None, None, Some(&by_date), "v1");
939
940 assert_ne!(key_name, key_date, "Different order_by fields must produce different keys");
941 }
942
943 #[test]
944 fn test_projection_key_includes_order_by() {
945 use crate::db::{OrderByClause, OrderDirection};
946
947 let clauses = [OrderByClause::new("name".into(), OrderDirection::Asc)];
948
949 let key_with =
950 generate_projection_query_key("v_user", None, None, None, None, Some(&clauses), "v1");
951 let key_without =
952 generate_projection_query_key("v_user", None, None, None, None, None, "v1");
953
954 assert_ne!(key_with, key_without, "Projection key must include order_by");
955 }
956}