dogsdogsdogs: one lookup primitive under the delta-join operators - #818
Open
frankmcsherry wants to merge 3 commits into
Open
dogsdogsdogs: one lookup primitive under the delta-join operators#818frankmcsherry wants to merge 3 commits into
frankmcsherry wants to merge 3 commits into
Conversation
`half_join`, `lookup_map`, `count`, `propose` and `validate` were five
hand-rolled `binary_frontier` operators, each carrying its own copy of the
stash-by-capability, frontier, cursor-seek and compaction logic. They are one
operator with five output functions:
for each (request, matching value): here are the consolidated (time, diff)
arrangement updates the cut admits. Do what you like with them.
`operators::lookup` is that operator. Two behaviours sit on it: `lookup_map`
sums the admitted updates into one diff and emits once (`count`), and
`lookup_join` visits them individually, joining each time into a time the
request carries (`propose`, `validate`). `half_join2` is promoted to
`half_join`, whose 366-line body becomes a 155-line derivation; the previous
implementation is removed.
Source is roughly flat at +903/-864 -- this consolidates duplicated operator
logic rather than shrinking the tree, and a little over half of the new
`lookup.rs` is doc comments. The +804 under `tests/` is the bulk of the change.
Cuts are derived, not chosen. `Cut::for_positions(seed, atom)` returns the
strict or lax comparison from the two atoms' positions: rule `seed` claims a
combination exactly when it is the largest index achieving the `Ord`-maximum of
the times, so exactly one rule claims each. Ordering by `Ord` rather than
`PartialOrder` is what makes that maximum exist -- under the partial order two
incomparable times have no maximum, no rule claims the pair, and the match is
lost. Every other value a caller could pass either double counts or drops
matches.
The order time stays the dataflow timestamp and the join time rides in the
payload. Progress tracking must see the time the cuts compare against, and
since order <= join, a frontier over join times would not bound the order times
still in flight. `propose` and `validate` therefore emit per admitted update
rather than accumulating, which matters as soon as a cut admits an update
incomparable to the request -- that is, inside a nested scope.
`CollectionIndex` holds local `Arranged` values rather than trace handles.
Re-importing an arrangement the same dataflow produced forfeits the scope's
progress tracking, and inside a recursive scope the timestamp then counts
upward without end. Documented on `TraceAgent::import`.
Two behaviour changes worth review:
- `validate` multiplies by the matched diff instead of passing the prefix
diff through. An atom contributes to the product whether it proposed an
extension or merely validated it. For set-valued relations the matched diff
is one, so nothing observable moves.
- `lookup_map` prunes a request only when the cut admits nothing, never when
the admitted diffs sum to zero. Those updates can carry different join
times, so their sum is not the count at any one of them; dropping on it
removes the request from the collection outright, so no atom proposes it
and the extension is lost. An inaccurate non-zero count merely picks a
worse proposer.
`count` still reports distinct-extension cardinality, which tracks changes but
measures the wrong thing: the work is the post-consolidation updates a walk
encounters. Reporting that needs a cursor hook that does not exist yet, so it
is left alone.
Tests, 11 in total, every case at one and four workers:
- `delta_join_property`, `wcoj_triangle_property`: two- and three-atom joins
against brute-force oracles over random `Product<usize, usize>` updates.
- `half_join_total_order`: exactly-once pinned from all three sides, on
incomparable and on equal times.
- `ktruss_iterative`: a worst-case-optimal join inside a recursive scope,
maintained incrementally across insertions and deletions.
Created using Claude Code
`AltNeu<T>` encoded a delta rule's strict-vs-lax choice in the timestamp: an atom read "before" the seed was entered at `neu` so that a plain `less_equal` excluded ties, and one read "at or before" was entered at `alt`. `Cut` now carries that distinction directly, derived from the atoms' positions, so the encoding is redundant -- the callers were passing a cut *and* relying on alt/neu timestamps to do the real work. Retiring it removes a scope. Each caller ran inside `scope.scoped::<AltNeu<_>>` and maintained an "old" and a "new" copy of every arrangement, because alt and new are separate `delay`ed collections. On plain `usize` times with derived cuts, one index per orientation suffices: the triangle query in `delta_query_wcoj` goes from four `CollectionIndex`es to two, and `lookup_map_regression` likewise. `calculus` (`Differentiate` / `Integrate`) is defined over `AltNeu` by construction -- `differentiate` splits each update into an alt and a neu half -- so it goes too. Its only user was the second half of `delta_query`, which cross-checked the propose/validate path against a calculus-based one with `assert_eq`. That cross-check is lost; the propose/validate path is now covered by property tests against brute-force oracles, which is a stronger check than agreement with a second implementation. `delta_query` keeps its first half, ported to derived cuts, and remains the one example of the raw `propose`/`validate` operators with the delta region bracketed by hand rather than by `extend`. Verified by `lookup_map_regression`, which asserts on its output: the same triangle is found with plain `usize` times and derived cuts as with the alt/neu encoding. Created using Claude Code
`lookup`'s module doc described the flat structure that existed before `lookup_join` was split out, so it named `lookup_map` as the only behaviour and claimed `propose` and `validate` went through it. They do not: they take `lookup_join`, and `validate` multiplies rather than filters. Replaces that with the actual three layers, and states why the choice between the two behaviours is forced rather than stylistic -- `lookup_map` collapses many admitted times into one output, which is correct only when the request's time dominates them all, so it suits `count` (a routing decision, contributing no record and no time) and not `propose`/`validate` (each contributing a record to a tuple that exists at the join of all contributing times). Also notes that `half_join` reaches `lookup` directly rather than through `lookup_join`, and why: its output function takes the whole admitted list and a container builder, because advancing times by a carried time can collapse distinct times and it must re-consolidate. Folding the two together is plausible and untried. Disambiguates the intra-doc links, each of which named a module and a function of the same name; `cargo doc` is now warning-free. Created using Claude Code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
dogsdogsdogs: one lookup primitive under the delta-join operators
half_join,lookup_map,count,proposeandvalidatewere five hand-rolledbinary_frontieroperators, each carrying its own copy of the stash-by-capability, frontier,cursor-seek and compaction logic. They are one operator with five output functions:
operators::lookupis that operator. Two behaviours sit on it —lookup_mapsums the admittedupdates into one diff and emits once (
count), andlookup_joinvisits them individually,joining each time into a time the request carries (
propose,validate).half_join2ispromoted to
half_join, whose 366-line body becomes a 155-line derivation.Two commits: the operator unification, then retiring
AltNeunow that the cut is explicit.Cuts are derived, not chosen
Cut::for_positions(seed, atom)returns the strict or lax comparison from the two atoms'positions in the query. Rule
seedclaims a combination exactly when it is the largest indexachieving the
Ord-maximum of the times, so exactly one rule claims each. Ordering byOrdrather than
PartialOrderis what makes that maximum exist — under the partial order twoincomparable times have no maximum, no rule claims the pair, and the match is silently lost.
Every other value a caller could pass either double counts or drops matches, so passing one is
now impossible.
The order time stays the timestamp; the join time rides in the payload
Progress tracking has to see the time the cuts compare against. Since
order ⪯ join, a frontierover join times would not bound the order times still in flight, and a downstream operator could
not set its compaction bound. So
proposeandvalidateemit per admitted update rather thanaccumulating — which matters as soon as a cut admits an update incomparable to the request, i.e.
inside a nested scope.
AltNeuretiredAltNeu<T>encoded the same strict/lax choice in the timestamp: an atom read "before" the seedwas entered at
neuso a plainless_equalexcluded ties. WithCutcarrying it directly theencoding is redundant — callers were passing a cut and relying on alt/neu to do the real work.
Retiring it removes a scope. Callers ran inside
scope.scoped::<AltNeu<_>>and maintained an"old" and a "new" copy of every arrangement, since alt and neu are separate
delayedcollections. On plain
usizetimes the triangle query indelta_query_wcojgoes from fourCollectionIndexes to two, andlookup_map_regressionlikewise.calculus(
Differentiate/Integrate) is defined overAltNeuby construction, so it goes too.Behaviour changes worth a look
validatemultiplies by the matched diff instead of passing the prefix diff through. Anatom contributes to the product whether it proposed an extension or merely validated it. For
set-valued relations the matched diff is one, so nothing observable moves.
lookup_mapprunes only when the cut admits nothing, never when the admitted diffs sum tozero. Those updates can carry different join times, so their sum is not the count at any one
of them; dropping on it removes the request from the collection outright, so no atom proposes
it and the extension is lost. An inaccurate non-zero count merely picks a worse proposer.
CollectionIndexholds localArrangedvalues rather than trace handles. Re-importing anarrangement the same dataflow produced forfeits the scope's progress tracking; inside a
recursive scope the timestamp then counts upward without end. Documented on
TraceAgent::import— the only change outsidedogsdogsdogs/, and it stands alone if you'drather it were separate.
extend_usinggains two parameters (the cut and its compaction bound).Tests
11, every case at one and four workers.
delta_join_property,wcoj_triangle_property— two- and three-atom joins againstbrute-force oracles over random
Product<usize, usize>updates. The three-atom one caught areal bug during development:
lookupsorted its ready prefix by payload, butseek_keyonlyadvances, so a key selector extracting anything other than a payload prefix could send the
cursor backwards and silently see no match.
half_join_total_order— exactly-once pinned from all three sides, on incomparable and onequal times.
ktruss_iterative— a worst-case-optimal join inside a recursive scope, maintainedincrementally across insertions and deletions. This caught the zero-count prune above.
lookup_map_regression— ported offAltNeu; it asserts on its output, so it is the evidencethat derived cuts and plain timestamps find the same triangles the alt/neu encoding did.
Deliberately left
countstill reports distinct-extension cardinality. That tracks changes and no longer reportsa false zero, but it measures the wrong thing — the work is the post-consolidation updates a
walk encounters, and reporting that needs a per-key update-count hook on the cursor that does
not exist. Left alone rather than swapped for a proxy that saturates.
There is no tactic trait here yet;
lookuptakes a closure-based probe. The natural boundary(cursor at a key, request's time in, admitted updates out) would own the admits test, the
release gate and the compaction bound together, and is where a columnar or int-proxy backend
would plug in.
Diffstat
Source shrinks modestly; the additions are tests and doc comments — a little over half of the
new
lookup.rsis documentation.