# `Electric.Shapes.Consumer.Subqueries.IndexChanges`
[🔗](https://github.com/electric-sql/electric/tree/%40core/sync-service%401.6.2/packages/sync-service/lib/electric/shapes/consumer/subqueries/index_changes.ex#L1)

Determines subquery index effects for dependency move events.

The subquery index tracks which values are present in the dependency view.
When a move event occurs, the index must be updated — but the *timing* of
that update depends on whether the move triggers buffering.

## Broadening and narrowing

While a move-in is being buffered, transactions continue to arrive and must
be filtered. To avoid missing relevant rows, the index is **broadened**
(made more permissive) as soon as buffering starts. Once the move-in query
completes and the splice is done, the index is **narrowed** back to its
final state.

For a positive (`IN`) subquery:
- Adding values to the index broadens the filter (more rows match).
- So a move-in adds to the index when **buffering starts**.

For a negated (`NOT IN`) subquery:
- Adding values to the index *narrows* the filter (fewer rows match).
- So a dependency move-in does **not** update the index when buffering starts
  (keeping the filter broad); the add is deferred until **complete**.
- A dependency move-out broadens the filter by removing the value from the
  index immediately, and that removal remains correct after the splice.

## Effect tables

### When buffering starts

| Dep move   | Polarity | Index effect              |
|------------|----------|---------------------------|
| move_in    | positive | AddToSubqueryIndex        |
| move_in    | negated  | *(none)*                  |
| move_out   | positive | *(none)*                  |
| move_out   | negated  | RemoveFromSubqueryIndex   |

### When complete (splice finished, or immediate for non-buffering cases)

| Dep move   | Polarity | Index effect              |
|------------|----------|---------------------------|
| move_in    | positive | *(none)*                  |
| move_in    | negated  | AddToSubqueryIndex        |
| move_out   | positive | RemoveFromSubqueryIndex   |
| move_out   | negated  | *(none)*                  |

## Caller conventions

- **Non-buffering cases** (positive move-out, negated move-in): the move
  completes atomically, so callers use `effects_for_complete/3`.
- **Buffering cases** (positive move-in, negated move-out): callers use
  `effects_for_buffering/3` when entering buffering and
  `effects_for_complete/3` at splice time.

# `move`

```elixir
@type move() :: {:move_in | :move_out, non_neg_integer(), list()}
```

# `effects_for_buffering`

```elixir
@spec effects_for_buffering(Electric.Shapes.DnfPlan.t(), move(), [String.t()]) :: [
  Electric.Shapes.Consumer.Effects.AddToSubqueryIndex.t()
  | Electric.Shapes.Consumer.Effects.RemoveFromSubqueryIndex.t()
]
```

Returns index effects to apply when a dependency move event starts buffering.

Used only by buffering cases to broaden the filter before the move-in query
runs. Calling this for an immediate (non-buffering) move is a bug in the
caller.

# `effects_for_complete`

```elixir
@spec effects_for_complete(Electric.Shapes.DnfPlan.t(), move(), [String.t()]) :: [
  Electric.Shapes.Consumer.Effects.AddToSubqueryIndex.t()
  | Electric.Shapes.Consumer.Effects.RemoveFromSubqueryIndex.t()
]
```

Returns index effects to apply when a move event completes.

For buffering cases this is called at splice time. For non-buffering cases
(where the move completes atomically) this is the only function called.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
