Contents
- Feature Composability Analysis — Major Proposed Features
- Executive Summary
- 1. Feature-by-Feature Analysis
- 1.1 Fuse — Anomalous Change Volume Protection
- 1.2 Watermark Gating — Cross-Source Temporal Alignment
- 1.3 Blue-Green Deployment — Hot-Swap Pipelines
- 1.4 External Process (Sidecar) — Extension-Free Deployment
- 1.5 Diamond Dependency Consistency — Multi-Path Refresh Atomicity
- 1.6 Cross-Source Snapshot Consistency — Independent-Source Coherence
- 1.7 Transactional IVM (IMMEDIATE Mode) — Same-Transaction Maintenance
- 2. Cross-Cutting Patterns
- 3. Composability Matrix
- 4. Extraction Strategy by Feature
- 5. Impact on Sidecar Feasibility
- 6. Recommendations
- R1: Introduce RefreshGate trait before implementing any of the four features
- R2: Diamond consistency validates the group execution pattern
- R3: Implement fuse next — smallest scope, highest standalone value
- R4: Build watermark gating with 'gate' mode only — defer 'hold_back'
- R5: Cross-source snapshot extends diamond — implement together or immediately after
- R6: Define DeltaSource before transactional IVM implementation
- R7: Treat blue-green as orchestration, not a gate
- R8: Do NOT extract crates until the sidecar work begins
- R9: Treat external process as the integration test for composability
- 7. Open Questions
- Related Documents
Feature Composability Analysis — Major Proposed Features
Date: 2026-03-03 Status: Exploration Type: REPORT
Executive Summary
This analysis examines seven major proposed features through the lens of composability:
- Fuse — anomalous change volume protection
- Watermark gating — cross-source temporal alignment
- Blue-green deployment — hot-swap pipeline evolution
- External process (sidecar) — extension-free deployment
- Diamond dependency consistency — multi-path refresh atomicity
- Cross-source snapshot consistency — independent-source temporal coherence
- Transactional IVM (IMMEDIATE mode) — same-transaction view maintenance
For each, we ask:
- Could this be a standalone project (separate crate or binary)?
- Could its internal components be composed from smaller, reusable pieces?
- What shared abstractions emerge across the features?
The key finding: most features share a common pattern — they are scheduling gates and orchestration layers that wrap the existing refresh pipeline. This suggests a unified RefreshGate trait abstraction that would make most features composable, testable independently, and stackable. Transactional IVM is the exception — it introduces a fundamentally different execution model (in-transaction triggers) that requires a separate DeltaSource abstraction for composability.
1. Feature-by-Feature Analysis
1.1 Fuse — Anomalous Change Volume Protection
Source: PLAN_FUSE.md
What It Does
A per-stream-table safety mechanism that halts refresh when change volume exceeds a statistical threshold or hard ceiling. Binary state machine: INTACT → BLOWN (manual reset required).
Standalone Project Potential: High
The fuse is fundamentally a data quality gate — a pattern used far beyond IVM. The core logic is:
f(change_count, baseline_μ, baseline_σ, ceiling, sensitivity) → blow | pass
This is a pure function with zero PostgreSQL dependency. The plan already
identifies this (§11 Step 1: should_blow() as a pure function).
Extractable as: pg-fuse or data-quality-gate crate
| Component | PG-coupled? | Extractable? |
|---|---|---|
should_blow() — trip decision logic |
No | ✅ Immediately |
FuseMode / FuseState enums |
No | ✅ Immediately |
| Rolling baseline computation (Welford’s algorithm / windowed stats) | No | ✅ Immediately |
| EWMA / fixed-window statistics | No | ✅ Immediately |
Fuse catalog storage (pgt_fuses table) |
Yes — SPI/SQL | Stays in extension |
reset_fuse() SQL function |
Yes — #[pg_extern] |
Stays in extension |
fuse_status() introspection |
Yes — SPI | Stays in extension |
| NOTIFY alert emission | Yes — PG channels | Stays in extension |
| Scheduler integration (pre-check gate) | Yes — scheduler loop | Adapter pattern |
Standalone value: Any system with a data pipeline (Kafka consumers, ETL jobs, dbt models) could use the statistical trip logic to detect anomalous data volumes. The gate pattern (check → pass/block) is universal.
Internal Decomposition Opportunities
Statistics engine — The rolling baseline (mean, stddev, Welford’s online algorithm, EWMA) is a general-purpose streaming statistics module. Could be shared with future adaptive threshold features (e.g., adaptive scheduling intervals based on change rate trends).
Gate interface — The fuse’s scheduler integration follows a pattern:
trait RefreshGate {
fn should_proceed(&self, ctx: &RefreshContext) -> GateDecision;
}
enum GateDecision {
Proceed,
Skip { reason: String },
Blow { reason: String }, // permanent block until reset
}
This same interface applies to watermark gating, diamond consistency checks, the existing adaptive fallback, and even blue-green convergence detection.
1.2 Watermark Gating — Cross-Source Temporal Alignment
Source: PLAN_WATERMARK_GATING.md
What It Does
User-injected watermarks per source table declare “external data is complete through timestamp T.” Watermark groups enforce alignment: downstream STs skip refresh until all sources in the group report sufficiently aligned watermarks.
Standalone Project Potential: Medium-High
The watermark concept has two layers:
Watermark algebra (pure logic) — monotonic advancement, group alignment predicate ($\max(W_i) - \min(W_i) \leq \tau$), effective watermark computation, tolerance checking.
Gating orchestration (PG-coupled) — catalog storage, scheduler integration, LSN mapping for hold-back mode, NOTIFY signaling.
Layer 1 is fully extractable. Layer 2 is an adapter.
Extractable as: watermark-gate crate (or part of a broader pipeline-gate crate)
| Component | PG-coupled? | Extractable? |
|---|---|---|
| Watermark monotonicity check | No | ✅ |
| Group alignment predicate | No | ✅ |
| Effective watermark computation | No | ✅ |
| Tolerance evaluation | No | ✅ |
WatermarkGroup / Watermark types |
No | ✅ |
pgt_watermarks / pgt_watermark_groups catalog |
Yes | Stays in extension |
advance_watermark() SQL function |
Yes | Stays in extension |
| LSN ↔ watermark mapping (hold-back) | Yes — WAL coupling | Stays in extension |
| Scheduler gating pre-check | Yes | Adapter pattern |
Standalone value: Any pipeline orchestrator dealing with multi-source temporal alignment (Airflow DAGs waiting for upstream datasets, Kafka Streams multi-topic joins, Flink watermark propagation) could use the watermark algebra. The tolerance-based alignment predicate is directly applicable to event-time processing systems.
Internal Decomposition Opportunities
Watermark algebra module — The core types and predicates are independent of pg_trickle. This module would contain:
Watermark(monotonic wrapper aroundDateTime<Utc>or genericOrd)WatermarkGroup(set of source IDs + tolerance)alignment_check(group, watermarks) → Aligned(effective_wm) | Misaligned(lag)
Gate interface — Watermark gating fits the same
RefreshGatetrait as the fuse:
impl RefreshGate for WatermarkGate {
fn should_proceed(&self, ctx: &RefreshContext) -> GateDecision {
match self.check_alignment(ctx.source_watermarks()) {
Aligned(wm) => GateDecision::Proceed,
Misaligned(lag) => GateDecision::Skip {
reason: format!("watermark lag {} exceeds tolerance", lag),
},
}
}
}
- Hold-back as a separate concern — The plan’s §5.2 describes “hold-back” mode where intermediate STs cap their change window to the effective watermark. This is a fundamentally different mechanism from gating (it changes what data is consumed, not whether to refresh). It should be a separate composition layer, not conflated with the gate.
1.3 Blue-Green Deployment — Hot-Swap Pipelines
Source: REPORT_BLUE_GREEN_DEPLOYMENT.md
What It Does
Create a “green” copy of a stream table (or pipeline) that catches up independently. Once converged with the “blue” (active) version, atomically swap them. Supports query changes, rollback, and zero-downtime evolution.
Standalone Project Potential: Low
Unlike fuse and watermark, blue-green deployment is deeply tied to pg_trickle’s specific catalog, CDC sharing, frontier tracking, and storage table management. The “green” ST is a full pg_trickle stream table — it uses all the same infrastructure (triggers, change buffers, DVM, scheduler).
What could be extracted is the convergence detection and
orchestration state machine, but these are thin layers on top of
pg_trickle-specific concepts (frontier LSN comparison, pgt_id isolation,
advisory locks).
Not a good candidate for a standalone project. Better served by internal decomposition.
Internal Decomposition Opportunities
- Convergence detector — The report identifies four convergence strategies (frontier LSN, data timestamp, lag threshold, content hash). These are composable — a user should be able to combine them:
trait ConvergenceCheck {
fn is_converged(&self, blue: &StMeta, green: &StMeta) -> ConvergenceResult;
}
// Composable: all checks must pass
struct CompositeConvergence(Vec<Box<dyn ConvergenceCheck>>);
This uses the same pattern as the RefreshGate trait — a composable
predicate evaluated before an action.
Pipeline lifecycle state machine — The blue-green lifecycle (
create_green → catching_up → converged → promote | rollback → cleanup) is a generic state machine. This pattern appears in:- Blue-green deployment (this feature)
- CDC mode transitions (trigger → WAL, per PLAN_HYBRID_CDC.md)
- Stream table status transitions (ACTIVE → SUSPENDED → ERROR)
A generic
LifecycleStateMachine<State, Event>could unify these.Gate interface — During promotion, the scheduler must recognize that a green ST is “catching up” and not yet the active version. This is another scheduling gate: the green ST participates in refresh, but the blue ST is the one downstream STs reference. The promote operation is an atomic gate swap.
1.4 External Process (Sidecar) — Extension-Free Deployment
Source: REPORT_EXTERNAL_PROCESS.md
What It Does
Run the entire pg_trickle engine as an external binary connecting to PostgreSQL over standard connections, removing the requirement to install a C extension. Enables managed PG services (RDS, Cloud SQL, Neon).
Standalone Project Potential: This IS a standalone project
The sidecar is not a “feature” to be extracted — it is the primary driver for all other extraction work. It requires every core component to be decoupled from pgrx:
- DVM engine →
pg-query-diffcrate (usespg_query.rsinstead ofpg_sys::raw_parser) - DAG →
pg-dagcrate (already nearly pure Rust) - CDC → SQL generators (trigger DDL, change buffer queries)
- Scheduler → Tokio-based main loop
- Catalog →
tokio-postgres/sqlxclient - Config → TOML file instead of GUCs
The sidecar report (§12.1) demonstrates that even IMMEDIATE mode (previously assumed to require the extension) can be delivered via pre-compiled PL/pgSQL triggers, achieving correctness parity.
Internal Decomposition Required by Sidecar
The sidecar’s crate restructuring (Phase S0 in the report) proposes:
crates/
├── pgtrickle-core/ # Pure Rust: DAG, DVM, diff, scheduling logic
├── pgtrickle-parser/ # pg_query.rs-based SQL parsing
├── pgtrickle-client/ # PgClient trait + tokio-postgres impl
├── pgtrickle-extension/ # pgrx shim (#[pg_extern] → core)
└── pgtrickle-sidecar/ # Tokio binary
This workspace structure is the end state that all other decomposition efforts converge toward. The trait abstractions proposed in REPORT_ENGINE_COMPOSABILITY.md (ParseFrontend, StorageBackend, CatalogAccess) are exactly what the sidecar needs.
Relationship to Other Three Features
The sidecar changes HOW features are deployed but not WHAT they do:
| Feature | Extension Mode | Sidecar Mode | Logic Shared? |
|---|---|---|---|
| Fuse | SPI catalog + bgworker gate | SQL catalog + Tokio gate | ✅ should_blow() is pure Rust |
| Watermark | SPI + GUC + bgworker gate | SQL + TOML + Tokio gate | ✅ Alignment predicate is pure Rust |
| Blue-green | SPI + advisory locks | SQL + advisory locks | ✅ Convergence checks are pure Rust |
| Diamond | SPI SAVEPOINT atomic groups | pgwire SAVEPOINT atomic groups | ✅ Detection is pure Rust; execution is standard SQL |
| Cross-source | REPEATABLE READ via SPI | REPEATABLE READ via pgwire | ✅ Group logic is pure Rust |
| Transactional IVM | Native Rust triggers + ENRs | Compiled PL/pgSQL triggers | ✅ Delta SQL templates shared; execution differs |
| All scheduling gates | BGWorker scheduler loop | Tokio scheduler loop | ✅ RefreshGate trait |
Key insight: If fuse, watermark, and blue-green are implemented with
the RefreshGate trait pattern, the sidecar gets them “for free” —
the trait implementations are in pgtrickle-core, and both scheduler
implementations (bgworker + Tokio) compose the same gates.
1.5 Diamond Dependency Consistency — Multi-Path Refresh Atomicity
Source: PLAN_DIAMOND_DEPENDENCY_CONSISTENCY.md
What It Does
When two intermediate STs (B, C) share a common upstream source (A) and converge at a downstream ST (D), the current sequential scheduler can produce inconsistent results if B refreshes but C fails. The diamond plan introduces epoch-based atomic refresh groups using SAVEPOINTs: either all members of a consistency group succeed or all roll back.
Standalone Project Potential: Medium
The feature has two distinct layers:
Diamond detection algorithm (pure graph logic) — find fan-in nodes, trace shared ancestors, compute consistency groups. This is already in
dag.rsand is nearly pure Rust.Atomic group execution (PG-coupled) — SAVEPOINT-based transaction control, epoch tracking, rollback-on-failure semantics.
Layer 1 is fully extractable as part of pg-dag. Layer 2 is scheduler
orchestration.
| Component | PG-coupled? | Extractable? |
|---|---|---|
detect_consistency_groups() — graph algorithm |
No | ✅ Part of pg-dag |
ConsistencyGroup struct + epoch tracking |
No | ✅ |
| Frontier alignment check (skip D if B/C diverge) | No (compares LSN values) | ✅ |
| SAVEPOINT-based atomic execution | Yes — SPI/SQL | Adapter pattern |
diamond_consistency GUC / config |
Yes — GUC in extension, TOML in sidecar | Per-mode config |
Standalone value: The diamond detection algorithm generalizes to any DAG
scheduler that needs atomic group semantics — CI/CD pipelines, build systems,
data pipeline orchestrators. The ConsistencyGroup concept with
frontier-alignment checks is reusable.
Internal Decomposition Opportunities
- Gate interface — Diamond consistency is a natural
RefreshGate:
impl RefreshGate for DiamondConsistencyGate {
fn evaluate(&self, st: &StreamTableMeta, ctx: &GateContext) -> GateDecision {
// If this ST is a convergence point and its upstream group
// members have divergent frontiers, skip.
match self.check_frontier_alignment(st, ctx) {
Aligned => GateDecision::Proceed,
Divergent(reason) => GateDecision::Skip { reason },
}
}
}
- Group execution wrapper — The SAVEPOINT-based atomic execution is separate from gating. It wraps a set of refresh calls in a transaction boundary. This is an execution strategy, not a gate:
trait GroupExecutionStrategy {
fn execute_group(&self, members: &[NodeId], refresh_fn: &dyn Fn(NodeId) -> Result<()>) -> Result<()>;
}
struct SavepointStrategy; // Extension: SPI SAVEPOINT
struct TokioTxStrategy; // Sidecar: pgwire BEGIN/SAVEPOINT
This cleanly separates the “should we refresh?” question (gate) from the “how do we execute the group atomically?” question (strategy).
1.6 Cross-Source Snapshot Consistency — Independent-Source Coherence
Source: PLAN_CROSS_SOURCE_SNAPSHOT_CONSISTENCY.md
What It Does
Addresses the case where D joins B2 and C2, but B2 and C2 depend on independent base tables (B1 and C1) with no shared ancestor. The diamond algorithm cannot detect this structurally. Three approaches:
- Approach A: Shared
REPEATABLE READtransaction for co-refresh groups - Approach B: User-declared co-refresh groups with configurable isolation
- Approach C: Global LSN watermark per scheduler tick
Standalone Project Potential: Low-Medium
This feature is fundamentally about PostgreSQL transaction isolation — it controls the isolation level of the refresh execution context. The logic is thin (choose isolation level, wrap execution in appropriate transaction), and the value is entirely PG-specific.
What is extractable is the group management and alignment checking logic:
| Component | PG-coupled? | Extractable? |
|---|---|---|
| Co-refresh group detection / management | No | ✅ |
LSN watermark computation (pg_current_wal_lsn) |
Yes | Stays in extension |
REPEATABLE READ transaction wrapping |
Yes | Adapter pattern |
create_refresh_group() / drop_refresh_group() SQL API |
Yes | Stays in extension |
| Group membership validation | Partially (graph queries are pure, catalog lookups are PG) | Split |
Internal Decomposition Opportunities
- Extends
GroupExecutionStrategy— Cross-source snapshot is the same group execution concept as diamond consistency, but with a stronger isolation level:
struct RepeatableReadStrategy; // REPEATABLE READ + SAVEPOINT
struct ReadCommittedStrategy; // READ COMMITTED + SAVEPOINT (diamond)
The ConsistencyGroup struct from the diamond plan gains an
isolation_level field — the cross-source plan explicitly proposes this.
- LSN watermark as a global gate — Approach C (capping all refreshes in a tick to a single WAL LSN) is a global gate applied before any per-ST gate. It fits the pipeline:
LSN Watermark (global) → Status → Fuse → Watermark → Diamond → Refresh
- User-declared groups compose with auto-detected groups — The plan specifies that declared co-refresh groups merge with auto-detected diamond groups during DAG rebuild. A declared group can override the isolation level of an auto-detected group. This is clean composition.
Relationship to Watermark Gating
These two features are complementary layers addressing different freshness domains:
| Concern | Mechanism | Scope |
|---|---|---|
| External temporal coherence | Watermark gating | Cross-source (external APIs, ETL) |
| PG-internal snapshot coherence | Cross-source snapshot | Cross-source (independent PG tables) |
| Same-source split-path atomicity | Diamond consistency | Same-source diamond DAGs |
All three can apply simultaneously to the same ST, and they compose naturally: watermark gates run first (external), then diamond/cross-source gates (PG-internal).
1.7 Transactional IVM (IMMEDIATE Mode) — Same-Transaction Maintenance
Source: PLAN_TRANSACTIONAL_IVM.md
What It Does
Update stream tables within the same transaction as base table DML, using statement-level AFTER triggers with transition tables. Provides read-your-writes consistency. Serves as a drop-in replacement for pg_ivm.
Standalone Project Potential: Low (but high internal decomposition value)
Transactional IVM is deeply PG-specific — it relies on PostgreSQL’s trigger infrastructure, transition tables, Ephemeral Named Relations (ENRs), and transaction isolation semantics. It cannot meaningfully exist outside PG.
However, the DVM engine output is pure SQL — the delta computation produces SQL strings, not runtime code. This is the critical insight from REPORT_EXTERNAL_PROCESS.md §12.1: the sidecar can pre-compile delta SQL into PL/pgSQL trigger functions, achieving IMMEDIATE mode without the extension.
| Component | PG-coupled? | Extractable? |
|---|---|---|
DeltaSource enum (how Scan operators emit SQL) |
No | ✅ Core abstraction |
| Delta SQL template generation | No | ✅ Already in DVM engine |
CachedMergeTemplate (INSERT/DELETE/MERGE SQL) |
No | ✅ |
| Trigger function installation (CREATE TRIGGER DDL) | Yes | SQL generation extractable |
| ENR registration / transition table access | Yes — pg_sys C API |
Extension only |
| Before/after trigger counting + locking | Yes — SPI/pg_sys | Extension only |
| PL/pgSQL compiled trigger bodies (sidecar) | Yes — SQL DDL | Sidecar-specific |
pg_ivm compatibility layer (pgivm.* functions) |
Yes | Extension only |
Internal Decomposition Opportunities
DeltaSourceabstraction — This is the key composability point. The DVM engine’s Scan operator already needs to know where delta rows come from. Making this a first-class enum enables three modes from the same operator tree:
pub enum DeltaSource {
/// Deferred mode: change buffer tables with LSN range filter.
ChangeBuffer { table: String, lsn_range: (String, String) },
/// Immediate mode (extension): ENRs from transition tables.
TransitionTable { old_name: String, new_name: String },
/// Immediate mode (sidecar): same SQL, embedded in PL/pgSQL.
CompiledTrigger { old_name: String, new_name: String },
}
In practice, TransitionTable and CompiledTrigger produce identical
SQL — only the execution context differs (C-level SPI vs PL/pgSQL
EXECUTE). They could be a single variant.
Template compiler — A
generate_immediate_trigger_sql()function that takes a delta template and produces a complete PL/pgSQL trigger function body. This lives inpgtrickle-coreand is consumed by:- The extension (for installing Rust-native triggers in Phase 1, or PL/pgSQL triggers as a fallback)
- The sidecar (for installing compiled triggers remotely)
NOT a RefreshGate — Transactional IVM is fundamentally different from the deferred features. It doesn’t participate in the scheduler loop at all — it fires synchronously within user transactions via triggers. The
RefreshGatepattern does not apply. Instead, IMMEDIATE mode STs bypass the scheduler entirely.Mode switching as a lifecycle transition — Switching between DIFFERENTIAL and IMMEDIATE mode (drop CDC triggers, create IVM triggers, full refresh) is a lifecycle state machine transition:
DIFFERENTIAL ←→ IMMEDIATE ←→ FULL
Each transition requires cleanup of the old mode’s infrastructure and
setup of the new mode’s. This fits the Lifecycle trait from §2.3.
Relationship to Other Features
| Feature | Interaction with Transactional IVM |
|---|---|
| Fuse | N/A — IMMEDIATE mode has no change buffer to count. Could monitor trigger-applied delta sizes instead, but the fuse concept is less relevant (changes are applied immediately, not batched). |
| Watermark | N/A — IMMEDIATE mode refreshes synchronously within user transactions. External watermarks are a deferred-mode concept. |
| Diamond | Inherently solved — trigger nesting ensures B and C are both updated within the same transaction as A’s modification. No consistency group needed. |
| Cross-source snapshot | Inherently solved — all changes visible within the same transaction snapshot. |
| Blue-green | Compatible — a green ST could use IMMEDIATE mode while the blue ST uses DIFFERENTIAL. But the catch-up semantics differ fundamentally. |
| Sidecar | ✅ Via compiled PL/pgSQL triggers (REPORT_EXTERNAL_PROCESS §12.1). Extension retains performance advantage (native Rust dispatch vs PL/pgSQL EXECUTE). |
2. Cross-Cutting Patterns
2.1 The RefreshGate Abstraction
All four features (plus existing mechanisms) follow the same pattern: a predicate evaluated before refresh that decides whether to proceed.
/// A composable gate that decides whether a stream table should refresh.
pub trait RefreshGate: Send + Sync {
/// Evaluate the gate for a given stream table and refresh context.
fn evaluate(&self, st: &StreamTableMeta, ctx: &GateContext) -> GateDecision;
/// Human-readable name for logging and introspection.
fn name(&self) -> &str;
}
pub enum GateDecision {
/// Proceed with refresh.
Proceed,
/// Skip this refresh cycle; re-evaluate next tick.
Skip { reason: String },
/// Permanently block until explicit reset (fuse-blown semantics).
Block { reason: String },
}
pub struct GateContext {
pub change_buffer_count: Option<i64>,
pub source_watermarks: HashMap<Oid, Option<DateTime<Utc>>>,
pub frontier: Frontier,
pub green_of: Option<i64>,
pub consistency_group: Option<ConsistencyGroupId>,
// ... extensible
}
Every existing and proposed scheduling check maps to this trait:
| Gate | Current Implementation | RefreshGate equivalent |
|---|---|---|
| Status check (ACTIVE?) | Inline in scheduler loop | StatusGate |
| Schedule check (due?) | Inline in scheduler loop | ScheduleGate |
| Advisory lock | Inline in scheduler loop | LockGate |
| Upstream changes check | Inline in scheduler loop | UpstreamChangesGate |
| Adaptive DIFF→FULL fallback | Inline in refresh logic | (Not a gate — mode selection) |
| Fuse | Proposed inline check | FuseGate |
| Watermark alignment | Proposed inline check | WatermarkGate |
| Diamond consistency | Proposed group check | DiamondConsistencyGate |
| Cross-source snapshot | Proposed group check | SnapshotCoherenceGate |
| Blue-green convergence | Proposed convergence check | ConvergenceGate |
| LSN tick watermark | Not yet implemented | LsnWatermarkGate (global) |
The scheduler becomes a gate pipeline:
fn should_refresh(st: &StreamTableMeta, ctx: &GateContext, gates: &[&dyn RefreshGate]) -> GateDecision {
for gate in gates {
match gate.evaluate(st, ctx) {
GateDecision::Proceed => continue,
decision => return decision,
}
}
GateDecision::Proceed
}
Benefits: - Each gate is independently testable (unit tests, no PG). - Gates compose: a deployment can stack fuse + watermark + diamond checks. - New gates can be added without modifying the scheduler loop. - The sidecar and extension share the same gate implementations. - Gate evaluation order is explicit and configurable.
2.2 Shared Statistics Engine
Both fuse and potential future features need streaming statistics:
| Feature | Needs |
|---|---|
| Fuse (adaptive mode) | Rolling mean + stddev of delta sizes |
| Adaptive scheduling | Change rate trends for interval tuning |
| Blue-green convergence | Lag rate estimation for ETA |
| Monitoring / alerting | Anomaly detection on any metric |
A small StreamingStats module would serve all:
pub struct RollingStats {
window: VecDeque<f64>,
max_window: usize,
}
impl RollingStats {
pub fn push(&mut self, value: f64);
pub fn mean(&self) -> Option<f64>;
pub fn stddev(&self) -> Option<f64>;
pub fn is_anomalous(&self, value: f64, sensitivity: f64) -> bool;
}
pub struct EwmaStats {
alpha: f64,
mean: f64,
variance: f64,
}
Pure Rust, no PG dependency, trivially extractable.
2.3 Lifecycle State Machines
Multiple features use state machines with similar patterns:
| Feature | States | Transitions |
|---|---|---|
| Fuse | INTACT → BLOWN | blow (automatic) / reset (manual) |
| Blue-green | NONE → GREEN_CATCHING_UP → CONVERGED → PROMOTED / ROLLED_BACK | create / converge / promote / rollback |
| CDC mode | TRIGGER → TRANSITIONING → WAL | trigger threshold / WAL ready |
| Refresh mode | DIFFERENTIAL ↔ IMMEDIATE ↔ FULL | alter_stream_table (mode switch triggers cleanup + setup) |
| ST status | ACTIVE → SUSPENDED → ERROR → ACTIVE | error / recovery / manual |
A generic state machine with transition validation:
pub trait Lifecycle: Sized {
type Event;
fn transition(self, event: Self::Event) -> Result<Self, InvalidTransition>;
fn is_terminal(&self) -> bool;
}
3. Composability Matrix
How the four features interact with each other and with the proposed abstractions:
┌──────────────────────────────────────────────────────────────────┐
│ Scheduler Loop │
│ │
│ ┌─────────┐ ┌──────┐ ┌─────────┐ ┌─────────┐ ┌────────────┐ │
│ │LSN Tick │→│Fuse │→│Watermark│→│Snapshot │→│Diamond │ │
│ │Watermark │ │Gate │ │Gate │ │Coherence│ │Consistency │ │
│ │(global) │ │ │ │ │ │Gate │ │Gate │ │
│ └─────────┘ └──────┘ └─────────┘ └─────────┘ └────────────┘ │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ All gates passed → GroupExecutionStrategy → Refresh │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Blue-Green: convergence check (post-refresh) │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ IMMEDIATE Mode (bypasses scheduler) │
│ │
│ User DML → BEFORE trigger → AFTER trigger (transition tables) │
│ → DeltaSource::TransitionTable → delta SQL → MERGE │
└──────────────────────────────────────────────────────────────────┘
Feature Interaction Table
| Feature A × Feature B | Interaction | Composable? |
|---|---|---|
| Fuse × Watermark | Independent gates — both must pass. Fuse checks change volume; watermark checks temporal alignment. Ordered: fuse first (cheaper). | ✅ Trivially composable |
| Fuse × Blue-green | Fuse protects both blue and green STs independently. A blown fuse on a green ST pauses catch-up but doesn’t affect blue. | ✅ Independent evaluation |
| Fuse × Diamond | Blown fuse on any diamond group member blocks the entire group (all-or-nothing semantics). | ✅ Via group-aware gate |
| Watermark × Blue-green | Green ST inherits watermark group membership from blue. Watermark gating applies to green during catch-up. | ✅ Inherited configuration |
| Watermark × Diamond | Diamond groups operate on PG-internal consistency; watermarks on external temporal consistency. Orthogonal layers. | ✅ Independent layers |
| Watermark × Cross-source | Watermark = external temporal coherence; cross-source = PG-internal snapshot coherence. Complementary layers; a ST can have both. | ✅ Independent layers |
| Diamond × Cross-source | Diamond detects shared-ancestor splits (auto). Cross-source handles independent-source joins (user-declared). Groups merge during DAG rebuild; declared isolation overrides auto-detected. | ✅ Merged groups |
| Diamond × Transactional IVM | IMMEDIATE mode inherently solves diamond inconsistency — trigger nesting ensures all paths update within the same transaction. No group needed. | ✅ Orthogonal (IMMEDIATE bypasses) |
| Cross-source × Transactional IVM | IMMEDIATE mode inherently provides snapshot coherence within a single transaction. Cross-source groups are a deferred-mode concept. | ✅ Orthogonal |
| Fuse × Transactional IVM | Fuse gates deferred refresh; IMMEDIATE STs bypass the scheduler entirely. Fuse does not apply to IMMEDIATE STs. | ✅ Independent (different modes) |
| Blue-green × Diamond | Green STs are new entities with their own pgt_id — not part of the blue ST’s diamond group. Promoted green ST joins the group. |
✅ Via catalog update |
| Fuse × Watermark × Blue-green | Green ST catching up: watermark gate must pass AND fuse must be intact. All three compose naturally through the gate pipeline. | ✅ Multi-gate pipeline |
| All deferred gates × Transactional IVM | IMMEDIATE STs are excluded from the gate pipeline — they don’t participate in the scheduler loop. Clean separation of execution models. | ✅ Mode-based dispatch |
4. Extraction Strategy by Feature
4.1 What to Extract (Separate Crates)
| Crate | Contains | Source Features | PG-Free? |
|---|---|---|---|
pgtrickle-gates |
RefreshGate trait, GateDecision, GateContext, FuseGate, WatermarkGate, DiamondConsistencyGate, SnapshotCoherenceGate, ConvergenceGate, LsnWatermarkGate |
Fuse, Watermark, Blue-green, Diamond, Cross-source, LSN watermark | ✅ Yes |
pgtrickle-stats |
RollingStats, EwmaStats, Welford’s algorithm, anomaly detection |
Fuse (adaptive), future adaptive scheduling | ✅ Yes |
pgtrickle-watermark |
Watermark, WatermarkGroup, alignment predicate, tolerance evaluation |
Watermark gating | ✅ Yes |
pgtrickle-groups |
ConsistencyGroup, GroupExecutionStrategy trait, IsolationLevel, group merge logic |
Diamond, Cross-source, (Watermark groups) | ✅ Yes (logic only; execution adapters are PG-coupled) |
These could live as modules in pgtrickle-core rather than independent
crates — the key point is that they have zero PG dependency and are
independently testable.
4.2 What to Keep Internal (Extension + Sidecar Adapters)
| Component | Why Not Extract | Shared How? |
|---|---|---|
| Blue-green orchestration (create/promote/rollback) | Deeply tied to catalog, CDC sharing, frontier management | PgClient trait — same orchestration logic, different DB client |
| Fuse catalog CRUD | SPI in extension, SQL in sidecar | CatalogAccess trait |
Watermark advance_watermark() |
Transaction semantics, LSN recording | #[pg_extern] in extension, SQL function in sidecar |
| Blue-green promote transaction | Advisory locks, table renames, catalog updates | Single SQL transaction in both modes |
| SAVEPOINT / REPEATABLE READ group execution | PG transaction control | GroupExecutionStrategy trait — SPI impl vs pgwire impl |
| IMMEDIATE mode trigger installation | PG DDL (CREATE TRIGGER) | SQL generation is extractable; execution is PG-coupled |
| IMMEDIATE mode delta application | ENR access (extension) or PL/pgSQL EXECUTE (sidecar) | Shared delta SQL template; different execution wrappers |
| pg_ivm compatibility layer | PL/pgSQL wrapper functions | Extension only (no sidecar equivalent needed) |
4.3 Sequencing
Phase 1 — Internal trait boundaries (no crate extraction)
└─ Define RefreshGate trait
└─ Define GroupExecutionStrategy trait
└─ Define DeltaSource enum in DVM engine
└─ Refactor existing scheduler checks into gate implementations
└─ All gates unit-testable without PG
Phase 2 — Feature implementation using shared abstractions
└─ Diamond: DiamondConsistencyGate + SavepointStrategy (IN PROGRESS)
└─ Fuse: FuseGate + catalog + SQL API + scheduler integration
└─ Cross-source snapshot: SnapshotCoherenceGate + RepeatableReadStrategy
+ user-declared groups + LSN tick watermark
└─ Watermark: WatermarkGate + catalog + SQL API + scheduler integration
└─ Transactional IVM: DeltaSource::TransitionTable + trigger installation
+ pg_ivm compatibility layer
└─ Blue-green: ConvergenceGate + orchestration layer + SQL API
Phase 3 — Crate extraction (when sidecar work begins)
└─ Move gate implementations to pgtrickle-core
└─ Move DeltaSource + template compiler to pgtrickle-core
└─ Sidecar scheduler composes same gates via Tokio loop
└─ Sidecar installs compiled PL/pgSQL triggers for IMMEDIATE mode
└─ Extension scheduler composes same gates via BGWorker loop
This ordering means features ship before extraction. The trait boundaries are established in Phase 1 so that Phase 3 is mechanical, not a redesign.
5. Impact on Sidecar Feasibility
The sidecar report (REPORT_EXTERNAL_PROCESS.md) estimated 15–22 weeks (including cross-plan concerns). How do the four features affect this?
| Feature | Sidecar Impact | Additional Effort |
|---|---|---|
| Fuse | should_blow() is pure Rust — zero sidecar-specific work beyond wiring the gate |
~0 (free via shared core) |
| Watermark | Alignment predicate is pure Rust. advance_watermark() needs a SQL function in sidecar mode (PL/pgSQL wrapper calling the catalog update). |
~1 day |
| Blue-green | Convergence checks are pure Rust. Promote/rollback are SQL transactions — identical in extension and sidecar. | ~2 days |
| Diamond | detect_consistency_groups() is pure Rust in pg-dag. SAVEPOINT execution is standard SQL — works identically over pgwire. |
~0 (free via shared core) |
| Cross-source | Group management is pure Rust. REPEATABLE READ wrapping is standard SQL. LSN watermark query is a single SQL call. |
~1 day |
| Transactional IVM | Delta SQL templates are shared. Sidecar compiles them into PL/pgSQL trigger functions. Extension uses native Rust triggers. This is the largest sidecar-specific effort among all features. | ~1-2 weeks (compiled trigger generator + testing) |
| RefreshGate pipeline | Both schedulers compose the same gates. The gate interface is the primary shared abstraction. | ~0 (architectural benefit) |
Net impact: If features are built with the RefreshGate pattern and
DeltaSource abstraction, the sidecar gets most features essentially for
free. Transactional IVM is the exception — compiled PL/pgSQL triggers require
sidecar-specific development, but the delta SQL generation is fully shared.
The RefreshGate trait and DeltaSource enum are the two highest-leverage
abstractions for making all seven features composable across deployment modes.
6. Recommendations
R1: Introduce RefreshGate trait before implementing any of the four features
This is the architectural foundation. Define it, refactor existing inline checks into gate implementations, then build fuse/watermark/blue-green as new gates. Estimated effort: 8–12 hours for the trait + refactoring.
R2: Diamond consistency validates the group execution pattern
Diamond consistency is already IN PROGRESS. It should be the first feature
to use both RefreshGate (frontier alignment check) and
GroupExecutionStrategy (SAVEPOINT atomic groups). This validates two
abstractions at once.
R3: Implement fuse next — smallest scope, highest standalone value
Fuse has the simplest interaction surface (single ST, single predicate,
no group semantics). It validates the RefreshGate pattern with a real
feature before watermark (groups, tolerance, LSN mapping) adds complexity.
R4: Build watermark gating with 'gate' mode only — defer 'hold_back'
The plan’s §5.4 already suggests this. Gate-only is a pure scheduling
predicate (fits RefreshGate). Hold-back changes the refresh data window,
requiring deeper frontier machinery changes — a separate phase.
R5: Cross-source snapshot extends diamond — implement together or immediately after
Cross-source snapshot reuses ConsistencyGroup and GroupExecutionStrategy
from the diamond plan, adding REPEATABLE READ as an isolation option and
user-declared groups as a configuration mechanism. Implementing them in
sequence minimizes rework.
R6: Define DeltaSource before transactional IVM implementation
The DeltaSource enum should be established in the DVM engine before
IMMediaTE mode work begins. This ensures the operator tree’s Scan node is
parameterized from day one, and the sidecar’s compiled-trigger path is
architecturally supported without retrofitting.
R7: Treat blue-green as orchestration, not a gate
Blue-green uses RefreshGate for convergence detection, but its core
complexity is lifecycle management (create/promote/rollback). Keep the
orchestration in a dedicated module, with the convergence check plugged
in as a composable RefreshGate.
R8: Do NOT extract crates until the sidecar work begins
Premature crate extraction adds build complexity without immediate benefit. The internal trait boundaries (R1) give all the testability and composability advantages without the workspace restructuring overhead. Extract when there is a consumer (the sidecar) that needs the crate.
R9: Treat external process as the integration test for composability
The sidecar is both a product and a forcing function. Every trait boundary established for the features above is validated when the sidecar composes the same logic. Plan the sidecar’s MVP to include at least one gate (fuse) and IMMEDIATE mode (compiled triggers) to prove both abstractions end-to-end.
7. Open Questions
Should
RefreshGateevaluation order be configurable? The current proposal uses a fixed order (status → fuse → watermark → diamond). But different deployments may want different gate priorities. Is this over-engineering, or a genuine requirement?Should gates be async? For the extension (bgworker), gates run synchronously via SPI. For the sidecar (Tokio), gate context retrieval (e.g., counting change buffer rows) is naturally async. Should the trait be
async fn evaluate()with a sync wrapper for the extension?Gate context cost: The
GateContextincludes fields likechange_buffer_countthat are expensive to compute. Should the context be lazily populated (each gate requests only what it needs), or eagerly computed once per ST per tick?Fuse × watermark ordering: If the fuse blows on a watermark-gated ST, should the fuse reason mention that the ST was also watermark-gated? Or are the two reasons independent? For user comprehension, showing all active gates and their states in
pgt_status()would be ideal.Blue-green and the
RefreshGatepattern: The promote/rollback lifecycle doesn’t fit the per-tick gate model. Should blue-green have its own orchestration interface separate fromRefreshGate, or shouldConvergenceGatebe sufficient to model the “is green ready?” question?Transactional IVM and fuse: Should IMMEDIATE mode STs have any form of anomaly protection? The fuse concept doesn’t directly apply (no batched change buffer), but a per-trigger delta size check could serve a similar role. Is this worth the complexity, or should users rely on application-level safeguards for IMMEDIATE mode?
GroupExecutionStrategyvs inline transaction control: Is the trait abstraction for group execution worth the indirection? The extension and sidecar both emit the same SQL (SAVEPOINT,BEGIN ISOLATION LEVEL REPEATABLE READ). The trait’s value is primarily testability (mock execution strategy for unit tests).Cross-source + watermark group unification: User-declared co-refresh groups (cross-source) and watermark groups both manage sets of sources with alignment semantics. Should they share a catalog table or remain separate? They serve different purposes (PG snapshot coherence vs external temporal alignment), but the management UX overlaps.
Related Documents
| Document | Relationship |
|---|---|
| REPORT_ENGINE_COMPOSABILITY.md | General module-level extraction analysis (complementary) |
| REPORT_EXTERNAL_PROCESS.md | Sidecar feasibility — primary consumer of extracted components |
| REPORT_BLUE_GREEN_DEPLOYMENT.md | Full blue-green design |
| PLAN_FUSE.md | Full fuse design |
| PLAN_WATERMARK_GATING.md | Full watermark gating design |
| PLAN_DIAMOND_DEPENDENCY_CONSISTENCY.md | Diamond consistency — atomic refresh groups |
| PLAN_CROSS_SOURCE_SNAPSHOT_CONSISTENCY.md | Cross-source snapshot consistency — REPEATABLE READ groups |
| PLAN_TRANSACTIONAL_IVM.md | Transactional IVM — IMMEDIATE mode with transition tables |
| PLAN_ECO_SYSTEM.md | Ecosystem integration plan |