pub struct Cluster { /* private fields */ }Expand description
The multi-threaded replica handle (cheap Rc clone). Single-thread-use on the
coordinator side; a server runs it on a dedicated owner thread and continuously
drains the ClusterEvent channel returned by open on whatever
thread it likes — but it must never STOP draining while writes flow (the channel-out
is bounded; see open’s draining contract).
Implementations§
Source§impl Cluster
impl Cluster
Sourcepub fn open(
path: impl AsRef<Path>,
n_workers: usize,
) -> Result<(Cluster, Receiver<ClusterEvent>), ReplicaError>
pub fn open( path: impl AsRef<Path>, n_workers: usize, ) -> Result<(Cluster, Receiver<ClusterEvent>), ReplicaError>
Open a parallel replica over a file-backed SQLite database with
n_workers IVM worker threads. Returns the handle and the channel-out
Receiver for derived ClusterEvents. A fresh file gets ordinary
journal_mode = wal (design 306 D5); an existing wal or wal2 file keeps
its mode. Asserts sqlite3_threadsafe() != 0, like Db::open.
§Draining contract
The consumer MUST drain the returned Receiver continuously, on its own
thread, for the whole life of the Cluster. The channel-out is BOUNDED
(FOLLOWER-LAG-SHED §4, rung 0b — event_channel_bound(n_workers) events, to bound
memory under a slow consumer). A full event channel blocks a worker, which can
block capture streaming or a later snapshot barrier on the writer. Reading
events only after writes return can therefore deadlock. The drain loop must
also avoid blocking on slow downstream subscribers. Commit return does not
mean all worker events have been delivered; use Progressed for that boundary.
Prefer ClusterConsumer, which spawns and owns that
continuous drain thread for you; reach for raw open only when you intend to run
the drain loop yourself.
Sourcepub fn open_with_planning(
path: impl AsRef<Path>,
n_workers: usize,
plan_queries: bool,
) -> Result<(Cluster, Receiver<ClusterEvent>), ReplicaError>
pub fn open_with_planning( path: impl AsRef<Path>, n_workers: usize, plan_queries: bool, ) -> Result<(Cluster, Receiver<ClusterEvent>), ReplicaError>
Like open, but with explicit control over the cost-based join-flip
planner. When plan_queries is true, every worker runs the planner (a
SqliteCostModel over its own connection, cached) at query registration to annotate
flip before lowering. Result-preserving; the plan is frozen per registration.
The planner is server-side (table-source) only and open enables it by default;
pass false here to opt out (e.g. to pin the unplanned path).
Sourcepub fn open_with_options(
path: impl AsRef<Path>,
n_workers: usize,
plan_queries: bool,
operator_storage: OperatorStorage,
) -> Result<(Cluster, Receiver<ClusterEvent>), ReplicaError>
pub fn open_with_options( path: impl AsRef<Path>, n_workers: usize, plan_queries: bool, operator_storage: OperatorStorage, ) -> Result<(Cluster, Receiver<ClusterEvent>), ReplicaError>
Like open_with_planning, but also selects the
operator-scratch-state backend for every worker Engine. Pass
OperatorStorage::SqliteSpill to spill stateful-operator state to a per-worker
on-disk temp database instead of RAM. The schema-only engine never builds stateful
operators, so it always stays memory-backed. Fresh files get plain wal
(design 306 D5); use open_with_journal for the
daemon’s wal2 opt-in.
Sourcepub fn open_with_journal(
path: impl AsRef<Path>,
n_workers: usize,
journal: JournalMode,
) -> Result<(Cluster, Receiver<ClusterEvent>), ReplicaError>
pub fn open_with_journal( path: impl AsRef<Path>, n_workers: usize, journal: JournalMode, ) -> Result<(Cluster, Receiver<ClusterEvent>), ReplicaError>
Like open, but with an explicit fresh-file JournalMode —
the daemon’s roles pass JournalMode::Wal2 here (design 306 D5: wal2 is
something the daemon requests, never something the library imposes). An
existing file already in wal or wal2 keeps its mode either way.
Sourcepub fn open_with(
path: impl AsRef<Path>,
n_workers: usize,
opts: OpenOptions,
) -> Result<(Cluster, Receiver<ClusterEvent>), ReplicaError>
pub fn open_with( path: impl AsRef<Path>, n_workers: usize, opts: OpenOptions, ) -> Result<(Cluster, Receiver<ClusterEvent>), ReplicaError>
The full-combination opener: every OpenOptions field is honored, so any
planner × operator-storage × journal combination is one call (e.g. a planned,
spill-backed cluster on a wal2 daemon store). The named rungs above are
conveniences over this.
Sourcepub fn register_table(&self, table: &str) -> Result<(), ReplicaError>
pub fn register_table(&self, table: &str) -> Result<(), ReplicaError>
Register a base table: discover its columns + PK, ensure the PK UNIQUE index
TableSource requires, teach the capture hook the column types, and build
the source on every worker (any may host a query referencing it).
Idempotent. Rejects BLOB columns / PK-less tables.
The table must be plain — SQL triggers and generated columns are rejected (see the crate-level “supported usage” docs). Foreign-key cascades are represented by the explicit row deltas observed by the preupdate hook.
Sourcepub fn query(&self, query_id: QueryId, ast: Ast) -> Result<usize, ReplicaError>
pub fn query(&self, query_id: QueryId, ast: Ast) -> Result<usize, ReplicaError>
Register a live query under the caller-supplied QueryId tag. The query is
sharded to a worker, built + hydrated there, and its Hydrated baseline emitted
to the channel-out. Returns the index of the worker that hosts it (a BuildError
surfaces synchronously here as Err). Later writes can emit provisional Changed
slices before commit. That worker emits Progressed after successful commit and
all its slices — so the returned index is what the async progress layer (the drain
thread) keys a query’s cv position by. The cluster owns the assignment; callers
pass the returned index along rather than recomputing it, so a future scheduling
change (work-stealing) stays internal to the cluster (§2.6).
Rejected while the open transaction is speculative
(view_is_speculative). Registration COLD-hydrates on the
worker, and a worker mid-stream holds the pushed chunks in its sources’ overlays — so
the baseline would be drawn from rows no one has committed, while tagged with the
PRE-commit hydrated_tx read below. That is strictly worse than the equivalent
one-shot leak: a one-shot is thrown away, but a subscription applies every later
delta on top of its baseline, so a poisoned one breaks view-after-write == fresh-query for the life of the subscription. (A stream that has pinned snapshots
but pushed nothing is fine — the hydrate is a pure read of the committed post-(N-1)
state; see the speculative field docs.)
Refusing (rather than deferring internally) is forced by the same threading that shapes
read_snapshot: WorkerPool::register_query_on blocks this thread
on the worker’s reply, and only this thread can send the transaction’s terminal marker — so
a worker that held the registration until commit would deadlock against its own
coordinator. Callers that can wait should re-issue after the commit.
Sourcepub fn family(
&self,
query_id: QueryId,
stripped: Ast,
params: Vec<Box<str>>,
bindings: Vec<Binding>,
) -> Result<usize, ReplicaError>
pub fn family( &self, query_id: QueryId, stripped: Ast, params: Vec<Box<str>>, bindings: Vec<Binding>, ) -> Result<usize, ReplicaError>
Register a parameterized query family (design 310 §5) under query_id: one
pipeline over stripped (the family’s template AST) partitioned on params, with
bindings bound and hydrated up front — each emits a
Update::PartitionHydrated instead of a
Hydrated. Placed on one worker by the same query_id % n rule as
query (design §6, v1: one worker per family). Returns the worker.
Sourcepub fn bind(
&self,
query_id: QueryId,
binding: Binding,
) -> Result<(), ReplicaError>
pub fn bind( &self, query_id: QueryId, binding: Binding, ) -> Result<(), ReplicaError>
Bind one more partition of the family registered under query_id (design 310
§4.4): it hydrates on the hosting worker’s latest committed snapshot and emits its
PartitionHydrated. Refused, like query, while a speculative
transaction is streaming.
Sourcepub fn unbind(
&self,
query_id: QueryId,
binding: Binding,
) -> Result<bool, ReplicaError>
pub fn unbind( &self, query_id: QueryId, binding: Binding, ) -> Result<bool, ReplicaError>
Unbind one partition of the family under query_id; nothing is emitted. Ok(false)
if it was not bound.
Sourcepub fn analyze_query(&self, ast: Ast) -> Result<AnalyzeReport, ReplicaError>
pub fn analyze_query(&self, ast: Ast) -> Result<AnalyzeReport, ReplicaError>
Analyze ast COLD against the current committed state and return where its time
and rows go (ANALYZE-QUERY-DESIGN.md). Read-only and fully self-contained:
it runs on the CALLING thread over a private connection
(analyze_standalone) — no worker engine, no live
statement cache, no lease — so it never enters a worker’s command queue and a
genuinely slow cold hydrate cannot stall (or be stalled by) commits. BuildError
(unknown table/column, unsupported shape) surfaces as Err.
Sourcepub fn analyze_spec(&self) -> AnalyzeSpec
pub fn analyze_spec(&self) -> AnalyzeSpec
The Send snapshot for running analyze_standalone
on another thread: the daemon’s /analyze builds one here on its command loop,
then runs the analysis on a dedicated thread so the loop — and with it the write
path — never blocks on a slow cold hydrate.
Sourcepub fn query_json(
&self,
query_id: QueryId,
json: &str,
) -> Result<usize, ReplicaError>
pub fn query_json( &self, query_id: QueryId, json: &str, ) -> Result<usize, ReplicaError>
Convenience: parse a Zero-wire AST from JSON, then Cluster::query.
Sourcepub fn destroy_query(&self, query_id: QueryId) -> bool
pub fn destroy_query(&self, query_id: QueryId) -> bool
Gracefully tear down a query the consumer no longer wants (destroyed /
dematerialized / unsubscribed): reclaim its pipeline on the hosting worker.
Unlike a fault, this is solicited, so it emits no ClusterEvent — after
it returns, no further events arrive for query_id. Returns true if a live
query was found and removed; idempotent (a repeat call returns false). The
reclaimed graph slots are recycled by a later query.
Sourcepub fn read_snapshot(
&self,
query_id: QueryId,
) -> Result<Vec<CaughtChange>, ReplicaError>
pub fn read_snapshot( &self, query_id: QueryId, ) -> Result<Vec<CaughtChange>, ReplicaError>
Re-read the current assembled view of query_id as a one-shot snapshot — the
SSR read path (SSR-DESIGN.md §3). Routes to the hosting worker (same query_id % n
shard as query) and returns the live view as hydration Adds, with
no subscription and no streaming state. A degraded shard or an unregistered query
yields an empty snapshot (the one-shot reports “no rows” rather than failing); a
raised read boundary on a live worker (§5.3 sum overflow) is Err — never a NULL
aggregate rendered as data. A snapshot is refused while the open transaction is
speculative (view_is_speculative): it has already
streamed uncommitted changes into the worker graph, and only its terminal commit marker
makes those changes publicly visible. Callers that can wait should poll that predicate and
defer the read to the commit rather than surfacing this error — a caller pinned to one
thread with the writer (rindled’s engine loop) must NOT block here: the same thread has to
keep running to reach the commit.
Sourcepub fn view_is_speculative(&self) -> bool
pub fn view_is_speculative(&self) -> bool
Whether the workers’ assembled views currently carry uncommitted rows — true from the
moment an open transaction streams its first chunk (PUSH_CHUNK_ROWS) until that
transaction commits or rolls back. This is the exact window in which
read_snapshot refuses, exposed as a predicate so a caller can
QUEUE a one-shot read until the transaction closes instead of failing it. An open
transaction that never crosses the streaming threshold is not speculative — it holds
its changes on the writer, so reads stay serviceable.
Sourcepub fn write(&self) -> Result<ClusterWriteTxn, ReplicaError>
pub fn write(&self) -> Result<ClusterWriteTxn, ReplicaError>
Open the single-writer transaction. Errors if one is already open.
Sourcepub fn committed_tx_id(&self) -> TxId
pub fn committed_tx_id(&self) -> TxId
The last durably-committed global tx id (0 if none yet).
The database’s schema cookie (PRAGMA schema_version), read on the writer connection so
an open mutation transaction’s uncommitted DDL is visible. Hosts whose trusted SQL facade
runs no statement classification compare it across a transaction to detect DDL that must
invalidate reader pools and capture registration.
Sourcepub fn foreign_key_audit(
&self,
max_rows: usize,
) -> Result<ForeignKeyAudit, ReplicaError>
pub fn foreign_key_audit( &self, max_rows: usize, ) -> Result<ForeignKeyAudit, ReplicaError>
Walk every declared foreign key and report violating rows — the opt-in audit a replica runs instead of paying for enforcement on every applied row.
PRAGMA foreign_key_check reports what is in the file, not what SQLite would have
refused, so it answers the same question whether this cluster was opened
ForeignKeys::Enforced or
ForeignKeys::Unenforced — which is exactly why a follower
can apply with enforcement off and still prove referential integrity on demand.
It is a full scan of the referencing tables: an operator command, a post-restore
gate, or a soak assertion, never a per-commit step. Runs on the read-only
connection, so it takes no write lock and does not disturb an applier.
Pass FOREIGN_KEY_AUDIT_ROW_CAP unless you
have a reason not to, or 0 for no cap.
Sourcepub fn sync(&self)
pub fn sync(&self)
Block until every worker has drained its command queue — i.e. emitted the
Hydrated/Changed events for all preceding query /
commit calls to the channel-out. A quiesce/test affordance (a steady-state
consumer just drains the channel continuously); relies on per-worker FIFO
command order, so a returned ping implies all prior work is done.
Doubles as the liveness sweep: a worker that died (or hung) is detected here and
respawned (its lost queries delivered a terminal ClusterEvent::Faulted),
so a steady-state consumer that drains + periodically sync()s also self-heals.
Sourcepub fn set_push_deadline_ms(&self, ms: u64)
pub fn set_push_deadline_ms(&self, ms: u64)
Set the per-push derive deadline (FOLLOWER-LAG-SHED §6.6 — the runaway-push bail),
in milliseconds; 0 disables it. A single chunk’s derive exceeding this faults the
worker’s queries promptly and cleanly (FaultCause::PushDeadline, no detached
thread) instead of wedging until the barrier watchdog detaches the whole worker.
Applies immediately to every worker, respawns included. Default:
10 s, aligned with the barrier watchdog it pre-empts.
Sourcepub fn set_join_precheck_bounds(
&self,
per_join: Option<usize>,
per_graph: usize,
)
pub fn set_join_precheck_bounds( &self, per_join: Option<usize>, per_graph: usize, )
Set the join membership pre-check bounds
(designs/311-JOIN-MEMBERSHIP-PRECHECK-DESIGN.md §8) on every worker engine —
respawns and post-fault rebuilds included. per_join is the distinct-key bound one
join may track (None = off, the engine default); per_graph caps the total across
a worker’s joins. Applied at each worker’s next commit barrier, before anything
derives; a change resets the workers’ sets, which rebuild by observing the next
hydrate-shaped fetch through each join.
Sourcepub fn read<T>(
&self,
f: impl FnOnce(&Connection) -> Result<T>,
) -> Result<T, ReplicaError>
pub fn read<T>( &self, f: impl FnOnce(&Connection) -> Result<T>, ) -> Result<T, ReplicaError>
Run SQL against a physically read-only connection using SQLite snapshot semantics.
This connection does not see the writer’s uncommitted changes. Schema changes
belong in Self::exec_ddl, and row mutations in Self::write.
Sourcepub fn view_schema(&self, ast: &Ast) -> Result<Schema, ReplicaError>
pub fn view_schema(&self, ast: &Ast) -> Result<Schema, ReplicaError>
The hierarchical view [Schema] ast materializes to over the registered tables —
the coordinator parity for Db::view_schema. Derived from
the schema-only engine (the same source-schema mapping the workers use), so the
shape, sort, singular flag, and in-view relationships line up with the change
stream. A layer that ships views to a remote (the normalized wire schema + hello)
needs this without registering the query. BuildError (unknown table/column)
surfaces as Err.
Sourcepub fn maintain(
&self,
opts: &MaintenanceOptions,
) -> Result<MaintenanceReport, ReplicaError>
pub fn maintain( &self, opts: &MaintenanceOptions, ) -> Result<MaintenanceReport, ReplicaError>
Run one online maintenance pass on the writer connection: refresh planner stats
(PRAGMA optimize, bounded by the analysis_limit set at open), return up to
incremental_vacuum_pages freelist pages to the OS once at least freelist_threshold_pages
have built up, and take a non-blocking PASSIVE WAL checkpoint — see MaintenanceOptions.
Designed to run on a timer in the gap between commits on the coordinator thread: each step
is bounded (no full VACUUM/ANALYZE, no exclusive lock) and the maintenance writes land
only in sqlite_stat* / the freelist, which the CDC capture hook ignores. It is skipped
(returning MaintenanceReport::skipped()) while a write transaction is open — e.g. a
follower stream holds one open across frames — so it never disturbs an in-flight txn.
Sourcepub fn checkpoint_truncate(&self) -> Result<WalCheckpoint, ReplicaError>
pub fn checkpoint_truncate(&self) -> Result<WalCheckpoint, ReplicaError>
Checkpoint and truncate the WAL through the coordinator writer. Snapshot/install code uses
this after quiescing writes so the portable main file carries every committed frame. The
public Self::read connection is physically read-only and deliberately cannot perform a
checkpoint.
Sourcepub fn exec_ddl(&self, sql: &str) -> Result<(), ReplicaError>
pub fn exec_ddl(&self, sql: &str) -> Result<(), ReplicaError>
Run schema DDL (CREATE/ALTER/DROP/REINDEX) against the writer connection — the
supported way to define the plain base tables you then register_table
and write through. The historical bounded ANALYZE call is also accepted; row-changing
statements and row-producing DDL are rejected. The complete batch and schema-envelope
validation commit atomically. Rejected while a write transaction is open (DDL there would
be invisible to the workers until commit). Mirrors Db::exec_ddl.
Sourcepub fn exec_ddl_with_marker(
&self,
marker_table: &str,
key: &str,
statements: &[String],
) -> Result<DdlApplyReport, ReplicaError>
pub fn exec_ddl_with_marker( &self, marker_table: &str, key: &str, statements: &[String], ) -> Result<DdlApplyReport, ReplicaError>
Apply schema statements AND stamp a durable idempotency marker (key → marker_table)
in ONE ordinary transaction on the writer connection. See
ApplyStore::exec_ddl_with_marker — this is the same primitive, delegated (design
309). A marker and its DDL commit together, so retrying an applied migration
can detect the marker without applying the statements again.
Sourcepub fn exec_ddl_with_marker_and_step_effects<F>(
&self,
marker_table: &str,
key: &str,
statements: &[String],
apply_step_effects: F,
) -> Result<DdlApplyReport, ReplicaError>
pub fn exec_ddl_with_marker_and_step_effects<F>( &self, marker_table: &str, key: &str, statements: &[String], apply_step_effects: F, ) -> Result<DdlApplyReport, ReplicaError>
exec_ddl_with_marker, with one caller-owned bookkeeping
hook invoked after every real statement and its authorizer actions. See
ApplyStore::exec_ddl_with_marker_and_step_effects.
Sourcepub fn analyze_full(&self) -> Result<(), ReplicaError>
pub fn analyze_full(&self) -> Result<(), ReplicaError>
Run a full ANALYZE on the writer to build deep planner statistics (sqlite_stat4),
not just the bounded sqlite_stat1 the maintenance tick refreshes. Call this once after a
bulk load / seed (or periodically) to sharpen the cost model. Restores the bounded
analysis_limit afterward, so it does not slow the ongoing maintenance tick. Rejected
while a write transaction is open.
This used to be described as what makes an ORDER BY … LIMIT n displacement re-fetch seek
rather than scan (GitHub #68). It is not, since the query builder began lifting a sargable
leading-column bound — see maintenance::analyze_full.
Sourcepub fn enable_client_mutations(&self) -> Result<(), ReplicaError>
pub fn enable_client_mutations(&self) -> Result<(), ReplicaError>
One-time (idempotent) setup for the client-mutations protocol (the coordinator
parity for Db::enable_client_mutations):
create CLIENT_MUTATIONS_TABLE and register it like any base table (CDC
capture + sources on the schema engine and every worker), so lmid rides the
change stream co-transactionally with the data and each client’s one-row system
query can host it. Rejected mid-write-txn.
Sourcepub fn enable_realtime_lifecycle(&self) -> Result<(), ReplicaError>
pub fn enable_realtime_lifecycle(&self) -> Result<(), ReplicaError>
One-time (idempotent) setup for the §4 realtime lifecycle (the coordinator parity
for Db::enable_realtime_lifecycle — see
its doc for why every table is REGISTERED, not just created): the scope-session
doorbell (§4.1), the downgrade watermark fence (§4.2), the mutation-outcomes
resolution surface (populated by Slice I-ii), plus
ROOM_CLIENT_MUTATIONS_TABLE — §7.1’s “ordinary footprint data after
downgrade”. Requires enable_client_mutations
first (it owns the room ledger’s DDL); rejected mid-write-txn.
Sourcepub fn client_lmid(&self, client_id: &str) -> Result<u64, ReplicaError>
pub fn client_lmid(&self, client_id: &str) -> Result<u64, ReplicaError>
The high-water mutation id durably recorded for client_id (0 if none — a new
client). Coordinator parity for Db::client_lmid;
requires enable_client_mutations.
Sourcepub fn apply_mutations(
&self,
registry: &MutatorRegistry,
envelopes: &[MutationEnvelope],
) -> Result<MutationOutcome, ReplicaError>
pub fn apply_mutations( &self, registry: &MutatorRegistry, envelopes: &[MutationEnvelope], ) -> Result<MutationOutcome, ReplicaError>
Apply a client’s mutation push over the cluster (the coordinator parity for
Db::apply_mutations): each envelope’s mutator runs
in its own transaction (with the co-transactional lmid upsert), in mid order.
Same per-envelope rules as the single-thread path — mid ≤ lmid skipped (idempotent
redelivery), mid == lmid + 1 applied, a gap rejected at that envelope; a mutator
error / panic / unknown name rolls effects back and commits lmid only
(processed-as-no-op — no rejection signal).
Unlike the Db path it does not return the changed-query set inline — every
commit’s batches (including the lmid row on the client’s own system query) flow
asynchronously through the drain. Must be called with no write transaction open
(it manages its own).