sluice

Quoted CREATE PUBLICATION preserves case; START_REPLICATION downcases it

SQL's quoting rules stop at the replication protocol: a quoted CREATE PUBLICATION name like sluice_MyPub preserves case because it's quoted DDL, but the name passed to START_REPLICATION's publication_names option is parsed as an unquoted identifier and folded to lowercase — so pgoutput looks up sluice_mypub, which doesn't exist. Nothing checks at stream start: the slot creates, the whole bulk copy runs green, and the 42704 fires only inside the first change callback — arbitrarily delayed, or never on a quiet source.

Landed in sluice · 2026-07-23 · Postgres

Observed — the 2026-07-23 blind audit, reproduced on a throwaway postgres:16 with pg_recvlogical. sluice's client-side guard shipped in v0.99.291 (--publication-name existed unvalidated v0.99.287–v0.99.290).

Two parsers for one name #

Postgres's identifier rules are consistent inside SQL: quote a name and its case is preserved; leave it unquoted and it folds to lowercase. The trap is that a publication name crosses OUT of SQL — into the replication protocol — and the parser on the other side is different:

CREATE PUBLICATION "sluice_MyPub" FOR TABLE t;     -- quoted DDL: case preserved

START_REPLICATION SLOT s LOGICAL 0/0
    (proto_version '1', publication_names 'sluice_MyPub');
-- pgoutput parses the option value as an UNQUOTED identifier list
-- -> folds to sluice_mypub -> which does not exist

The catalog holds sluice_MyPub; pgoutput looks up sluice_mypub. Two objects, one spelling, no error yet.

The failure geometry: green through the whole bulk copy #

“No error yet” is the nasty part. Nothing validates the publication at stream start — the slot creates, streaming begins, and the lookup happens lazily, inside the decoding of the first change. So the entire bulk copy runs green, the stream reports healthy for as long as the source is idle, and the SQLSTATE 42704 (publication "sluice_mypub" does not exist) fires minutes, hours, or days later — naming a lowercase object the operator never created, while their \dRp shows sluice_MyPub sitting right there. On a quiet source it never fires at all: a permanently idle stream, indistinguishable from “no changes happened.” Loud eventually, but arbitrarily delayed and misleading — and before the guard, sluice's control-state ratchet even recorded the broken name, so every resume faithfully repeated the mistake.

The 63-byte sibling, same class #

The length limit has the same two-parsers shape: CREATE PUBLICATION silently truncates a >63-byte name to 63 bytes with only a NOTICE — while publication_names matching at stream time is verbatim, so the over-length spelling you pass never matches the truncated object the DDL actually created. Two spellings of one lesson: the DDL layer and the replication protocol parse the same name differently, and every divergence is a delayed or silent failure.

Validate client-side, exactly as PG does for slots #

The asymmetry that makes this preventable: Postgres already enforces a safe charset server-side for replication slot names — but publications are ordinary quoted-identifier catalog objects, so they escape that enforcement. The fix is to apply the slot rule yourself before creating anything: validate replication-object names to ^[a-z0-9_]+$, at most 63 bytes. sluice's --publication-name now refuses at resolve time with SLUICE-E-CDC-PUBLICATION-NAME-INVALID — before the slot, before the copy, before the ratchet can record anything — turning a green-for-days geometry into an immediate one-line refusal.

The transferable lesson #

When a name you create in one layer is consumed by another layer with its own parser — SQL to replication protocol, DDL to config file, catalog to wire option — the safe subset is the intersection of the layers' rules, and you should enforce it at creation time, client-side, even when every individual layer would accept more. A name that only some layers fold, quote, or truncate is a latent lookup miss whose loudness depends on traffic — the worst kind of delayed failure.

Primary sources #