query-flow
An ergonomic, runtime-agnostic framework for incremental computation.
[!WARNING] Currently in dogfooding phase with the Eure project's CLI, LSP, and Web Playground.
Features
- Runtime-agnostic: Sync query logic with suspense pattern — works with any event loop or async runtime
- Automatic caching: Query results are cached and invalidated based on dependencies
- Type-safe: Per-query-type caching with compile-time guarantees
- Lock-free API: Concurrent access from multiple threads via whale
Quick Start
use ;
let runtime = new;
let result = runtime.query.unwrap;
assert_eq!;
Core Concepts
- Query: A derived computation that is cached and automatically invalidated when its dependencies change. Queries can depend on other queries or assets.
- Asset: An external input (files, network data, user input) that queries can depend on. Assets are resolved asynchronously and trigger the suspense pattern when not yet available.
- Runtime: The
QueryRuntimemanages query execution, caching, dependency tracking, and asset resolution.
Defining Queries
Using the #[query] Macro
The #[query] macro transforms a function into a query struct implementing the Query trait:
use ;
// Basic query - generates `Add` struct
// Query with dependencies
Macro Options
| Option | Example | Description |
|---|---|---|
keys(...) |
#[query(keys(id))] |
Only specified fields used as cache key |
name = "..." |
#[query(name = "FetchUserById")] |
Custom struct name |
output_eq = fn |
#[query(output_eq = my_eq)] |
Custom equality for early cutoff |
Manual Query Implementation
For full control, implement the Query trait directly:
use Arc;
use ;
Assets: External Inputs
Assets represent external resources (files, network data) that queries can depend on:
Defining Asset Keys
use ;
use PathBuf;
// Using the macro
;
;
// With selective key fields (only `path` used for Hash/Eq)
// Manual implementation
;
Using Assets in Queries
Asset Locators (Optional)
Locators are optional. Without a locator, assets always return Pending and must be resolved externally via resolve_asset() or resolve_asset_error().
Register a locator when you need:
- Immediate resolution: Return
Readyfor assets available synchronously - Validation/hooks: Reject invalid keys or log access patterns
- Query-based DI: Use
db.query()to determine loading behavior dynamically
use ;
runtime.register_asset_locator;
The #[asset_locator] macro generates a struct (PascalCase of function name) implementing AssetLocator.
Durability Levels
Durability is specified when resolving assets and helps optimize invalidation propagation:
| Level | Description |
|---|---|
Volatile |
Changes frequently (user input, live feeds) |
Transient |
Changes occasionally (configuration, session data) |
Stable |
Changes rarely (external dependencies) |
Static |
Fixed for this session (bundled assets, constants) |
runtime.resolve_asset;
runtime.resolve_asset;
Asset Invalidation
// File was modified externally
runtime.invalidate_asset;
// Dependent queries will now suspend until resolved
// Remove asset entirely
runtime.remove_asset;
Suspense Pattern
The suspense pattern allows sync query code to handle async operations. db.asset() returns the asset value directly, suspending automatically if not ready.
Pattern 1: Suspend until ready (default)
Use db.asset() to get an asset. It automatically returns Err(QueryError::Suspend) if loading.
When a query suspends, the runtime tracks which assets are pending. In your event loop, resolve assets when they become available:
// You can check what's pending:
for pending in runtime.
// In your event loop, when file content is loaded:
runtime.resolve_asset;
// Or if loading failed:
runtime.resolve_asset_error;
// Then retry the query
let result = runtime.query?;
Pattern 2: Handle loading state explicitly
Use db.asset_state() to get an AssetLoadingState for explicit loading state handling:
Error Handling
Queries return Result<Arc<Output>, QueryError>. The error variants are:
System errors (not cached):
Suspend- An asset is not yet available. See Suspense Pattern.Cycle- A dependency cycle was detected in the query graph.Cancelled- Query explicitly returned cancellation (not cached, unlikeUserError).MissingDependency- An asset locator indicated the asset is not found or not allowed.DependenciesRemoved- Dependencies were removed by another thread during execution.InconsistentAssetResolution- An asset was resolved during query execution, possibly causing inconsistent state.
User errors (cached like successful results):
UserError(Arc<anyhow::Error>)- Domain errors from your query logic, automatically converted via?operator.
// User errors with ? operator - errors are automatically converted
// System errors propagate automatically
Handling Specific Error Types
Use downcast_err() to handle specific user error types while propagating others:
use QueryResultExt;
let result = db.query.?;
match result
Error Comparator for Early Cutoff
By default, all UserError values are considered different (conservative). Use QueryRuntimeBuilder to customize:
let runtime = builder
.error_comparator
.build;
Subscription Pattern
Use runtime.poll() to track query changes with revision numbers. This is useful for push-based notifications (e.g., LSP diagnostics).
// Poll and return only when changed
QueryRuntime API
let runtime = new;
// Execute queries
let result = runtime.query?;
// Invalidation
runtime.;
runtime.clear_cache;
// Asset management
runtime.register_asset_locator;
runtime.resolve_asset;
runtime.resolve_asset_error;
runtime.invalidate_asset;
runtime.remove_asset;
// Pending assets
runtime.pending_assets; // All pending
runtime.; // Filtered by type
runtime.has_pending_assets;
Crates
| Crate | Description |
|---|---|
query-flow |
High-level query framework with automatic caching and dependency tracking |
query-flow-macros |
Procedural macros for defining queries |
query-flow-inspector |
Debugging and inspection tools |
whale |
Low-level lock-free dependency-tracking primitive |
Whale
Whale is the low-level primitive that powers query-flow. It provides lock-free dependency tracking without opinions about what queries are or how to store their results.
When to Use Whale Directly
Use query-flow if you want a batteries-included incremental computation framework. Use whale directly if you need:
- Full control over query representation and storage
- Custom invalidation strategies
- Integration with existing systems
- Maximum flexibility
Whale Design
Whale is designed to be a minimal primitive for building high-level incremental computing systems. It does not provide:
- What actually the "query" is
- How to calculate a query ID
- Any data storage to store the result of a query
- Rich high-level APIs
Whale Architecture
Whale is built around a lock-free dependency graph where nodes represent computations and edges represent their dependencies.
Core Components:
- Runtime: The central coordinator that manages the dependency graph. Lock-free and safe to clone across threads.
- Node: A vertex representing a computation with version, dependencies, dependents, and invalidation state.
- Pointer: A reference to a specific version of a computation (query ID + version).
- RevisionPointer: An extended pointer including invalidation state for precise state tracking.
Lock-free Design:
The system uses atomic operations and immutable data structures:
- Nodes are updated through atomic compare-and-swap operations
- Dependencies and dependents are stored in immutable collections
- Version numbers are managed through atomic counters
This allows multiple threads to concurrently query states, propagate invalidations, and modify the dependency graph.
Consistency Guarantees:
- Version Monotonicity: Version numbers only increase per query
- Cyclic Safety: Remains functional even with cycles in the dependency graph
- Invalidation Guarantees: All dependents are notified of changes
Alternatives
- salsa: A well-known library for incremental computing with a different design philosophy.
License
Licensed under either of
- Apache License, Version 2.0
- MIT license
at your option.