Contents
- PLAN: Feature Cleanup — Remove Low-Value Surface Before Public Release
- Motivation
- Items
- C1 — Remove pg_trickle.merge_strategy GUC entirely
- C2 — Fix SUSPENDED status: implement missing resume_stream_table()
- C3 — Remove pg_trickle.max_concurrent_refreshes GUC (or mark as no-op)
- C4 — Consolidate pg_trickle.merge_work_mem_mb and pg_trickle.merge_planner_hints
- C5 — Simplify pg_trickle.user_triggers GUC values
- C6 — Hide WAL-mode GUCs until WAL CDC is production-ready
- Summary
- What Does Not Exist (Speculative List Corrections)
PLAN: Feature Cleanup — Remove Low-Value Surface Before Public Release
Status: In Progress Target milestone: v0.2.0 Last updated: 2026-02-27
Motivation
Before the first public release accepts external users, every GUC, SQL function, and status value becomes a compatibility commitment. This plan identifies surface to remove or consolidate now — while there are zero external users — to keep the API simple, honest, and maintainable.
Items are sourced from direct code audit of config.rs, api.rs,
monitor.rs, refresh.rs, and scheduler.rs. This plan does not
include features that were speculated about but verified not to exist
(ON COMMIT REFRESH, named schedules, per-table delta_threshold,
stream_table_definition, force_full_refresh — none of these were built).
Items
C1 — Remove pg_trickle.merge_strategy GUC entirely
Priority: P0 — remove before public release
Effort: 1–2 hours
Location: config.rs, refresh.rs
Background
merge_strategy accepts three values: auto, merge, delete_insert.
delete_insertis being removed (F1 — unsafe, superseded byauto).- After removing
delete_insert, the only remaining values areautoandmerge. autouses MERGE for all deltas except when the estimated delta exceeds ~25% of the stream table row count, at which point it switches to DELETE+INSERT. Since DELETE+INSERT is being removed,autowill always use MERGE — making it identical tomerge.- There is no remaining scenario where a user would get a different
behaviour from
mergevsauto.
Options
A. Keep merge_strategy but reduce to a single auto value
- ✅ Technically harmless; GUC still validates the value
- ❌ A GUC that accepts one value is confusing and adds documentation debt
- ❌ The name “merge_strategy” implies there are strategies to choose from
B. Remove merge_strategy GUC entirely
- ✅ Eliminates a GUC that provides zero choice after delete_insert removal
- ✅ Simplifies refresh.rs: removes the strategy-selection branch
- ✅ Simplifies config.rs: removes one GUC registration
- ✅ Users who set pg_trickle.merge_strategy = 'merge' or 'auto' get an
unknown-GUC warning (harmless) rather than silent no-op behaviour
- ❌ Breaking change for any existing configuration — acceptable at pre-1.0
Decision: Option B — remove merge_strategy GUC entirely.
Status: DONE ✅
Implementation
Remove✅PGS_MERGE_STRATEGYfromconfig.rsRemove✅pg_trickle_merge_strategy()accessorRemove✅use_delete_insertbranch inrefresh.rsRemove✅delete_insert_templateanddelete_insert_sqlfields from the refresh cache entry struct- Update docs and GUC documentation table ✅
C2 — Fix SUSPENDED status: implement missing resume_stream_table()
Priority: P0 — referenced in error message but function does not exist
Effort: 1–2 hours
Location: api.rs line 500, scheduler.rs
Background
When a stream table accumulates pg_trickle.max_consecutive_errors
consecutive refresh failures, the scheduler sets its status to SUSPENDED
and fires an auto_suspended NOTIFY alert. The error message in
refresh_stream_table() tells the user:
"stream table {}.{} is suspended; use pgtrickle.resume_stream_table() first"
pgtrickle.resume_stream_table() does not exist as a pg_extern
function. The user is told to call a function that doesn’t exist.
This is a bug, not a feature to remove. It reveals that the SUSPENDED lifecycle is half-implemented: the scheduler can set the state, but there is no user-facing way to clear it and re-enable refreshes other than directly mutating the catalog table.
Options
A. Remove SUSPENDED status and auto-suspension logic - ✅ Eliminates the inconsistency - ✅ Simplifies the scheduler — no error-count tracking or state transitions - ❌ Removes a safety mechanism: without auto-suspension, a table with a permanent error (e.g., dropped source column) will retry indefinitely, consuming background worker cycles and filling the log - ❌ Users lose NOTIFY alerting for persistent failures
B. Implement pgtrickle.resume_stream_table(name text) function
- ✅ Makes the documented lifecycle complete and consistent
- ✅ Fixes the dangling error message
- ✅ Auto-suspension remains as a safety valve for persistent errors
- ✅ Small, clean implementation: reset status = 'ACTIVE',
consecutive_errors = 0 in catalog, emit a NOTIFY
- ❌ Adds one more SQL function to the public API (minor)
Decision: Option B — implement resume_stream_table(). The function is
already promised by the error message; this just fulfils that promise.
Status: DONE ✅
Implementation
#[pg_extern(schema = "pgtrickle")]
fn resume_stream_table(name: &str) {
// Parse schema.name, look up in catalog, assert status == SUSPENDED,
// reset status = ACTIVE + consecutive_errors = 0, emit NOTIFY.
}
C3 — Remove pg_trickle.max_concurrent_refreshes GUC (or mark as no-op)
Priority: P1 — documents a capability that does not exist
Effort: 30 min (document) or 2 hours (remove)
Location: config.rs, scheduler.rs
Background
pg_trickle.max_concurrent_refreshes (default: 4, max: 32) implies the
scheduler runs multiple refreshes in parallel. It does not. The scheduler
processes stream tables sequentially in topological order within a single
background worker (G8.5 in GAP_SQL_PHASE_7.md). The GUC is read but has no
effect on behaviour.
Options
A. Document as “reserved for future use” in GUC description - ✅ Zero code change - ❌ A GUC that does nothing is misleading; users will set it expecting an effect and see none - ❌ Will need to remain backward-compatible once documented
B. Remove the GUC entirely until parallel refresh is implemented (v0.3.0) - ✅ Eliminates false advertising - ✅ Pre-1.0: no compatibility cost - ✅ Re-add when the feature is real (P2/P3 in v0.3.0) - ❌ Minor churn: add back in v0.3.0
C. Hard-code to 1 and remove the GUC - Same as B, just more explicit about the current behaviour
Decision: Option A — document max_concurrent_refreshes as “reserved for
future use” in the GUC description. No code change; update the description
string in config.rs to make it clear the value is not yet honoured.
Status: DONE ✅
C4 — Consolidate pg_trickle.merge_work_mem_mb and pg_trickle.merge_planner_hints
Priority: P2 — low user value, high documentation burden
Effort: 1–2 hours
Location: config.rs, refresh.rs
Background
Two GUCs control planner hint injection before MERGE execution:
pg_trickle.merge_planner_hints(bool, default: true) — enables injection ofSET LOCAL enable_nestloop = offandSET LOCAL work_mem = '<N>MB'pg_trickle.merge_work_mem_mb(int, default: 64) — sets thework_meminjected when delta ≥ 10,000 rows
These exist because PostgreSQL’s planner sometimes chooses nested-loop plans for medium/large delta MERGEs, causing P95 latency spikes. The solution is correct but exposes two internal tuning knobs.
Options
A. Keep both GUCs as-is
- ✅ Maximum operator control
- ❌ Two GUCs that 99% of users will never adjust
- ❌ merge_work_mem_mb is only meaningful when merge_planner_hints = true
— the interdependency is confusing
B. Remove merge_work_mem_mb, hard-code a sensible value
- ✅ Reduces to one GUC (merge_planner_hints on/off)
- ✅ The 64 MB default is well-chosen; there is no evidence any user needs
to tune this independently
- ❌ Removes one escape hatch for edge cases with unusually large deltas
- ✅ Users who genuinely need a different work_mem can set it at the
session/role level
C. Remove both GUCs, always inject hints - ✅ Simplest — the hints are always beneficial; no reason to disable - ❌ Removes the ability to disable hints if they cause regressions on specific PostgreSQL minor versions or hardware configurations - ❌ More risk than benefit
Decision: Option A — keep both pg_trickle.merge_work_mem_mb and
pg_trickle.merge_planner_hints as-is. Maximum operator control is preserved
for users who need to tune MERGE performance on large deltas.
Status: No action required ✅
C5 — Simplify pg_trickle.user_triggers GUC values
Priority: P3 — minor API surface simplification
Effort: 1 hour
Location: config.rs, refresh.rs
Background
pg_trickle.user_triggers originally accepted: auto (default), on, off.
auto: detect user-defined row-level triggers on the stream table and use explicit DML (DELETE + UPDATE + INSERT) so they fire correctlyon: always use explicit DML, even if no user triggers existoff: always use MERGE; user triggers will NOT fire correctly
The on value is only useful if trigger detection is unreliable. If
detection is reliable (which it is — pg_trickle checks pg_trigger
directly), on is redundant with auto.
The off value explicitly disables trigger compatibility, which means
user triggers silently fire with wrong TG_OP / OLD / NEW — a
footgun.
Options
A. Keep all three values
- ✅ Maximum flexibility
- ❌ on is redundant with auto; off is a footgun
B. Remove on (redundant with auto) and rename off to
pg_trickle.user_triggers = false (boolean GUC)
- ✅ Cleaner: a boolean “respect user triggers yes/no” is clearer than
three string values
- ✅ Auto-detection is reliable enough that on adds no value
- ❌ Minor API change
C. Remove off entirely, keep auto and on as true/false
- ✅ Eliminates the footgun (off silently breaks user triggers)
- ❌ Removes an escape hatch for the rare case where explicit DML has
unacceptable overhead and the user knowingly has no triggers
Decision: Implemented in v0.2.3 with backward compatibility. Runtime
behavior is now canonicalized to auto and off; the legacy on value is
still accepted as a deprecated alias for auto so existing deployments do
not break.
Status: DONE ✅
C6 — Hide WAL-mode GUCs until WAL CDC is production-ready
Priority: P3 — reduces false impression of production readiness
Effort: 30 min (rename/comment)
Location: config.rs
Background
pg_trickle.wal_transition_timeout is visible to users, implying that WAL
CDC is a normal operational mode to configure. It is not — WAL CDC has
three P1 correctness issues (F2, F3, F4 in GAP_SQL_PHASE_7.md) and is not
production-ready until v0.3.0.
Options
A. Prefix WAL-mode GUCs with _experimental_ or _unsafe_
- ✅ Signals that these are not stable
- ❌ Ugly; PostgreSQL GUC convention doesn’t use such prefixes
B. Keep GUCs but add prominent warnings in documentation and GUC
description strings
- ✅ No API change
- ✅ GUC descriptions are shown by SHOW pgtrickle.wal_transition_timeout
- ✅ The existing GUC description can say explicitly: “WAL CDC is not
production-ready in v0.2.0; see docs.”
C. Remove or hide WAL-mode GUCs until v0.3.0
- ✅ Cleanest signal of “not ready”
- ❌ pg_trickle.cdc_mode = 'auto' (the default) will attempt WAL
transition after a successful refresh — the GUC is needed to control
the timeout even in pre-production
Decision: Option B — update GUC description strings to explicitly state WAL CDC is pre-production in v0.2.0. No code change.
Status: DONE ✅
Summary
| Item | Action | Priority | Effort | Status |
|---|---|---|---|---|
| C1 | Remove merge_strategy GUC (entire) |
P0 | 1–2h | ✅ Done |
| C2 | Implement missing resume_stream_table() |
P0 | 1–2h | ✅ Done |
| C3 | Document max_concurrent_refreshes as reserved |
P1 | 30 min | ✅ Done |
| C4 | Keep both merge GUCs as-is | P2 | — | ✅ No-op |
| C5 | Simplify user_triggers GUC |
P3 | Canonical auto / off, keep on as deprecated alias |
✅ Done |
| C6 | Document WAL GUCs as pre-production | P3 | 30 min | ✅ Done |
Total for v0.2.0 (C1–C3 + C6): completed.
What Does Not Exist (Speculative List Corrections)
The following items were considered but verified to not exist in the codebase and therefore require no removal:
| Feature | Status |
|---|---|
ON COMMIT REFRESH clause |
Never built |
pgtrickle.force_full_refresh() separate function |
Never built — full refresh is refresh_stream_table(name, force := true) |
Per-table delta_threshold override |
Never built |
pgtrickle.stream_table_definition() DDL reconstruction |
Never built |
| Named/reusable refresh schedules | Never built |
Separate pgtrickle.pause_stream_table() function |
Never built — suspension is automatic only |
| Per-operator timing breakdowns | Never built — explain_st() exposes the operator tree but not timings |