Skip to content

feat(store): cascade-safe partial rebuild + control rebuild-projections CLI (spec 21)#512

Merged
PaulDotterer merged 13 commits into
mainfrom
feat/505-rebuild-operability
Jul 4, 2026
Merged

feat(store): cascade-safe partial rebuild + control rebuild-projections CLI (spec 21)#512
PaulDotterer merged 13 commits into
mainfrom
feat/505-rebuild-operability

Conversation

@PaulDotterer

Copy link
Copy Markdown
Contributor

Closes #505. Closes #506. Implements spec 21 (RebuildAll operability & cascade-safety) — the second spec-19 prerequisite (after #507/#511); spec 19's snapshot-restore replays subsets by definition and rides on this machinery.

What

  • F-01 (F-01: control rebuild-projections production entry point for RebuildAll #505) — production entry point. New control rebuild-projections [target…] subcommand (doctor pattern): resolves and prints the plan before any destructive statement, reports applied/skipped/duration per target, exits 0/1/2 (success / rolled-back failure / could-not-run), and re-runs auth.ReconcileSystemRoles after success so the rebuilt DB is immediately RBAC-correct without a control restart. Deliberately CLI-only — authz is host access, no remote RPC (AC 3). Documented in deploy/QUICKSTART.md (docref-anchored).
  • F-03 (F-03: cascade-safe partial rebuild (RebuildAll(target) must not silently wipe FK children) #506) — cascade-safe partial rebuild. RebuildAll expands a partial selection to a fixpoint over the live FK graph (pg_constraint, not a hand-list): every table a TRUNCATE … CASCADE reaches pulls in its owning target; an unowned cascade child pulls in its FK parents' targets — that second-order rule is what saves user_group_members_projection, which hangs off users_projection but is replayed by user_groups. A closure table nobody can replay refuses the rebuild (fail-closed). ResolveTargets exposes the same expansion for the CLI preview. Regression pin: TOTP enrollment, identity link, AND group membership survive rebuild-projections users.
  • F-14 — separate Skipped counter on TargetResult; ErrSkipEvent no longer counts as applied.
  • F-04 (AC 6) — full-fidelity round-trip test. Rich fixture → ordered row::text dump of every AllRebuildTargets table → no-arg rebuild → byte-identical re-dump. Self-discovering over the registry.

Real bugs the round-trip test caught (fixed here)

  1. Migration-seeded rows died on rebuild. The system Admin/User roles and the server_settings 'global' row exist only as migration seeds, not events — a rebuild TRUNCATEd them away permanently (settings reads then fail; RBAC loses its system roles). rebuildTarget gains SeedSQL, re-applied post-TRUNCATE. The seeds mirror the post-migration state — 009 deliberately blanks system-role permissions (reconciler-owned) — with fixed timestamps (008 updated; beta Path A). The round-trip test is the standing drift guard between the migration seed and the rebuild seed.
  2. user_roles_projection had two writers split across two targets (ApplyUser writes creation-time role_ids, ApplyUserRole writes grants) — whichever TRUNCATEd second wiped the other's replay. The user_roles target is folded into users (both tables, both streams, ApplyUserWithRoles dispatcher), replaying in true sequence order.
  3. Existing single-target tests assumed res.Targets has exactly one entry — cascade expansion legitimately widens the run set (a roles rebuild expands to 6 targets via ugr → user_groups → users), so they now locate their target by name.

Verification

  • go vet, staticcheck, gofmt, docref check clean; full go test ./... -count=1 green (33 packages).
  • New guards red-checked: the cascade test failed against the old code (children wiped), the Skipped assertion failed pre-field, the fidelity test failed against all three real bugs above before their fixes.
  • New dynamic-SQL allowlist entry for the SeedSQL exec (values are package-level string constants only).
  • Local coderabbit review: no findings.

…ns CLI (#505, #506)

Spec 21: RebuildAll operability & cascade-safety — prerequisite for
spec 19's snapshot-restore, which replays subsets by definition.

- F-01 (#505): new `control rebuild-projections [target…]` subcommand
  (doctor pattern) — prints the resolved plan before running, reports
  applied/skipped per target, re-runs auth.ReconcileSystemRoles after
  a successful rebuild, and is deliberately CLI-only (no remote RPC;
  authz = host access). Documented in deploy/QUICKSTART.md with docref
  anchors.
- F-03 (#506): partial rebuilds are cascade-safe. RebuildAll expands a
  partial selection to a fixpoint over the LIVE FK graph: every table a
  TRUNCATE ... CASCADE reaches pulls in its owning target, and unowned
  cascade children pull in their FK parents' targets (the second-order
  case: user_group_members_projection hangs off users_projection but is
  replayed by user_groups). A closure table nobody can replay refuses
  the rebuild. ResolveTargets exposes the same expansion for the CLI
  plan preview. Regression pins: TOTP enrollment, identity link, AND
  group membership survive `rebuild-projections users`.
- F-14: TargetResult gains a separate Skipped counter — applied and
  skipped events are no longer conflated.
- F-04 (AC 6): new full-fidelity round-trip test — rich fixture, full
  ordered row::text dump over every AllRebuildTargets table, no-arg
  rebuild, byte-identical re-dump; self-discovering over the registry.

The round-trip test immediately found three real replay-infidelity
bugs, fixed here:
- migration-seeded rows (system Admin/User roles, the server_settings
  'global' row) exist only as rows, not events — a rebuild destroyed
  them. rebuildTarget gains SeedSQL, re-applied post-TRUNCATE; the
  seeds mirror the post-migration state (008 seeds + 009's deliberate
  permission blanking) with FIXED timestamps (008 changed accordingly),
  and the round-trip test permanently guards the two against drift.
- user_roles_projection had two writers (ApplyUser inserts creation-time
  role_ids, ApplyUserRole applies grants) split across two rebuild
  targets — whichever truncated second wiped the other's replay. The
  user_roles target is folded into users (both tables, both streams,
  ApplyUserWithRoles dispatcher), replaying in true sequence order.
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@PaulDotterer, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9b3877a5-c7ed-45a1-849b-38adda83eae8

📥 Commits

Reviewing files that changed from the base of the PR and between f21386b and 73d2186.

⛔ Files ignored due to path filters (1)
  • docref.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • cmd/control/main.go
  • cmd/control/rebuild.go
  • cmd/control/rebuild_test.go
  • deploy/QUICKSTART.md
  • internal/archtest/no_dynamic_sql_test.go
  • internal/projectors/user_role_listener.go
  • internal/projectors/wire.go
  • internal/store/migrations/008_seeds.sql
  • internal/store/rebuild.go
  • internal/store/rebuild_497_test.go
  • internal/store/rebuild_cascade_test.go
  • internal/store/rebuild_fidelity_test.go
  • internal/store/rebuild_test.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/505-rebuild-operability

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@PaulDotterer PaulDotterer merged commit 3846eda into main Jul 4, 2026
11 checks passed
@PaulDotterer PaulDotterer deleted the feat/505-rebuild-operability branch July 4, 2026 15:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant