Postgrex Integration#
Sequin uses Postgrex as its PostgreSQL driver for both regular query execution and logical replication streaming. The integration spans four areas: a unified query interface, compile-time type registration, runtime type-safety gating, and JSON serialization of Postgrex structs.
Key Source Files#
| File | Role |
|---|---|
lib/sequin/postgres/postgrex_types.ex | Compile-time type module definition |
lib/sequin/postgres/postgres.ex | Unified query interface, row serialization, type utilities |
lib/sequin/postgrex/encoders.ex | Jason.Encoder / String.Chars protocol impls for Postgrex structs |
lib/sequin/postgres/postgrex_backend.ex | Delegates Postgrex.Protocol for logical replication streaming |
lib/sequin/runtime/keyset_cursor.ex | Per-type cursor cast logic for backfill pagination |
Compile-Time Type Registration#
Sequin defines a single custom Postgrex type module at compile time :
Postgrex.Types.define(Sequin.Postgres.PostgrexTypes,
Pgvector.extensions() ++ Ecto.Adapters.Postgres.extensions(),
allow_infinite_timestamps: true
)
This registers Sequin.Postgres.PostgrexTypes with:
Pgvector.extensions()— adds pgvector support forvectorcolumnsEcto.Adapters.Postgres.extensions()— all standard Ecto/Postgres type extensionsallow_infinite_timestamps: true— accepts PostgreSQL'sinfinity/-infinitytimestamp values without error
No application-level config is needed; this module is used wherever a Postgrex connection is opened.
Unified Query Interface (Sequin.Postgres.query/4)#
Sequin.Postgres.query/4 dispatches queries across three connection types:
- PID /
DBConnectionstruct — callsPostgrex.query/4directly, normalizing timeout errors into{:error, Error.service(..., code: :query_timeout)} - Module atom — delegates to
mod.query/3(e.g.,Sequin.Repoin tests) %PostgresDatabase{}— resolves a connection viaConnectionCache, and on:undefined_columnerrors, triggersDatabaseUpdateWorkerto refresh the schema cache
All three paths return {:ok, %Postgrex.Result{}} or {:error, error}. The bang variant query!/4 raises on error .
Parameter Passing#
$N-style placeholders (Postgrex native)#
Postgrex uses positional $1, $2, ... placeholders. Two helpers in Sequin.Postgres assist with parameterization:
parameterize_sql/1— converts?-style SQL to$1-style (used when building dynamic SQL that is more readable with?)parameterized_tuple/2— generates($1, $2, ...)for multi-value inserts; accepts anoffsetfor composing multi-clause queries
OID type encoding gotcha#
When passing sequence names as parameters, the explicit cast $1::text::regclass is required . See the Postgrex OID type encoding docs — Postgrex cannot infer OID type from a string parameter alone.
Type Safety: has_encoder? and safe_select_columns#
Before issuing a backfill SELECT *, Sequin gates which columns are included using has_encoder?/1. This predicate:
- Allows all
time*variants via a prefix match - Passes composite types (
c), domains (d), and enums (e) through optimistically — Postgrex is assumed to handle them - Validates against a
@safe_typeswhitelist of ~27 known-safe types
safe_select_columns/1 uses has_encoder? to build a SELECT list that omits unsupported columns rather than crashing at decode time.
Row Serialization: load_rows/2#
load_rows/2 converts each %Postgrex.Result{} row into a JSON-safe map during backfills. Per-type handling:
| Column type | Conversion |
|---|---|
bytea, bit, varbit | Hex-encoded with \x prefix |
uuid | Binary → UUID string via Sequin.String.binary_to_string!/1 |
uuid[] / _uuid | Each element converted |
Postgrex.Range struct | Bracket-notation string, e.g. [1,10) via format_range/1 |
Postgrex.Interval struct | Map %{"months" => ..., "days" => ..., "microseconds" => ...} via format_interval/1 |
pgvector (Pgvector struct) | Pgvector.to_list/1 → plain list |
| No Jason encoder | nil + debug log — safety net for unknown Postgrex structs |
The no-encoder fallback (Jason.Encoder.impl_for(value) == Jason.Encoder.Any) prevents runtime crashes but silently drops the value. If a column type is new and falls through here, check load_rows and add handling.
Custom Jason / String.Chars Encoders#
lib/sequin/postgrex/encoders.ex adds protocol implementations for types not covered by default Jason encoding:
Postgrex.Lexeme→%{type: :lexeme, word: ..., positions: [[pos, weight], ...]}Postgrex.Range→String.Charsimplementation for ISO 8601 bound formatting , plus aJason.Encoderthat delegates toto_string/1
These encoders are loaded globally at compile time and apply to all JSON serialization in the application.
Logical Replication Backend#
Sequin.Postgres.PostgrexBackend wraps Postgrex.Protocol as the default backend for SlotProducer's replication connection. It delegates connect, handle_streaming, handle_copy_recv, handle_copy_send, handle_simple, checkin, and disconnect directly to Postgrex.Protocol. The Backend behaviour allows this to be swapped with a test double in non-production environments.