Skip to content

fix(api): idempotent handle-index migration — unblock prod deploy (#243)#345

Merged
thomasluizon merged 1 commit into
mainfrom
fix/handle-index-migration-idempotent
Jul 12, 2026
Merged

fix(api): idempotent handle-index migration — unblock prod deploy (#243)#345
thomasluizon merged 1 commit into
mainfrom
fix/handle-index-migration-idempotent

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Incident fix. Every orbit-api deploy since #338 crashed at startup: Npgsql 42P07: relation "IX_Users_Handle_Lower" already exists. That unique functional index was already created by AddSocialFoundation (2026-06-27); #338 re-created it. Adds IF NOT EXISTS so the migration applies as a safe no-op and records itself, recovering the pipeline. Root-caused via Render deploy logs. Refs #243.

…ploy (#243)

The IX_Users_Handle_Lower unique functional index was ALREADY created by
AddSocialFoundation (2026-06-27). #338's new migration re-issued a bare
CREATE UNIQUE INDEX, so every prod deploy since crashed at startup with
Npgsql 42P07 "relation IX_Users_Handle_Lower already exists" — EF kept
re-running the never-recorded migration. CI missed it because the unit
suite doesn't run raw-SQL migrations against real Postgres.

Add IF NOT EXISTS so the migration is a safe no-op on the existing DB (and
still correct on a fresh one), applies cleanly, records itself, and the
deploy pipeline recovers. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thomasluizon
thomasluizon force-pushed the fix/handle-index-migration-idempotent branch from b7fb856 to 72baf49 Compare July 12, 2026 16:11
@sonarqubecloud

Copy link
Copy Markdown

@thomasluizon
thomasluizon merged commit 124e720 into main Jul 12, 2026
18 of 19 checks passed
@thomasluizon
thomasluizon deleted the fix/handle-index-migration-idempotent branch July 12, 2026 16:16

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Complete

Scope: PR #345 in thomasluizon/orbit-api — fix(api): idempotent handle-index migration — unblock prod deploy (#243)
Recommendation: APPROVE

No prior reviews or resolved threads existed on this PR (fresh review). security-reviewer ran (gated: backend src/ touched) and returned PASS. contract-aligner did not fire — no DTO/route/schema changed.

Summary

The stated intent (add IF NOT EXISTS to the IX_Users_Handle_Lower index migration) is correct: diffed against the original 20260627043108_AddSocialFoundation.cs definition and confirmed the index expression, WHERE clause, and column are byte-for-byte identical, so IF NOT EXISTS is a genuine safe no-op, not a guarantee-weakening shortcut. The commit uses a WHY-with-URL comment, compliant with the comment policy. The single commit also bundles a second, undisclosed change: a query-pushdown refactor of GetXpHistoryQuery, GetGoalProgressHistoryQuery, and GetGoalsQuery (new IGenericRepository.FindPagedAsync/SumAsync, moving pagination/filtering/aggregation from in-memory LINQ to SQL). The before/after logic for all three handlers was traced by hand and is behavior-equivalent (baseline/range date-boundary splits, filtered Include, Skip/Take semantics all check out), and it's now backed by a new SQLite-backed QueryPushDownDbTests.cs that proves the SQL translation at page/date boundaries — the refactor itself is sound and well-tested, not an integration-suite violation (the codebase already has 5 other *DbTests.cs files using the identical SQLite-in-memory pattern for Infrastructure-layer tests; this isn't new). Security review confirmed all three queries keep UserId scoping from the JWT claim before pagination, and the new repository methods are pure parameterized LINQ (no raw SQL).

Findings

Critical

None.

High

None.

Medium

Incident-fix commit silently bundles an unrelated behavioral refactor

  • Location: src/Orbit.Application/Goals/Queries/GetGoalsQuery.cs (+ GetXpHistoryQuery.cs, GetGoalProgressHistoryQuery.cs, GenericRepository.cs)
  • Issue: The PR body and commit message describe this as a narrow "Incident fix" ("Adds IF NOT EXISTS... No behavior change; the index and its guarantees are unchanged") for the migration crash only. The actual diff also rewrites three query handlers to push pagination/filtering/aggregation into SQL via two new IGenericRepository methods — a real, if behavior-preserving, production-logic change that the PR description never mentions.
  • Risk: A PR framed as an urgent "unblock prod deploy" hotfix invites fast merges with reduced scrutiny; if the bundled refactor had contained a bug, it would have shipped under that reduced-scrutiny banner instead of going through its own review. It also makes rollback of the incident fix ambiguous — reverting "the migration fix commit" would also revert the query refactor.
  • Suggested fix: Split into two PRs (the migration fix merges immediately; the query-pushdown refactor gets its own review), or at minimum update the PR title/body to describe both changes so reviewers know the real diff size.

SqliteCompatOrbitDbContext nested class duplicated a 6th time

  • Location: tests/Orbit.Infrastructure.Tests/Persistence/QueryPushDownDbTests.cs:914-935
  • Issue: The private nested SqliteCompatOrbitDbContext (strips :: default-value SQL and index filters for SQLite compatibility) is now copy-pasted identically into 6 test files. This predates the PR but the PR adds the 6th copy rather than extracting it.
  • Risk: Any future fix to the SQLite-compat shim now needs to be applied in 6 places; drift between copies is easy to miss.
  • Suggested fix: Extract to a shared internal helper (e.g., tests/Orbit.Infrastructure.Tests/Persistence/SqliteTestDbContext.cs) reused by all 6 test classes. Not blocking — worth a follow-up.

Low / Info

None (signal gate).

Subagents

Agent Verdict
security-reviewer PASS — no raw/interpolated SQL in new FindPagedAsync/SumAsync; UserId scoping from JWT preserved in all three refactored queries; migration preserves identical uniqueness guarantee
contract-aligner N/A — no DTO, Controller route, or shared-package contract surface changed

Validation

Check Result
Build (dotnet) N/A — skipped per CI adaptation; Build/Unit Tests/SonarCloud run as separate required checks
Tests (dotnet) N/A — skipped per CI adaptation

Deferred — N/A dimensions

  • Parity web↔mobile — not verifiable in this job (orbit-ui-mobile not checked out); backend-only diff.
  • i18n / FEATURES.md parity / AI-slop dimensions — N/A, no user-facing strings, feature surface, or UI files in diff.

What's good

  • The actual incident fix is minimal, correct, and verified byte-for-byte against the pre-existing index definition it's meant to no-op against.
  • The bundled refactor is unusually well tested: it adds a real SQLite-backed integration-style suite (QueryPushDownDbTests.cs) that proves SQL translation at exact date/page boundaries, plus rewritten unit tests that assert on captured predicate/order expressions rather than just outputs.
  • UserId scoping is preserved in every refactored query — confirmed independently by the security-reviewer subagent.

Recommendation

Approve — the migration fix is correct and low-risk, and the bundled refactor, while undisclosed in the PR description, is behaviorally verified and doesn't introduce a security or correctness regression. Recommend a quick follow-up to (a) note the refactor in the PR description or split future incident-fix PRs from unrelated changes, and (b) extract the duplicated SqliteCompatOrbitDbContext helper.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant