Expand description
Spec 06/07 operators implemented outside graph.rs — the operator
fan-out seam.
§Why this module exists
Operators are variants of the closed graph::Operator enum, and Graph
dispatches fetch/push/input_schema/… over it. That enum + its match sites
are a single file: if every operator’s logic also lived in graph.rs, N
agents each implementing one operator would all edit the same file and collide.
The seam (the decision in the prep wave) keeps each operator’s struct and all
its logic in its own file here (op/<name>.rs), and leaves only thin,
stable wiring in graph.rs:
- one
Operator::<Name>(<Name>)enum variant, - a delegating arm in each dispatch fn —
Op::<Name>(o) => o.fetch(self, req)/o.push(self, change), - a typed
input_schemaarm (pass-through for the non-reshaping operators), - an
add_<name>(<Name>) -> NodeIdbuilder that takes a fully-constructed value — so changing the struct’s fields never changesgraph.rs.
The operator drives the graph through the pub(crate) Graph API:
Graph::fetch,
Graph::push,
Graph::input_schema, and Graph::storage (stateful
operators get a StorageId from
Graph::alloc_storage).
§Adding an operator (the template)
Skip is the worked example. To add Take/Cap/Exists/FlippedJoin/
UnionFanOut/UnionFanIn:
- Copy
skip.rs→op/<name>.rs; define the struct (fields private to the module — grow them freely) andimplitsfetch/push(+ the filter-chain methods if it’s a chain link likeExists). Stateful operators store aStorageIdand read state viag.storage(id). - Add the four wiring lines to
graph.rs(variant + the two delegating arms + theinput_schemaarm; plus theset_outputarm andadd_<name>builder). mod <name>; pub use <name>::<Name>;here.
Two agents adding two operators touch disjoint files plus a handful of
distinct, auto-mergeable lines in graph.rs — not the same match arm. That is
what makes the operator port fan out.
Structs§
- Cap
Cap: unordered top-N by PK-set membership, backed by aStorageIdslot.- Exists
Exists: a relationship-size gate in thewherefilter sub-graph.- Flipped
Join FlippedJoin: child-driven inner join (spec06§3.2). Two input ports (Port::JoinParent/Port::JoinChild) likeJoin; the child drives the fetch but the parent is the output row, sorel_slotresolves against the parent schema andinput_schemais the parent’s.- Reduce
Reduce: an invertible aggregate, global or grouped. The running accumulator(s) live inGraphstorage underReduce::storage, not in the struct, so they survive acrossfetch/push.- Skip
Skip: a stateless, ordered operator that drops rows up tobound(the ASTstart). Stateless ⇒ noStorageId.- Take
Take: ordered, bounded top-N per partition, backed by aStorageIdslot.- Union
FanIn UnionFanIn(union-fan-in.ts): N branch inputs, one output.fetchk-way-merges the branch fetches with PK dedup (merge_node_streams).pusheither accumulates (during a fan-out broadcast — the owned model) or does a direct cross-branch dedup (a flipped child pushed while the fan-out is idle). On drain it collapses the accumulation viapush_accumulated_changesand forwards the single result.- Union
FanOut UnionFanOut(union-fan-out.ts): one input, N branches, built over the source.fetchdelegates to the input;pushbroadcasts the change to every branch (each a filter pipeline and/or aFlippedJoin) then drives the pairedUnionFanIn’s collapse. Unlike the filterFanOutit operates on full nodes via side-effectingGraph::push, not the return-based filterchain_push.