Skip to content

Store filter other_shapes as one ETS row per shape - #4785

Open
alco wants to merge 1 commit into
mainfrom
fix/other-shapes-per-row-ets
Open

Store filter other_shapes as one ETS row per shape#4785
alco wants to merge 1 commit into
mainfrom
fix/other-shapes-per-row-ets

Conversation

@alco

@alco alco commented Aug 27, 2026

Copy link
Copy Markdown
Member

Fixes #4743

Problem

WhereCondition kept a node's non-indexable shapes (other_shapes) as a single map inside the node's where_cond_table row. Every add or remove did lookup → Map.put/Map.delete → insert, copying every where clause on the node in and out of ETS each time. So:

  • adding a non-indexed shape was O(n) in the number of non-indexed shapes already on the node, and creating n of them — or re-adding them all when ShapeLogCollector restores shapes on startup — was O(n²);
  • adding an indexed shape to such a node paid the same cost, because the index_keys bookkeeping lived in the same row and was rewritten through the same lookup/insert.

The copy is proportional to the total size of the clause ASTs on the node, so shapes with large residual clauses (e.g. an IN (...) list) make it worse.

Fix

Non-indexed shapes now live in a dedicated :ordered_set table with one row per shape, keyed {condition_id, shape_id, branch_key}. Insert and delete are single-row operations, and routing / all_shape_ids / the node-emptiness check use key-prefix matches with condition_id bound, which an :ordered_set traverses as a range rather than a full scan (same technique SubqueryIndex already uses). The where_cond_table row is reduced to {condition_id, index_keys}.

Numbers

Per-shape Filter.add_shape cost on one machine, n = non-indexed shapes already on the node, K = size of an IN list in the clause:

clause n before after
number > i 5,000 1,854 µs 2 µs
id IN (K=10) AND number > i 3,000 10,342 µs 21 µs
id IN (K=150) AND number > i 1,000 52,680 µs 811 µs

Removal shows the same change. affected_shapes latency on such nodes is unchanged within noise — that path is dominated by evaluating each clause, not by the ETS read.

Independent of #4742, which reduces how many shapes end up on this path in the first place; the two compound.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FfoVxddTs2Y2nZewNozbYR

@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 60.06%. Comparing base (ab53baa) to head (63fa1df).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4785   +/-   ##
=======================================
  Coverage   60.05%   60.06%           
=======================================
  Files         397      397           
  Lines       43772    43772           
  Branches    12590    12590           
=======================================
+ Hits        26289    26290    +1     
  Misses      17401    17401           
+ Partials       82       81    -1     
Flag Coverage Δ
packages/agents 72.77% <ø> (ø)
packages/agents-mcp 77.70% <ø> (ø)
packages/agents-mobile 80.67% <ø> (ø)
packages/agents-runtime 83.73% <ø> (ø)
packages/agents-server 75.65% <ø> (ø)
packages/agents-server-ui 8.32% <ø> (ø)
packages/electric-ax 51.06% <ø> (ø)
packages/experimental 87.73% <ø> (ø)
packages/react-hooks 86.48% <ø> (ø)
packages/start 82.83% <ø> (ø)
packages/typescript-client 91.95% <ø> (+0.11%) ⬆️
packages/y-electric 56.05% <ø> (ø)
typescript 60.06% <ø> (+<0.01%) ⬆️
unit-tests 60.06% <ø> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9e97bd849f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/sync-service/lib/electric/shapes/filter.ex
WhereCondition kept a node's non-indexable shapes in a single map inside
the node's where_cond_table row. Every add or remove looked the whole map
up, modified it and inserted it back, copying every where clause on the
node in and out of ETS each time. Adding a shape was therefore O(n) in the
number of non-indexed shapes already on the node and (re)building the
filter — including restoring all shapes on startup — was O(n²). Adding an
indexed shape to such a node paid the same cost because the index_keys
bookkeeping lived in the same row.

Non-indexed shapes now live in a dedicated :ordered_set table with one row
per shape keyed {condition_id, shape_id, branch_key}, so insert and delete
are O(1), and routing/emptiness checks use key-prefix matches that only
traverse the node's own rows. The where_cond_table row holds just the
node's index_keys.

Measured on one machine, per-shape add cost with 5,000 non-indexed shapes
on a node went from ~1.9 ms to ~2 µs; routing latency is unchanged.

Fixes #4743

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfoVxddTs2Y2nZewNozbYR
@alco
alco force-pushed the fix/other-shapes-per-row-ets branch from 9e97bd8 to 63fa1df Compare September 1, 2026 09:30
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.

add_shape is O(node population) for unindexed clauses: the whole other_shapes map is copied in and out of ETS per insert

1 participant