Skip to content

electricCollectionOptions: an update for an unseen key materializes a permanent partial row #1698

Description

@iwimarin

Summary

electricCollectionOptions forwards an Electric update to the collection even when that primary key was never received as an insert. Electric update payloads contain only changed columns plus the PK. In a persisted/resumed collection, the collection then materializes that partial payload as a row. Subsequent updates merge into it, so immutable columns that were absent from the first update never recover.

This can make rows permanently invisible to normal application filters while their primary keys still exist locally.

Why this matters

A persisted Electric resume offset assumes the local materialization is complete. If the initial snapshot was interrupted/partially persisted but the resume offset advanced, a later update for a missed row is the first message the client sees for that key. The current behavior turns that partial update into a durable ghost row instead of recognizing the broken resume invariant and starting a fresh snapshot.

We observed this in production with persisted SQLite collections: external calendar rows were missing immutable fields (createdAt, type, etc.), failed the UI filters, and survived reloads because later updates merged into the ghost. A count-only integrity check also misses the condition because the correct ID is present.

Minimal reproduction

  1. Compose createCollection(persistedCollectionOptions(electricCollectionOptions(...))) with an Electric shape whose rows include an immutable createdAt column.
  2. Persist a complete electric:resume marker while omitting row B from the local SQLite materialization (for example, filter B from the initial snapshot while preserving the response headers/offset).
  3. Resume the shape and update a mutable column on B server-side.
  4. Electric sends operation: update for B with { id, changedColumn }.
  5. The collection now contains B, but it lacks createdAt and any other immutable columns omitted by the update. Later updates do not repair those fields.

Expected: an update for a key absent from the current materialization should be treated as a consistency failure: clear/reset the resume state and request a fresh snapshot (or otherwise refuse to materialize a partial row).

Actual: the partial update is written as a new row and becomes durable.

Versions

  • @tanstack/electric-db-collection 0.3.4
  • @tanstack/db-sqlite-persistence-core 0.1.10
  • @electric-sql/client 1.5.18
  • Electric server 1.6.10

The relevant path appears to be processChangeMessage: it marks every non-delete key as synced and passes operation through to write without checking whether the key is already materialized.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions