Backfill Data Pipeline#
Sequin's backfill pipeline reads existing Postgres table rows and emits them as ConsumerEvent messages, filling in data before (or independently of) the WAL replication stream. The entry point is TableReader (lib/sequin/runtime/table_reader.ex), orchestrated by TableReaderServer β a GenStateMachine that manages the backfill lifecycle. Cursor state is stored in Redis as a keyset cursor, enabling resumable pagination.
Fetch Pipeline#
The full-row backfill runs in two query stages:
-
PK-only scan β
fetch_batch_pks/4issues a SELECT of only the keyset cursor columns (PKs + optional sort column) to page through the table efficiently. Theselect_only_pk_and_cursor_columnsflag bypassessafe_select_columnsfor this pass, selecting only what's needed for pagination . -
Full-row fetch β
fetch_batch/6issues a SELECT for all safe columns, converts thePostgrex.Resultto maps viaPostgres.result_to_maps/1, then post-processes values throughPostgres.load_rows/2.
Query construction happens in fetch_batch_query/3, which uses KeysetCursor helpers to build tuple-comparison WHERE (col1, col2) > ($1, $2) and ORDER BY clauses for efficient keyset pagination.
Safe Column Selection#
Postgres.safe_select_columns/1 builds the SELECT list by filtering every column through the private has_encoder?/1. A column is included if:
- Its
typestarts with"time"(all timestamp/timezone variants) - Its
pg_typtypeis"c"(composite),"d"(domain), or"e"(enum) β included optimistically, assuming Postgrex has a matching encoder (see known gap below) - Its base type (after stripping trailing
[]) appears in the@safe_typesallowlist of ~40 known Postgrex-encodable types (int4,uuid,jsonb,vector,hstore, etc.)
Columns failing all three checks are silently excluded from the SELECT and will be absent from backfill row maps β logged at info level .
Known Gap: Composite Types#
Custom composite types pass the pg_typtype = "c" check and appear in the SELECT. However, if their Postgrex-decoded struct has no Jason.Encoder implementation, load_rows/2 silently returns nil for that column's value . The code comment acknowledges this: the actual base type isn't stored in Column, so the check is optimistic . This is the primary source of composite type columns being nulled out during backfill.
Postgrex Decoding vs. WAL Text Decoding#
This is the sharpest architectural divergence in Sequin's decoding layer. The two paths share no code and handle the same semantic values very differently:
| Backfill (SELECT) | WAL Replication | |
|---|---|---|
| Input format | Binary protocol β Postgrex returns native Elixir types | Text strings from WAL logical messages |
| Cast mechanism | load_rows/2 post-processes already-decoded structs | ValueCaster.cast/2 parses raw text |
| Arrays | Postgrex binary array decoding | ArrayLexer.lex/1 text parsing |
| Booleans | Elixir true/false from Postgrex | "t" β true, "f" β false |
| Ranges/Intervals | Postgrex.Range/Postgrex.Interval structs β formatted via format_range/format_interval | Text string cast via Ecto.Type or custom parser |
| Column exclusion | Unsupported types excluded pre-query by safe_select_columns | All WAL columns present; ValueCaster falls back to original value on cast failure |
| Error handling | Non-Jason.Encoder values silently become nil | JSON decode failures raise immediately with row IDs attached |
load_rows/2 runs over a full row map after Postgrex binary decoding. It handles serialization concerns: hex-encoding bytea/bit, string-converting UUID binaries, formatting ranges/intervals β and returns nil for any value with no Jason.Encoder .
ValueCaster is invoked per field in data_tuple_to_fields/3 inside SlotProducer.Processor. It receives text-format column values from the WAL decoder and wraps each as a Field struct. JSON decode errors raise immediately with row IDs for debugging .
Key Source Files#
| File | Role |
|---|---|
lib/sequin/runtime/table_reader.ex | Batch query construction, two-stage fetch pipeline, cursor management |
lib/sequin/runtime/table_reader_server.ex | GenStateMachine orchestrating the backfill lifecycle |
lib/sequin/postgres/postgres.ex | safe_select_columns/1, has_encoder?/1, @safe_types, load_rows/2 |
lib/sequin/postgres/value_caster.ex | WAL text-to-typed-value casting |
lib/sequin/runtime/slot_producer/processor.ex | WAL field extraction via data_tuple_to_fields/3 |
lib/sequin/runtime/keyset_cursor.ex | Keyset cursor helpers for pagination |