Rindle docs and package mapSkip to main content

rindle_replica/
query.rs

1//! The `Query` handle + its subscription bookkeeping.
2
3use std::rc::Rc;
4
5use rindle::graph::NodeId;
6use rindle::Ast;
7
8use crate::{ChangeEvent, Inner, QueryId, TxId, Update};
9
10/// A query's subscriber callbacks. Factored into an alias (clippy::type_complexity) and
11/// reused by the writer, which takes them out of the entry before firing to avoid a
12/// re-borrow deadlock on `subs`.
13pub(crate) type Callbacks = Vec<Box<dyn FnMut(&Update)>>;
14
15/// The `Db`-internal registration id `subs` is keyed by — STABLE across a delta-overflow
16/// shed rebuild (design 306 D4), unlike the engine's generational sink `NodeId`, which a
17/// rebuild replaces. The handle → entry association must survive the rebuild so a
18/// pre-shed [`Query`] handle keeps working.
19#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
20pub(crate) struct RegId(pub(crate) u64);
21
22/// Per-query subscription state, keyed by [`RegId`] in `Inner`.
23pub(crate) struct QueryEntry {
24    /// The query's current change-sink `NodeId` in the engine — updated in place when a
25    /// shed rebuild re-registers the query on a fresh engine.
26    pub sink: NodeId,
27    /// The caller's opaque tag, carried so a shed rebuild re-registers under it.
28    pub query_id: QueryId,
29    /// The registered AST, retained so a shed rebuild can re-register + re-hydrate the
30    /// query without the caller's involvement.
31    pub ast: Ast,
32    /// The hydration change set (all `Add`s) captured at registration — or at the most
33    /// recent shed re-hydration. Ordinary writes do not update this cache.
34    pub hydrated: Vec<ChangeEvent>,
35    /// The committed tx the hydration reflects.
36    pub hydrated_tx: u64,
37    /// Subscriber callbacks. Fired with `Hydrated` on subscribe, then `Changed` per
38    /// committed write that touches this query — and with a fresh `Hydrated` again
39    /// after a delta-overflow shed (see [`Update`]).
40    pub callbacks: Callbacks,
41}
42
43/// A registered live query. Subscribe to receive its raw change events.
44///
45/// Tear it down explicitly with [`Query::destroy`]. Dropping the handle is still a
46/// no-op (the query keeps running and any callbacks keep firing), so an accidental
47/// drop never silently stops delivery — teardown is always a deliberate call.
48pub struct Query {
49    pub(crate) inner: Rc<Inner>,
50    pub(crate) reg: RegId,
51    pub(crate) query_id: QueryId,
52}
53
54impl Query {
55    /// The caller-supplied [`QueryId`] tag this query was registered under (echoed
56    /// verbatim; the engine never interprets it).
57    pub fn id(&self) -> QueryId {
58        self.query_id
59    }
60
61    /// Tear this query down: stop delivering its events and reclaim its pipeline.
62    ///
63    /// Removes its subscription registry entry (so no further `Changed` callbacks fire
64    /// and the hydration buffer is dropped), then disconnects it from the shared
65    /// sources and frees its operator/storage slots in the engine graph
66    /// ([`rindle::graph::Graph::destroy_pipeline`]). The shared sources — and every *other*
67    /// query — are untouched. Consumes the handle; the freed slots are recycled by a
68    /// later [`crate::Db::query`].
69    ///
70    /// Like registration, this borrows the engine mutably, so it must not be called
71    /// re-entrantly from inside a subscriber callback.
72    pub fn destroy(self) {
73        // Drop the subscription first (no callback can fire for a half-torn query),
74        // then tear the pipeline out of the shared graph.
75        let entry = self.inner.subs.borrow_mut().remove(&self.reg);
76        if let Some(entry) = entry {
77            self.inner.engine.borrow_mut().deregister_query(entry.sink);
78        }
79    }
80    /// Register a callback and immediately call it with the cached `Update::Hydrated`.
81    /// This baseline is from registration or the latest shed recovery; ordinary writes
82    /// do not update it. Subscribe before later writes. A late consumer that needs the
83    /// current result should create a new query registration.
84    ///
85    /// Later `Changed` callbacks run synchronously on the writer thread after commit.
86    /// A callback panic cannot roll back that SQL commit. Keep callbacks short, avoid
87    /// re-entering the `Db`, and forward events to a channel for other threads.
88    /// A shed recovery delivers a replacing `Hydrated`; see [`Update`]. Call
89    /// [`Query::destroy`] to stop the registration and all its callbacks.
90    ///
91    /// A no-op (the callback is dropped without firing) in one edge: the query was torn
92    /// down because its post-shed re-hydration failed (see [`crate::CommitInfo::shed`]).
93    pub fn subscribe(&self, mut cb: impl FnMut(&Update) + 'static) {
94        // Build the Hydrated payload without holding the `subs` borrow across the
95        // user callback. Callback execution must still avoid re-entering this Db.
96        let hydrated = {
97            let subs = self.inner.subs.borrow();
98            let Some(entry) = subs.get(&self.reg) else {
99                return;
100            };
101            Update::Hydrated {
102                tx_id: TxId(entry.hydrated_tx),
103                changes: entry.hydrated.clone(),
104            }
105        };
106        cb(&hydrated);
107        self.inner
108            .subs
109            .borrow_mut()
110            .get_mut(&self.reg)
111            .expect("query entry exists")
112            .callbacks
113            .push(Box::new(cb));
114    }
115}