# v0.60.0 — Code Quality, Test Coverage & CI (Full Details)

> **Summary:** [v0.60.0.md](v0.60.0.md)
> **Assessment source:** [plans/PLAN_OVERALL_ASSESSMENT_12.md](../plans/PLAN_OVERALL_ASSESSMENT_12.md)
> **Findings addressed:** Q-1, Q-2, Q-3, T-1, T-2, T-3, T-4, T-5, C-5, C-6, CI-1, CI-2, CI-3

---

## Code Quality

### QUAL-1: Scheduler Log Level Standardisation

**Assessment finding:** Q-1 (MEDIUM)
**Files:** `src/scheduler/dispatch.rs`, `src/scheduler/pool.rs`,
`src/scheduler/scheduler_loop.rs`

Prior to this release, routine scheduler events (worker registration, lock
acquisition, dispatch start) were logged at `log!()` (DEBUG5 in PostgreSQL
terms), invisible at the default `log_min_messages = warning`.  Recoverable
failures also used `warning!()`, making it hard to distinguish between
"expected noise" and "something an operator should act on".

After this release, the convention is:
- `info!()` — normal state transitions and progress (scheduler start, dispatch
  complete, worker pool size changes).
- `warning!()` — recoverable failures that do not stop the scheduler
  (e.g., a worker failed to acquire a lock and will retry).
- `error!()` — failures requiring operator attention (e.g., the scheduler
  cannot reconnect after 3 retries).

The change affects ~40 log call sites across the three files.

### QUAL-2: refresh/codegen.rs Decomposition

**Assessment finding:** Q-2 (MEDIUM)
**Files:** `src/refresh/codegen.rs` → `src/refresh/sql_fragments.rs`
(new), `src/refresh/codegen.rs` (reduced)

The new `sql_fragments.rs` module contains:

```rust
// Builds the WHERE predicate that restricts a CTE scan to the delta window.
pub(crate) fn build_cte_predicate(lsn_col: &str, prev_lsn: &str, new_lsn: &str) -> String

// Builds the ON clause for a MERGE statement.
pub(crate) fn build_merge_join_condition(key_cols: &[&str]) -> String

// Builds the MD5 content-hash column expression for deduplication.
pub(crate) fn build_content_hash_column(data_cols: &[&str]) -> String

// Builds the MERGE WHEN MATCHED / NOT MATCHED target lists.
pub(crate) fn build_delta_target_list(
    action: MergeAction,
    cols: &[ColumnSpec],
) -> String
```

Each function has a dedicated unit-test block verifying its output against
expected SQL strings for common and edge-case inputs (empty column lists,
columns with reserved characters, single-key tables).

### QUAL-3: src/cdc.rs Module Decomposition

**Assessment finding:** Q-3 (MEDIUM)

The 3 383-line `src/cdc.rs` is split into:

| New module | Contents | ~LOC |
|------------|----------|-------|
| `src/cdc/triggers.rs` | Trigger creation/drop SQL builders, column-name escaping (`cb_col_name`), statement vs row mode, trigger function SQL | ~900 |
| `src/cdc/buffer.rs` | Change-buffer DDL, `buffer_base_name_for_oid`, `buffer_qualified_name_for_oid`, buffer initialisation | ~600 |
| `src/cdc/compact.rs` | Compaction scheduling, `pg_try_advisory_xact_lock` wrapper, `CompactionResult` enum, holdback classification | ~400 |
| `src/cdc/partition.rs` | `should_promote_to_partitioned`, partition-promotion logic, `check_if_publication_needs_rebuild` | ~300 |
| `src/cdc/mod.rs` | Public SQL-callable functions, re-exports, shared constants | ~400 |

No public API changes.

---

## Test Coverage

### TEST-1: Refresh Orchestrator and Merge Unit Tests

**Assessment finding:** T-1 (MEDIUM)

New `#[cfg(test)]` modules added to:

**`src/refresh/orchestrator.rs`** (12 tests):
- `test_cost_model_cold_start_requires_minimum_observations` — asserts mode is
  not activated until ≥ 3 DIFFERENTIAL + 1 FULL observations.
- `test_cost_model_prefers_differential_when_change_ratio_low` — change ratio
  below `differential_max_change_ratio` → DIFFERENTIAL preferred.
- `test_cost_model_prefers_full_when_change_ratio_high` — change ratio above
  threshold → FULL preferred.
- `test_cost_model_recovery_after_consecutive_errors` — after N consecutive
  errors the mode resets to cold-start.
- `test_orchestrator_adaptive_score_boundary_values` — verifies the ±0.15
  dead-zone does not flip the mode on noisy data.

**`src/refresh/merge/columns.rs`** (8 tests):
- Column-list builder for various schema shapes (all-nullable, mixed types,
  empty list, single column).

**`src/refresh/codegen.rs`** → deferred to `sql_fragments.rs` tests (see QUAL-2).

### TEST-2: CDC Pure-Logic Unit Tests

**Assessment finding:** T-2 (MEDIUM)

New `#[cfg(test)]` modules added to `src/cdc/triggers.rs` and
`src/cdc/buffer.rs`:

- `test_cb_col_name_escapes_reserved_pgt_prefix` — `__pgt_action` →
  `__usr___pgt_action`
- `test_cb_col_name_passes_through_normal_name` — `order_id` → `order_id`
- `test_buffer_base_name_for_oid` — OID 12345 → `changes_12345`
- `test_build_changed_cols_bitmask_expr_single_col` — single column → correct
  bitmask expression
- `test_build_changed_cols_bitmask_expr_empty` — no columns → `0::bit(1)`
- `test_should_promote_to_partitioned_false_for_regular` — `relkind = 'r'` →
  no promotion
- `test_should_promote_to_partitioned_true_for_partitioned` — `relkind = 'p'`
  → promote

### TEST-3: DDL Hook Classification Unit Tests

**Assessment finding:** T-3 (MEDIUM)
**File:** `src/hooks.rs`

New `#[cfg(test)]` block with 14 table-driven tests covering:

| Input | Expected outcome |
|-------|-----------------|
| `ALTER TABLE ADD COLUMN` on tracked table | `DdlCommandKind::AddColumn` → mark needs_reinit |
| `ALTER TABLE DROP COLUMN` on tracked table | `DdlCommandKind::DropColumn` → mark needs_reinit |
| `ALTER TABLE ALTER COLUMN TYPE` | `DdlCommandKind::AlterColumnType` → mark needs_reinit |
| `ALTER TABLE RENAME TO` | `DdlCommandKind::RenameTable` → mark needs_reinit |
| `CREATE INDEX` | `DdlCommandKind::CreateIndex` → no-op |
| Any DDL on untracked table | no-op |
| DDL on `pgtrickle.*` internal table | no-op |

### TEST-4: Remaining Fixed Sleep Replacement

**Assessment finding:** T-4 (LOW)

Fixed sleeps replaced with `wait_for_*` polling helpers in:
- `tests/e2e_quota_tests.rs` (3 sleeps)
- `tests/e2e_bgworker_tests.rs` (4 sleeps)
- `tests/e2e_wal_cdc_tests.rs` (2 sleeps)
- `tests/e2e_immediate_tests.rs` (1 sleep)

Total sleeps eliminated: 10.  The `wait_for_auto_refresh`,
`wait_for_condition`, and `wait_for_row_count` helpers in
`tests/common/mod.rs` cover all cases.

### TEST-5: Differential Idempotence Proptest

**Assessment finding:** T-5 (LOW)
**File:** `src/dvm/diff.rs`

New `proptest!` block:

```
Given: randomly-generated sequence of INSERT/UPDATE/DELETE rows partitioned
       into two batches in random order
When: batch 1 is applied and the stream table is refreshed,
      then batch 2 is applied and the stream table is refreshed
Then: the stream table contents equal the result of applying all rows in
      any order followed by a single refresh
```

This tests the idempotence and commutativity of the Z-set delta engine under
arbitrary ingest ordering.

---

## Correctness

### COR-5: WAL Decoder OID-Based Table Filter

**Assessment finding:** C-5 (MEDIUM)
**File:** `src/wal_decoder.rs`

The `should_include_change()` filter previously compared the decoded table
name (from `test_decoding` output) against the tracked source table's
qualified name using string equality.  This was fragile for:
- Partition routing (child arrives instead of root)
- Search-path-sensitive unqualified names
- Case-sensitive quoted identifiers

After this fix, the decoder resolves each tracked source table's OID via
`to_regclass()` once at the start of each poll cycle and compares relation
OIDs numerically.  String matching is retained only as a fallback for
`test_decoding`-format output that does not carry a relation OID.

### COR-6: Publication Rebuild Detects Table-Becomes-Partition

**Assessment finding:** C-6 (MEDIUM)
**File:** `src/cdc/partition.rs` — `check_if_publication_needs_rebuild`

`check_if_publication_needs_rebuild()` now checks:
1. Has `relkind` changed from `'r'` (regular) to `'p'` (partitioned root)?
   (Existing check.)
2. Has the source OID appeared in `pg_inherits.inhrelid`?  (New check.)

When condition 2 is detected, the function rebuilds the publication with
`publish_via_partition_root = true` and marks the stream table for
reinitialisation, preventing the silent CDC freeze that otherwise occurs when
a plain table is converted to a partition of another parent.

---

## CI/CD

### CI-1: Path-Filtered Full E2E on PRs

**Assessment finding:** CI-1 (MEDIUM)
**File:** `.github/workflows/ci.yml`

New job `full-e2e-on-dvm-change`:
```yaml
if: |
  github.event_name == 'pull_request' &&
  contains(github.event.pull_request.changed_files, 'src/dvm/') ||
  contains(github.event.pull_request.changed_files, 'src/refresh/') ||
  contains(github.event.pull_request.changed_files, 'src/cdc/')
```

Uses the `docker/build-push-action` cache and the existing
`test-full-e2e` recipe.  Estimated additional PR gate time: 20 minutes,
triggered only when the above paths change.

### CI-2: Dockerfile.e2e Non-Root User

**Assessment finding:** CI-2 (LOW)
**File:** `tests/Dockerfile.e2e`

Added before `CMD`:
```dockerfile
USER postgres
```

All test containers now run as the unprivileged `postgres` OS user.

### CI-3: Codecov Per-Module Thresholds

**Assessment finding:** CI-3 (LOW)
**File:** `codecov.yml`

New per-path coverage gates (patch coverage):
```yaml
coverage:
  status:
    patch:
      src/cdc/:
        target: 50%
      src/hooks.rs:
        target: 40%
      src/wal_decoder.rs:
        target: 40%
```

---

## Upgrade Notes

No SQL schema changes.  No `ALTER EXTENSION` migration is required.

---

## Implementation Status

| ID | Title | Status |
|----|-------|--------|
| QUAL-1 | Scheduler Log Level Standardisation | ✅ Done |
| QUAL-2 | refresh/codegen.rs Decomposition | ✅ Done |
| QUAL-3 | src/cdc.rs Module Decomposition | ✅ Done |
| TEST-1 | Refresh Orchestrator and Merge Unit Tests | ✅ Done |
| TEST-2 | CDC Pure-Logic Unit Tests | ✅ Done |
| TEST-3 | DDL Hook Classification Unit Tests | ✅ Done |
| TEST-4 | Remaining Fixed Sleep Replacement | ✅ Done |
| TEST-5 | Differential Idempotence Proptest | ✅ Done |
| COR-5 | WAL Decoder OID-Based Table Filter | ✅ Done |
| COR-6 | Publication Rebuild Detects Table-Becomes-Partition | ✅ Done |
| CI-1 | Path-Filtered Full E2E on PRs | ✅ Done |
| CI-2 | Dockerfile.e2e Non-Root User | ✅ Done |
| CI-3 | Codecov Per-Module Thresholds | ✅ Done |

---

## Exit Criteria

- [x] All items ✅ Done
- [x] `just test-unit` passes
- [x] `just lint` passes with zero warnings
- [x] CHANGELOG.md entry written
- [x] ROADMAP.md v0.60.0 row marked ✅ Released
