> **Plain-language companion:** [v0.2.2.md](v0.2.2.md) ## v0.2.2 — OFFSET, AUTO Mode, ALTER QUERY, Edge Cases & CDC Hardening **Status: Released (2026-03-08).** This milestone shipped paged TopK OFFSET support, AUTO-by-default refresh selection, ALTER QUERY, the remaining upgrade-tooling work, edge-case and WAL CDC hardening, IMMEDIATE-mode parity fixes, and the outstanding documentation sweep. ### ORDER BY + LIMIT + OFFSET (Paged TopK) — Finalization ✅ > **In plain terms:** Extends TopK to support OFFSET — so you can define a > stream table as "rows 11–20 of the top-20 best-selling products" (page 2 of > a ranked list). Useful for paginated leaderboards, ranked feeds, or any > use case where you want a specific window into a sorted result. Core implementation is complete (parser, catalog, refresh path, docs, 9 E2E tests). The `topk_offset` catalog column shipped in v0.2.1 and is exercised by the paged TopK feature here. | Item | Description | Status | Ref | |------|-------------|--------|-----| | OS1 | 9 OFFSET E2E tests in `e2e_topk_tests.rs` | ✅ Done | [PLAN_OFFSET_SUPPORT.md](plans/sql/PLAN_OFFSET_SUPPORT.md) §Step 6 | | OS2 | `sql/pg_trickle--0.2.1--0.2.2.sql` — function signature updates (no schema DDL needed) | ✅ Done | [PLAN_OFFSET_SUPPORT.md](plans/sql/PLAN_OFFSET_SUPPORT.md) §Step 2 | ### AUTO Refresh Mode ✅ > **In plain terms:** Changes the default from "always try differential > (incremental) refresh" to a smart automatic selection: use differential when > the query supports it, fall back to a full re-scan when it doesn't. New stream > tables also get a calculated schedule interval instead of a hardcoded > 1-minute default. | Item | Description | Status | Ref | |------|-------------|--------|-----| | AM1 | `RefreshMode::Auto` — uses DIFFERENTIAL when supported, falls back to FULL | ✅ Done | [PLAN_REFRESH_MODE_DEFAULT.md](plans/sql/PLAN_REFRESH_MODE_DEFAULT.md) | | AM2 | `create_stream_table` default changed from `'DIFFERENTIAL'` to `'AUTO'` | ✅ Done | — | | AM3 | `create_stream_table` schedule default changed from `'1m'` to `'calculated'` | ✅ Done | — | ### ALTER QUERY ✅ > **In plain terms:** Lets you change the SQL query of an existing stream table > without dropping and recreating it. pg_trickle inspects the old and new > queries, determines what type of change was made (added a column, dropped a > column, or fundamentally incompatible change), and performs the most minimal > migration possible — updating in place where it can, rebuilding only when it > must. | Item | Description | Status | Ref | |------|-------------|--------|-----| | AQ1 | `alter_stream_table(query => ...)` — validate, classify schema change, migrate storage | ✅ Done | [PLAN_ALTER_QUERY.md](plans/PLAN_ALTER_QUERY.md) | | AQ2 | Schema classification: same, compatible (ADD/DROP COLUMN), incompatible (full rebuild) | ✅ Done | — | | AQ3 | ALTER-aware cycle detection (`check_for_cycles_alter`) | ✅ Done | — | | AQ4 | CDC dependency migration (add/remove triggers for changed sources) | ✅ Done | — | | AQ5 | SQL Reference & CHANGELOG documentation | ✅ Done | — | ### Upgrade Tooling ✅ > **In plain terms:** If the compiled extension library (`.so` file) is a > different version than the SQL objects in the database, the scheduler now > warns loudly at startup instead of failing in confusing ways later. Also > adds FAQ entries and cross-links for common upgrade questions. | Item | Description | Status | Ref | |------|-------------|--------|-----| | UG1 | Version mismatch check — scheduler warns if `.so` version ≠ SQL version | ✅ Done | [PLAN_UPGRADE_MIGRATIONS.md](plans/sql/PLAN_UPGRADE_MIGRATIONS.md) §5.2 | | UG2 | FAQ upgrade section — 3 new entries with UPGRADING.md cross-links | ✅ Done | [PLAN_UPGRADE_MIGRATIONS.md](plans/sql/PLAN_UPGRADE_MIGRATIONS.md) §5.4 | | UG3 | CI and local upgrade automation now target 0.2.2 (`upgrade-check`, upgrade-image defaults, upgrade E2E env) | ✅ Done | [PLAN_UPGRADE_MIGRATIONS.md](plans/sql/PLAN_UPGRADE_MIGRATIONS.md) | ### IMMEDIATE Mode Parity ✅ > **In plain terms:** Closes two remaining SQL patterns that worked in > DIFFERENTIAL mode but not in IMMEDIATE mode. Recursive CTEs (queries that > reference themselves to compute e.g. graph reachability or org-chart > hierarchies) now work in IMMEDIATE mode with a configurable depth guard. > TopK (ORDER BY + LIMIT) queries also get a dedicated fast micro-refresh path > in IMMEDIATE mode. Close the gap between DIFFERENTIAL and IMMEDIATE mode SQL coverage for the two remaining high-risk patterns — recursive CTEs and TopK queries. | Item | Description | Effort | Ref | |------|-------------|--------|-----| | IM1 | Validate recursive CTE semi-naive in IMMEDIATE mode; add stack-depth guard for deeply recursive defining queries | 2–3d | [PLAN_EDGE_CASES_TIVM_IMPL_ORDER.md](plans/PLAN_EDGE_CASES_TIVM_IMPL_ORDER.md) Stage 6 §5.1 | ✅ Done — `check_for_delete_changes` handles `TransitionTable`; `generate_change_buffer_from` uses NEW transition table in IMMEDIATE mode; `ivm_recursive_max_depth` GUC (default 100) injects `__pgt_depth` counter into semi-naive SQL | | IM2 | TopK in IMMEDIATE mode: statement-level micro-refresh + `ivm_topk_max_limit` GUC | 2–3d | [PLAN_EDGE_CASES_TIVM_IMPL_ORDER.md](plans/PLAN_EDGE_CASES_TIVM_IMPL_ORDER.md) Stage 6 §5.2 | ✅ Done — `apply_topk_micro_refresh()` in ivm.rs; GUC threshold check in api.rs; 10 E2E tests (basic, insert, delete, update, aggregate, offset, multi-DML, threshold rejection, mode switch) | > **IMMEDIATE parity subtotal: ✅ Complete (IM1 + IM2)** ### Edge Case Hardening ✅ > **In plain terms:** Three targeted fixes for uncommon-but-real scenarios: > a cap on CUBE/ROLLUP combinatorial explosion (which can generate thousands > of grouping variants from a single query and crash the database); automatic > recovery when CDC gets stuck in a "transitioning" state after a database > restart; and polling-based change detection for foreign tables (tables in > external databases) that can't use triggers or WAL. Self-contained items from Stage 7 of the edge-cases/TIVM implementation plan. | Item | Description | Effort | Ref | |------|-------------|--------|-----| | EC1 | `pg_trickle.max_grouping_set_branches` GUC — cap CUBE/ROLLUP branch-count explosion | 4h | [PLAN_EDGE_CASES.md](plans/PLAN_EDGE_CASES.md) EC-02 | ✅ Done — GUC in config.rs (default 64, range 1–65536); parser.rs rejects when branch count exceeds limit; 3 E2E tests (rejection, within-limit, raised limit) | | EC2 | Post-restart CDC `TRANSITIONING` health check — detect stuck CDC transitions after crash or restart | 1d | [PLAN_EDGE_CASES.md](plans/PLAN_EDGE_CASES.md) EC-20 | ✅ Done — `check_cdc_transition_health()` in scheduler.rs; detects missing replication slots; rolls back to TRIGGER mode | | EC3 | Foreign table support: polling-based change detection via periodic re-execution | 2–3d | [PLAN_EDGE_CASES.md](plans/PLAN_EDGE_CASES.md) EC-05 | ✅ Done — `pg_trickle.foreign_table_polling` GUC; `setup_foreign_table_polling()` creates snapshot table; `poll_foreign_table_changes()` uses EXCEPT ALL deltas; 3 E2E tests (rejection, FULL mode, polling correctness) | > **Edge-case hardening subtotal: ✅ Complete (EC1 + EC2 + EC3)** ### Documentation Sweep > **In plain terms:** Filled three documentation gaps: what happens to an > in-flight refresh if you run DDL (ALTER TABLE, DROP INDEX) at the same time; > limitations when using pg_trickle on standby replicas; and a PgBouncer > configuration guide explaining the session-mode requirement and incompatible > settings. Remaining documentation gaps identified in Stage 7 of the gap analysis. | Item | Description | Effort | Status | Ref | |------|-------------|--------|--------|-----| | DS1 | DDL-during-refresh behaviour: document safe patterns and races | 2h | ✅ Done | [PLAN_EDGE_CASES.md](plans/PLAN_EDGE_CASES.md) EC-17 | | DS2 | Replication/standby limitations: document in FAQ and Architecture | 3h | ✅ Done | [PLAN_EDGE_CASES.md](plans/PLAN_EDGE_CASES.md) EC-21/22/23 | | DS3 | PgBouncer configuration guide: session-mode requirements and known incompatibilities | 2h | ✅ Done | [PLAN_EDGE_CASES.md](plans/PLAN_EDGE_CASES.md) EC-28 | > **Documentation sweep subtotal: ✅ Complete** ### WAL CDC Hardening > **In plain terms:** WAL (Write-Ahead Log) mode tracks changes by reading > PostgreSQL's internal replication stream rather than using row-level triggers > — which is more efficient and works across concurrent sessions. This work > added a complete E2E test suite for WAL mode, hardened the automatic fallback > from WAL to trigger mode when WAL isn't available, and promoted `cdc_mode = > 'auto'` (try WAL first, fall back to triggers) as the default. > WAL decoder F2–F3 fixes (keyless pk_hash, `old_*` columns for UPDATE) landed in v0.1.3. | Item | Description | Effort | Status | Ref | |------|-------------|--------|--------|-----| | W1 | WAL mode E2E test suite (parallel to trigger suite) | 8–12h | ✅ Done | [PLAN_HYBRID_CDC.md](plans/sql/PLAN_HYBRID_CDC.md) | | W2 | WAL→trigger automatic fallback hardening | 4–6h | ✅ Done | [PLAN_HYBRID_CDC.md](plans/sql/PLAN_HYBRID_CDC.md) | | W3 | Promote `pg_trickle.cdc_mode = 'auto'` to default | ~1h | ✅ Done | [PLAN_HYBRID_CDC.md](plans/sql/PLAN_HYBRID_CDC.md) | > **WAL CDC subtotal: ~13–19 hours** **Exit criteria:** - [x] `ORDER BY + LIMIT + OFFSET` defining queries accepted, refreshed, and E2E-tested - [x] `sql/pg_trickle--0.2.1--0.2.2.sql` exists (column pre-provisioned in 0.2.1; function signature updates) - [x] Upgrade completeness check passes for 0.2.1→0.2.2 - [x] CI and local upgrade-E2E defaults target 0.2.2 - [x] Version check fires at scheduler startup if `.so`/SQL versions diverge - [x] IMMEDIATE mode: recursive CTE semi-naive validated; `ivm_recursive_max_depth` depth guard added - [x] IMMEDIATE mode: TopK micro-refresh fully tested end-to-end (10 E2E tests) - [x] `max_grouping_set_branches` GUC guards CUBE/ROLLUP explosion (3 E2E tests) - [x] Post-restart CDC TRANSITIONING health check in place - [x] Foreign table polling-based CDC implemented (3 E2E tests) - [x] DDL-during-refresh and standby/replication limitations documented - [x] WAL CDC mode passes full E2E suite - [x] E2E tests pass (`just build-e2e-image && just test-e2e`) ---