You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fix O(n) shape removal from the subquery index (#4594)
## Summary
Removing a shape from the subquery index in the where-clause filter had
become **O(n)** in the number of shapes — and **O(shapes × values)**
once subqueries were seeded. Shape removal runs on the replication path
and blocks it, so slow removal caused WAL lag in production (the
"Materializer shape invalidated" path). This restores **O(1)** removal
(as in v1.5).
Fixes#4279
## Problem
The subquery index is a single ETS `:bag` table. Several deletions used
`:ets.match_delete/2` with a **wildcard in the key position** (e.g.
`{:membership, handle, :_, :_}`, `{:node_positive_member, node, :_}`). A
`:bag` (hash) table can only use its key index when the key is **fully
bound**, so a partially-bound key forces a **full-table scan** that
grows with the number of shapes. The worst offender was
`delete_node_members`, which scanned every shape's per-`(node, value)`
routing row on each removal — O(shapes × values).
## Solution
Switch the table from `:bag` to **`:ordered_set`** and lift the
discriminating fields (`value`, `shape`, `branch`, `next_cond`) out of
the value tuples into the **keys**. In an `:ordered_set`, a delete whose
key has a bound **prefix** is range-limited (not a full scan), and a
point delete by a fully-bound key is O(log n). Concretely:
- `unregister_shape/2` replaces its full-table `match_delete` scans with
prefix-bounded `select_delete`s.
- `remove_shape/5` point-deletes the shape's node rows, and **derives**
its `node_*_member` deletions from the removed shape's own membership
rows (scoped by `subquery_ref`), applying exact point-deletes. This
touches only the removed shape's rows — never the other shapes sharing a
value.
Removal is now **O(V_shape · log n)** — proportional to the removed
shape's own view size, independent of the total number of shapes, the
number of shapes on a node, and the number of shapes sharing a value.
This is a pure internal storage change: every public function and all
externally-observable behavior is preserved. The per-change routing hot
path (`affected_shapes`) and the exact-evaluation `member?` path are
equal-or-faster (the `:ordered_set` prefix `select` projects in C);
table memory is roughly flat (measured slightly lower in a
microbenchmark).
> Note: a dedicated **memory** reduction (interning / row-shrinking) is
intentionally **out of scope** here — this PR is the removal-complexity
fix only, kept minimal and behavior-preserving. Memory can be tackled
separately.
## Test plan
- [x] `:performance` **flatness** tests prove removal is independent of
each dimension — total shapes, shapes-on-node, and shapes-per-value — by
measuring removal at two well-separated sizes and asserting the delta
stays within noise (observed delta 0 at 1k vs 20k).
- [x] **Positive control**: removal cost *does* scale with the removed
shape's own view size (pins the complexity class as O(V_shape), so a
regression can't hide under a flat budget).
- [x] **No-orphan guard**: a full `Filter.remove_shape` of a seeded
shape, and draining a shared node one-by-one, leaves the subquery index
byte-identical to its pre-add state.
- [x] Existing `subquery_index` / `filter` / `consumer` /
`shape_log_collector` suites green; full `mix test` green.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restore O(1) removal of subquery shapes from the where-clause filter. The v1.6 subquery index removed shapes with full-table ETS scans, so removing a shape cost O(total shapes × values) and blocked replication processing — causing WAL lag when many seeded subquery shapes were present. The index now uses an `:ordered_set` with prefix-bounded deletes, making shape removal independent of the total number of shapes, the number of shapes on a node, and the number of shapes sharing a value.
0 commit comments