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, ¤t_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, ¤t_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}