> **Plain-language companion:** [v0.32.0.md](v0.32.0.md)

## v0.32.0 — Citus: Stable Naming & Per-Source Frontier Foundation

**Status: Planned.** Derived from
[plans/ecosystem/PLAN_CITUS.md](../plans/ecosystem/PLAN_CITUS.md)
§6 Phase 1 and Phase 2.

> **Release Theme**
> v0.32.0 is the first of two releases delivering world-class Citus support.
> It ships two additive, non-breaking layers: (1) a `src/citus.rs` module
> with Citus detection and placement helpers; and (2) a stable-hash naming
> scheme that replaces OID-keyed internal objects (`changes_{oid}`,
> `pg_trickle_cdc_fn_{oid}`, slot names, frontier keys) with names derived
> from `stable_hash(database_oid || '/' || schema || '.' || table)`. Both
> layers produce immediate quality benefits for all users — stable names
> survive `pg_dump`/restore and major-version upgrades — and they are
> mandatory prerequisites for v0.35.0's per-worker slot CDC. There is zero
> behaviour change on a single-node deployment.

---

### Correctness

| ID | Title | Effort | Priority |
|----|-------|--------|----------|
| CITUS-1 | `src/citus.rs`: detection helpers and placement query | M | P0 |
| CITUS-2 | `SourceIdentifier` struct carrying `(oid, stable_name)` | S | P0 |
| CITUS-3 | Catalog migration: add `source_stable_name`, `source_placement`, `st_placement` columns | S | P0 |
| CITUS-4 | Rename all OID-keyed objects to `stable_name` form | M | P0 |
| CITUS-5 | Frontier rekey: `Frontier.sources` from OID string to `stable_name` | S | P1 |
| CITUS-6 | Audit and remove cross-source global LSN comparisons | M | P1 |
| CITUS-7 | Add `frontier_per_node JSONB` column for distributed sources | S | P1 |
| CITUS-8 | SQL migration: rename existing `changes_{oid}` tables and trigger functions in a single transaction | M | P0 |

**CITUS-1** — New module `src/citus.rs`. Detection helpers:
- `is_citus_loaded()` — `SELECT 1 FROM pg_extension WHERE extname='citus'`
- `placement(oid)` → `Local | Reference | Distributed { dist_column }` via
  `pg_dist_partition`
- `worker_nodes()` → `Vec<NodeAddr>` from `pg_dist_node`
- `shard_placements(table_oid)` — which workers host shards

None of these panic if Citus is absent; all return sensible defaults
(`Local`, empty vecs). Citus-specific code paths are always guarded by
`is_citus_loaded()`.

**CITUS-2** — `SourceIdentifier { oid: pg_sys::Oid, stable_name: String }`.
`stable_name = lower_hex(FNV-1a-64(database_oid || "/" || schema || "." || table))[..16]`.
Deterministic, short, URL-safe, identical on every Citus node. Serialised
in `pgt_change_tracking.source_stable_name` and used for all object names.

**CITUS-3** — Schema changes (nullable, backward-compatible):
```sql
ALTER TABLE pgtrickle.pgt_change_tracking
  ADD COLUMN source_stable_name TEXT,
  ADD COLUMN source_placement TEXT NOT NULL DEFAULT 'local';
ALTER TABLE pgtrickle.pgt_dependencies
  ADD COLUMN source_stable_name TEXT,
  ADD COLUMN source_placement TEXT NOT NULL DEFAULT 'local';
ALTER TABLE pgtrickle.pgt_stream_tables
  ADD COLUMN st_placement TEXT NOT NULL DEFAULT 'local';
```
Old rows receive a synthetic stable name (hash of existing OID-based
name) and continue to function unchanged.

**CITUS-4** — Replace `changes_{oid}`, `pg_trickle_cdc_ins_{oid}`,
`pg_trickle_cdc_fn_{oid}`, `idx_changes_{oid}_*`, WAL slot names
(`pgtrickle_{oid}`), and the `__PGS_PREV_LSN_{oid}__` placeholder tokens
in `src/dvm/diff.rs` with `{stable_name}` forms. Affected files:
`src/cdc.rs`, `src/wal_decoder.rs`, `src/dvm/diff.rs`,
`src/refresh/codegen.rs`, `src/dvm/operators/*`.

**CITUS-5 / CITUS-6** — `Frontier.sources` is already a `HashMap`; rekey
from OID string to `stable_name`. Audit `src/refresh/codegen.rs` and
`src/scheduler.rs` for reads of `pg_current_wal_lsn()` used in cross-source
comparisons; replace with per-source watermark logic from
`src/wal_decoder.rs`.

**CITUS-7** — Add `frontier_per_node JSONB` to `pgt_change_tracking` for
distributed sources. When the source is `Local` this column is NULL; for
`Distributed` sources it records `{ "worker_id": lsn, ... }` tuples.

**CITUS-8** — `sql/pg_trickle--<prev>--<next>.sql` performs column adds
then backfills `source_stable_name` from `pg_class + pg_namespace`,
renames existing buffer tables and trigger functions to the stable-hash
form, updates the WAL slot name in `pg_replication_slots` (via
`pg_rename_logical_replication_slot` where available), and updates
`pgt_change_tracking`. Provides a downgrade path in the same file.

---

### Stability

| ID | Title | Effort | Priority |
|----|-------|--------|----------|
| STAB-1 | Keep OID-based lookup as fallback for rows without `source_stable_name` | S | P0 |
| STAB-2 | Detect and surface a clear error if stable hash collides (astronomically unlikely; check at create time) | S | P1 |

---

### Test Coverage

| ID | Title | Effort | Priority |
|----|-------|--------|----------|
| TEST-1 | Unit tests for `stable_hash()` — known vectors, cross-platform determinism | S | P0 |
| TEST-2 | Unit tests for `SourceIdentifier` round-trip serialisation | S | P0 |
| TEST-3 | Integration: `pg_dump` / `pg_restore` cycle preserves stream table with stable-named objects | M | P0 |
| TEST-4 | Integration: major-version upgrade simulation — OID changes, stable name unchanged | M | P1 |
| TEST-5 | Integration: frontier rekey — multi-source ST with different write rates converges correctly | M | P0 |
| TEST-6 | Integration: existing installation upgrade (v0.31.0 → v0.32.0) renames objects correctly | M | P0 |

---

### Documentation

| ID | Title | Effort | Priority |
|----|-------|--------|----------|
| DOCS-1 | Changelog entry explaining stable naming motivation | S | P0 |
| DOCS-2 | Upgrade guide: what changes, what to expect, rollback instructions | S | P0 |

---

### Exit Criteria

- [ ] CITUS-1: `is_citus_loaded()`, `placement()`, `worker_nodes()` compile and return sensible defaults when Citus is absent
- [ ] CITUS-2: `stable_hash("public.orders")` produces identical output on two fresh PG instances with different OIDs
- [ ] CITUS-4: no `{oid}` tokens remain in object names created by `setup_cdc_for_source()`
- [ ] CITUS-8: migration script renames all existing objects; `just test-upgrade-all` passes
- [ ] TEST-3: `pg_dump` / `pg_restore` round-trip test passes
- [ ] TEST-6: v0.31.0 → v0.32.0 upgrade integration test passes
- [ ] Zero performance regression on the single-node benchmark suite (Citus detection check adds ≤ 1 µs per `create_stream_table()` call when Citus is absent)
- [ ] `just check-version-sync` passes
- [ ] `just lint` passes (zero warnings)

---
