qbice/query.rs
1//! Query definitions and related types for the QBICE engine.
2//!
3//! This module defines the core [`Query`] trait that all queries must
4//! implement, along with supporting types for query identification and
5//! execution behavior.
6//!
7//! # Defining Queries
8//!
9//! A query represents a computation with an input key and an output value.
10//! To define a query:
11//!
12//! 1. Create a struct representing the query key
13//! 2. Derive the required traits: `StableHash`, `Identifiable`, `Debug`,
14//! `Clone`, `PartialEq`, `Eq`, and `Hash`
15//! 3. Implement the [`Query`] trait, specifying the output `Value` type
16//!
17//! # Query Identification
18//!
19//! Each query instance is uniquely identified by a [`QueryID`], which combines:
20//! - A stable type identifier (from the [`Identifiable`] trait)
21//! - A 128-bit hash of the query's contents (from the [`StableHash`] trait)
22//!
23//! This ensures that different query types never collide, and different
24//! instances of the same query type are distinguished by their content.
25//!
26//! # Execution Styles
27//!
28//! The [`ExecutionStyle`] enum controls how queries interact with the
29//! dependency tracking system:
30//!
31//! - [`ExecutionStyle::Normal`]: Standard queries with full dependency tracking
32//! - [`ExecutionStyle::Projection`]: Fast extractors for parts of other queries
33//! - [`ExecutionStyle::Firewall`]: Boundary queries that limit dirty
34//! propagation
35//!
36//! Most queries should use `Normal`. Advanced users can leverage `Projection`
37//! and `Firewall` for performance optimization in complex dependency graphs.
38
39use std::{any::Any, fmt::Debug, hash::Hash};
40
41use qbice_serialize::{Decode, Encode};
42use qbice_stable_hash::{Compact128, StableHash};
43use qbice_stable_type_id::{Identifiable, StableTypeID};
44
45/// The query interface of the QBICE engine.
46///
47/// A type implementing [`Query`] represents a query input (key) that is
48/// associated with a specific output value type. The query itself only defines
49/// the *what* - the actual computation is provided by an [`crate::Executor`].
50///
51/// # Required Traits
52///
53/// Query types must implement several traits:
54///
55/// - [`StableHash`]: For consistent hashing across program runs
56/// - [`Identifiable`]: For stable type identification
57/// - [`Eq`] + [`Hash`]: For use in hash maps
58/// - [`Clone`]: For storing query keys
59/// - [`Debug`]: For error messages and debugging
60/// - [`Send`] + [`Sync`]: For thread-safe access
61///
62/// Most of these can be derived automatically:
63///
64/// ```rust
65/// use qbice::{Decode, Encode, Identifiable, Query, StableHash};
66///
67/// #[derive(
68/// Debug,
69/// Clone,
70/// PartialEq,
71/// Eq,
72/// Hash,
73/// StableHash,
74/// Identifiable,
75/// Encode,
76/// Decode,
77/// )]
78/// struct MyQuery {
79/// id: u64,
80/// name: String,
81/// }
82///
83/// impl Query for MyQuery {
84/// type Value = Vec<u8>;
85/// }
86/// ```
87///
88/// # Value Type Requirements
89///
90/// The associated `Value` type must also satisfy certain bounds:
91///
92/// - `'static`: No borrowed data
93/// - [`Send`] + [`Sync`]: For thread-safe caching
94/// - [`Clone`]: For returning cached values
95/// - [`Debug`]: For debugging and visualization
96/// - [`StableHash`]: For change detection (fingerprinting)
97///
98/// # Performance: Cheap Cloning
99///
100/// **Both the query type and its value type should be cheaply cloneable.**
101///
102/// The engine frequently clones queries and values internally for:
103/// - Storing query keys in the dependency graph
104/// - Caching computed results
105/// - Returning values to callers
106/// - Tracking dependencies across async boundaries
107///
108/// For types containing heap-allocated data, consider using shared ownership:
109///
110/// | Instead of | Use |
111/// |------------|-----|
112/// | `String` | `Arc<str>` or a shared string type |
113/// | `Vec<T>` | `Arc<[T]>` |
114/// | `HashMap<K, V>` | `Arc<HashMap<K, V>>` |
115/// | Large structs | `Arc<T>` |
116pub trait Query:
117 StableHash
118 + Identifiable
119 + Any
120 + Eq
121 + Hash
122 + Clone
123 + Debug
124 + Encode
125 + Decode
126 + Send
127 + Sync
128 + 'static
129{
130 /// The output value type associated with this query.
131 ///
132 /// This is the type returned when querying the engine for this query key.
133 type Value: 'static
134 + Send
135 + Sync
136 + Clone
137 + Debug
138 + StableHash
139 + Encode
140 + Decode;
141}
142
143/// Specifies the execution style of a query.
144///
145/// The execution style determines how a query participates in the dependency
146/// tracking and dirty propagation system. Different styles offer trade-offs
147/// between fine-grained reactivity and computational efficiency.
148///
149/// # Choosing an Execution Style
150///
151/// - **Most queries**: Use [`Normal`](ExecutionStyle::Normal) - it provides
152/// standard incremental computation semantics
153/// - **External data**: Use [`ExternalInput`](ExecutionStyle::ExternalInput)
154/// for queries that read from files, network, or other external sources
155/// - **Dirty Popagation Optimization**: Use
156/// [`Firewall`](ExecutionStyle::Firewall) and
157/// [`Projection`](ExecutionStyle::Projection) to optimize dirty propagation
158/// in large dependency graphs
159///
160/// # Variants
161///
162/// ## Normal
163///
164/// Standard queries with full dependency tracking. This is the default and
165/// appropriate for most use cases.
166///
167/// **Behavior:**
168/// - Dependencies are tracked automatically
169/// - Changes to dependencies cause immediate dirty propagation
170/// - Results are cached and reused when dependencies haven't changed
171///
172/// **When to use:**
173/// - Most computation queries
174/// - Queries with stable, well-defined dependencies
175/// - When you want automatic reactivity to changes
176///
177/// ## Firewall
178///
179/// Boundary queries that limit dirty propagation based on output changes
180/// rather than input changes. This is a key optimization for reducing
181/// unnecessary dirty propagation in large computation graphs.
182///
183/// **Behavior:**
184/// - When dependencies change, the firewall is recomputed
185/// - Downstream queries are marked dirty **only if** the firewall's output
186/// value actually changed (based on fingerprint)
187/// - Acts as an incremental **dirty propagation boundary** in the large
188/// computation graph system
189///
190/// **When to use:**
191/// - A computation that has a large graph, the dirty propagation of which
192/// becomes expensive
193/// - When many inputs change frequently, but the overall output is stable
194///
195/// **Note:** You almost always want to visualize your computation graph first
196/// to identify where firewalls will be most effective. Moreover, firewalls
197/// often work best when paired with projections downstream to minimize dirty
198/// propagation.
199///
200/// ## Projection
201///
202/// Lightweight queries that extract data from firewall queries. Projections
203/// are designed to be very fast (essentially field access) and work in
204/// conjunction with firewalls to provide fine-grained access to coarse
205/// computations.
206///
207/// **Behavior:**
208/// - Must be extremely fast to execute (no expensive computation)
209/// - Used internally to extract specific fields from firewall results
210/// - Prevents unnecessary full recomputation when only a small part is needed
211///
212/// **Example pattern:**
213/// ```text
214/// Firewall Query: ComputeAllMetrics
215/// ├─ Projection: ExtractMetricA
216/// ├─ Projection: ExtractMetricB
217/// └─ Projection: ExtractMetricC
218/// ```
219///
220/// ## `ExternalInput`
221///
222/// Queries that interact with the outside world (files, network, system time,
223/// etc.). These are leaf queries that don't depend on other queries but can
224/// be explicitly refreshed during input sessions.
225///
226/// **Behavior:**
227/// - Cannot depend on other queries (must be leaf nodes)
228/// - Not automatically refreshed when other inputs change
229/// - Can be explicitly refreshed via input session methods
230/// - Executor may perform I/O or other impure operations
231///
232/// **When to use:**
233/// - Reading configuration files
234/// - Fetching data from network APIs
235/// - Querying databases
236/// - Reading system time or environment variables
237/// - Any external state that should be explicitly controlled
238///
239/// **Example:**
240/// ```rust,ignore
241/// #[derive(Query)]
242/// struct ConfigFileQuery;
243///
244/// impl Executor<ConfigFileQuery> for ConfigFileExecutor {
245/// fn execution_style() -> ExecutionStyle {
246/// ExecutionStyle::ExternalInput
247/// }
248///
249/// async fn execute(&self, ...) -> Result<Config, CyclicError> {
250/// // Read from file system
251/// let config = read_config_from_disk()?;
252/// Ok(config)
253/// }
254/// }
255/// ```
256#[derive(
257 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Encode, Decode,
258)]
259pub enum ExecutionStyle {
260 /// A normal query with standard dependency tracking.
261 ///
262 /// This is the default and appropriate for most queries.
263 Normal,
264
265 /// A projection query for extracting parts of firewall query outputs.
266 ///
267 /// Projections should be very fast to compute (essentially field access)
268 /// and are used to prevent unnecessary dirty propagation across firewall
269 /// boundaries.
270 Projection,
271
272 /// A firewall query that acts as a change boundary.
273 ///
274 /// Dirty propagation stops at firewall queries - downstream queries are
275 /// only marked dirty if the firewall's output value actually changes,
276 /// not just because its inputs changed.
277 Firewall,
278
279 /// An external input query that can interact with the outside world.
280 ///
281 /// This is similar to input queries that are explicitly set with input
282 /// sessions. However, external input queries are modeled by executors
283 /// invoking impure operations, such as reading from files or network.
284 ///
285 /// The queries executed as [`Self::ExternalInput`] are assumed to be the
286 /// leaf nodes in the computation graph (cannot depend on other queries).
287 ///
288 /// During the input session, the input session can ask the engine to
289 /// refresh the values of external input queries, causing them to be
290 /// re-executed and possibly dirtying dependent queries if their values
291 /// change.
292 ExternalInput,
293}
294
295/// A unique identifier for a query instance.
296///
297/// A `QueryID` uniquely identifies a specific query within the engine by
298/// combining:
299///
300/// - A [`StableTypeID`] that identifies the query type
301/// - A 128-bit hash of the query's contents
302///
303/// This combination ensures that:
304/// - Different query types never collide (due to type ID)
305/// - Different instances of the same type are distinguished (due to content
306/// hash)
307/// - Identification is stable across program runs (due to stable hashing)
308///
309/// # Hash Collision
310///
311/// While the probability of hash collision is astronomically low (2^-128),
312/// QBICE assumes no collisions occur. In practice, this is safe for all
313/// reasonable use cases.
314///
315/// # Internal Use
316///
317/// You typically don't need to work with `QueryID` directly - the engine
318/// handles identification automatically. However, it's useful for:
319/// - Debugging dependency graphs
320/// - Custom visualization
321/// - Advanced introspection
322#[derive(
323 Debug,
324 Clone,
325 Copy,
326 PartialEq,
327 Eq,
328 PartialOrd,
329 Ord,
330 Hash,
331 StableHash,
332 Encode,
333 Decode,
334 Identifiable,
335)]
336pub struct QueryID {
337 stable_type_id: Compact128,
338 hash_128: Compact128,
339}
340
341impl QueryID {
342 /// Creates a new `QueryID` for the given query type and content hash.
343 #[must_use]
344 pub fn new<Q: Query>(hash_128: Compact128) -> Self {
345 let stable_type_id = Q::STABLE_TYPE_ID.as_u128();
346
347 Self { stable_type_id: stable_type_id.into(), hash_128 }
348 }
349
350 /// Returns the stable type ID of this query.
351 ///
352 /// The type ID uniquely identifies the query's Rust type.
353 #[must_use]
354 pub const fn stable_type_id(&self) -> StableTypeID {
355 unsafe {
356 StableTypeID::from_raw_parts(
357 self.stable_type_id.high(),
358 self.stable_type_id.low(),
359 )
360 }
361 }
362
363 /// Returns the 128-bit content hash of this query.
364 ///
365 /// This hash is computed from the query's fields using stable hashing,
366 /// ensuring consistency across program runs.
367 #[must_use]
368 pub fn hash_128(&self) -> u128 { self.hash_128.to_u128() }
369
370 /// Returns the compact representation of the 128-bit content hash.
371 #[must_use]
372 pub const fn compact_hash_128(&self) -> Compact128 { self.hash_128 }
373}