Skip to main content

fraiseql_core/cache/
key.rs

1//! Cache key generation for query results.
2//!
3//! # Security Critical
4//!
5//! This module is **security-critical**. Cache keys MUST include variable values
6//! to prevent data leakage between different users or requests. Incorrect key
7//! generation could allow User A to see User B's cached data.
8//!
9//! # Key Composition
10//!
11//! Cache keys are generated from a single-pass ahash over:
12//! 1. Query string bytes
13//! 2. Recursively hashed variable values (canonical ordering)
14//! 3. WHERE clause structure (hashed structurally, not via serde)
15//! 4. Schema version string
16//!
17//! The hasher uses fixed seeds so that keys are deterministic across restarts.
18//!
19//! # Example
20//!
21//! ```rust
22//! use fraiseql_core::cache::generate_cache_key;
23//! use fraiseql_core::db::{WhereClause, WhereOperator};
24//! use serde_json::json;
25//!
26//! // Two different users querying their own data
27//! let key1 = generate_cache_key(
28//!     "query { user(id: $id) { name } }",
29//!     &json!({"id": "alice"}),
30//!     None,
31//!     "v1"
32//! );
33//!
34//! let key2 = generate_cache_key(
35//!     "query { user(id: $id) { name } }",
36//!     &json!({"id": "bob"}),
37//!     None,
38//!     "v1"
39//! );
40//!
41//! // Different variables MUST produce different keys (security requirement)
42//! assert_ne!(key1, key2);
43//! ```
44
45use 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
55// Fixed seeds for deterministic hashing across process restarts.
56// These are arbitrary constants — changing them invalidates all cached entries.
57const SEED_K0: u64 = 0x5241_4953_454F_4E31; // "RAISEON1"
58const SEED_K1: u64 = 0x4652_4149_5345_514C; // "FRAISEQL"
59const SEED_K2: u64 = 0x4341_4348_454B_4559; // "CACHEKEY"
60const SEED_K3: u64 = 0x5632_5F43_4143_4845; // "V2_CACHE"
61
62/// Create a new hasher from the fixed-seed `RandomState`.
63fn new_hasher() -> impl Hasher {
64    RandomState::with_seeds(SEED_K0, SEED_K1, SEED_K2, SEED_K3).build_hasher()
65}
66
67/// Generate cache key for query result.
68///
69/// # Security Critical
70///
71/// **DIFFERENT VARIABLE VALUES MUST PRODUCE DIFFERENT KEYS** to prevent data
72/// leakage between users. This function feeds the full query, variables, WHERE
73/// clause, and schema version into a single-pass ahash for a fast, deterministic
74/// `u64` key.
75///
76/// # Key Composition
77///
78/// The cache key is a single ahash pass over:
79/// ```text
80/// ahash(
81///   query_bytes          +
82///   hash(variables)      +   ← recursive, canonical key ordering
83///   hash(WHERE_clause)   +   ← structural, not serde-dependent
84///   schema_version_bytes
85/// )
86/// ```
87///
88/// This ensures:
89/// - Same query + variables = same key (cache hit)
90/// - Different variables = different key (security)
91/// - Different WHERE clauses = different key (correctness)
92/// - Schema changes = different key (validity)
93///
94/// # Arguments
95///
96/// * `query` - GraphQL query string
97/// * `variables` - Query variables from GraphQL request (optional)
98/// * `where_clause` - WHERE filter from auto-params (optional)
99/// * `schema_version` - Schema hash from `CompiledSchema`
100///
101/// # Returns
102///
103/// A `u64` cache key suitable for use as a hash-map key.
104///
105/// # Security Examples
106///
107/// ```rust
108/// use fraiseql_core::cache::generate_cache_key;
109/// use serde_json::json;
110///
111/// let query = "query getUser($id: ID!) { user(id: $id) { name } }";
112///
113/// // Different users MUST get different cache keys
114/// let alice_key = generate_cache_key(query, &json!({"id": "alice"}), None, "v1");
115/// let bob_key = generate_cache_key(query, &json!({"id": "bob"}), None, "v1");
116/// assert_ne!(alice_key, bob_key, "Security: different variables must produce different keys");
117///
118/// // Same user MUST get same key (determinism)
119/// let alice_key2 = generate_cache_key(query, &json!({"id": "alice"}), None, "v1");
120/// assert_eq!(alice_key, alice_key2, "Determinism: same inputs must produce same key");
121/// ```
122#[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    // Domain-separate the four sections with unique tags so that, e.g.,
132    // a query ending with "v1" and an empty schema_version can never
133    // collide with a shorter query and schema_version = "v1".
134    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/// Fast cache key for a view query — **zero heap allocations**.
155///
156/// Hashes `view + where_clause + limit + offset + schema_version` directly
157/// without constructing an intermediate `String` or `serde_json::Value`.
158/// Use this instead of [`generate_cache_key`] in the cache adapter hot path.
159///
160/// Domain tag `"v:"` separates these keys from projection keys (`"p:"`) and
161/// generic query keys (`"q:"`), preventing cross-path collisions.
162///
163/// # Arguments
164///
165/// * `view` - Database view / table name
166/// * `where_clause` - Optional WHERE filter (e.g. from RLS injection)
167/// * `limit` - Optional row limit
168/// * `offset` - Optional row offset
169/// * `schema_version` - Schema hash from `CompiledSchema::content_hash()`
170#[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/// Fast cache key for a projection query — **zero heap allocations**.
213///
214/// Like [`generate_view_query_key`] but also hashes the projection template.
215/// Domain tag `"p:"` separates these keys from plain view keys.
216///
217/// # Arguments
218///
219/// * `view` - Database view / table name
220/// * `projection` - Optional SQL projection hint (column subset)
221/// * `where_clause` - Optional WHERE filter
222/// * `limit` - Optional row limit
223/// * `offset` - Optional row offset
224/// * `schema_version` - Schema hash from `CompiledSchema::content_hash()`
225#[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
276/// Recursively hash a `serde_json::Value` into the given hasher.
277///
278/// Object keys are sorted before hashing so that insertion order does not
279/// affect the output (critical for variable-order independence).
280fn hash_json_value(h: &mut impl Hasher, value: &JsonValue) {
281    // Write a type discriminant so that `null`, `false`, `0`, `""`, `[]`, and `{}`
282    // all produce distinct hashes.
283    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            // Use the canonical string form so that 1.0 and 1 hash identically
292            // when serde represents them the same way.
293            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            // Sort keys for canonical ordering.
310            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
320/// Hash a `WhereClause` tree structurally.
321///
322/// Uses discriminant tags and recursion so that structurally different clauses
323/// always produce different hash contributions.
324fn 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); // separator
336            }
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        // WhereClause is #[non_exhaustive]; unknown variants get a distinct tag
359        // plus their Debug representation as a conservative fallback.
360        _ => {
361            h.write_u8(b'?');
362            h.write(format!("{clause:?}").as_bytes());
363        },
364    }
365}
366
367/// Hash a `WhereOperator` by its `Debug` representation.
368///
369/// `WhereOperator` is `#[non_exhaustive]` with 40+ variants (including
370/// `Extended(ExtendedOperator)`). Using the `Debug` string is stable across
371/// refactors and automatically covers new variants without maintenance.
372/// Hash a `WhereOperator` without allocating.
373///
374/// Uses `std::mem::discriminant` for the variant tag (zero-allocation).
375/// For the `Extended(op)` variant which carries data, also hashes the
376/// Debug representation of the inner operator (rare path, acceptable allocation).
377fn hash_where_operator(h: &mut impl Hasher, op: &WhereOperator) {
378    // discriminant is a fixed-size hashable value — no allocation
379    std::mem::discriminant(op).hash(h);
380
381    // Extended operators carry inner data that affects the hash.
382    // All other variants are fully distinguished by their discriminant.
383    if let WhereOperator::Extended(inner) = op {
384        // Rare path: Extended operators are uncommon. The Debug allocation
385        // here is acceptable because it only triggers for rich-filter queries.
386        let inner_str = format!("{inner:?}");
387        h.write(inner_str.as_bytes());
388    }
389}
390
391/// Hash an optional `OrderByClause` slice into the given hasher.
392///
393/// Hashes each clause's `storage_key()` (`snake_case`) and `direction` discriminant,
394/// ensuring that different orderings produce different cache keys.
395fn 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/// Extract accessed views from query definition.
411///
412/// We track which database views/tables a query accesses for view-based
413/// cache invalidation. When a mutation modifies a view, we can invalidate
414/// all cached queries that read from that view.
415///
416/// # Current Scope
417///
418/// Currently extracts only the primary SQL source from the query definition.
419/// Does not analyze:
420/// - JOIN clauses (requires compiled SQL)
421/// - Resolver chains (requires runtime context)
422/// - Nested queries (requires query analyzer)
423///
424/// # Future Enhancements
425///
426/// - Extract views from JOIN clauses in compiled SQL
427/// - Extract views from resolver chains
428/// - Support for custom resolver view tracking
429/// - Entity-level tracking (extract IDs from results)
430///
431/// # Arguments
432///
433/// * `query_def` - The compiled query definition from schema
434///
435/// # Returns
436///
437/// List of view/table names accessed by this query
438///
439/// # Examples
440///
441/// ```rust
442/// use fraiseql_core::cache::extract_accessed_views;
443/// use fraiseql_core::schema::QueryDefinition;
444///
445/// let query_def = QueryDefinition::new("users", "User")
446///     .returning_list()
447///     .with_sql_source("v_user");
448///
449/// let views = extract_accessed_views(&query_def);
450/// assert_eq!(views, vec!["v_user"]);
451/// ```
452#[must_use]
453pub fn extract_accessed_views(query_def: &QueryDefinition) -> Vec<String> {
454    let mut views = Vec::new();
455
456    // Add primary SQL source
457    if let Some(sql_source) = &query_def.sql_source {
458        views.push(sql_source.clone());
459    }
460
461    // Add developer-declared secondary views (JOINs, nested queries, etc.)
462    // Required for correct invalidation when a query reads from multiple views.
463    views.extend(query_def.additional_views.iter().cloned());
464
465    views
466}
467
468/// Verify cache key generation is deterministic.
469///
470/// Used in testing to ensure cache hits work correctly.
471/// Same inputs must always produce the same key.
472///
473/// # Arguments
474///
475/// * `query` - GraphQL query string
476/// * `variables` - Query variables
477/// * `schema_version` - Schema version hash
478///
479/// # Returns
480///
481/// `true` if two sequential key generations produce identical keys
482#[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    // ========================================================================
501    // Security Tests (CRITICAL)
502    // ========================================================================
503
504    #[test]
505    fn test_different_variables_produce_different_keys() {
506        // SECURITY CRITICAL: Different variables MUST produce different keys
507        // to prevent User A from seeing User B's cached data
508        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        // Object keys are sorted before hashing, so insertion order should
548        // not affect the result. serde_json's default Map is BTreeMap (sorted),
549        // but we sort explicitly in hash_json_value to be safe regardless.
550        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    // ========================================================================
559    // Determinism Tests
560    // ========================================================================
561
562    #[test]
563    fn test_cache_key_deterministic() {
564        // Same inputs must always produce same output
565        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    // ========================================================================
583    // WHERE Clause Tests
584    // ========================================================================
585
586    #[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        // Should not panic; produces a valid u64.
664        let _key = generate_cache_key(query, &json!({}), Some(&where_clause), "v1");
665    }
666
667    // ========================================================================
668    // Schema Version Tests
669    // ========================================================================
670
671    #[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        // When schema changes, all cache keys change (automatic invalidation)
684        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    // ========================================================================
696    // Collision Avoidance Test
697    // ========================================================================
698
699    #[test]
700    fn test_no_collisions_in_sample() {
701        // Generate a sample of cache keys from varied inputs and verify
702        // that no two distinct inputs produce the same u64.
703        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    // ========================================================================
742    // Extract Views Tests
743    // ========================================================================
744
745    #[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, // No SQL source (custom resolver)
792            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    // ========================================================================
849    // Edge Case Tests
850    // ========================================================================
851
852    #[test]
853    fn test_empty_query_string() {
854        // Should not panic; produces a valid u64.
855        let _key = generate_cache_key("", &json!({}), None, "v1");
856    }
857
858    #[test]
859    fn test_null_variables() {
860        // Should not panic; produces a valid u64.
861        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        // Should not panic; produces a valid u64.
879        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        // Should not panic; produces a valid u64.
886        let _key = generate_cache_key(query, &json!({}), None, "v1");
887    }
888
889    // ========================================================================
890    // ORDER BY Cache Key Tests
891    // ========================================================================
892
893    #[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}