Skip to content

DDIR: a delta join's strict paths drop the imports' history at the as-of time - #1026

Merged
frankmcsherry merged 1 commit into
TimelyDataflow:master-nextfrom
frankmcsherry:ddir-delta-asof-drop
Oct 24, 2026
Merged

frankmcsherry merged 1 commit into
TimelyDataflow:master-nextfrom
frankmcsherry:ddir-delta-asof-drop

Conversation

@frankmcsherry

Copy link
Copy Markdown
Member

Since #1024 and #1025, a program reads every import as of its as-of time (e - 1, []), and runs at times at or beyond it. So every time a half join's arrangement presents is at or beyond the as-of time.

  • Strict half joins: an update at exactly the as-of time meets only arranged rows at that time, and a strict half join meets none.
  • Along a path: an update keeps its own time along a chain of half joins. Once every path from a lifted stream reaches a strict half join, its updates at the as-of time produce nothing.
  • In a delta join, that is every path but the one that is non-strict throughout. Today those paths carry the imports' whole history, only to discard it.

This is a certain rewrite, so it is automatic. Install runs it after the projection step, with the program's as-of time (interactive/src/as_of.rs).

  • Where it filters: it drops those updates where the stream is lifted, with a filter on the time lift writes. Any backend runs it with the operators it has.
  • What may sit between the lift and the strict half join: only time-preserving stages: project, filter, negate, lift_iter, flat_map. Any other reader makes the stream live.
  • Shared lifts: when other readers share a lift, as both paths of a self-join do, the lift keeps its rows, and the filter goes only on the edges to the readers it is dead for.
  • Nested scopes: it applies there too, where the as-of time has zero coordinates added.
  • Safety net: shapes are inferred again afterwards, and install falls back to the program as written if they disagree.

Tests

  • Unit tests in as_of.rs:
    • a strict-only path filtered;
    • a three-way delta join with 2 of its 3 paths filtered;
    • a shared lift filtered on its strict edge only;
    • no filter for a non-strict path, a delay before the half join, or an exported lift;
    • nested scopes comparing every lane of the time.
  • server_late_import.rs: a three-way delta join over three imports, installed at epochs 0, 7 and 12, with column and arranged imports, matches the plain join. If the rule is deliberately broken so non-strict paths drop too, this test and the existing self-join delta test both fail.
  • Crate tests: cargo test -p interactive -p ddir-server: 211 passed. cargo check --all-targets: no warnings. Clippy: nothing new.
  • Difftest: release, 1,000 cases with all nine oracles: all passed. delta,split,bind,reinstall, 2,000 seeds with late installs: all passed.

The saving

A three-way delta join over imports with eight epochs of history: three inputs, 50,000 rows each per epoch, with retractions, installed late. Install plus first tick, three alternated runs each:

keys with the drop without
100,000 (dense, about 1.15M join rows) 696–713 ms 783–795 ms
1,000,000 (sparse) 61 ms 124–125 ms

Steady ticks are unchanged, and the outputs are identical. The probe is kept out of the tree.

🤖 Generated with Claude Code

https://claude.ai/code/session_019XsakwFGwtESwZ1gSYwG4o

…-of time

A program reads every import as of `(e - 1, [])` and runs at times at or
beyond it, so every time a half join's arrangement presents is at or beyond
the as-of time. An update at exactly that time meets only arranged rows at
it, and a strict half join meets none. An update keeps its own time along a
chain of half joins, so once every path from a lifted stream reaches a strict
half join, its updates at the as-of time produce nothing. In a delta join that
is every path but the one that is non-strict throughout, which today carries
the imports' whole history only to discard it.

Install now drops those updates where the stream is lifted, with a filter on
the time `lift` writes, so any backend runs it with the operators it has. A
lift that other readers share, as both paths of a self-join do, keeps its
rows, and the filter goes on the edges to the readers it is dead for. Only
time-preserving stages may sit between the lift and the strict half join. The
rewrite applies in nested scopes too, where the as-of time has zero
coordinates. The shapes are inferred again after it, and install falls back
to the program as written if they disagree.

On a late-installed three-way delta join over imports with eight epochs of
history, install plus first tick took 696-713 ms against 783-795 ms with
dense keys and 61 ms against 124 ms with sparse ones; steady ticks are
unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019XsakwFGwtESwZ1gSYwG4o
@frankmcsherry
frankmcsherry merged commit 7eb7da8 into TimelyDataflow:master-next Oct 24, 2026
7 checks passed
frankmcsherry added a commit that referenced this pull request Oct 24, 2026
…te (#1028)

The as-of rewrite (#1026) re-checks shapes after it runs, which cannot show
that results match. `Server::set_as_of_drop` (default on) turns the rewrite
off for programs installed afterwards, and a new `asof` difftest oracle uses
it: each case's delta form is installed after its inputs have history, once
with the rewrite and once without, and every epoch's exports must agree.
Over 2,000 seeds the rewrite fired 2,938 times in place and 1,264 times on
shared-lift edges, 1,892 of them in nested scopes.

When a lift is shared, the rewrite put filters on the dead readers' edges
only if the lift was its node's last op. The filter reads the time `lift`
wrote into the row, and `filter` and `negate` leave the row as it is, so the
edge filters now also go after a node whose ops past the lift are only
those. Other tails (`project`, `flat_map`, `lift_iter`) may move or drop the
lifted time and are left alone; handling them would need tracking the time
through lambdas, or duplicating the tail on the dead edges at a cost every
tick.



Claude-Session: https://claude.ai/code/session_019XsakwFGwtESwZ1gSYwG4o

Co-authored-by: GPT-6 Sol (Codex) <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant