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}