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:
12//! 1. Query string + variables (via APQ's security-audited `hash_query_with_variables`)
13//! 2. WHERE clause structure (ensures different filters = different keys)
14//! 3. Schema version (auto-invalidates on schema changes)
15//!
16//! # Example
17//!
18//! ```rust
19//! use fraiseql_core::cache::generate_cache_key;
20//! use fraiseql_core::db::{WhereClause, WhereOperator};
21//! use serde_json::json;
22//!
23//! // Two different users querying their own data
24//! let key1 = generate_cache_key(
25//!     "query { user(id: $id) { name } }",
26//!     &json!({"id": "alice"}),
27//!     None,
28//!     "v1"
29//! );
30//!
31//! let key2 = generate_cache_key(
32//!     "query { user(id: $id) { name } }",
33//!     &json!({"id": "bob"}),
34//!     None,
35//!     "v1"
36//! );
37//!
38//! // Different variables MUST produce different keys (security requirement)
39//! assert_ne!(key1, key2);
40//! ```
41
42use serde_json::Value as JsonValue;
43use sha2::{Digest, Sha256};
44
45use crate::{
46    apq::hasher::hash_query_with_variables, db::where_clause::WhereClause, schema::QueryDefinition,
47};
48
49/// Generate cache key for query result.
50///
51/// # Security Critical
52///
53/// **DIFFERENT VARIABLE VALUES MUST PRODUCE DIFFERENT KEYS** to prevent data
54/// leakage between users. This function leverages APQ's security-audited
55/// `hash_query_with_variables()` which correctly handles variable normalization.
56///
57/// # Key Composition
58///
59/// The cache key is a SHA-256 hash of:
60/// ```text
61/// SHA256(
62///   hash_query_with_variables(query, variables) +
63///   WHERE_clause_structure +
64///   schema_version
65/// )
66/// ```
67///
68/// This ensures:
69/// - Same query + variables = same key (cache hit)
70/// - Different variables = different key (security)
71/// - Different WHERE clauses = different key (correctness)
72/// - Schema changes = different key (validity)
73///
74/// # Arguments
75///
76/// * `query` - GraphQL query string
77/// * `variables` - Query variables from GraphQL request (optional)
78/// * `where_clause` - WHERE filter from auto-params (optional)
79/// * `schema_version` - Schema hash from `CompiledSchema`
80///
81/// # Returns
82///
83/// 64-character hex string (SHA-256 hash)
84///
85/// # Security Examples
86///
87/// ```rust
88/// use fraiseql_core::cache::generate_cache_key;
89/// use serde_json::json;
90///
91/// let query = "query getUser($id: ID!) { user(id: $id) { name } }";
92///
93/// // Different users MUST get different cache keys
94/// let alice_key = generate_cache_key(query, &json!({"id": "alice"}), None, "v1");
95/// let bob_key = generate_cache_key(query, &json!({"id": "bob"}), None, "v1");
96/// assert_ne!(alice_key, bob_key, "Security: different variables must produce different keys");
97///
98/// // Same user MUST get same key (determinism)
99/// let alice_key2 = generate_cache_key(query, &json!({"id": "alice"}), None, "v1");
100/// assert_eq!(alice_key, alice_key2, "Determinism: same inputs must produce same key");
101/// ```
102///
103/// # Examples
104///
105/// ```rust
106/// use fraiseql_core::cache::generate_cache_key;
107/// use fraiseql_core::db::{WhereClause, WhereOperator};
108/// use serde_json::json;
109///
110/// // Simple query with variables
111/// let key = generate_cache_key(
112///     "query { users(limit: $limit) { id } }",
113///     &json!({"limit": 10}),
114///     None,
115///     "abc123"
116/// );
117/// assert_eq!(key.len(), 64); // SHA-256 hex
118///
119/// // Query with WHERE clause
120/// let where_clause = WhereClause::Field {
121///     path: vec!["email".to_string()],
122///     operator: WhereOperator::Icontains,
123///     value: json!("example.com"),
124/// };
125///
126/// let key_with_where = generate_cache_key(
127///     "query { users { id } }",
128///     &json!({}),
129///     Some(&where_clause),
130///     "abc123"
131/// );
132/// assert_eq!(key_with_where.len(), 64);
133/// ```
134#[must_use]
135pub fn generate_cache_key(
136    query: &str,
137    variables: &JsonValue,
138    where_clause: Option<&WhereClause>,
139    schema_version: &str,
140) -> String {
141    // Step 1: Base key from APQ (query + variables)
142    // This is security-audited and handles variable ordering correctly
143    // Different variables WILL produce different hashes (critical for security)
144    let base_key = hash_query_with_variables(query, variables);
145
146    // Step 2: Add WHERE clause structure if present
147    // Different WHERE clauses must produce different keys for correctness
148    // Using Debug format captures full structure including operators and values
149    let where_structure = where_clause.map(|w| format!("{:?}", w)).unwrap_or_default();
150
151    // Step 3: Combine with schema version
152    // Schema changes invalidate all cached queries automatically
153    let combined = format!("{}:{}:{}", base_key, where_structure, schema_version);
154
155    // Step 4: Hash the combination for final cache key
156    // SHA-256 provides collision resistance and fixed-length output
157    let mut hasher = Sha256::new();
158    hasher.update(combined.as_bytes());
159    hex::encode(hasher.finalize())
160}
161
162/// Extract accessed views from query definition.
163///
164/// We track which database views/tables a query accesses for view-based
165/// cache invalidation. When a mutation modifies a view, we can invalidate
166/// all cached queries that read from that view.
167///
168/// # Current Scope
169///
170/// Currently extracts only the primary SQL source from the query definition.
171/// Does not analyze:
172/// - JOIN clauses (requires compiled SQL)
173/// - Resolver chains (requires runtime context)
174/// - Nested queries (requires query analyzer)
175///
176/// # Future Enhancements
177///
178/// - Extract views from JOIN clauses in compiled SQL
179/// - Extract views from resolver chains
180/// - Support for custom resolver view tracking
181/// - Entity-level tracking (extract IDs from results)
182///
183/// # Arguments
184///
185/// * `query_def` - The compiled query definition from schema
186///
187/// # Returns
188///
189/// List of view/table names accessed by this query
190///
191/// # Examples
192///
193/// ```ignore
194/// use fraiseql_core::cache::extract_accessed_views;
195/// use fraiseql_core::schema::{QueryDefinition, AutoParams};
196///
197/// let query_def = QueryDefinition {
198///     name: "users".to_string(),
199///     return_type: "User".to_string(),
200///     returns_list: true,
201///     nullable: false,
202///     arguments: vec![],
203///     sql_source: Some("v_user".to_string()),
204///     auto_params: AutoParams {
205///         has_where: true,
206///         has_order_by: false,
207///         has_limit: true,
208///         has_offset: false,
209///     },
210/// };
211///
212/// let views = extract_accessed_views(&query_def);
213/// assert_eq!(views, vec!["v_user"]);
214/// ```
215#[must_use]
216pub fn extract_accessed_views(query_def: &QueryDefinition) -> Vec<String> {
217    let mut views = Vec::new();
218
219    // Add primary SQL source
220    // Note: FraiseQL uses single-table compiled templates (no JOINs or resolver chains),
221    // so the sql_source is the complete set of accessed views for cache invalidation.
222    if let Some(sql_source) = &query_def.sql_source {
223        views.push(sql_source.clone());
224    }
225
226    views
227}
228
229/// Verify cache key generation is deterministic.
230///
231/// Used in testing to ensure cache hits work correctly.
232/// Same inputs must always produce the same key.
233///
234/// # Arguments
235///
236/// * `query` - GraphQL query string
237/// * `variables` - Query variables
238/// * `schema_version` - Schema version hash
239///
240/// # Returns
241///
242/// `true` if two sequential key generations produce identical keys
243///
244/// # Example
245///
246/// ```rust
247/// use fraiseql_core::cache::verify_deterministic;
248/// use serde_json::json;
249///
250/// assert!(verify_deterministic(
251///     "query { users { id } }",
252///     &json!({}),
253///     "v1"
254/// ));
255/// ```
256#[cfg(test)]
257#[must_use]
258pub fn verify_deterministic(query: &str, variables: &JsonValue, schema_version: &str) -> bool {
259    let key1 = generate_cache_key(query, variables, None, schema_version);
260    let key2 = generate_cache_key(query, variables, None, schema_version);
261    key1 == key2
262}
263
264#[cfg(test)]
265mod tests {
266    use serde_json::json;
267
268    use super::*;
269    use crate::db::WhereOperator;
270
271    // ========================================================================
272    // Security Tests (CRITICAL)
273    // ========================================================================
274
275    #[test]
276    fn test_different_variables_produce_different_keys() {
277        // SECURITY CRITICAL: Different variables MUST produce different keys
278        // to prevent User A from seeing User B's cached data
279        let query = "query getUser($id: ID!) { user(id: $id) { name email } }";
280
281        let key_alice = generate_cache_key(query, &json!({"id": "alice"}), None, "v1");
282        let key_bob = generate_cache_key(query, &json!({"id": "bob"}), None, "v1");
283
284        assert_ne!(
285            key_alice, key_bob,
286            "SECURITY: Different variables MUST produce different cache keys"
287        );
288    }
289
290    #[test]
291    fn test_different_variable_values_produce_different_keys() {
292        let query = "query getUsers($limit: Int!) { users(limit: $limit) { id } }";
293
294        let key_10 = generate_cache_key(query, &json!({"limit": 10}), None, "v1");
295        let key_20 = generate_cache_key(query, &json!({"limit": 20}), None, "v1");
296
297        assert_ne!(
298            key_10, key_20,
299            "SECURITY: Different variable values MUST produce different keys"
300        );
301    }
302
303    #[test]
304    fn test_empty_vs_non_empty_variables() {
305        let query = "query { users { id } }";
306
307        let key_empty = generate_cache_key(query, &json!({}), None, "v1");
308        let key_with_vars = generate_cache_key(query, &json!({"limit": 10}), None, "v1");
309
310        assert_ne!(
311            key_empty, key_with_vars,
312            "Empty variables must produce different key than non-empty"
313        );
314    }
315
316    #[test]
317    fn test_variable_order_independence() {
318        // APQ handles variable ordering, so this should be deterministic
319        let query = "query($a: Int, $b: Int) { users { id } }";
320
321        // Note: serde_json maintains insertion order, so we can't easily test
322        // reordering without custom JSON construction. This test documents
323        // the expectation that APQ handles this correctly.
324        let key1 = generate_cache_key(query, &json!({"a": 1, "b": 2}), None, "v1");
325        let key2 = generate_cache_key(query, &json!({"a": 1, "b": 2}), None, "v1");
326
327        assert_eq!(key1, key2, "Same variables must produce same key");
328    }
329
330    // ========================================================================
331    // Determinism Tests
332    // ========================================================================
333
334    #[test]
335    fn test_cache_key_deterministic() {
336        // Same inputs must always produce same output
337        let query = "query { users { id } }";
338        let vars = json!({"limit": 10});
339
340        let key1 = generate_cache_key(query, &vars, None, "v1");
341        let key2 = generate_cache_key(query, &vars, None, "v1");
342
343        assert_eq!(key1, key2, "Cache keys must be deterministic");
344    }
345
346    #[test]
347    fn test_verify_deterministic_helper() {
348        assert!(
349            verify_deterministic("query { users }", &json!({}), "v1"),
350            "Helper should verify determinism"
351        );
352    }
353
354    // ========================================================================
355    // WHERE Clause Tests
356    // ========================================================================
357
358    #[test]
359    fn test_different_where_clauses_produce_different_keys() {
360        let query = "query { users { id } }";
361
362        let where1 = WhereClause::Field {
363            path:     vec!["email".to_string()],
364            operator: WhereOperator::Eq,
365            value:    json!("alice@example.com"),
366        };
367
368        let where2 = WhereClause::Field {
369            path:     vec!["email".to_string()],
370            operator: WhereOperator::Eq,
371            value:    json!("bob@example.com"),
372        };
373
374        let key1 = generate_cache_key(query, &json!({}), Some(&where1), "v1");
375        let key2 = generate_cache_key(query, &json!({}), Some(&where2), "v1");
376
377        assert_ne!(key1, key2, "Different WHERE clauses must produce different keys");
378    }
379
380    #[test]
381    fn test_different_where_operators_produce_different_keys() {
382        let query = "query { users { id } }";
383
384        let where_eq = WhereClause::Field {
385            path:     vec!["age".to_string()],
386            operator: WhereOperator::Eq,
387            value:    json!(30),
388        };
389
390        let where_gt = WhereClause::Field {
391            path:     vec!["age".to_string()],
392            operator: WhereOperator::Gt,
393            value:    json!(30),
394        };
395
396        let key_eq = generate_cache_key(query, &json!({}), Some(&where_eq), "v1");
397        let key_gt = generate_cache_key(query, &json!({}), Some(&where_gt), "v1");
398
399        assert_ne!(key_eq, key_gt, "Different operators must produce different keys");
400    }
401
402    #[test]
403    fn test_with_and_without_where_clause() {
404        let query = "query { users { id } }";
405
406        let where_clause = WhereClause::Field {
407            path:     vec!["active".to_string()],
408            operator: WhereOperator::Eq,
409            value:    json!(true),
410        };
411
412        let key_without = generate_cache_key(query, &json!({}), None, "v1");
413        let key_with = generate_cache_key(query, &json!({}), Some(&where_clause), "v1");
414
415        assert_ne!(key_without, key_with, "Presence of WHERE clause must change key");
416    }
417
418    #[test]
419    fn test_complex_where_clause() {
420        let query = "query { users { id } }";
421
422        let where_clause = WhereClause::And(vec![
423            WhereClause::Field {
424                path:     vec!["age".to_string()],
425                operator: WhereOperator::Gte,
426                value:    json!(18),
427            },
428            WhereClause::Field {
429                path:     vec!["active".to_string()],
430                operator: WhereOperator::Eq,
431                value:    json!(true),
432            },
433        ]);
434
435        let key = generate_cache_key(query, &json!({}), Some(&where_clause), "v1");
436
437        assert_eq!(key.len(), 64, "Should produce valid SHA-256 hex");
438    }
439
440    // ========================================================================
441    // Schema Version Tests
442    // ========================================================================
443
444    #[test]
445    fn test_different_schema_versions_produce_different_keys() {
446        let query = "query { users { id } }";
447
448        let key_v1 = generate_cache_key(query, &json!({}), None, "v1");
449        let key_v2 = generate_cache_key(query, &json!({}), None, "v2");
450
451        assert_ne!(key_v1, key_v2, "Different schema versions must produce different keys");
452    }
453
454    #[test]
455    fn test_schema_version_invalidation() {
456        // When schema changes, all cache keys change (automatic invalidation)
457        let query = "query { users { id } }";
458
459        let old_schema = "abc123";
460        let new_schema = "def456";
461
462        let key_old = generate_cache_key(query, &json!({}), None, old_schema);
463        let key_new = generate_cache_key(query, &json!({}), None, new_schema);
464
465        assert_ne!(key_old, key_new, "Schema changes should invalidate cache");
466    }
467
468    // ========================================================================
469    // Output Format Tests
470    // ========================================================================
471
472    #[test]
473    fn test_cache_key_length() {
474        let key = generate_cache_key("query { users }", &json!({}), None, "v1");
475        assert_eq!(key.len(), 64, "SHA-256 hex should be 64 characters");
476    }
477
478    #[test]
479    fn test_cache_key_format() {
480        let key = generate_cache_key("query { users }", &json!({}), None, "v1");
481
482        // Verify it's valid hexadecimal
483        assert!(key.chars().all(|c| c.is_ascii_hexdigit()), "Cache key should be hexadecimal");
484    }
485
486    // ========================================================================
487    // Extract Views Tests
488    // ========================================================================
489
490    #[test]
491    fn test_extract_accessed_views_with_sql_source() {
492        use crate::schema::AutoParams;
493
494        let query_def = QueryDefinition {
495            name:         "users".to_string(),
496            return_type:  "User".to_string(),
497            returns_list: true,
498            nullable:     false,
499            arguments:    vec![],
500            sql_source:   Some("v_user".to_string()),
501            description:  None,
502            auto_params:  AutoParams {
503                has_where:    true,
504                has_order_by: false,
505                has_limit:    true,
506                has_offset:   false,
507            },
508            deprecation:  None,
509            jsonb_column: "data".to_string(),
510        };
511
512        let views = extract_accessed_views(&query_def);
513        assert_eq!(views, vec!["v_user"]);
514    }
515
516    #[test]
517    fn test_extract_accessed_views_without_sql_source() {
518        use crate::schema::AutoParams;
519
520        let query_def = QueryDefinition {
521            name:         "customQuery".to_string(),
522            return_type:  "Custom".to_string(),
523            returns_list: false,
524            nullable:     false,
525            arguments:    vec![],
526            sql_source:   None, // No SQL source (custom resolver)
527            description:  None,
528            auto_params:  AutoParams {
529                has_where:    false,
530                has_order_by: false,
531                has_limit:    false,
532                has_offset:   false,
533            },
534            deprecation:  None,
535            jsonb_column: "data".to_string(),
536        };
537
538        let views = extract_accessed_views(&query_def);
539        assert_eq!(views, Vec::<String>::new());
540    }
541
542    // ========================================================================
543    // Edge Case Tests
544    // ========================================================================
545
546    #[test]
547    fn test_empty_query_string() {
548        let key = generate_cache_key("", &json!({}), None, "v1");
549        assert_eq!(key.len(), 64, "Empty query should still produce valid key");
550    }
551
552    #[test]
553    fn test_null_variables() {
554        let key = generate_cache_key("query { users }", &json!(null), None, "v1");
555        assert_eq!(key.len(), 64, "Null variables should produce valid key");
556    }
557
558    #[test]
559    fn test_large_variable_object() {
560        let large_vars = json!({
561            "filter": {
562                "age": 30,
563                "active": true,
564                "tags": ["rust", "graphql", "database"],
565                "metadata": {
566                    "created_after": "2024-01-01",
567                    "updated_before": "2024-12-31"
568                }
569            }
570        });
571
572        let key = generate_cache_key("query { users }", &large_vars, None, "v1");
573        assert_eq!(key.len(), 64, "Large variables should produce valid key");
574    }
575
576    #[test]
577    fn test_special_characters_in_query() {
578        let query = r#"query { user(email: "test@example.com") { name } }"#;
579        let key = generate_cache_key(query, &json!({}), None, "v1");
580        assert_eq!(key.len(), 64, "Special characters should be handled");
581    }
582}