Skip to main content

query_flow/
runtime.rs

1//! Query runtime and context.
2
3use std::any::{Any, TypeId};
4use std::cell::RefCell;
5use std::ops::Deref;
6use std::rc::Rc;
7use std::sync::Arc;
8
9use whale::{Durability, GetOrInsertResult, RevisionCounter, Runtime as WhaleRuntime};
10
11use crate::asset::{AssetKey, AssetLocator, DurabilityLevel, PendingAsset};
12use crate::db::Db;
13use crate::key::{
14    AssetCacheKey, AssetKeySetSentinelKey, FullCacheKey, QueryCacheKey, QuerySetSentinelKey,
15};
16use crate::loading::AssetLoadingState;
17use crate::query::Query;
18use crate::storage::{
19    AssetKeyRegistry, CachedEntry, CachedValue, ErasedLocateResult, LocatorStorage, PendingStorage,
20    QueryRegistry, VerifierStorage,
21};
22use crate::tracer::{
23    ExecutionResult, InvalidationReason, NoopTracer, SpanContext, SpanId, TraceId, Tracer,
24    TracerAssetState,
25};
26use crate::QueryError;
27
28/// Function type for comparing user errors during early cutoff.
29///
30/// Used by `QueryRuntimeBuilder::error_comparator` to customize how
31/// `QueryError::UserError` values are compared for caching purposes.
32pub type ErrorComparator = fn(&anyhow::Error, &anyhow::Error) -> bool;
33
34/// Number of durability levels (matches whale's default).
35const DURABILITY_LEVELS: usize = 4;
36
37// Thread-local query execution stack for cycle detection.
38thread_local! {
39    static QUERY_STACK: RefCell<Vec<FullCacheKey>> = const { RefCell::new(Vec::new()) };
40
41    /// Current consistency tracker for leaf asset checks.
42    /// Set during query execution, used by all nested locator calls.
43    /// Uses Rc since thread-local is single-threaded.
44    static CONSISTENCY_TRACKER: RefCell<Option<Rc<ConsistencyTracker>>> = const { RefCell::new(None) };
45
46    /// Stack for tracking parent-child query relationships.
47    static SPAN_STACK: RefCell<SpanStack> = const { RefCell::new(SpanStack::Empty) };
48}
49
50/// Thread-local span stack state.
51/// Empty when no query is executing; Active with trace_id and span stack during execution.
52enum SpanStack {
53    Empty,
54    Active(TraceId, Vec<SpanId>),
55}
56
57/// Check a leaf asset against the current consistency tracker (if any).
58/// Returns Ok if no tracker is set or if the check passes.
59fn check_leaf_asset_consistency(dep_changed_at: RevisionCounter) -> Result<(), QueryError> {
60    CONSISTENCY_TRACKER.with(|tracker| {
61        if let Some(ref t) = *tracker.borrow() {
62            t.check_leaf_asset(dep_changed_at)
63        } else {
64            Ok(())
65        }
66    })
67}
68
69/// RAII guard that sets the consistency tracker for the current thread.
70struct ConsistencyTrackerGuard {
71    previous: Option<Rc<ConsistencyTracker>>,
72}
73
74impl ConsistencyTrackerGuard {
75    fn new(tracker: Rc<ConsistencyTracker>) -> Self {
76        let previous = CONSISTENCY_TRACKER.with(|t| t.borrow_mut().replace(tracker));
77        Self { previous }
78    }
79}
80
81impl Drop for ConsistencyTrackerGuard {
82    fn drop(&mut self) {
83        CONSISTENCY_TRACKER.with(|t| {
84            *t.borrow_mut() = self.previous.take();
85        });
86    }
87}
88
89/// Check for cycles in the query stack and return error if detected.
90fn check_cycle(key: &FullCacheKey) -> Result<(), QueryError> {
91    let cycle_detected = QUERY_STACK.with(|stack| stack.borrow().iter().any(|k| k == key));
92    if cycle_detected {
93        let path = QUERY_STACK.with(|stack| {
94            let stack = stack.borrow();
95            let mut path: Vec<FullCacheKey> = stack.iter().cloned().collect();
96            path.push(key.clone());
97            path
98        });
99        return Err(QueryError::Cycle { path });
100    }
101    Ok(())
102}
103
104/// RAII guard for pushing/popping from query stack.
105struct StackGuard;
106
107impl StackGuard {
108    fn push(key: FullCacheKey) -> Self {
109        QUERY_STACK.with(|stack| stack.borrow_mut().push(key));
110        StackGuard
111    }
112}
113
114impl Drop for StackGuard {
115    fn drop(&mut self) {
116        QUERY_STACK.with(|stack| {
117            stack.borrow_mut().pop();
118        });
119    }
120}
121
122/// RAII guard for pushing/popping from span stack.
123struct SpanStackGuard;
124
125impl SpanStackGuard {
126    /// Push a span onto the stack. Sets trace_id if this is the root span.
127    fn push(trace_id: TraceId, span_id: SpanId) -> Self {
128        SPAN_STACK.with(|stack| {
129            let mut s = stack.borrow_mut();
130            match &mut *s {
131                SpanStack::Empty => *s = SpanStack::Active(trace_id, vec![span_id]),
132                SpanStack::Active(_, spans) => spans.push(span_id),
133            }
134        });
135        SpanStackGuard
136    }
137}
138
139impl Drop for SpanStackGuard {
140    fn drop(&mut self) {
141        SPAN_STACK.with(|stack| {
142            let mut s = stack.borrow_mut();
143            if let SpanStack::Active(_, spans) = &mut *s {
144                spans.pop();
145                if spans.is_empty() {
146                    *s = SpanStack::Empty;
147                }
148            }
149        });
150    }
151}
152
153/// Execution context passed through query execution.
154///
155/// Contains a SpanContext for tracing correlation with parent-child relationships.
156#[derive(Clone, Copy)]
157pub struct ExecutionContext {
158    span_ctx: SpanContext,
159}
160
161impl ExecutionContext {
162    /// Create a new execution context with the given span context.
163    #[inline]
164    pub fn new(span_ctx: SpanContext) -> Self {
165        Self { span_ctx }
166    }
167
168    /// Get the span context for this execution context.
169    #[inline]
170    pub fn span_ctx(&self) -> &SpanContext {
171        &self.span_ctx
172    }
173}
174
175/// Result of polling a query, containing the value and its revision.
176///
177/// This is returned by [`QueryRuntime::poll`] and provides both the query result
178/// and its change revision, enabling efficient change detection for subscription
179/// patterns.
180///
181/// # Example
182///
183/// ```
184/// use query_flow::{query, Db, QueryError, QueryRuntime, RevisionCounter};
185///
186/// #[query]
187/// fn my_query(db: &impl Db) -> Result<i32, QueryError> {
188///     let _ = db;
189///     Ok(42)
190/// }
191///
192/// let runtime = QueryRuntime::new();
193/// let mut last_known_revision: RevisionCounter = 0;
194///
195/// let result = runtime.poll(MyQuery::new()).unwrap();
196///
197/// // `value` is `Result<Arc<Output>, Arc<anyhow::Error>>`: `Ok` for a successful
198/// // query, `Err` for a cached user error.
199/// match &result.value {
200///     Ok(value) => assert_eq!(**value, 42),
201///     Err(err) => panic!("query failed: {err}"),
202/// }
203///
204/// // Check if changed since last poll
205/// if result.revision > last_known_revision {
206///     last_known_revision = result.revision;
207/// }
208/// ```
209#[derive(Debug, Clone)]
210pub struct Polled<T> {
211    /// The query result value.
212    pub value: T,
213    /// The revision at which this value was last changed.
214    ///
215    /// Compare this with a previously stored revision to detect changes.
216    pub revision: RevisionCounter,
217}
218
219impl<T: Deref> Deref for Polled<T> {
220    type Target = T::Target;
221
222    fn deref(&self) -> &Self::Target {
223        &self.value
224    }
225}
226
227/// The query runtime manages query execution, caching, and dependency tracking.
228///
229/// This is cheap to clone - all data is behind `Arc`.
230///
231/// # Type Parameter
232///
233/// - `T: Tracer` - The tracer type for observability. Use `NoopTracer` (default)
234///   for zero-cost when tracing is not needed.
235///
236/// # Example
237///
238/// ```
239/// use query_flow::{query, Db, NoopTracer, QueryError, QueryRuntime};
240///
241/// #[query]
242/// fn my_query(db: &impl Db, x: i32) -> Result<i32, QueryError> {
243///     let _ = db;
244///     Ok(x * 2)
245/// }
246///
247/// // Without tracing (default)
248/// let runtime = QueryRuntime::new();
249///
250/// // With tracing (see the `tracer` module for writing a custom tracer)
251/// let tracer = NoopTracer;
252/// let runtime = QueryRuntime::with_tracer(tracer);
253///
254/// // Sync query execution
255/// let result = runtime.query(MyQuery::new(21)).unwrap();
256/// assert_eq!(*result, 42);
257/// ```
258pub struct QueryRuntime<T: Tracer = NoopTracer> {
259    /// Whale runtime for dependency tracking and cache storage.
260    /// Query outputs and asset values are stored in Node.data as Option<CachedEntry>.
261    whale: WhaleRuntime<FullCacheKey, Option<CachedEntry>, DURABILITY_LEVELS>,
262    /// Registered asset locators
263    locators: Arc<LocatorStorage<T>>,
264    /// Pending asset requests
265    pending: Arc<PendingStorage>,
266    /// Registry for tracking query instances (for list_queries)
267    query_registry: Arc<QueryRegistry>,
268    /// Registry for tracking asset keys (for list_asset_keys)
269    asset_key_registry: Arc<AssetKeyRegistry>,
270    /// Verifiers for re-executing queries (for verify-then-decide pattern)
271    verifiers: Arc<VerifierStorage>,
272    /// Comparator for user errors during early cutoff
273    error_comparator: ErrorComparator,
274    /// Tracer for observability
275    tracer: Arc<T>,
276}
277
278#[test]
279fn test_runtime_send_sync() {
280    fn assert_send_sync<T: Send + Sync>() {}
281    assert_send_sync::<QueryRuntime<NoopTracer>>();
282}
283
284impl Default for QueryRuntime<NoopTracer> {
285    fn default() -> Self {
286        Self::new()
287    }
288}
289
290impl<T: Tracer> Clone for QueryRuntime<T> {
291    fn clone(&self) -> Self {
292        Self {
293            whale: self.whale.clone(),
294            locators: self.locators.clone(),
295            pending: self.pending.clone(),
296            query_registry: self.query_registry.clone(),
297            asset_key_registry: self.asset_key_registry.clone(),
298            verifiers: self.verifiers.clone(),
299            error_comparator: self.error_comparator,
300            tracer: self.tracer.clone(),
301        }
302    }
303}
304
305/// Default error comparator that treats all errors as different.
306///
307/// This is conservative - it always triggers recomputation when an error occurs.
308fn default_error_comparator(_a: &anyhow::Error, _b: &anyhow::Error) -> bool {
309    false
310}
311
312impl<T: Tracer> QueryRuntime<T> {
313    /// Get cached output along with its revision (single atomic access).
314    fn get_cached_with_revision<Q: Query>(
315        &self,
316        key: &FullCacheKey,
317    ) -> Option<(CachedValue<Arc<Q::Output>>, RevisionCounter)> {
318        let (data, revision) = self.whale.get_data(key)?;
319        let cached = data.as_ref()?.to_cached_value::<Q::Output>()?;
320        Some((cached, revision))
321    }
322
323    /// Get a reference to the tracer.
324    #[inline]
325    pub fn tracer(&self) -> &T {
326        &self.tracer
327    }
328}
329
330impl QueryRuntime<NoopTracer> {
331    /// Create a new query runtime with default settings.
332    pub fn new() -> Self {
333        Self::with_tracer(NoopTracer)
334    }
335
336    /// Create a builder for customizing the runtime.
337    ///
338    /// # Example
339    ///
340    /// ```
341    /// use std::fmt;
342    ///
343    /// use query_flow::QueryRuntime;
344    ///
345    /// #[derive(Debug, PartialEq)]
346    /// struct MyError(u32);
347    ///
348    /// impl fmt::Display for MyError {
349    ///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350    ///         write!(f, "my error {}", self.0)
351    ///     }
352    /// }
353    ///
354    /// impl std::error::Error for MyError {}
355    ///
356    /// let runtime = QueryRuntime::builder()
357    ///     .error_comparator(|a, b| {
358    ///         // Custom error comparison logic
359    ///         match (a.downcast_ref::<MyError>(), b.downcast_ref::<MyError>()) {
360    ///             (Some(a), Some(b)) => a == b,
361    ///             _ => false,
362    ///         }
363    ///     })
364    ///     .build();
365    /// ```
366    pub fn builder() -> QueryRuntimeBuilder<NoopTracer> {
367        QueryRuntimeBuilder::new()
368    }
369}
370
371impl<T: Tracer> QueryRuntime<T> {
372    /// Create a new query runtime with the specified tracer.
373    pub fn with_tracer(tracer: T) -> Self {
374        QueryRuntimeBuilder::new().tracer(tracer).build()
375    }
376
377    /// Execute a query synchronously.
378    ///
379    /// Returns the cached result if valid, otherwise executes the query.
380    ///
381    /// # Errors
382    ///
383    /// - `QueryError::Suspend` - Query is waiting for async loading
384    /// - `QueryError::Cycle` - Dependency cycle detected
385    pub fn query<Q: Query>(&self, query: Q) -> Result<Arc<Q::Output>, QueryError> {
386        self.query_internal(query)
387            .and_then(|(inner_result, _)| inner_result.map_err(QueryError::UserError))
388    }
389
390    /// Internal implementation shared by query() and poll().
391    ///
392    /// Returns (result, revision) tuple where result is either Ok(output) or Err(user_error).
393    /// System errors (Suspend, Cycle, etc.) are returned as the outer Err.
394    #[allow(clippy::type_complexity)]
395    fn query_internal<Q: Query>(
396        &self,
397        query: Q,
398    ) -> Result<(Result<Arc<Q::Output>, Arc<anyhow::Error>>, RevisionCounter), QueryError> {
399        let query_cache_key = QueryCacheKey::new(query.clone());
400        let full_key: FullCacheKey = query_cache_key.clone().into();
401
402        // Create SpanContext with parent relationship from SPAN_STACK
403        let span_id = self.tracer.new_span_id();
404        let (trace_id, parent_span_id) = SPAN_STACK.with(|stack| match &*stack.borrow() {
405            SpanStack::Empty => (self.tracer.new_trace_id(), None),
406            SpanStack::Active(tid, spans) => (*tid, spans.last().copied()),
407        });
408        let span_ctx = SpanContext {
409            span_id,
410            trace_id,
411            parent_span_id,
412        };
413
414        // Push to span stack and create execution context
415        let _span_guard = SpanStackGuard::push(trace_id, span_id);
416        let exec_ctx = ExecutionContext::new(span_ctx);
417
418        // GC tracking hook: fires on every access, cache hits included, so
419        // external LRU/TTL trackers see the full access pattern.
420        self.tracer.on_query_key(&full_key);
421
422        self.tracer.on_query_start(&span_ctx, &query_cache_key);
423
424        // Check for cycles using thread-local stack
425        let cycle_detected = QUERY_STACK.with(|stack| {
426            let stack = stack.borrow();
427            stack.iter().any(|k| k == &full_key)
428        });
429
430        if cycle_detected {
431            let path = QUERY_STACK.with(|stack| {
432                let stack = stack.borrow();
433                let mut path: Vec<FullCacheKey> = stack.iter().cloned().collect();
434                path.push(full_key.clone());
435                path
436            });
437
438            self.tracer.on_cycle_detected(&path);
439            self.tracer
440                .on_query_end(&span_ctx, &query_cache_key, ExecutionResult::CycleDetected);
441
442            return Err(QueryError::Cycle { path });
443        }
444
445        // Check if cached and valid (with verify-then-decide pattern)
446        let current_rev = self.whale.current_revision();
447
448        // Fast path: already verified at current revision
449        if self.whale.is_verified_at(&full_key, &current_rev) {
450            // Single atomic access to get both cached value and revision
451            if let Some((cached, revision)) = self.get_cached_with_revision::<Q>(&full_key) {
452                self.tracer
453                    .on_cache_check(&span_ctx, &query_cache_key, true);
454                self.tracer
455                    .on_query_end(&span_ctx, &query_cache_key, ExecutionResult::CacheHit);
456
457                return match cached {
458                    CachedValue::Ok(output) => Ok((Ok(output), revision)),
459                    CachedValue::UserError(err) => Ok((Err(err), revision)),
460                };
461            }
462        }
463
464        // Check shallow validity (deps' changed_at ok) and try verify-then-decide
465        if self.whale.is_valid(&full_key) {
466            // Single atomic access to get both cached value and revision
467            if let Some((cached, revision)) = self.get_cached_with_revision::<Q>(&full_key) {
468                // Shallow valid but not verified - verify deps first
469                let mut deps_verified = true;
470                if let Some(deps) = self.whale.get_dependency_ids(&full_key) {
471                    for dep in deps {
472                        if let Some(verifier) = self.verifiers.get(&dep) {
473                            // Re-run query/asset to verify it (triggers recursive verification)
474                            // For assets, this re-accesses the asset which may re-run the locator
475                            if verifier.verify(self as &dyn std::any::Any).is_err() {
476                                deps_verified = false;
477                                break;
478                            }
479                        }
480                        // Note: deps without verifiers are assumed valid (they're verified
481                        // by the final is_valid check if their changed_at increased)
482                    }
483                }
484
485                // Re-check validity after deps are verified
486                if deps_verified && self.whale.is_valid(&full_key) {
487                    // Deps didn't change their changed_at, mark as verified and use cached
488                    self.whale.mark_verified(&full_key, &current_rev);
489
490                    self.tracer
491                        .on_cache_check(&span_ctx, &query_cache_key, true);
492                    self.tracer.on_query_end(
493                        &span_ctx,
494                        &query_cache_key,
495                        ExecutionResult::CacheHit,
496                    );
497
498                    return match cached {
499                        CachedValue::Ok(output) => Ok((Ok(output), revision)),
500                        CachedValue::UserError(err) => Ok((Err(err), revision)),
501                    };
502                }
503                // A dep's changed_at increased, fall through to execute
504            }
505        }
506
507        self.tracer
508            .on_cache_check(&span_ctx, &query_cache_key, false);
509
510        // Execute the query with cycle tracking
511        let _guard = StackGuard::push(full_key.clone());
512        let result = self.execute_query::<Q>(&query, &query_cache_key, &full_key, exec_ctx);
513        drop(_guard);
514
515        // Emit end event
516        let exec_result = match &result {
517            Ok((_, true, _)) => ExecutionResult::Changed,
518            Ok((_, false, _)) => ExecutionResult::Unchanged,
519            Err(QueryError::Suspend { .. }) => ExecutionResult::Suspended,
520            Err(QueryError::Cycle { .. }) => ExecutionResult::CycleDetected,
521            Err(e) => ExecutionResult::Error {
522                message: format!("{:?}", e),
523            },
524        };
525        self.tracer
526            .on_query_end(&span_ctx, &query_cache_key, exec_result);
527
528        result.map(|(inner_result, _, revision)| (inner_result, revision))
529    }
530
531    /// Execute a query, caching the result if appropriate.
532    ///
533    /// Returns (result, output_changed, revision) tuple.
534    /// - `result`: Ok(output) for success, Err(user_error) for user errors
535    /// - System errors (Suspend, Cycle, etc.) are returned as outer Err
536    #[allow(clippy::type_complexity)]
537    fn execute_query<Q: Query>(
538        &self,
539        query: &Q,
540        query_cache_key: &QueryCacheKey,
541        full_key: &FullCacheKey,
542        exec_ctx: ExecutionContext,
543    ) -> Result<
544        (
545            Result<Arc<Q::Output>, Arc<anyhow::Error>>,
546            bool,
547            RevisionCounter,
548        ),
549        QueryError,
550    > {
551        // Capture current global revision at query start for consistency checking
552        let start_revision = self.whale.current_revision().get(Durability::volatile());
553
554        // Create consistency tracker for this query execution
555        let tracker = Rc::new(ConsistencyTracker::new(start_revision));
556
557        // Set thread-local tracker for nested locator calls
558        let _tracker_guard = ConsistencyTrackerGuard::new(tracker);
559
560        // Create context for this query execution
561        let ctx = QueryContext {
562            runtime: self,
563            current_key: full_key.clone(),
564            exec_ctx,
565            deps: RefCell::new(Vec::new()),
566        };
567
568        // Execute the query (clone because query() takes ownership)
569        let db = DbDispatch::QueryContext(&ctx);
570        let result = query.clone().query(&db);
571
572        // Get collected dependencies
573        let deps: Vec<FullCacheKey> = ctx.deps.borrow().clone();
574
575        // Query durability defaults to stable - Whale will automatically reduce
576        // the effective durability to min(requested, min(dep_durabilities)).
577        // A pure query with no dependencies remains stable.
578        // A query depending on volatile assets becomes volatile.
579        let durability = Durability::stable();
580
581        match result {
582            Ok(output) => {
583                // Check if output changed (for early cutoff)
584                // existing_revision is Some only when output is unchanged (can reuse revision)
585                let existing_revision = if let Some((CachedValue::Ok(old), rev)) =
586                    self.get_cached_with_revision::<Q>(full_key)
587                {
588                    if Q::output_eq(&*old, &*output) {
589                        Some(rev) // Same output - reuse revision
590                    } else {
591                        None // Different output
592                    }
593                } else {
594                    None // No previous Ok value
595                };
596                let output_changed = existing_revision.is_none();
597
598                // Emit early cutoff check event
599                self.tracer.on_early_cutoff_check(
600                    exec_ctx.span_ctx(),
601                    query_cache_key,
602                    output_changed,
603                );
604
605                // Update whale with cached entry (atomic update of value + dependency state)
606                let entry = CachedEntry::Ok(output.clone() as Arc<dyn std::any::Any + Send + Sync>);
607                let revision = if let Some(existing_rev) = existing_revision {
608                    // confirm_unchanged doesn't change changed_at, use existing
609                    let _ = self.whale.confirm_unchanged(full_key, deps);
610                    existing_rev
611                } else {
612                    // Use new_rev from register result
613                    match self
614                        .whale
615                        .register(full_key.clone(), Some(entry), durability, deps)
616                    {
617                        Ok(result) => result.new_rev,
618                        Err(missing) => {
619                            return Err(QueryError::DependenciesRemoved {
620                                missing_keys: missing,
621                            })
622                        }
623                    }
624                };
625
626                // Register query in registry for list_queries
627                let is_new_query = self.query_registry.register(query);
628                if is_new_query {
629                    let sentinel = QuerySetSentinelKey::new::<Q>().into();
630                    let _ = self
631                        .whale
632                        .register(sentinel, None, Durability::stable(), vec![]);
633                }
634
635                // Store verifier for this query (for verify-then-decide pattern)
636                self.verifiers
637                    .insert::<Q, T>(full_key.clone(), query.clone());
638
639                Ok((Ok(output), output_changed, revision))
640            }
641            Err(QueryError::UserError(err)) => {
642                // Check if error changed (for early cutoff)
643                // existing_revision is Some only when error is unchanged (can reuse revision)
644                let existing_revision = if let Some((CachedValue::UserError(old_err), rev)) =
645                    self.get_cached_with_revision::<Q>(full_key)
646                {
647                    if (self.error_comparator)(old_err.as_ref(), err.as_ref()) {
648                        Some(rev) // Same error - reuse revision
649                    } else {
650                        None // Different error
651                    }
652                } else {
653                    None // No previous UserError
654                };
655                let output_changed = existing_revision.is_none();
656
657                // Emit early cutoff check event
658                self.tracer.on_early_cutoff_check(
659                    exec_ctx.span_ctx(),
660                    query_cache_key,
661                    output_changed,
662                );
663
664                // Update whale with cached error (atomic update of value + dependency state)
665                let entry = CachedEntry::UserError(err.clone());
666                let revision = if let Some(existing_rev) = existing_revision {
667                    // confirm_unchanged doesn't change changed_at, use existing
668                    let _ = self.whale.confirm_unchanged(full_key, deps);
669                    existing_rev
670                } else {
671                    // Use new_rev from register result
672                    match self
673                        .whale
674                        .register(full_key.clone(), Some(entry), durability, deps)
675                    {
676                        Ok(result) => result.new_rev,
677                        Err(missing) => {
678                            return Err(QueryError::DependenciesRemoved {
679                                missing_keys: missing,
680                            })
681                        }
682                    }
683                };
684
685                // Register query in registry for list_queries
686                let is_new_query = self.query_registry.register(query);
687                if is_new_query {
688                    let sentinel = QuerySetSentinelKey::new::<Q>().into();
689                    let _ = self
690                        .whale
691                        .register(sentinel, None, Durability::stable(), vec![]);
692                }
693
694                // Store verifier for this query (for verify-then-decide pattern)
695                self.verifiers
696                    .insert::<Q, T>(full_key.clone(), query.clone());
697
698                Ok((Err(err), output_changed, revision))
699            }
700            Err(e) => {
701                // System errors (Suspend, Cycle, Cancelled) are not cached
702                Err(e)
703            }
704        }
705    }
706
707    /// Invalidate a query, forcing recomputation on next access.
708    ///
709    /// This also invalidates any queries that depend on this one.
710    pub fn invalidate<Q: Query>(&self, query: &Q) {
711        let query_cache_key = QueryCacheKey::new(query.clone());
712        let full_key: FullCacheKey = query_cache_key.clone().into();
713
714        self.tracer
715            .on_query_invalidated(&query_cache_key, InvalidationReason::ManualInvalidation);
716
717        // Update whale to invalidate dependents (register with None to clear cached value)
718        // Use stable durability to increment all revision counters, ensuring queries
719        // at any durability level will see this as a change.
720        let _ = self
721            .whale
722            .register(full_key, None, Durability::stable(), vec![]);
723    }
724
725    /// Remove a query from the cache entirely, freeing memory.
726    ///
727    /// Use this for GC when a query is no longer needed.
728    /// Unlike `invalidate`, this removes all traces of the query from storage.
729    /// The query will be recomputed from scratch on next access.
730    ///
731    /// This also invalidates any queries that depend on this one.
732    pub fn remove_query<Q: Query>(&self, query: &Q) {
733        let query_cache_key = QueryCacheKey::new(query.clone());
734        let full_key: FullCacheKey = query_cache_key.clone().into();
735
736        self.tracer
737            .on_query_invalidated(&query_cache_key, InvalidationReason::ManualInvalidation);
738
739        // Remove verifier if exists
740        self.verifiers.remove(&full_key);
741
742        // Remove from whale storage (this also handles dependent invalidation)
743        self.whale.remove(&full_key);
744
745        // Remove from registry and update sentinel for list_queries
746        if self.query_registry.remove::<Q>(query) {
747            let sentinel = QuerySetSentinelKey::new::<Q>().into();
748            let _ = self
749                .whale
750                .register(sentinel, None, Durability::stable(), vec![]);
751        }
752    }
753
754    /// Clear all cached values by removing all nodes from whale.
755    ///
756    /// Note: This is a relatively expensive operation as it iterates through all keys.
757    pub fn clear_cache(&self) {
758        let keys = self.whale.keys();
759        for key in keys {
760            self.whale.remove(&key);
761        }
762    }
763
764    /// Poll a query, returning both the result and its change revision.
765    ///
766    /// This is useful for implementing subscription patterns where you need to
767    /// detect changes efficiently. Compare the returned `revision` with a
768    /// previously stored value to determine if the query result has changed.
769    ///
770    /// The returned `Polled` contains a `Result<Arc<Q::Output>, Arc<anyhow::Error>>`
771    /// as its value, allowing you to track revision changes for both success and
772    /// user error cases.
773    ///
774    /// # Example
775    ///
776    /// ```
777    /// use query_flow::{asset_key, query, Db, DurabilityLevel, QueryError, QueryRuntime};
778    ///
779    /// #[asset_key(asset = i32)]
780    /// struct Input(&'static str);
781    ///
782    /// #[query]
783    /// fn doubled(db: &impl Db) -> Result<i32, QueryError> {
784    ///     Ok(*db.asset(Input("x"))? * 2)
785    /// }
786    ///
787    /// let runtime = QueryRuntime::new();
788    /// runtime.resolve_asset(Input("x"), 21, DurabilityLevel::Volatile);
789    ///
790    /// let result = runtime.poll(Doubled::new()).unwrap();
791    /// assert_eq!(**result.value.as_ref().unwrap(), 42);
792    /// let last_revision = result.revision;
793    ///
794    /// // Polling again without any change leaves the revision untouched,
795    /// // so a subscriber knows there is nothing to send.
796    /// let again = runtime.poll(Doubled::new()).unwrap();
797    /// assert_eq!(again.revision, last_revision);
798    ///
799    /// // Changing the input bumps the revision.
800    /// runtime.resolve_asset(Input("x"), 50, DurabilityLevel::Volatile);
801    /// let changed = runtime.poll(Doubled::new()).unwrap();
802    /// assert!(changed.revision > last_revision);
803    /// assert_eq!(**changed.value.as_ref().unwrap(), 100);
804    /// ```
805    ///
806    /// # Errors
807    ///
808    /// Returns `Err` only for system errors (Suspend, Cycle, etc.).
809    /// User errors are returned as `Ok(Polled { value: Err(error), ... })`.
810    #[allow(clippy::type_complexity)]
811    pub fn poll<Q: Query>(
812        &self,
813        query: Q,
814    ) -> Result<Polled<Result<Arc<Q::Output>, Arc<anyhow::Error>>>, QueryError> {
815        let (value, revision) = self.query_internal(query)?;
816        Ok(Polled { value, revision })
817    }
818
819    /// Get the change revision of a query without executing it.
820    ///
821    /// Returns `None` if the query has never been executed.
822    ///
823    /// This is useful for checking if a query has changed since the last poll
824    /// without the cost of executing the query.
825    ///
826    /// # Example
827    ///
828    /// ```
829    /// use query_flow::{query, Db, QueryError, QueryRuntime, RevisionCounter};
830    ///
831    /// #[query]
832    /// fn my_query(db: &impl Db, key: i32) -> Result<i32, QueryError> {
833    ///     let _ = db;
834    ///     Ok(key * 2)
835    /// }
836    ///
837    /// let runtime = QueryRuntime::new();
838    /// let key = 21;
839    /// let last_known_revision: RevisionCounter = 0;
840    ///
841    /// // Never executed yet, so there is no revision to compare against.
842    /// assert!(runtime.changed_at(&MyQuery::new(key)).is_none());
843    ///
844    /// runtime.query(MyQuery::new(key)).unwrap();
845    ///
846    /// // Check if query has changed before deciding to poll
847    /// if let Some(rev) = runtime.changed_at(&MyQuery::new(key)) {
848    ///     if rev > last_known_revision {
849    ///         let result = runtime.query(MyQuery::new(key)).unwrap();
850    ///         assert_eq!(*result, 42);
851    ///     }
852    /// }
853    /// ```
854    pub fn changed_at<Q: Query>(&self, query: &Q) -> Option<RevisionCounter> {
855        let full_key = QueryCacheKey::new(query.clone()).into();
856        self.whale
857            .get_data(&full_key)
858            .map(|(_, changed_at)| changed_at)
859    }
860}
861
862// ============================================================================
863// GC (Garbage Collection) API
864// ============================================================================
865
866impl<T: Tracer> QueryRuntime<T> {
867    /// Get all query keys currently in the cache.
868    ///
869    /// This is useful for implementing custom garbage collection strategies.
870    /// Use this in combination with [`Tracer::on_query_key`] to track access
871    /// times and implement LRU, TTL, or other GC algorithms externally.
872    ///
873    /// # Example
874    ///
875    /// ```
876    /// use query_flow::{query, Db, QueryError, QueryRuntime};
877    ///
878    /// #[query]
879    /// fn leaf(db: &impl Db, x: i32) -> Result<i32, QueryError> {
880    ///     let _ = db;
881    ///     Ok(x)
882    /// }
883    ///
884    /// #[query]
885    /// fn root(db: &impl Db, x: i32) -> Result<i32, QueryError> {
886    ///     Ok(*db.query(Leaf::new(x))? + 1)
887    /// }
888    ///
889    /// let runtime = QueryRuntime::new();
890    /// runtime.query(Root::new(1)).unwrap();
891    /// runtime.query(Root::new(2)).unwrap();
892    ///
893    /// // Both roots and their dependencies are cached. Note that the returned
894    /// // keys also include the internal per-type set sentinels used by
895    /// // `list_queries`, so the count is larger than the number of queries.
896    /// assert!(runtime.query_keys().len() >= 4);
897    ///
898    /// // Collect the keys that haven't been accessed recently. A real GC would
899    /// // consult access times recorded through `Tracer::on_query_key`; here
900    /// // every `Leaf` stands in for the stale set.
901    /// let stale_keys: Vec<_> = runtime
902    ///     .query_keys()
903    ///     .into_iter()
904    ///     .filter(|key| key.downcast::<Leaf>().is_some())
905    ///     .collect();
906    /// assert_eq!(stale_keys.len(), 2);
907    ///
908    /// // The sweep keeps both leaves: each one still has a root depending on it,
909    /// // and `remove_if_unused` never breaks a live dependent.
910    /// for key in &stale_keys {
911    ///     assert!(!runtime.remove_if_unused(key));
912    /// }
913    ///
914    /// // The roots themselves have no dependents, so they are reclaimed.
915    /// assert!(runtime.remove_query_if_unused(&Root::new(1)));
916    /// ```
917    pub fn query_keys(&self) -> Vec<FullCacheKey> {
918        self.whale.keys()
919    }
920
921    /// Remove a query if it has no dependents.
922    ///
923    /// Returns `true` if the query was removed, `false` if it has dependents
924    /// or doesn't exist. This is the safe way to remove queries during GC,
925    /// as it won't break queries that depend on this one.
926    ///
927    /// # Example
928    ///
929    /// ```
930    /// use query_flow::{query, Db, QueryError, QueryRuntime};
931    ///
932    /// #[query]
933    /// fn leaf(db: &impl Db, x: i32) -> Result<i32, QueryError> {
934    ///     let _ = db;
935    ///     Ok(x)
936    /// }
937    ///
938    /// #[query]
939    /// fn root(db: &impl Db, x: i32) -> Result<i32, QueryError> {
940    ///     Ok(*db.query(Leaf::new(x))? + 1)
941    /// }
942    ///
943    /// let runtime = QueryRuntime::new();
944    /// runtime.query(Root::new(1)).unwrap();
945    ///
946    /// // `Leaf` has a dependent (`Root`), so it is kept.
947    /// assert!(!runtime.remove_query_if_unused(&Leaf::new(1)));
948    ///
949    /// // `Root` has no dependents, so it is removed.
950    /// assert!(runtime.remove_query_if_unused(&Root::new(1)));
951    /// ```
952    pub fn remove_query_if_unused<Q: Query>(&self, query: &Q) -> bool {
953        let full_key = QueryCacheKey::new(query.clone()).into();
954        self.remove_if_unused(&full_key)
955    }
956
957    /// Remove a query by its [`FullCacheKey`].
958    ///
959    /// This is the type-erased version of [`remove_query`](Self::remove_query).
960    /// Use this when you have a `FullCacheKey` from [`query_keys`](Self::query_keys)
961    /// or [`Tracer::on_query_key`].
962    ///
963    /// Returns `true` if the query was removed, `false` if it doesn't exist.
964    ///
965    /// # Warning
966    ///
967    /// This forcibly removes the query even if other queries depend on it.
968    /// Dependent queries will be recomputed on next access. For safe GC,
969    /// use [`remove_if_unused`](Self::remove_if_unused) instead.
970    pub fn remove(&self, key: &FullCacheKey) -> bool {
971        // Remove verifier if exists
972        self.verifiers.remove(key);
973
974        // Remove from whale storage
975        self.whale.remove(key).is_some()
976    }
977
978    /// Remove a query by its [`FullCacheKey`] if it has no dependents.
979    ///
980    /// This is the type-erased version of [`remove_query_if_unused`](Self::remove_query_if_unused).
981    /// Use this when you have a `FullCacheKey` from [`query_keys`](Self::query_keys)
982    /// or [`Tracer::on_query_key`].
983    ///
984    /// Returns `true` if the query was removed, `false` if it has dependents
985    /// or doesn't exist.
986    ///
987    /// # Example
988    ///
989    /// ```
990    /// use std::collections::HashSet;
991    ///
992    /// use query_flow::{query, Db, FullCacheKey, QueryError, QueryRuntime};
993    ///
994    /// #[query]
995    /// fn my_query(db: &impl Db, x: i32) -> Result<i32, QueryError> {
996    ///     let _ = db;
997    ///     Ok(x * 2)
998    /// }
999    ///
1000    /// let runtime = QueryRuntime::new();
1001    /// runtime.query(MyQuery::new(1)).unwrap();
1002    /// runtime.query(MyQuery::new(2)).unwrap();
1003    ///
1004    /// // Your GC tracker decides what has expired; here everything has.
1005    /// let expired: HashSet<FullCacheKey> = runtime.query_keys().into_iter().collect();
1006    ///
1007    /// // Implement LRU GC
1008    /// for key in runtime.query_keys() {
1009    ///     if expired.contains(&key) {
1010    ///         runtime.remove_if_unused(&key);
1011    ///     }
1012    /// }
1013    ///
1014    /// assert!(runtime.query_keys().is_empty());
1015    /// ```
1016    pub fn remove_if_unused(&self, key: &FullCacheKey) -> bool {
1017        if self.whale.remove_if_unused(key.clone()).is_some() {
1018            // Successfully removed - clean up verifier
1019            self.verifiers.remove(key);
1020            true
1021        } else {
1022            false
1023        }
1024    }
1025}
1026
1027// ============================================================================
1028// Builder
1029// ============================================================================
1030
1031/// Builder for [`QueryRuntime`] with customizable settings.
1032///
1033/// # Example
1034///
1035/// ```
1036/// use query_flow::QueryRuntime;
1037///
1038/// let runtime = QueryRuntime::builder()
1039///     .error_comparator(|a, b| {
1040///         // Treat all errors of the same type as equal
1041///         a.downcast_ref::<std::io::Error>().is_some()
1042///             == b.downcast_ref::<std::io::Error>().is_some()
1043///     })
1044///     .build();
1045/// ```
1046pub struct QueryRuntimeBuilder<T: Tracer = NoopTracer> {
1047    error_comparator: ErrorComparator,
1048    tracer: T,
1049}
1050
1051impl Default for QueryRuntimeBuilder<NoopTracer> {
1052    fn default() -> Self {
1053        Self::new()
1054    }
1055}
1056
1057impl QueryRuntimeBuilder<NoopTracer> {
1058    /// Create a new builder with default settings.
1059    pub fn new() -> Self {
1060        Self {
1061            error_comparator: default_error_comparator,
1062            tracer: NoopTracer,
1063        }
1064    }
1065}
1066
1067impl<T: Tracer> QueryRuntimeBuilder<T> {
1068    /// Set the error comparator function for early cutoff optimization.
1069    ///
1070    /// When a query returns `QueryError::UserError`, this function is used
1071    /// to compare it with the previously cached error. If they are equal,
1072    /// downstream queries can skip recomputation (early cutoff).
1073    ///
1074    /// The default comparator returns `false` for all errors, meaning errors
1075    /// are always considered different (conservative, always recomputes).
1076    ///
1077    /// # Example
1078    ///
1079    /// ```
1080    /// use query_flow::QueryRuntime;
1081    ///
1082    /// // Treat errors as equal if they have the same display message
1083    /// let runtime = QueryRuntime::builder()
1084    ///     .error_comparator(|a, b| a.to_string() == b.to_string())
1085    ///     .build();
1086    /// ```
1087    pub fn error_comparator(mut self, f: ErrorComparator) -> Self {
1088        self.error_comparator = f;
1089        self
1090    }
1091
1092    /// Set the tracer for observability.
1093    pub fn tracer<U: Tracer>(self, tracer: U) -> QueryRuntimeBuilder<U> {
1094        QueryRuntimeBuilder {
1095            error_comparator: self.error_comparator,
1096            tracer,
1097        }
1098    }
1099
1100    /// Build the runtime with the configured settings.
1101    pub fn build(self) -> QueryRuntime<T> {
1102        QueryRuntime {
1103            whale: WhaleRuntime::new(),
1104            locators: Arc::new(LocatorStorage::new()),
1105            pending: Arc::new(PendingStorage::new()),
1106            query_registry: Arc::new(QueryRegistry::new()),
1107            asset_key_registry: Arc::new(AssetKeyRegistry::new()),
1108            verifiers: Arc::new(VerifierStorage::new()),
1109            error_comparator: self.error_comparator,
1110            tracer: Arc::new(self.tracer),
1111        }
1112    }
1113}
1114
1115// ============================================================================
1116// Asset API
1117// ============================================================================
1118
1119impl<T: Tracer> QueryRuntime<T> {
1120    /// Register an asset locator for a specific asset key type.
1121    ///
1122    /// Only one locator can be registered per key type. Later registrations
1123    /// replace earlier ones.
1124    ///
1125    /// # Example
1126    ///
1127    /// ```
1128    /// use query_flow::{
1129    ///     asset_key, AssetLocator, Db, DurabilityLevel, LocateResult, QueryError, QueryRuntime,
1130    /// };
1131    ///
1132    /// #[asset_key(asset = String)]
1133    /// struct FilePath(String);
1134    ///
1135    /// struct InMemoryLocator {
1136    ///     prefix: String,
1137    /// }
1138    ///
1139    /// impl AssetLocator<FilePath> for InMemoryLocator {
1140    ///     fn locate(&self, db: &impl Db, key: &FilePath) -> Result<LocateResult<String>, QueryError> {
1141    ///         let _ = db;
1142    ///         Ok(LocateResult::Ready {
1143    ///             value: format!("{}{}", self.prefix, key.0),
1144    ///             durability: DurabilityLevel::Static,
1145    ///         })
1146    ///     }
1147    /// }
1148    ///
1149    /// let runtime = QueryRuntime::new();
1150    /// runtime.register_asset_locator(InMemoryLocator {
1151    ///     prefix: "/assets/".to_string(),
1152    /// });
1153    /// ```
1154    pub fn register_asset_locator<K, L>(&self, locator: L)
1155    where
1156        K: AssetKey,
1157        L: AssetLocator<K>,
1158    {
1159        self.locators.insert::<K, L>(locator);
1160    }
1161
1162    /// Get an iterator over pending asset requests.
1163    ///
1164    /// Returns assets that have been requested but not yet resolved.
1165    /// The user should fetch these externally and call `resolve_asset()`.
1166    ///
1167    /// # Example
1168    ///
1169    /// ```
1170    /// use query_flow::{
1171    ///     asset_key, asset_locator, query, Db, DurabilityLevel, LocateResult, QueryError,
1172    ///     QueryRuntime,
1173    /// };
1174    ///
1175    /// #[asset_key(asset = String)]
1176    /// struct FilePath(String);
1177    ///
1178    /// #[asset_locator]
1179    /// fn pending(_db: &impl Db, _key: &FilePath) -> Result<LocateResult<String>, QueryError> {
1180    ///     Ok(LocateResult::Pending)
1181    /// }
1182    ///
1183    /// #[query]
1184    /// fn read_file(db: &impl Db, path: FilePath) -> Result<usize, QueryError> {
1185    ///     Ok(db.asset(path)?.len())
1186    /// }
1187    ///
1188    /// fn fetch_file(path: &FilePath) -> String {
1189    ///     format!("contents of {}", path.0)
1190    /// }
1191    ///
1192    /// let runtime = QueryRuntime::new();
1193    /// runtime.register_asset_locator(Pending);
1194    ///
1195    /// // The query suspends, which registers a pending asset request.
1196    /// assert!(runtime
1197    ///     .query(ReadFile::new(FilePath("a.txt".into())))
1198    ///     .is_err());
1199    ///
1200    /// for pending in runtime.pending_assets() {
1201    ///     if let Some(path) = pending.key::<FilePath>() {
1202    ///         let content = fetch_file(path);
1203    ///         runtime.resolve_asset(path.clone(), content, DurabilityLevel::Volatile);
1204    ///     }
1205    /// }
1206    ///
1207    /// assert_eq!(
1208    ///     *runtime
1209    ///         .query(ReadFile::new(FilePath("a.txt".into())))
1210    ///         .unwrap(),
1211    ///     17
1212    /// );
1213    /// ```
1214    pub fn pending_assets(&self) -> Vec<PendingAsset> {
1215        self.pending.get_all()
1216    }
1217
1218    /// Get pending assets filtered by key type.
1219    pub fn pending_assets_of<K: AssetKey>(&self) -> Vec<K> {
1220        self.pending.get_of_type::<K>()
1221    }
1222
1223    /// Check if there are any pending assets.
1224    pub fn has_pending_assets(&self) -> bool {
1225        !self.pending.is_empty()
1226    }
1227
1228    /// Resolve an asset with its loaded value.
1229    ///
1230    /// This marks the asset as ready and invalidates any queries that
1231    /// depend on it (if the value changed), triggering recomputation on next access.
1232    ///
1233    /// This method is idempotent - resolving with the same value (via `asset_eq`)
1234    /// will not trigger downstream recomputation.
1235    ///
1236    /// # Arguments
1237    ///
1238    /// * `key` - The asset key identifying this resource
1239    /// * `value` - The loaded asset value
1240    /// * `durability` - How frequently this asset is expected to change
1241    ///
1242    /// # Example
1243    ///
1244    /// ```
1245    /// use query_flow::{asset_key, query, Db, DurabilityLevel, QueryError, QueryRuntime};
1246    ///
1247    /// #[asset_key(asset = String)]
1248    /// struct FilePath(String);
1249    ///
1250    /// #[query]
1251    /// fn byte_len(db: &impl Db, path: FilePath) -> Result<usize, QueryError> {
1252    ///     Ok(db.asset(path)?.len())
1253    /// }
1254    ///
1255    /// let runtime = QueryRuntime::new();
1256    /// let path = "config.json".to_string();
1257    ///
1258    /// // In a real program this would be `std::fs::read_to_string(&path)?`.
1259    /// let content = "hello".to_string();
1260    /// runtime.resolve_asset(FilePath(path.clone()), content, DurabilityLevel::Volatile);
1261    ///
1262    /// assert_eq!(*runtime.query(ByteLen::new(FilePath(path))).unwrap(), 5);
1263    /// ```
1264    pub fn resolve_asset<K: AssetKey>(&self, key: K, value: K::Asset, durability: DurabilityLevel) {
1265        self.resolve_asset_internal(key, value, durability);
1266    }
1267
1268    /// Resolve an asset with an error.
1269    ///
1270    /// This marks the asset as errored and caches the error. Queries depending
1271    /// on this asset will receive `Err(QueryError::UserError(...))`.
1272    ///
1273    /// Use this when async loading fails (e.g., network error, file not found,
1274    /// access denied).
1275    ///
1276    /// # Arguments
1277    ///
1278    /// * `key` - The asset key identifying this resource
1279    /// * `error` - The error to cache (will be wrapped in `Arc`)
1280    /// * `durability` - How frequently this error state is expected to change
1281    ///
1282    /// # Example
1283    ///
1284    /// ```
1285    /// use std::io;
1286    ///
1287    /// use query_flow::{asset_key, query, Db, DurabilityLevel, QueryError, QueryRuntime};
1288    ///
1289    /// #[asset_key(asset = String)]
1290    /// struct FilePath(String);
1291    ///
1292    /// #[query]
1293    /// fn byte_len(db: &impl Db, path: FilePath) -> Result<usize, QueryError> {
1294    ///     Ok(db.asset(path)?.len())
1295    /// }
1296    ///
1297    /// fn fetch_file(path: &str) -> io::Result<String> {
1298    ///     Err(io::Error::new(io::ErrorKind::NotFound, path.to_string()))
1299    /// }
1300    ///
1301    /// let runtime = QueryRuntime::new();
1302    /// let path = "missing.json".to_string();
1303    ///
1304    /// match fetch_file(&path) {
1305    ///     Ok(content) => runtime.resolve_asset(FilePath(path.clone()), content, DurabilityLevel::Volatile),
1306    ///     Err(e) => runtime.resolve_asset_error(FilePath(path.clone()), e, DurabilityLevel::Volatile),
1307    /// }
1308    ///
1309    /// // Queries depending on the asset now observe the cached user error.
1310    /// let err = runtime.query(ByteLen::new(FilePath(path))).unwrap_err();
1311    /// assert!(err.is::<io::Error>());
1312    /// ```
1313    pub fn resolve_asset_error<K: AssetKey>(
1314        &self,
1315        key: K,
1316        error: impl Into<anyhow::Error>,
1317        durability: DurabilityLevel,
1318    ) {
1319        let asset_cache_key = AssetCacheKey::new(key.clone());
1320
1321        // Remove from pending BEFORE registering the error
1322        self.pending.remove(&asset_cache_key);
1323
1324        // Prepare the error entry
1325        let error_arc = Arc::new(error.into());
1326        let entry = CachedEntry::AssetError(error_arc.clone());
1327        let durability =
1328            Durability::new(durability.as_u8() as usize).unwrap_or(Durability::volatile());
1329
1330        // Atomic compare-and-update (errors are always considered changed for now)
1331        let result = self
1332            .whale
1333            .update_with_compare(
1334                asset_cache_key.into(),
1335                Some(entry),
1336                |old_data, _new_data| {
1337                    // Compare old and new errors using error_comparator
1338                    match old_data.and_then(|d| d.as_ref()) {
1339                        Some(CachedEntry::AssetError(old_err)) => {
1340                            !(self.error_comparator)(old_err.as_ref(), error_arc.as_ref())
1341                        }
1342                        _ => true, // Loading, Ready, or not present -> changed
1343                    }
1344                },
1345                durability,
1346                vec![],
1347            )
1348            .expect("update_with_compare with no dependencies cannot fail");
1349
1350        // Emit asset resolved event (with changed status)
1351        let asset_cache_key = AssetCacheKey::new(key.clone());
1352        self.tracer
1353            .on_asset_resolved(&asset_cache_key, result.changed);
1354
1355        // Register asset key in registry for list_asset_keys
1356        let is_new_asset = self.asset_key_registry.register(&key);
1357        if is_new_asset {
1358            // Update sentinel to invalidate list_asset_keys dependents
1359            let sentinel = AssetKeySetSentinelKey::new::<K>().into();
1360            let _ = self
1361                .whale
1362                .register(sentinel, None, Durability::stable(), vec![]);
1363        }
1364    }
1365
1366    fn resolve_asset_internal<K: AssetKey>(
1367        &self,
1368        key: K,
1369        value: K::Asset,
1370        durability_level: DurabilityLevel,
1371    ) {
1372        let asset_cache_key = AssetCacheKey::new(key.clone());
1373
1374        // Remove from pending BEFORE registering the value
1375        self.pending.remove(&asset_cache_key);
1376
1377        // Prepare the new entry
1378        let value_arc: Arc<K::Asset> = Arc::new(value);
1379        let entry = CachedEntry::AssetReady(value_arc.clone() as Arc<dyn Any + Send + Sync>);
1380        let durability =
1381            Durability::new(durability_level.as_u8() as usize).unwrap_or(Durability::volatile());
1382
1383        // Atomic compare-and-update
1384        let result = self
1385            .whale
1386            .update_with_compare(
1387                asset_cache_key.into(),
1388                Some(entry),
1389                |old_data, _new_data| {
1390                    // Compare old and new values
1391                    match old_data.and_then(|d| d.as_ref()) {
1392                        Some(CachedEntry::AssetReady(old_arc)) => {
1393                            match old_arc.clone().downcast::<K::Asset>() {
1394                                Ok(old_value) => !K::asset_eq(&old_value, &value_arc),
1395                                Err(_) => true, // Type mismatch, treat as changed
1396                            }
1397                        }
1398                        _ => true, // Loading, NotFound, or not present -> changed
1399                    }
1400                },
1401                durability,
1402                vec![],
1403            )
1404            .expect("update_with_compare with no dependencies cannot fail");
1405
1406        // Emit asset resolved event
1407        let asset_cache_key = AssetCacheKey::new(key.clone());
1408        self.tracer
1409            .on_asset_resolved(&asset_cache_key, result.changed);
1410
1411        // Register asset key in registry for list_asset_keys
1412        let is_new_asset = self.asset_key_registry.register(&key);
1413        if is_new_asset {
1414            // Update sentinel to invalidate list_asset_keys dependents
1415            let sentinel = AssetKeySetSentinelKey::new::<K>().into();
1416            let _ = self
1417                .whale
1418                .register(sentinel, None, Durability::stable(), vec![]);
1419        }
1420    }
1421
1422    /// Invalidate an asset, forcing queries to re-request it.
1423    ///
1424    /// The asset will be marked as loading and added to pending assets.
1425    /// Dependent queries will suspend until the asset is resolved again.
1426    ///
1427    /// # Example
1428    ///
1429    /// ```
1430    /// use query_flow::{asset_key, query, Db, DurabilityLevel, QueryError, QueryRuntime};
1431    ///
1432    /// #[asset_key(asset = String)]
1433    /// struct FilePath(String);
1434    ///
1435    /// #[query]
1436    /// fn byte_len(db: &impl Db, path: FilePath) -> Result<usize, QueryError> {
1437    ///     Ok(db.asset(path)?.len())
1438    /// }
1439    ///
1440    /// let runtime = QueryRuntime::new();
1441    /// let path = FilePath("config.json".into());
1442    /// runtime.resolve_asset(path.clone(), "hello".into(), DurabilityLevel::Volatile);
1443    /// assert_eq!(*runtime.query(ByteLen::new(path.clone())).unwrap(), 5);
1444    ///
1445    /// // File was modified externally
1446    /// runtime.invalidate_asset(&path);
1447    ///
1448    /// // Queries depending on this asset will now suspend
1449    /// let err = runtime.query(ByteLen::new(path.clone())).unwrap_err();
1450    /// assert!(matches!(err, QueryError::Suspend { .. }));
1451    ///
1452    /// // User should fetch the new value and call resolve_asset
1453    /// runtime.resolve_asset(path.clone(), "hello world".into(), DurabilityLevel::Volatile);
1454    /// assert_eq!(*runtime.query(ByteLen::new(path)).unwrap(), 11);
1455    /// ```
1456    pub fn invalidate_asset<K: AssetKey>(&self, key: &K) {
1457        let asset_cache_key = AssetCacheKey::new(key.clone());
1458        let full_cache_key: FullCacheKey = asset_cache_key.clone().into();
1459
1460        // Emit asset invalidated event
1461        self.tracer.on_asset_invalidated(&asset_cache_key);
1462
1463        // Add to pending FIRST (before clearing whale state)
1464        // This ensures: readers see either old value, or Loading+pending
1465        self.pending
1466            .insert::<K>(asset_cache_key.clone(), key.clone());
1467
1468        // Atomic: clear cached value + invalidate dependents
1469        // Using None for data means "needs to be loaded"
1470        // Use stable durability to ensure queries at any durability level see the change.
1471        let _ = self
1472            .whale
1473            .register(full_cache_key, None, Durability::stable(), vec![]);
1474    }
1475
1476    /// Remove an asset from the cache entirely.
1477    ///
1478    /// Unlike `invalidate_asset`, this removes all traces of the asset.
1479    /// Dependent queries will go through the locator again on next access.
1480    pub fn remove_asset<K: AssetKey>(&self, key: &K) {
1481        let asset_cache_key = AssetCacheKey::new(key.clone());
1482        let full_cache_key: FullCacheKey = asset_cache_key.clone().into();
1483
1484        // Remove from pending first
1485        self.pending.remove(&asset_cache_key);
1486
1487        // Remove from whale (this also cleans up dependency edges)
1488        // whale.remove() invalidates dependents before removing
1489        self.whale.remove(&full_cache_key);
1490
1491        // Remove from registry and update sentinel for list_asset_keys
1492        if self.asset_key_registry.remove::<K>(key) {
1493            let sentinel = AssetKeySetSentinelKey::new::<K>().into();
1494            let _ = self
1495                .whale
1496                .register(sentinel, None, Durability::stable(), vec![]);
1497        }
1498    }
1499
1500    /// Get an asset by key without tracking dependencies.
1501    ///
1502    /// Unlike `QueryContext::asset()`, this method does NOT register the caller
1503    /// as a dependent of the asset. Use this for direct asset access outside
1504    /// of query execution.
1505    ///
1506    /// # Returns
1507    ///
1508    /// - `Ok(AssetLoadingState::ready(...))` - Asset is loaded and ready
1509    /// - `Ok(AssetLoadingState::loading(...))` - Asset is still loading (added to pending)
1510    /// - `Err(QueryError::UserError)` - Asset was not found or locator returned an error
1511    pub fn get_asset<K: AssetKey>(&self, key: K) -> Result<AssetLoadingState<K>, QueryError> {
1512        self.get_asset_internal(key)
1513    }
1514
1515    /// Internal: Get asset state and its changed_at revision atomically.
1516    ///
1517    /// This is the "direct" version called from QueryRuntime::asset (no dependency tracking).
1518    /// For calls from QueryContext::asset, use `get_asset_with_revision_ctx`.
1519    ///
1520    /// Returns (AssetLoadingState, changed_at) where changed_at is from the same
1521    /// whale node that provided the asset value.
1522    fn get_asset_with_revision<K: AssetKey>(
1523        &self,
1524        key: K,
1525    ) -> Result<(AssetLoadingState<K>, RevisionCounter), QueryError> {
1526        let asset_cache_key = AssetCacheKey::new(key.clone());
1527        let full_cache_key: FullCacheKey = asset_cache_key.clone().into();
1528
1529        // Create a span for this asset request (like queries do)
1530        // This ensures child queries called from locators show as children of this asset
1531        let asset_span_id = self.tracer.new_span_id();
1532        let (trace_id, parent_span_id) = SPAN_STACK.with(|stack| match &*stack.borrow() {
1533            SpanStack::Empty => (self.tracer.new_trace_id(), None),
1534            SpanStack::Active(tid, spans) => (*tid, spans.last().copied()),
1535        });
1536        let span_ctx = SpanContext {
1537            span_id: asset_span_id,
1538            trace_id,
1539            parent_span_id,
1540        };
1541
1542        // Push asset span to stack so child queries see this asset as their parent
1543        let _span_guard = SpanStackGuard::push(trace_id, asset_span_id);
1544
1545        // Check whale cache first (single atomic read)
1546        if let Some((cached_data, changed_at)) = self.whale.get_data(&full_cache_key) {
1547            // Check if valid at current revision (shallow check)
1548            if self.whale.is_valid(&full_cache_key) {
1549                // Verify dependencies recursively (like query path does)
1550                let mut deps_verified = true;
1551                if let Some(deps) = self.whale.get_dependency_ids(&full_cache_key) {
1552                    for dep in deps {
1553                        if let Some(verifier) = self.verifiers.get(&dep) {
1554                            // Re-run query/asset to verify it (triggers recursive verification)
1555                            if verifier.verify(self as &dyn std::any::Any).is_err() {
1556                                deps_verified = false;
1557                                break;
1558                            }
1559                        }
1560                    }
1561                }
1562
1563                // Re-check validity after deps are verified
1564                if deps_verified && self.whale.is_valid(&full_cache_key) {
1565                    // For cached entries, check consistency for leaf assets (no locator deps).
1566                    // This detects if resolve_asset/resolve_asset_error was called during query execution.
1567                    let has_locator_deps = self
1568                        .whale
1569                        .get_dependency_ids(&full_cache_key)
1570                        .is_some_and(|deps| !deps.is_empty());
1571
1572                    match &cached_data {
1573                        Some(CachedEntry::AssetReady(arc)) => {
1574                            // Check consistency for cached leaf assets
1575                            if !has_locator_deps {
1576                                check_leaf_asset_consistency(changed_at)?;
1577                            }
1578                            // Cache hit: start + end immediately (no locator runs)
1579                            self.tracer.on_asset_requested(&span_ctx, &asset_cache_key);
1580                            self.tracer.on_asset_located(
1581                                &span_ctx,
1582                                &asset_cache_key,
1583                                TracerAssetState::Ready,
1584                            );
1585                            match arc.clone().downcast::<K::Asset>() {
1586                                Ok(value) => {
1587                                    return Ok((AssetLoadingState::ready(key, value), changed_at))
1588                                }
1589                                Err(_) => {
1590                                    unreachable!("Asset type mismatch: {:?}", key)
1591                                }
1592                            }
1593                        }
1594                        Some(CachedEntry::AssetError(err)) => {
1595                            // Check consistency for cached leaf errors
1596                            if !has_locator_deps {
1597                                check_leaf_asset_consistency(changed_at)?;
1598                            }
1599                            // Cache hit: start + end immediately (no locator runs)
1600                            self.tracer.on_asset_requested(&span_ctx, &asset_cache_key);
1601                            self.tracer.on_asset_located(
1602                                &span_ctx,
1603                                &asset_cache_key,
1604                                TracerAssetState::NotFound,
1605                            );
1606                            return Err(QueryError::UserError(err.clone()));
1607                        }
1608                        None => {
1609                            // Loading state - no value to be inconsistent with
1610                            // Cache hit: start + end immediately (no locator runs)
1611                            self.tracer.on_asset_requested(&span_ctx, &asset_cache_key);
1612                            self.tracer.on_asset_located(
1613                                &span_ctx,
1614                                &asset_cache_key,
1615                                TracerAssetState::Loading,
1616                            );
1617                            return Ok((AssetLoadingState::loading(key), changed_at));
1618                        }
1619                        _ => {
1620                            // Query-related entries (Ok, UserError) shouldn't be here
1621                            // Fall through to locator
1622                        }
1623                    }
1624                }
1625            }
1626        }
1627
1628        // Not in cache or invalid - try locator
1629        // Use LocatorContext to track deps on the asset itself
1630        check_cycle(&full_cache_key)?;
1631        let _guard = StackGuard::push(full_cache_key.clone());
1632
1633        // Notify tracer BEFORE locator runs (START event) so child queries appear as children
1634        self.tracer.on_asset_requested(&span_ctx, &asset_cache_key);
1635
1636        let locator_ctx = LocatorContext::new(self, full_cache_key.clone());
1637        let locator_result =
1638            self.locators
1639                .locate_with_locator_ctx(TypeId::of::<K>(), &locator_ctx, &key);
1640
1641        if let Some(result) = locator_result {
1642            // Get collected dependencies from the locator context
1643            let locator_deps = locator_ctx.into_deps();
1644            match result {
1645                Ok(ErasedLocateResult::Ready {
1646                    value: arc,
1647                    durability: durability_level,
1648                }) => {
1649                    // END event after locator completes
1650                    self.tracer.on_asset_located(
1651                        &span_ctx,
1652                        &asset_cache_key,
1653                        TracerAssetState::Ready,
1654                    );
1655
1656                    let typed_value: Arc<K::Asset> = match arc.downcast::<K::Asset>() {
1657                        Ok(v) => v,
1658                        Err(_) => {
1659                            unreachable!("Asset type mismatch: {:?}", key);
1660                        }
1661                    };
1662
1663                    // Store in whale atomically with early cutoff
1664                    // Include locator's dependencies so the asset is invalidated when they change
1665                    let entry = CachedEntry::AssetReady(typed_value.clone());
1666                    let durability = Durability::new(durability_level.as_u8() as usize)
1667                        .unwrap_or(Durability::volatile());
1668                    let new_value = typed_value.clone();
1669                    let result = self
1670                        .whale
1671                        .update_with_compare(
1672                            full_cache_key.clone(),
1673                            Some(entry),
1674                            |old_data, _new_data| {
1675                                let Some(CachedEntry::AssetReady(old_arc)) =
1676                                    old_data.and_then(|d| d.as_ref())
1677                                else {
1678                                    return true;
1679                                };
1680                                let Ok(old_value) = old_arc.clone().downcast::<K::Asset>() else {
1681                                    return true;
1682                                };
1683                                !K::asset_eq(&old_value, &new_value)
1684                            },
1685                            durability,
1686                            locator_deps,
1687                        )
1688                        .expect("update_with_compare should succeed");
1689
1690                    // Register verifier for this asset (for verify-then-decide pattern)
1691                    self.verifiers
1692                        .insert_asset::<K, T>(full_cache_key, key.clone());
1693
1694                    return Ok((AssetLoadingState::ready(key, typed_value), result.revision));
1695                }
1696                Ok(ErasedLocateResult::Pending) => {
1697                    // END event after locator completes with pending
1698                    self.tracer.on_asset_located(
1699                        &span_ctx,
1700                        &asset_cache_key,
1701                        TracerAssetState::Loading,
1702                    );
1703
1704                    // Add to pending list for Pending result
1705                    self.pending
1706                        .insert::<K>(asset_cache_key.clone(), key.clone());
1707                    match self
1708                        .whale
1709                        .get_or_insert(full_cache_key, None, Durability::volatile(), locator_deps)
1710                        .expect("get_or_insert should succeed")
1711                    {
1712                        GetOrInsertResult::Inserted(node) => {
1713                            return Ok((AssetLoadingState::loading(key), node.changed_at));
1714                        }
1715                        GetOrInsertResult::Existing(node) => {
1716                            let changed_at = node.changed_at;
1717                            match &node.data {
1718                                Some(CachedEntry::AssetReady(arc)) => {
1719                                    match arc.clone().downcast::<K::Asset>() {
1720                                        Ok(value) => {
1721                                            return Ok((
1722                                                AssetLoadingState::ready(key, value),
1723                                                changed_at,
1724                                            ))
1725                                        }
1726                                        Err(_) => {
1727                                            return Ok((
1728                                                AssetLoadingState::loading(key),
1729                                                changed_at,
1730                                            ))
1731                                        }
1732                                    }
1733                                }
1734                                Some(CachedEntry::AssetError(err)) => {
1735                                    return Err(QueryError::UserError(err.clone()));
1736                                }
1737                                _ => return Ok((AssetLoadingState::loading(key), changed_at)),
1738                            }
1739                        }
1740                    }
1741                }
1742                Err(QueryError::UserError(err)) => {
1743                    // END event after locator completes with error
1744                    self.tracer.on_asset_located(
1745                        &span_ctx,
1746                        &asset_cache_key,
1747                        TracerAssetState::NotFound,
1748                    );
1749                    // Locator returned a user error - cache it as AssetError
1750                    let entry = CachedEntry::AssetError(err.clone());
1751                    let _ = self.whale.register(
1752                        full_cache_key,
1753                        Some(entry),
1754                        Durability::volatile(),
1755                        locator_deps,
1756                    );
1757                    return Err(QueryError::UserError(err));
1758                }
1759                Err(e) => {
1760                    // Other errors (Cycle, Suspended, etc.) - do NOT cache, propagate directly
1761                    return Err(e);
1762                }
1763            }
1764        }
1765
1766        // No locator registered or locator returned None - mark as pending
1767        // (no locator was called, so no deps to track)
1768        // END event - no locator ran
1769        self.tracer
1770            .on_asset_located(&span_ctx, &asset_cache_key, TracerAssetState::Loading);
1771        self.pending
1772            .insert::<K>(asset_cache_key.clone(), key.clone());
1773
1774        match self
1775            .whale
1776            .get_or_insert(full_cache_key, None, Durability::volatile(), vec![])
1777            .expect("get_or_insert with no dependencies cannot fail")
1778        {
1779            GetOrInsertResult::Inserted(node) => {
1780                Ok((AssetLoadingState::loading(key), node.changed_at))
1781            }
1782            GetOrInsertResult::Existing(node) => {
1783                let changed_at = node.changed_at;
1784                match &node.data {
1785                    Some(CachedEntry::AssetReady(arc)) => {
1786                        match arc.clone().downcast::<K::Asset>() {
1787                            Ok(value) => Ok((AssetLoadingState::ready(key, value), changed_at)),
1788                            Err(_) => Ok((AssetLoadingState::loading(key), changed_at)),
1789                        }
1790                    }
1791                    Some(CachedEntry::AssetError(err)) => Err(QueryError::UserError(err.clone())),
1792                    _ => Ok((AssetLoadingState::loading(key), changed_at)),
1793                }
1794            }
1795        }
1796    }
1797
1798    /// Internal: Get asset state and its changed_at revision atomically (with QueryContext).
1799    ///
1800    /// This version is called from QueryContext::asset. Consistency checking for
1801    /// cached leaf assets is done inside this function before returning.
1802    fn get_asset_with_revision_ctx<K: AssetKey>(
1803        &self,
1804        key: K,
1805        _ctx: &QueryContext<'_, T>,
1806    ) -> Result<(AssetLoadingState<K>, RevisionCounter), QueryError> {
1807        let asset_cache_key = AssetCacheKey::new(key.clone());
1808        let full_cache_key: FullCacheKey = asset_cache_key.clone().into();
1809
1810        // Create a span for this asset request (like queries do)
1811        // This ensures child queries called from locators show as children of this asset
1812        let asset_span_id = self.tracer.new_span_id();
1813        let (trace_id, parent_span_id) = SPAN_STACK.with(|stack| match &*stack.borrow() {
1814            SpanStack::Empty => (self.tracer.new_trace_id(), None),
1815            SpanStack::Active(tid, spans) => (*tid, spans.last().copied()),
1816        });
1817        let span_ctx = SpanContext {
1818            span_id: asset_span_id,
1819            trace_id,
1820            parent_span_id,
1821        };
1822
1823        // Push asset span to stack so child queries see this asset as their parent
1824        let _span_guard = SpanStackGuard::push(trace_id, asset_span_id);
1825
1826        // Check whale cache first (single atomic read)
1827        if let Some((cached_data, changed_at)) = self.whale.get_data(&full_cache_key) {
1828            // Check if valid at current revision (shallow check)
1829            if self.whale.is_valid(&full_cache_key) {
1830                // Verify dependencies recursively (like query path does)
1831                let mut deps_verified = true;
1832                if let Some(deps) = self.whale.get_dependency_ids(&full_cache_key) {
1833                    for dep in deps {
1834                        if let Some(verifier) = self.verifiers.get(&dep) {
1835                            // Re-run query/asset to verify it (triggers recursive verification)
1836                            if verifier.verify(self as &dyn std::any::Any).is_err() {
1837                                deps_verified = false;
1838                                break;
1839                            }
1840                        }
1841                    }
1842                }
1843
1844                // Re-check validity after deps are verified
1845                if deps_verified && self.whale.is_valid(&full_cache_key) {
1846                    // For cached entries, check consistency for leaf assets (no locator deps).
1847                    // This detects if resolve_asset/resolve_asset_error was called during query execution.
1848                    let has_locator_deps = self
1849                        .whale
1850                        .get_dependency_ids(&full_cache_key)
1851                        .is_some_and(|deps| !deps.is_empty());
1852
1853                    match &cached_data {
1854                        Some(CachedEntry::AssetReady(arc)) => {
1855                            // Check consistency for cached leaf assets
1856                            if !has_locator_deps {
1857                                check_leaf_asset_consistency(changed_at)?;
1858                            }
1859                            // Cache hit: start + end immediately (no locator runs)
1860                            self.tracer.on_asset_requested(&span_ctx, &asset_cache_key);
1861                            self.tracer.on_asset_located(
1862                                &span_ctx,
1863                                &asset_cache_key,
1864                                TracerAssetState::Ready,
1865                            );
1866                            match arc.clone().downcast::<K::Asset>() {
1867                                Ok(value) => {
1868                                    return Ok((AssetLoadingState::ready(key, value), changed_at))
1869                                }
1870                                Err(_) => {
1871                                    unreachable!("Asset type mismatch: {:?}", key)
1872                                }
1873                            }
1874                        }
1875                        Some(CachedEntry::AssetError(err)) => {
1876                            // Check consistency for cached leaf errors
1877                            if !has_locator_deps {
1878                                check_leaf_asset_consistency(changed_at)?;
1879                            }
1880                            // Cache hit: start + end immediately (no locator runs)
1881                            self.tracer.on_asset_requested(&span_ctx, &asset_cache_key);
1882                            self.tracer.on_asset_located(
1883                                &span_ctx,
1884                                &asset_cache_key,
1885                                TracerAssetState::NotFound,
1886                            );
1887                            return Err(QueryError::UserError(err.clone()));
1888                        }
1889                        None => {
1890                            // Loading state - no value to be inconsistent with
1891                            // Cache hit: start + end immediately (no locator runs)
1892                            self.tracer.on_asset_requested(&span_ctx, &asset_cache_key);
1893                            self.tracer.on_asset_located(
1894                                &span_ctx,
1895                                &asset_cache_key,
1896                                TracerAssetState::Loading,
1897                            );
1898                            return Ok((AssetLoadingState::loading(key), changed_at));
1899                        }
1900                        _ => {
1901                            // Query-related entries (Ok, UserError) shouldn't be here
1902                            // Fall through to locator
1903                        }
1904                    }
1905                }
1906            }
1907        }
1908
1909        // Not in cache or invalid - try locator
1910        // Use LocatorContext to track deps on the asset itself (not the calling query)
1911        // Consistency tracking is handled via thread-local storage
1912        check_cycle(&full_cache_key)?;
1913        let _guard = StackGuard::push(full_cache_key.clone());
1914
1915        // START event before locator runs
1916        self.tracer.on_asset_requested(&span_ctx, &asset_cache_key);
1917
1918        let locator_ctx = LocatorContext::new(self, full_cache_key.clone());
1919        let locator_result =
1920            self.locators
1921                .locate_with_locator_ctx(TypeId::of::<K>(), &locator_ctx, &key);
1922
1923        if let Some(result) = locator_result {
1924            // Get collected dependencies from the locator context
1925            let locator_deps = locator_ctx.into_deps();
1926            match result {
1927                Ok(ErasedLocateResult::Ready {
1928                    value: arc,
1929                    durability: durability_level,
1930                }) => {
1931                    // END event after locator completes
1932                    self.tracer.on_asset_located(
1933                        &span_ctx,
1934                        &asset_cache_key,
1935                        TracerAssetState::Ready,
1936                    );
1937
1938                    let typed_value: Arc<K::Asset> = match arc.downcast::<K::Asset>() {
1939                        Ok(v) => v,
1940                        Err(_) => {
1941                            unreachable!("Asset type mismatch: {:?}", key);
1942                        }
1943                    };
1944
1945                    // Store in whale atomically with early cutoff
1946                    // Include locator's dependencies so the asset is invalidated when they change
1947                    let entry = CachedEntry::AssetReady(typed_value.clone());
1948                    let durability = Durability::new(durability_level.as_u8() as usize)
1949                        .unwrap_or(Durability::volatile());
1950                    let new_value = typed_value.clone();
1951                    let result = self
1952                        .whale
1953                        .update_with_compare(
1954                            full_cache_key.clone(),
1955                            Some(entry),
1956                            |old_data, _new_data| {
1957                                let Some(CachedEntry::AssetReady(old_arc)) =
1958                                    old_data.and_then(|d| d.as_ref())
1959                                else {
1960                                    return true;
1961                                };
1962                                let Ok(old_value) = old_arc.clone().downcast::<K::Asset>() else {
1963                                    return true;
1964                                };
1965                                !K::asset_eq(&old_value, &new_value)
1966                            },
1967                            durability,
1968                            locator_deps,
1969                        )
1970                        .expect("update_with_compare should succeed");
1971
1972                    // Register verifier for this asset (for verify-then-decide pattern)
1973                    self.verifiers
1974                        .insert_asset::<K, T>(full_cache_key, key.clone());
1975
1976                    return Ok((AssetLoadingState::ready(key, typed_value), result.revision));
1977                }
1978                Ok(ErasedLocateResult::Pending) => {
1979                    // END event after locator completes with pending
1980                    self.tracer.on_asset_located(
1981                        &span_ctx,
1982                        &asset_cache_key,
1983                        TracerAssetState::Loading,
1984                    );
1985
1986                    // Add to pending list for Pending result
1987                    self.pending
1988                        .insert::<K>(asset_cache_key.clone(), key.clone());
1989                    match self
1990                        .whale
1991                        .get_or_insert(full_cache_key, None, Durability::volatile(), locator_deps)
1992                        .expect("get_or_insert should succeed")
1993                    {
1994                        GetOrInsertResult::Inserted(node) => {
1995                            return Ok((AssetLoadingState::loading(key), node.changed_at));
1996                        }
1997                        GetOrInsertResult::Existing(node) => {
1998                            let changed_at = node.changed_at;
1999                            match &node.data {
2000                                Some(CachedEntry::AssetReady(arc)) => {
2001                                    match arc.clone().downcast::<K::Asset>() {
2002                                        Ok(value) => {
2003                                            return Ok((
2004                                                AssetLoadingState::ready(key, value),
2005                                                changed_at,
2006                                            ));
2007                                        }
2008                                        Err(_) => {
2009                                            return Ok((
2010                                                AssetLoadingState::loading(key),
2011                                                changed_at,
2012                                            ))
2013                                        }
2014                                    }
2015                                }
2016                                Some(CachedEntry::AssetError(err)) => {
2017                                    return Err(QueryError::UserError(err.clone()));
2018                                }
2019                                _ => return Ok((AssetLoadingState::loading(key), changed_at)),
2020                            }
2021                        }
2022                    }
2023                }
2024                Err(QueryError::UserError(err)) => {
2025                    // END event after locator completes with error
2026                    self.tracer.on_asset_located(
2027                        &span_ctx,
2028                        &asset_cache_key,
2029                        TracerAssetState::NotFound,
2030                    );
2031                    // Locator returned a user error - cache it as AssetError
2032                    let entry = CachedEntry::AssetError(err.clone());
2033                    let _ = self.whale.register(
2034                        full_cache_key,
2035                        Some(entry),
2036                        Durability::volatile(),
2037                        locator_deps,
2038                    );
2039                    return Err(QueryError::UserError(err));
2040                }
2041                Err(e) => {
2042                    // Other errors (Cycle, Suspended, etc.) - do NOT cache, propagate directly
2043                    return Err(e);
2044                }
2045            }
2046        }
2047
2048        // No locator registered or locator returned None - mark as pending
2049        // END event - no locator ran
2050        self.tracer
2051            .on_asset_located(&span_ctx, &asset_cache_key, TracerAssetState::Loading);
2052        self.pending
2053            .insert::<K>(asset_cache_key.clone(), key.clone());
2054
2055        match self
2056            .whale
2057            .get_or_insert(full_cache_key, None, Durability::volatile(), vec![])
2058            .expect("get_or_insert with no dependencies cannot fail")
2059        {
2060            GetOrInsertResult::Inserted(node) => {
2061                Ok((AssetLoadingState::loading(key), node.changed_at))
2062            }
2063            GetOrInsertResult::Existing(node) => {
2064                let changed_at = node.changed_at;
2065                match &node.data {
2066                    Some(CachedEntry::AssetReady(arc)) => {
2067                        match arc.clone().downcast::<K::Asset>() {
2068                            Ok(value) => Ok((AssetLoadingState::ready(key, value), changed_at)),
2069                            Err(_) => Ok((AssetLoadingState::loading(key), changed_at)),
2070                        }
2071                    }
2072                    Some(CachedEntry::AssetError(err)) => Err(QueryError::UserError(err.clone())),
2073                    _ => Ok((AssetLoadingState::loading(key), changed_at)),
2074                }
2075            }
2076        }
2077    }
2078
2079    /// Internal: Get asset state, checking cache and locator.
2080    fn get_asset_internal<K: AssetKey>(&self, key: K) -> Result<AssetLoadingState<K>, QueryError> {
2081        self.get_asset_with_revision(key).map(|(state, _)| state)
2082    }
2083}
2084
2085impl<T: Tracer> Db for QueryRuntime<T> {
2086    fn query<Q: Query>(&self, query: Q) -> Result<Arc<Q::Output>, QueryError> {
2087        QueryRuntime::query(self, query)
2088    }
2089
2090    fn asset<K: AssetKey>(&self, key: K) -> Result<Arc<K::Asset>, QueryError> {
2091        self.get_asset_internal(key)?.suspend()
2092    }
2093
2094    fn asset_state<K: AssetKey>(&self, key: K) -> Result<AssetLoadingState<K>, QueryError> {
2095        self.get_asset_internal(key)
2096    }
2097
2098    fn list_queries<Q: Query>(&self) -> Vec<Q> {
2099        self.query_registry.get_all::<Q>()
2100    }
2101
2102    fn list_asset_keys<K: AssetKey>(&self) -> Vec<K> {
2103        self.asset_key_registry.get_all::<K>()
2104    }
2105}
2106
2107/// Tracks consistency of leaf asset accesses during query execution.
2108///
2109/// A "leaf" asset is one without dependencies (externally resolved via `resolve_asset`).
2110/// This tracker ensures that all leaf assets accessed during a query execution
2111/// (including those accessed by locators) are consistent - i.e., none were modified
2112/// via `resolve_asset` mid-execution.
2113///
2114/// The tracker is shared across `QueryContext` and `LocatorContext` to propagate
2115/// consistency checking through the entire execution tree.
2116#[derive(Debug)]
2117pub(crate) struct ConsistencyTracker {
2118    /// Global revision at query start. All leaf assets must have changed_at <= this.
2119    start_revision: RevisionCounter,
2120}
2121
2122impl ConsistencyTracker {
2123    /// Create a new tracker with the given start revision.
2124    pub fn new(start_revision: RevisionCounter) -> Self {
2125        Self { start_revision }
2126    }
2127
2128    /// Check consistency for a leaf asset access.
2129    ///
2130    /// A leaf asset is consistent if its changed_at <= start_revision.
2131    /// This detects if resolve_asset was called during query execution.
2132    ///
2133    /// Returns Ok(()) if consistent, Err if inconsistent.
2134    pub fn check_leaf_asset(&self, dep_changed_at: RevisionCounter) -> Result<(), QueryError> {
2135        if dep_changed_at > self.start_revision {
2136            Err(QueryError::InconsistentAssetResolution)
2137        } else {
2138            Ok(())
2139        }
2140    }
2141}
2142
2143/// Context provided to queries during execution.
2144///
2145/// Use this to access dependencies via `query()`.
2146pub(crate) struct QueryContext<'a, T: Tracer = NoopTracer> {
2147    runtime: &'a QueryRuntime<T>,
2148    current_key: FullCacheKey,
2149    exec_ctx: ExecutionContext,
2150    deps: RefCell<Vec<FullCacheKey>>,
2151}
2152
2153impl<'a, T: Tracer> QueryContext<'a, T> {
2154    /// Query a dependency.
2155    ///
2156    /// The dependency is automatically tracked for invalidation.
2157    ///
2158    /// # Example
2159    ///
2160    /// ```
2161    /// use query_flow::{query, Db, QueryError, QueryRuntime};
2162    ///
2163    /// #[query]
2164    /// fn other_query(db: &impl Db, id: u64) -> Result<u64, QueryError> {
2165    ///     let _ = db;
2166    ///     Ok(id * 2)
2167    /// }
2168    ///
2169    /// #[query]
2170    /// fn caller(db: &impl Db, id: u64) -> Result<u64, QueryError> {
2171    ///     let dep_result = db.query(OtherQuery::new(id))?;
2172    ///     Ok(*dep_result + 1)
2173    /// }
2174    ///
2175    /// let runtime = QueryRuntime::new();
2176    /// assert_eq!(*runtime.query(Caller::new(21)).unwrap(), 43);
2177    /// ```
2178    pub fn query<Q: Query>(&self, query: Q) -> Result<Arc<Q::Output>, QueryError> {
2179        let full_key: FullCacheKey = QueryCacheKey::new(query.clone()).into();
2180
2181        // Emit dependency registered event
2182        self.runtime.tracer.on_dependency_registered(
2183            self.exec_ctx.span_ctx(),
2184            &self.current_key,
2185            &full_key,
2186        );
2187
2188        // Record this as a dependency
2189        self.deps.borrow_mut().push(full_key);
2190
2191        // Execute the query
2192        self.runtime.query(query)
2193    }
2194
2195    /// Access an asset, tracking it as a dependency.
2196    ///
2197    /// Returns the asset value if ready, or `Err(QueryError::Suspend)` if still loading.
2198    /// Use this with the `?` operator for automatic suspension on loading.
2199    ///
2200    /// # Example
2201    ///
2202    /// ```
2203    /// use query_flow::{asset_key, query, Db, DurabilityLevel, QueryError, QueryRuntime};
2204    ///
2205    /// #[asset_key(asset = String)]
2206    /// struct FilePath(String);
2207    ///
2208    /// #[query]
2209    /// fn process_file(db: &impl Db, path: FilePath) -> Result<usize, QueryError> {
2210    ///     let content = db.asset(path)?;
2211    ///     // Process content...
2212    ///     Ok(content.len())
2213    /// }
2214    ///
2215    /// let runtime = QueryRuntime::new();
2216    /// runtime.resolve_asset(
2217    ///     FilePath("a".into()),
2218    ///     "hello".into(),
2219    ///     DurabilityLevel::Volatile,
2220    /// );
2221    /// assert_eq!(
2222    ///     *runtime.query(ProcessFile::new(FilePath("a".into()))).unwrap(),
2223    ///     5
2224    /// );
2225    /// ```
2226    ///
2227    /// # Errors
2228    ///
2229    /// - Returns `Err(QueryError::Suspend)` if the asset is still loading.
2230    /// - Returns `Err(QueryError::UserError)` if the asset was not found.
2231    pub fn asset<K: AssetKey>(&self, key: K) -> Result<Arc<K::Asset>, QueryError> {
2232        self.asset_state(key)?.suspend()
2233    }
2234
2235    /// Access an asset's loading state, tracking it as a dependency.
2236    ///
2237    /// Unlike [`asset()`](Self::asset), this method returns the full loading state,
2238    /// allowing you to check if an asset is loading without triggering suspension.
2239    ///
2240    /// # Example
2241    ///
2242    /// ```
2243    /// use query_flow::{asset_key, query, Db, DurabilityLevel, QueryError, QueryRuntime};
2244    ///
2245    /// #[asset_key(asset = String)]
2246    /// struct FilePath(String);
2247    ///
2248    /// #[query]
2249    /// fn describe(db: &impl Db, key: FilePath) -> Result<String, QueryError> {
2250    ///     let state = db.asset_state(key)?;
2251    ///     if state.is_loading() {
2252    ///         // Handle loading case explicitly
2253    ///         Ok("loading".to_string())
2254    ///     } else {
2255    ///         let value = state.get().unwrap();
2256    ///         Ok(format!("{} bytes", value.len()))
2257    ///     }
2258    /// }
2259    ///
2260    /// let runtime = QueryRuntime::new();
2261    /// runtime.resolve_asset(
2262    ///     FilePath("a".into()),
2263    ///     "hello".into(),
2264    ///     DurabilityLevel::Volatile,
2265    /// );
2266    /// assert_eq!(
2267    ///     *runtime.query(Describe::new(FilePath("a".into()))).unwrap(),
2268    ///     "5 bytes"
2269    /// );
2270    /// ```
2271    ///
2272    /// # Errors
2273    ///
2274    /// Returns `Err(QueryError::UserError)` if the asset was not found.
2275    pub fn asset_state<K: AssetKey>(&self, key: K) -> Result<AssetLoadingState<K>, QueryError> {
2276        let full_cache_key: FullCacheKey = AssetCacheKey::new(key.clone()).into();
2277
2278        // 1. Emit asset dependency registered event
2279        self.runtime.tracer.on_asset_dependency_registered(
2280            self.exec_ctx.span_ctx(),
2281            &self.current_key,
2282            &full_cache_key,
2283        );
2284
2285        // 2. Record dependency on this asset
2286        self.deps.borrow_mut().push(full_cache_key);
2287
2288        // 3. Get asset from cache/locator
2289        // Consistency checking for cached leaf assets is done inside get_asset_with_revision_ctx
2290        let (state, _changed_at) = self.runtime.get_asset_with_revision_ctx(key, self)?;
2291
2292        Ok(state)
2293    }
2294
2295    /// List all query instances of type Q that have been registered.
2296    ///
2297    /// This method establishes a dependency on the "set" of queries of type Q.
2298    /// The calling query will be invalidated when:
2299    /// - A new query of type Q is first executed (added to set)
2300    ///
2301    /// The calling query will NOT be invalidated when:
2302    /// - An individual query of type Q has its value change
2303    ///
2304    /// # Example
2305    ///
2306    /// ```
2307    /// use query_flow::{query, Db, QueryError, QueryRuntime};
2308    ///
2309    /// #[query]
2310    /// fn my_query(db: &impl Db, x: i32) -> Result<i32, QueryError> {
2311    ///     let _ = db;
2312    ///     Ok(x * 2)
2313    /// }
2314    ///
2315    /// #[query]
2316    /// fn all_results(db: &impl Db) -> Result<Vec<i32>, QueryError> {
2317    ///     let queries = db.list_queries::<MyQuery>();
2318    ///     let mut results = Vec::new();
2319    ///     for q in queries {
2320    ///         results.push(*db.query(q)?);
2321    ///     }
2322    ///     results.sort();
2323    ///     Ok(results)
2324    /// }
2325    ///
2326    /// let runtime = QueryRuntime::new();
2327    /// runtime.query(MyQuery::new(1)).unwrap();
2328    /// runtime.query(MyQuery::new(2)).unwrap();
2329    ///
2330    /// assert_eq!(*runtime.query(AllResults::new()).unwrap(), vec![2, 4]);
2331    /// ```
2332    pub fn list_queries<Q: Query>(&self) -> Vec<Q> {
2333        // Record dependency on the sentinel (set-level dependency)
2334        let sentinel: FullCacheKey = QuerySetSentinelKey::new::<Q>().into();
2335
2336        self.runtime.tracer.on_dependency_registered(
2337            self.exec_ctx.span_ctx(),
2338            &self.current_key,
2339            &sentinel,
2340        );
2341
2342        // Ensure sentinel exists in whale (for dependency tracking)
2343        if self.runtime.whale.get(&sentinel).is_none() {
2344            let _ =
2345                self.runtime
2346                    .whale
2347                    .register(sentinel.clone(), None, Durability::volatile(), vec![]);
2348        }
2349
2350        self.deps.borrow_mut().push(sentinel);
2351
2352        // Return all registered queries
2353        self.runtime.query_registry.get_all::<Q>()
2354    }
2355
2356    /// List all asset keys of type K that have been registered.
2357    ///
2358    /// This method establishes a dependency on the "set" of asset keys of type K.
2359    /// The calling query will be invalidated when:
2360    /// - A new asset of type K is resolved for the first time (added to set)
2361    /// - An asset of type K is removed via remove_asset
2362    ///
2363    /// The calling query will NOT be invalidated when:
2364    /// - An individual asset's value changes (use `db.asset()` for that)
2365    ///
2366    /// # Example
2367    ///
2368    /// ```
2369    /// use query_flow::{asset_key, query, Db, DurabilityLevel, QueryError, QueryRuntime};
2370    ///
2371    /// #[asset_key(asset = String)]
2372    /// struct ConfigFile(String);
2373    ///
2374    /// #[query]
2375    /// fn all_configs(db: &impl Db) -> Result<Vec<String>, QueryError> {
2376    ///     let keys = db.list_asset_keys::<ConfigFile>();
2377    ///     let mut contents = Vec::new();
2378    ///     for key in keys {
2379    ///         let content = db.asset(key)?;
2380    ///         contents.push((*content).clone());
2381    ///     }
2382    ///     contents.sort();
2383    ///     Ok(contents)
2384    /// }
2385    ///
2386    /// let runtime = QueryRuntime::new();
2387    /// runtime.resolve_asset(ConfigFile("a".into()), "one".into(), DurabilityLevel::Volatile);
2388    /// runtime.resolve_asset(ConfigFile("b".into()), "two".into(), DurabilityLevel::Volatile);
2389    ///
2390    /// assert_eq!(
2391    ///     *runtime.query(AllConfigs::new()).unwrap(),
2392    ///     vec!["one".to_string(), "two".to_string()]
2393    /// );
2394    /// ```
2395    pub fn list_asset_keys<K: AssetKey>(&self) -> Vec<K> {
2396        // Record dependency on the sentinel (set-level dependency)
2397        let sentinel: FullCacheKey = AssetKeySetSentinelKey::new::<K>().into();
2398
2399        self.runtime.tracer.on_asset_dependency_registered(
2400            self.exec_ctx.span_ctx(),
2401            &self.current_key,
2402            &sentinel,
2403        );
2404
2405        // Ensure sentinel exists in whale (for dependency tracking)
2406        if self.runtime.whale.get(&sentinel).is_none() {
2407            let _ =
2408                self.runtime
2409                    .whale
2410                    .register(sentinel.clone(), None, Durability::volatile(), vec![]);
2411        }
2412
2413        self.deps.borrow_mut().push(sentinel);
2414
2415        // Return all registered asset keys
2416        self.runtime.asset_key_registry.get_all::<K>()
2417    }
2418}
2419
2420impl<'a, T: Tracer> Db for QueryContext<'a, T> {
2421    fn query<Q: Query>(&self, query: Q) -> Result<Arc<Q::Output>, QueryError> {
2422        QueryContext::query(self, query)
2423    }
2424
2425    fn asset<K: AssetKey>(&self, key: K) -> Result<Arc<K::Asset>, QueryError> {
2426        QueryContext::asset(self, key)
2427    }
2428
2429    fn asset_state<K: AssetKey>(&self, key: K) -> Result<AssetLoadingState<K>, QueryError> {
2430        QueryContext::asset_state(self, key)
2431    }
2432
2433    fn list_queries<Q: Query>(&self) -> Vec<Q> {
2434        QueryContext::list_queries(self)
2435    }
2436
2437    fn list_asset_keys<K: AssetKey>(&self) -> Vec<K> {
2438        QueryContext::list_asset_keys(self)
2439    }
2440}
2441
2442/// Context for collecting dependencies during asset locator execution.
2443///
2444/// Unlike `QueryContext`, this is specifically for locators and does not
2445/// register dependencies on any parent query. Dependencies collected here
2446/// are stored with the asset itself.
2447pub(crate) struct LocatorContext<'a, T: Tracer> {
2448    runtime: &'a QueryRuntime<T>,
2449    deps: RefCell<Vec<FullCacheKey>>,
2450}
2451
2452impl<'a, T: Tracer> LocatorContext<'a, T> {
2453    /// Create a new locator context for the given asset key.
2454    ///
2455    /// Consistency tracking is handled via thread-local storage, so leaf asset
2456    /// accesses will be checked against any active tracker from a parent query.
2457    pub(crate) fn new(runtime: &'a QueryRuntime<T>, _asset_key: FullCacheKey) -> Self {
2458        Self {
2459            runtime,
2460            deps: RefCell::new(Vec::new()),
2461        }
2462    }
2463
2464    /// Consume this context and return the collected dependencies.
2465    pub(crate) fn into_deps(self) -> Vec<FullCacheKey> {
2466        self.deps.into_inner()
2467    }
2468}
2469
2470impl<T: Tracer> Db for LocatorContext<'_, T> {
2471    fn query<Q: Query>(&self, query: Q) -> Result<Arc<Q::Output>, QueryError> {
2472        let full_key = QueryCacheKey::new(query.clone()).into();
2473
2474        // Record this as a dependency of the asset being located
2475        self.deps.borrow_mut().push(full_key);
2476
2477        // Execute the query via the runtime
2478        self.runtime.query(query)
2479    }
2480
2481    fn asset<K: AssetKey>(&self, key: K) -> Result<Arc<K::Asset>, QueryError> {
2482        self.asset_state(key)?.suspend()
2483    }
2484
2485    fn asset_state<K: AssetKey>(&self, key: K) -> Result<AssetLoadingState<K>, QueryError> {
2486        let full_cache_key = AssetCacheKey::new(key.clone()).into();
2487
2488        // Record this as a dependency of the asset being located
2489        self.deps.borrow_mut().push(full_cache_key);
2490
2491        // Access the asset - consistency checking is done inside get_asset_with_revision
2492        let (state, _changed_at) = self.runtime.get_asset_with_revision(key)?;
2493
2494        Ok(state)
2495    }
2496
2497    fn list_queries<Q: Query>(&self) -> Vec<Q> {
2498        self.runtime.list_queries()
2499    }
2500
2501    fn list_asset_keys<K: AssetKey>(&self) -> Vec<K> {
2502        self.runtime.list_asset_keys()
2503    }
2504}
2505
2506/// Enum dispatch wrapper for Db implementations.
2507///
2508/// Reduces monomorphization by providing a single concrete type
2509/// for `&impl Db` parameters in user code.
2510pub(crate) enum DbDispatch<'a, T: Tracer = NoopTracer> {
2511    /// Query execution context (tracks query dependencies)
2512    QueryContext(&'a QueryContext<'a, T>),
2513    /// Locator execution context (tracks asset dependencies)
2514    LocatorContext(&'a LocatorContext<'a, T>),
2515}
2516
2517impl<T: Tracer> Db for DbDispatch<'_, T> {
2518    fn query<Q: Query>(&self, query: Q) -> Result<Arc<Q::Output>, QueryError> {
2519        match self {
2520            DbDispatch::QueryContext(ctx) => ctx.query(query),
2521            DbDispatch::LocatorContext(ctx) => ctx.query(query),
2522        }
2523    }
2524
2525    fn asset<K: AssetKey>(&self, key: K) -> Result<Arc<K::Asset>, QueryError> {
2526        match self {
2527            DbDispatch::QueryContext(ctx) => ctx.asset(key),
2528            DbDispatch::LocatorContext(ctx) => ctx.asset(key),
2529        }
2530    }
2531
2532    fn asset_state<K: AssetKey>(&self, key: K) -> Result<AssetLoadingState<K>, QueryError> {
2533        match self {
2534            DbDispatch::QueryContext(ctx) => ctx.asset_state(key),
2535            DbDispatch::LocatorContext(ctx) => ctx.asset_state(key),
2536        }
2537    }
2538
2539    fn list_queries<Q: Query>(&self) -> Vec<Q> {
2540        match self {
2541            DbDispatch::QueryContext(ctx) => ctx.list_queries(),
2542            DbDispatch::LocatorContext(ctx) => ctx.list_queries(),
2543        }
2544    }
2545
2546    fn list_asset_keys<K: AssetKey>(&self) -> Vec<K> {
2547        match self {
2548            DbDispatch::QueryContext(ctx) => ctx.list_asset_keys(),
2549            DbDispatch::LocatorContext(ctx) => ctx.list_asset_keys(),
2550        }
2551    }
2552}
2553
2554#[cfg(test)]
2555mod tests {
2556    use super::*;
2557
2558    #[test]
2559    fn test_simple_query() {
2560        #[derive(Clone, Debug, Hash, PartialEq, Eq)]
2561        struct Add {
2562            a: i32,
2563            b: i32,
2564        }
2565
2566        impl Query for Add {
2567            type Output = i32;
2568
2569            fn query(self, _db: &impl Db) -> Result<Arc<Self::Output>, QueryError> {
2570                Ok(Arc::new(self.a + self.b))
2571            }
2572
2573            fn output_eq(old: &Self::Output, new: &Self::Output) -> bool {
2574                old == new
2575            }
2576        }
2577
2578        let runtime = QueryRuntime::new();
2579
2580        let result = runtime.query(Add { a: 1, b: 2 }).unwrap();
2581        assert_eq!(*result, 3);
2582
2583        // Second query should be cached
2584        let result2 = runtime.query(Add { a: 1, b: 2 }).unwrap();
2585        assert_eq!(*result2, 3);
2586    }
2587
2588    #[test]
2589    fn test_dependent_queries() {
2590        #[derive(Clone, Debug, Hash, PartialEq, Eq)]
2591        struct Base {
2592            value: i32,
2593        }
2594
2595        impl Query for Base {
2596            type Output = i32;
2597
2598            fn query(self, _db: &impl Db) -> Result<Arc<Self::Output>, QueryError> {
2599                Ok(Arc::new(self.value * 2))
2600            }
2601
2602            fn output_eq(old: &Self::Output, new: &Self::Output) -> bool {
2603                old == new
2604            }
2605        }
2606
2607        #[derive(Clone, Debug, Hash, PartialEq, Eq)]
2608        struct Derived {
2609            base_value: i32,
2610        }
2611
2612        impl Query for Derived {
2613            type Output = i32;
2614
2615            fn query(self, db: &impl Db) -> Result<Arc<Self::Output>, QueryError> {
2616                let base = db.query(Base {
2617                    value: self.base_value,
2618                })?;
2619                Ok(Arc::new(*base + 10))
2620            }
2621
2622            fn output_eq(old: &Self::Output, new: &Self::Output) -> bool {
2623                old == new
2624            }
2625        }
2626
2627        let runtime = QueryRuntime::new();
2628
2629        let result = runtime.query(Derived { base_value: 5 }).unwrap();
2630        assert_eq!(*result, 20); // 5 * 2 + 10
2631    }
2632
2633    #[test]
2634    fn test_cycle_detection() {
2635        #[derive(Clone, Debug, Hash, PartialEq, Eq)]
2636        struct CycleA {
2637            id: i32,
2638        }
2639
2640        #[derive(Clone, Debug, Hash, PartialEq, Eq)]
2641        struct CycleB {
2642            id: i32,
2643        }
2644
2645        impl Query for CycleA {
2646            type Output = i32;
2647
2648            fn query(self, db: &impl Db) -> Result<Arc<Self::Output>, QueryError> {
2649                let b = db.query(CycleB { id: self.id })?;
2650                Ok(Arc::new(*b + 1))
2651            }
2652
2653            fn output_eq(old: &Self::Output, new: &Self::Output) -> bool {
2654                old == new
2655            }
2656        }
2657
2658        impl Query for CycleB {
2659            type Output = i32;
2660
2661            fn query(self, db: &impl Db) -> Result<Arc<Self::Output>, QueryError> {
2662                let a = db.query(CycleA { id: self.id })?;
2663                Ok(Arc::new(*a + 1))
2664            }
2665
2666            fn output_eq(old: &Self::Output, new: &Self::Output) -> bool {
2667                old == new
2668            }
2669        }
2670
2671        let runtime = QueryRuntime::new();
2672
2673        let result = runtime.query(CycleA { id: 1 });
2674        assert!(matches!(result, Err(QueryError::Cycle { .. })));
2675    }
2676
2677    #[test]
2678    fn test_fallible_query() {
2679        #[derive(Clone, Debug, Hash, PartialEq, Eq)]
2680        struct ParseInt {
2681            input: String,
2682        }
2683
2684        impl Query for ParseInt {
2685            type Output = Result<i32, std::num::ParseIntError>;
2686
2687            fn query(self, _db: &impl Db) -> Result<Arc<Self::Output>, QueryError> {
2688                Ok(Arc::new(self.input.parse()))
2689            }
2690
2691            fn output_eq(old: &Self::Output, new: &Self::Output) -> bool {
2692                old == new
2693            }
2694        }
2695
2696        let runtime = QueryRuntime::new();
2697
2698        // Valid parse
2699        let result = runtime
2700            .query(ParseInt {
2701                input: "42".to_string(),
2702            })
2703            .unwrap();
2704        assert_eq!(*result, Ok(42));
2705
2706        // Invalid parse - system succeeds, user error in output
2707        let result = runtime
2708            .query(ParseInt {
2709                input: "not_a_number".to_string(),
2710            })
2711            .unwrap();
2712        assert!(result.is_err());
2713    }
2714
2715    // Macro tests
2716    mod macro_tests {
2717        use super::*;
2718        use crate::query;
2719
2720        #[query]
2721        fn add(db: &impl Db, a: i32, b: i32) -> Result<i32, QueryError> {
2722            let _ = db; // silence unused warning
2723            Ok(a + b)
2724        }
2725
2726        #[test]
2727        fn test_macro_basic() {
2728            let runtime = QueryRuntime::new();
2729            let result = runtime.query(Add::new(1, 2)).unwrap();
2730            assert_eq!(*result, 3);
2731        }
2732
2733        #[query]
2734        fn simple_double(db: &impl Db, x: i32) -> Result<i32, QueryError> {
2735            let _ = db;
2736            Ok(x * 2)
2737        }
2738
2739        #[test]
2740        fn test_macro_simple() {
2741            let runtime = QueryRuntime::new();
2742            let result = runtime.query(SimpleDouble::new(5)).unwrap();
2743            assert_eq!(*result, 10);
2744        }
2745
2746        #[query(keys(id))]
2747        fn with_key_selection(
2748            db: &impl Db,
2749            id: u32,
2750            include_extra: bool,
2751        ) -> Result<String, QueryError> {
2752            let _ = db;
2753            Ok(format!("id={}, extra={}", id, include_extra))
2754        }
2755
2756        #[test]
2757        fn test_macro_key_selection() {
2758            let runtime = QueryRuntime::new();
2759
2760            // Same id, different include_extra - should return cached
2761            let r1 = runtime.query(WithKeySelection::new(1, true)).unwrap();
2762            let r2 = runtime.query(WithKeySelection::new(1, false)).unwrap();
2763
2764            // Both should have same value because only `id` is the key
2765            assert_eq!(*r1, "id=1, extra=true");
2766            assert_eq!(*r2, "id=1, extra=true"); // Cached!
2767        }
2768
2769        #[query]
2770        fn dependent(db: &impl Db, a: i32, b: i32) -> Result<i32, QueryError> {
2771            let sum = db.query(Add::new(a, b))?;
2772            Ok(*sum * 2)
2773        }
2774
2775        #[test]
2776        fn test_macro_dependencies() {
2777            let runtime = QueryRuntime::new();
2778            let result = runtime.query(Dependent::new(3, 4)).unwrap();
2779            assert_eq!(*result, 14); // (3 + 4) * 2
2780        }
2781
2782        #[query(output_eq)]
2783        fn with_output_eq(db: &impl Db, x: i32) -> Result<i32, QueryError> {
2784            let _ = db;
2785            Ok(x * 2)
2786        }
2787
2788        #[test]
2789        fn test_macro_output_eq() {
2790            let runtime = QueryRuntime::new();
2791            let result = runtime.query(WithOutputEq::new(5)).unwrap();
2792            assert_eq!(*result, 10);
2793        }
2794
2795        #[query(name = "CustomName")]
2796        fn original_name(db: &impl Db, x: i32) -> Result<i32, QueryError> {
2797            let _ = db;
2798            Ok(x)
2799        }
2800
2801        #[test]
2802        fn test_macro_custom_name() {
2803            let runtime = QueryRuntime::new();
2804            let result = runtime.query(CustomName::new(42)).unwrap();
2805            assert_eq!(*result, 42);
2806        }
2807
2808        // Test that attribute macros like #[tracing::instrument] are preserved
2809        // We use #[allow(unused_variables)] and #[inline] as test attributes since
2810        // they don't require external dependencies.
2811        #[allow(unused_variables)]
2812        #[inline]
2813        #[query]
2814        fn with_attributes(db: &impl Db, x: i32) -> Result<i32, QueryError> {
2815            // This would warn without #[allow(unused_variables)] on the generated method
2816            let unused_var = 42;
2817            Ok(x * 2)
2818        }
2819
2820        #[test]
2821        fn test_macro_preserves_attributes() {
2822            let runtime = QueryRuntime::new();
2823            // If attributes weren't preserved, this might warn about unused_var
2824            let result = runtime.query(WithAttributes::new(5)).unwrap();
2825            assert_eq!(*result, 10);
2826        }
2827    }
2828
2829    // Tests for poll() and changed_at()
2830    mod poll_tests {
2831        use super::*;
2832
2833        #[derive(Clone, Debug, Hash, PartialEq, Eq)]
2834        struct Counter {
2835            id: i32,
2836        }
2837
2838        impl Query for Counter {
2839            type Output = i32;
2840
2841            fn query(self, _db: &impl Db) -> Result<Arc<Self::Output>, QueryError> {
2842                Ok(Arc::new(self.id * 10))
2843            }
2844
2845            fn output_eq(old: &Self::Output, new: &Self::Output) -> bool {
2846                old == new
2847            }
2848        }
2849
2850        #[test]
2851        fn test_poll_returns_value_and_revision() {
2852            let runtime = QueryRuntime::new();
2853
2854            let result = runtime.poll(Counter { id: 1 }).unwrap();
2855
2856            // Value should be correct - access through Result and Arc
2857            assert_eq!(**result.value.as_ref().unwrap(), 10);
2858
2859            // Revision should be non-zero after first execution
2860            assert!(result.revision > 0);
2861        }
2862
2863        #[test]
2864        fn test_poll_revision_stable_on_cache_hit() {
2865            let runtime = QueryRuntime::new();
2866
2867            // First poll
2868            let result1 = runtime.poll(Counter { id: 1 }).unwrap();
2869            let rev1 = result1.revision;
2870
2871            // Second poll (cache hit)
2872            let result2 = runtime.poll(Counter { id: 1 }).unwrap();
2873            let rev2 = result2.revision;
2874
2875            // Revision should be the same (no change)
2876            assert_eq!(rev1, rev2);
2877        }
2878
2879        #[test]
2880        fn test_poll_revision_changes_on_invalidate() {
2881            let runtime = QueryRuntime::new();
2882
2883            // First poll
2884            let result1 = runtime.poll(Counter { id: 1 }).unwrap();
2885            let rev1 = result1.revision;
2886
2887            // Invalidate and poll again
2888            runtime.invalidate(&Counter { id: 1 });
2889            let result2 = runtime.poll(Counter { id: 1 }).unwrap();
2890            let rev2 = result2.revision;
2891
2892            // Revision should increase (value was recomputed)
2893            // Note: Since output_eq returns true (same value), this might not change
2894            // depending on early cutoff behavior. Let's verify the value is still correct.
2895            assert_eq!(**result2.value.as_ref().unwrap(), 10);
2896
2897            // With early cutoff, revision might stay the same if value didn't change
2898            // This is expected behavior
2899            assert!(rev2 >= rev1);
2900        }
2901
2902        #[test]
2903        fn test_changed_at_returns_none_for_unexecuted_query() {
2904            let runtime = QueryRuntime::new();
2905
2906            // Query has never been executed
2907            let rev = runtime.changed_at(&Counter { id: 1 });
2908            assert!(rev.is_none());
2909        }
2910
2911        #[test]
2912        fn test_changed_at_returns_revision_after_execution() {
2913            let runtime = QueryRuntime::new();
2914
2915            // Execute the query
2916            let _ = runtime.query(Counter { id: 1 }).unwrap();
2917
2918            // Now changed_at should return Some
2919            let rev = runtime.changed_at(&Counter { id: 1 });
2920            assert!(rev.is_some());
2921            assert!(rev.unwrap() > 0);
2922        }
2923
2924        #[test]
2925        fn test_changed_at_matches_poll_revision() {
2926            let runtime = QueryRuntime::new();
2927
2928            // Poll the query
2929            let result = runtime.poll(Counter { id: 1 }).unwrap();
2930
2931            // changed_at should match the revision from poll
2932            let rev = runtime.changed_at(&Counter { id: 1 });
2933            assert_eq!(rev, Some(result.revision));
2934        }
2935
2936        #[test]
2937        fn test_poll_value_access() {
2938            let runtime = QueryRuntime::new();
2939
2940            let result = runtime.poll(Counter { id: 5 }).unwrap();
2941
2942            // Access through Result and Arc
2943            let value: &i32 = result.value.as_ref().unwrap();
2944            assert_eq!(*value, 50);
2945
2946            // Access Arc directly via field after unwrapping Result
2947            let arc: &Arc<i32> = result.value.as_ref().unwrap();
2948            assert_eq!(**arc, 50);
2949        }
2950
2951        #[test]
2952        fn test_subscription_pattern() {
2953            let runtime = QueryRuntime::new();
2954
2955            // Simulate subscription pattern
2956            let mut last_revision: RevisionCounter = 0;
2957            let mut notifications = 0;
2958
2959            // First poll - should notify (new value)
2960            let result = runtime.poll(Counter { id: 1 }).unwrap();
2961            if result.revision > last_revision {
2962                notifications += 1;
2963                last_revision = result.revision;
2964            }
2965
2966            // Second poll - should NOT notify (no change)
2967            let result = runtime.poll(Counter { id: 1 }).unwrap();
2968            if result.revision > last_revision {
2969                notifications += 1;
2970                last_revision = result.revision;
2971            }
2972
2973            // Third poll - should NOT notify (no change)
2974            let result = runtime.poll(Counter { id: 1 }).unwrap();
2975            if result.revision > last_revision {
2976                notifications += 1;
2977                #[allow(unused_assignments)]
2978                {
2979                    last_revision = result.revision;
2980                }
2981            }
2982
2983            // Only the first poll should have triggered a notification
2984            assert_eq!(notifications, 1);
2985        }
2986    }
2987
2988    // Tests for GC APIs
2989    mod gc_tests {
2990        use super::*;
2991        use crate::tracer::{SpanContext, SpanId, TraceId};
2992        use std::collections::HashSet;
2993        use std::sync::atomic::{AtomicUsize, Ordering};
2994        use std::sync::Mutex;
2995
2996        #[derive(Clone, Debug, Hash, PartialEq, Eq)]
2997        struct Leaf {
2998            id: i32,
2999        }
3000
3001        impl Query for Leaf {
3002            type Output = i32;
3003
3004            fn query(self, _db: &impl Db) -> Result<Arc<Self::Output>, QueryError> {
3005                Ok(Arc::new(self.id * 10))
3006            }
3007
3008            fn output_eq(old: &Self::Output, new: &Self::Output) -> bool {
3009                old == new
3010            }
3011        }
3012
3013        #[derive(Clone, Debug, Hash, PartialEq, Eq)]
3014        struct Parent {
3015            child_id: i32,
3016        }
3017
3018        impl Query for Parent {
3019            type Output = i32;
3020
3021            fn query(self, db: &impl Db) -> Result<Arc<Self::Output>, QueryError> {
3022                let child = db.query(Leaf { id: self.child_id })?;
3023                Ok(Arc::new(*child + 1))
3024            }
3025
3026            fn output_eq(old: &Self::Output, new: &Self::Output) -> bool {
3027                old == new
3028            }
3029        }
3030
3031        #[test]
3032        fn test_query_keys_returns_all_cached_queries() {
3033            let runtime = QueryRuntime::new();
3034
3035            // Execute some queries
3036            let _ = runtime.query(Leaf { id: 1 }).unwrap();
3037            let _ = runtime.query(Leaf { id: 2 }).unwrap();
3038            let _ = runtime.query(Leaf { id: 3 }).unwrap();
3039
3040            // Get all keys
3041            let keys = runtime.query_keys();
3042
3043            // Should have at least 3 keys (might have more due to sentinels)
3044            assert!(keys.len() >= 3);
3045        }
3046
3047        #[test]
3048        fn test_remove_removes_query() {
3049            let runtime = QueryRuntime::new();
3050
3051            // Execute a query
3052            let _ = runtime.query(Leaf { id: 1 }).unwrap();
3053
3054            // Get the key
3055            let full_key = QueryCacheKey::new(Leaf { id: 1 }).into();
3056
3057            // Query should exist
3058            assert!(runtime.changed_at(&Leaf { id: 1 }).is_some());
3059
3060            // Remove it
3061            assert!(runtime.remove(&full_key));
3062
3063            // Query should no longer exist
3064            assert!(runtime.changed_at(&Leaf { id: 1 }).is_none());
3065        }
3066
3067        #[test]
3068        fn test_remove_if_unused_removes_leaf_query() {
3069            let runtime = QueryRuntime::new();
3070
3071            // Execute a leaf query (no dependents)
3072            let _ = runtime.query(Leaf { id: 1 }).unwrap();
3073
3074            // Should be removable since no other query depends on it
3075            assert!(runtime.remove_query_if_unused(&Leaf { id: 1 }));
3076
3077            // Query should no longer exist
3078            assert!(runtime.changed_at(&Leaf { id: 1 }).is_none());
3079        }
3080
3081        #[test]
3082        fn test_remove_if_unused_does_not_remove_query_with_dependents() {
3083            let runtime = QueryRuntime::new();
3084
3085            // Execute parent query (which depends on Leaf)
3086            let _ = runtime.query(Parent { child_id: 1 }).unwrap();
3087
3088            // Leaf query should not be removable since Parent depends on it
3089            assert!(!runtime.remove_query_if_unused(&Leaf { id: 1 }));
3090
3091            // Leaf query should still exist
3092            assert!(runtime.changed_at(&Leaf { id: 1 }).is_some());
3093
3094            // But Parent should be removable (no dependents)
3095            assert!(runtime.remove_query_if_unused(&Parent { child_id: 1 }));
3096        }
3097
3098        #[test]
3099        fn test_remove_if_unused_with_full_cache_key() {
3100            let runtime = QueryRuntime::new();
3101
3102            // Execute a leaf query
3103            let _ = runtime.query(Leaf { id: 1 }).unwrap();
3104
3105            let full_key = QueryCacheKey::new(Leaf { id: 1 }).into();
3106
3107            // Should be removable via FullCacheKey
3108            assert!(runtime.remove_if_unused(&full_key));
3109
3110            // Query should no longer exist
3111            assert!(runtime.changed_at(&Leaf { id: 1 }).is_none());
3112        }
3113
3114        // Test tracer receives on_query_start calls (for GC tracking)
3115        struct GcTracker {
3116            accessed_keys: Mutex<HashSet<String>>,
3117            access_count: AtomicUsize,
3118        }
3119
3120        impl GcTracker {
3121            fn new() -> Self {
3122                Self {
3123                    accessed_keys: Mutex::new(HashSet::new()),
3124                    access_count: AtomicUsize::new(0),
3125                }
3126            }
3127        }
3128
3129        impl Tracer for GcTracker {
3130            fn new_span_id(&self) -> SpanId {
3131                SpanId(1)
3132            }
3133
3134            fn new_trace_id(&self) -> TraceId {
3135                TraceId(1)
3136            }
3137
3138            fn on_query_start(&self, _ctx: &SpanContext, query_key: &QueryCacheKey) {
3139                self.accessed_keys
3140                    .lock()
3141                    .unwrap()
3142                    .insert(query_key.debug_repr().to_string());
3143                self.access_count.fetch_add(1, Ordering::Relaxed);
3144            }
3145        }
3146
3147        #[test]
3148        fn test_tracer_receives_on_query_start() {
3149            let tracker = GcTracker::new();
3150            let runtime = QueryRuntime::with_tracer(tracker);
3151
3152            // Execute some queries
3153            let _ = runtime.query(Leaf { id: 1 }).unwrap();
3154            let _ = runtime.query(Leaf { id: 2 }).unwrap();
3155
3156            // Tracer should have received on_query_start calls
3157            let count = runtime.tracer().access_count.load(Ordering::Relaxed);
3158            assert_eq!(count, 2);
3159
3160            // Check that the keys were recorded
3161            let keys = runtime.tracer().accessed_keys.lock().unwrap();
3162            assert!(keys.iter().any(|k| k.contains("Leaf")));
3163        }
3164
3165        #[test]
3166        fn test_tracer_receives_on_query_start_for_cache_hits() {
3167            let tracker = GcTracker::new();
3168            let runtime = QueryRuntime::with_tracer(tracker);
3169
3170            // Execute query twice (second is cache hit)
3171            let _ = runtime.query(Leaf { id: 1 }).unwrap();
3172            let _ = runtime.query(Leaf { id: 1 }).unwrap();
3173
3174            // Tracer should have received on_query_start for both calls
3175            let count = runtime.tracer().access_count.load(Ordering::Relaxed);
3176            assert_eq!(count, 2);
3177        }
3178
3179        #[test]
3180        fn test_gc_workflow() {
3181            let tracker = GcTracker::new();
3182            let runtime = QueryRuntime::with_tracer(tracker);
3183
3184            // Execute some queries
3185            let _ = runtime.query(Leaf { id: 1 }).unwrap();
3186            let _ = runtime.query(Leaf { id: 2 }).unwrap();
3187            let _ = runtime.query(Leaf { id: 3 }).unwrap();
3188
3189            // Simulate GC: remove all queries that are not in use
3190            let mut removed = 0;
3191            for key in runtime.query_keys() {
3192                if runtime.remove_if_unused(&key) {
3193                    removed += 1;
3194                }
3195            }
3196
3197            // All leaf queries should be removable
3198            assert!(removed >= 3);
3199
3200            // Queries should no longer exist
3201            assert!(runtime.changed_at(&Leaf { id: 1 }).is_none());
3202            assert!(runtime.changed_at(&Leaf { id: 2 }).is_none());
3203            assert!(runtime.changed_at(&Leaf { id: 3 }).is_none());
3204        }
3205    }
3206}