Skip to content

fix(api): enforce case-insensitive uniqueness on User.Handle#338

Merged
thomasluizon merged 4 commits into
mainfrom
fix/user-handle-unique-index
Jul 12, 2026
Merged

fix(api): enforce case-insensitive uniqueness on User.Handle#338
thomasluizon merged 4 commits into
mainfrom
fix/user-handle-unique-index

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

What & why

User.Handle is compared case-insensitively everywhere (u.Handle.ToLower() == normalized in SetHandleCommand and FriendGraphService), but the column had no unique index. So two concurrent SetHandle calls for the same handle could both pass the AnyAsync pre-check and persist duplicate handles — a classic check-then-act race (OrbitDbContext.cs User config, alongside the existing Email/ReferralCode/PlayPurchaseToken/PublicProfileSlug unique indexes).

Change

  • Migration AddUserHandleUniqueIndex creates a partial functional unique index IX_Users_Handle_Lower on LOWER("Handle") WHERE "Handle" IS NOT NULL.
    • Case-insensitive to match the app's comparison — a plain unique index on Handle would let Bob/bob coexist and wouldn't serve the lower() lookups.
    • The index is declared in raw migration SQL rather than fluent model config because Npgsql exposes no fluent functional/expression-index API (verified against the Npgsql EF Core docs). Consequently the model snapshot is intentionally unchanged and ef migrations has-pending-model-changes reports clean.
    • Up first dedupes any pre-existing case-insensitive collisions (keeping the earliest account's handle; later racers fall back to their deterministic seeded user_<id> handle) so the CREATE UNIQUE INDEX cannot fail on deploy. Mirrors the HabitLogCompletionUniqueIndex precedent.
    • Partial WHERE Handle IS NOT NULL matches the other nullable unique indexes and allows multiple NULLs.

Guard (already merged, now tested)

The create/update path (SetHandleCommandHandler) already wraps SaveChangesAsync in try/catch (DbUpdateException when DbUniqueViolation.IsUniqueViolation) → HandleTaken. That fallback was dead until this index existed and was untested. Added Handle_ConcurrentInsertLosesUniqueIndexRace_ReturnsHandleTaken proving a 23505 at save time surfaces the friendly HandleTaken error instead of a 500. Signup paths seed unique user_<id> handles, so they are unaffected.

Validation

  • dotnet build — 0 errors.
  • dotnet test Application (2629) + Domain (504) — all green.
  • ef migrations has-pending-model-changes — no drift.
  • Generated SQL reviewed; no bare // narration (ORBIT0001).

Refs thomasluizon/orbit-ui-mobile#243

Handles are compared case-insensitively app-wide (lower(Handle) == normalized),
but the column had no unique index, so two concurrent SetHandle calls could both
pass the AnyAsync pre-check and persist duplicate handles under a race.

Add a partial functional unique index on lower(Handle) WHERE Handle IS NOT NULL,
deduping any pre-existing case-insensitive collisions first (colliding rows keep
the earliest account's handle; the rest fall back to their seeded user_<id> handle).
The index lives in raw migration SQL because Npgsql exposes no fluent
functional-index API; a plain unique index would be case-sensitive and wrong.

The SetHandleCommand race fallback already maps the resulting 23505 to the
friendly HandleTaken error; add the missing test covering that guard.

Refs thomasluizon/orbit-ui-mobile#243

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@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.

test-dry-run-check

@claude

claude Bot commented Jul 12, 2026

Copy link
Copy Markdown

Correcting an accidental placeholder: a prior automated review action on this PR was submitted with body text "test-dry-run-check" due to a tooling error while verifying command permissions. It was not a real review and should be disregarded (the automation has no way to delete or edit that submitted review via the available tooling). Posting the actual review below.


Code Review: PR #338

Scope: PR #338 in thomasluizon/orbit-api -- fix(api): enforce case-insensitive uniqueness on User.Handle
Recommendation: APPROVE

Summary

Adds a partial, case-insensitive functional unique index (IX_Users_Handle_Lower on LOWER("Handle") WHERE "Handle" IS NOT NULL) to close a check-then-act race in SetHandleCommandHandler, where two concurrent SetHandle calls for the same handle could both pass the pre-check AnyAsync and persist duplicate handles. The Up() migration first deduplicates any pre-existing case-insensitive collisions (keeping the earliest-created row, reassigning the rest to the same deterministic fallback handle formula already used by User.SeedDefaultHandle()), then creates the index via raw SQL because Npgsql exposes no fluent functional-index API. A new unit test (Handle_ConcurrentInsertLosesUniqueIndexRace_ReturnsHandleTaken) exercises the previously-untested "catch DbUpdateException when DbUniqueViolation.IsUniqueViolation" fallback in the handler. Changes are limited to a new EF migration plus its generated Designer.cs snapshot and one test file -- no DTO, controller, or contract-surface changes.

Findings

Critical

None.

High

None.

Medium

None.

Low / Info

[INFO] Non-concurrent CREATE UNIQUE INDEX briefly locks writes on Users during deploy

  • dimension: 1 (Correctness) / operational note
  • location: src/Orbit.Infrastructure/Migrations/20260712120428_AddUserHandleUniqueIndex.cs:7-11
  • issue: CREATE UNIQUE INDEX (no CONCURRENTLY) takes a lock that blocks concurrent writes to Users while it builds.
  • risk: On a large Users table this could cause a brief write-blocking window during deploy -- negligible today but worth remembering if the table grows.
  • fix: No action needed now; CONCURRENTLY can't run inside an EF migration transaction anyway. This mirrors the accepted HabitLogCompletionUniqueIndex precedent the PR body cites, so it is a known, already-sanctioned tradeoff, not a new regression.
  • reference: precedent src/Orbit.Infrastructure/Migrations/20260608175140_HabitLogCompletionUniqueIndex.cs

Subagents

Agent Verdict
security-reviewer PASS -- no injection surface in the raw migrationBuilder.Sql() calls (hardcoded columns plus a static per-row Id-derived formula, no external/user input concatenated); dedup logic is deterministic and PII-safe; SetHandleCommandHandler's existing catch maps DbUpdateException to a generic HandleTaken error with no internal detail leak; test file has no secrets or unsafe mocking.
contract-aligner N/A -- no DTO, Controller route, or packages/shared type/endpoints.ts change in this diff.

Validation

Check Result
Build (dotnet) N/A in this review environment (sandboxed; no approval available for dotnet build / dotnet test / git fetch). PR description states dotnet build 0 errors and dotnet test Application (2629) + Domain (504) all green.
Tests (dotnet) N/A, same as above.

Deferred -- N/A dimensions and files not verdicted

  • Dimension 8 (DESIGN.md/AI-slop): N/A -- no apps/* UI files in this diff.
  • Dimension 9 (Parity web to mobile): N/A -- backend-only diff.
  • Dimension 10 (i18n): N/A -- no new user-facing strings.
  • Dimension 11 (Contract drift + backward-compat guard): N/A -- no DTO, Zod schema, or endpoint change; nothing for an old mobile client to break against.
  • Dimension 14 (FEATURES.md parity): N/A -- pure bugfix, no user-facing feature-surface change.
  • 20260712120428_AddUserHandleUniqueIndex.Designer.cs: not read line-by-line (2566 lines of auto-generated point-in-time entity-mapping boilerplate, excluded from the ORBIT0001 comment analyzer). Spot-checked that it does not encode the new functional index, consistent with OrbitDbContextModelSnapshot.cs being untouched and the PR's "ef migrations has-pending-model-changes reports clean" claim.
  • Phase 6 adversarial/second-opinion pass: not invoked -- no Critical/High finding survived Phase 3 to require it.

What's good

  • Root-causes the actual bug (missing DB-level enforcement for an app-wide case-insensitive comparison) instead of adding more application-level checking.
  • The dedup fallback formula matches the existing User.SeedDefaultHandle() domain formula exactly, so reassigned handles look identical to normal seeded ones.
  • Closes a previously dead/untested code path: the DbUpdateException to HandleTaken catch in SetHandleCommandHandler had no test and no way to actually trigger before this index existed.
  • PR body clearly explains the raw-SQL choice and the intentionally-unchanged model snapshot, and cites the precedent it mirrors.
  • Down() migration correctly drops only the index, matching the additive nature of the change.

Recommendation

Approve as-is. No Critical or High findings; the one Info-level note about the non-concurrent index build is an accepted, precedented tradeoff and needs no follow-up.

@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.

Correcting an accidental placeholder: a prior automated review action on this PR was submitted with body text "test-dry-run-check" due to a tooling error while verifying command permissions. It was not a real review and should be disregarded (the automation had no way to delete or edit that submitted review via the available tooling). This review supersedes it.


Code Review: PR #338

Scope: PR #338 in thomasluizon/orbit-api -- fix(api): enforce case-insensitive uniqueness on User.Handle
Recommendation: APPROVE

Summary

Adds a partial, case-insensitive functional unique index (IX_Users_Handle_Lower on LOWER("Handle") WHERE "Handle" IS NOT NULL) to close a check-then-act race in SetHandleCommandHandler, where two concurrent SetHandle calls for the same handle could both pass the pre-check AnyAsync and persist duplicate handles. The Up() migration first deduplicates any pre-existing case-insensitive collisions (keeping the earliest-created row, reassigning the rest to the same deterministic fallback handle formula already used by User.SeedDefaultHandle()), then creates the index via raw SQL because Npgsql exposes no fluent functional-index API. A new unit test (Handle_ConcurrentInsertLosesUniqueIndexRace_ReturnsHandleTaken) exercises the previously-untested "catch DbUpdateException when DbUniqueViolation.IsUniqueViolation" fallback in the handler. Changes are limited to a new EF migration plus its generated Designer.cs snapshot and one test file -- no DTO, controller, or contract-surface changes.

Findings

Critical

None.

High

None.

Medium

None.

Low / Info

[INFO] Non-concurrent CREATE UNIQUE INDEX briefly locks writes on Users during deploy

  • dimension: 1 (Correctness) / operational note
  • location: src/Orbit.Infrastructure/Migrations/20260712120428_AddUserHandleUniqueIndex.cs:7-11
  • issue: CREATE UNIQUE INDEX (no CONCURRENTLY) takes a lock that blocks concurrent writes to Users while it builds.
  • risk: On a large Users table this could cause a brief write-blocking window during deploy -- negligible today but worth remembering if the table grows.
  • fix: No action needed now; CONCURRENTLY can't run inside an EF migration transaction anyway. This mirrors the accepted HabitLogCompletionUniqueIndex precedent the PR body cites, so it is a known, already-sanctioned tradeoff, not a new regression.
  • reference: precedent src/Orbit.Infrastructure/Migrations/20260608175140_HabitLogCompletionUniqueIndex.cs

Subagents

Agent Verdict
security-reviewer PASS -- no injection surface in the raw migrationBuilder.Sql() calls (hardcoded columns plus a static per-row Id-derived formula, no external/user input concatenated); dedup logic is deterministic and PII-safe; SetHandleCommandHandler's existing catch maps DbUpdateException to a generic HandleTaken error with no internal detail leak; test file has no secrets or unsafe mocking.
contract-aligner N/A -- no DTO, Controller route, or packages/shared type/endpoints.ts change in this diff.

Validation

Check Result
Build (dotnet) N/A in this review environment (sandboxed). Skipped per workflow instructions -- Build / Unit Tests / SonarCloud already run as separate required checks on this PR. PR description states dotnet build 0 errors and dotnet test Application (2629) + Domain (504) all green.
Tests (dotnet) N/A, same as above.

Deferred -- N/A dimensions and files not verdicted

  • Dimension 8 (DESIGN.md/AI-slop): N/A -- no apps/* UI files in this diff.
  • Dimension 9 (Parity web to mobile): N/A -- backend-only diff.
  • Dimension 10 (i18n): N/A -- no new user-facing strings.
  • Dimension 11 (Contract drift + backward-compat guard): N/A -- no DTO, Zod schema, or endpoint change; nothing for an old mobile client to break against.
  • Dimension 14 (FEATURES.md parity): N/A -- pure bugfix, no user-facing feature-surface change.
  • 20260712120428_AddUserHandleUniqueIndex.Designer.cs: not read line-by-line (2566 lines of auto-generated point-in-time entity-mapping boilerplate, excluded from the ORBIT0001 comment analyzer). Spot-checked that it does not encode the new functional index, consistent with OrbitDbContextModelSnapshot.cs being untouched and the PR's "ef migrations has-pending-model-changes reports clean" claim.
  • Phase 6 adversarial/second-opinion pass: not invoked -- no Critical/High finding survived Phase 3 to require it.

What's good

  • Root-causes the actual bug (missing DB-level enforcement for an app-wide case-insensitive comparison) instead of adding more application-level checking.
  • The dedup fallback formula matches the existing User.SeedDefaultHandle() domain formula exactly, so reassigned handles look identical to normal seeded ones.
  • Closes a previously dead/untested code path: the DbUpdateException to HandleTaken catch in SetHandleCommandHandler had no test and no way to actually trigger before this index existed.
  • PR body clearly explains the raw-SQL choice and the intentionally-unchanged model snapshot, and cites the precedent it mirrors.
  • Down() migration correctly drops only the index, matching the additive nature of the change.

Recommendation

Approve as-is. No Critical or High findings; the one Info-level note about the non-concurrent index build is an accepted, precedented tradeoff and needs no follow-up.

@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.

Code Review: PR #338

Scope: PR #338fix/user-handle-unique-index (case-insensitive uniqueness on User.Handle)
Recommendation: APPROVE

Severity Count
Critical (incl. old-client breaks) 0
High 0
Medium 1
Low / Info 0 (not posted per signal gate)

Findings

MediumCREATE UNIQUE INDEX on Users runs without CONCURRENTLY

  • location: src/Orbit.Infrastructure/Migrations/20260712120428_AddUserHandleUniqueIndex.cs:2607-2610
  • issue: the index build takes an ACCESS EXCLUSIVE lock on Users for its duration; unlike the HabitLogCompletionUniqueIndex precedent this cites, Users sits on every authenticated request's path (JWT auth), so the blast radius of a lock here is broader.
  • fix: acceptable as-is if Users is still small (matches house style, and EF wraps migrations in a transaction which CONCURRENTLY can't run inside); otherwise split into a non-transactional CONCURRENTLY step.
  • Non-blocking — Medium never forces a request-changes outcome per the rubric's deterministic outcome rule.

Verification performed

  • Read the full diff: migration Designer.cs snapshot, migration .cs (raw SQL), and tests/Orbit.Application.Tests/Social/SetHandleCommandTests.cs.
  • Verified the migration's dedup fallback handle ('user_' || LEFT(REPLACE(Id::text,'-',''),12)) is byte-identical to User.SeedDefaultHandle() (src/Orbit.Domain/Entities/User.cs:402, $"user_{Id:N}"[..17]).
  • Confirmed CreatedAtUtc (User.cs:40) is non-nullable, so the dedup's PARTITION BY LOWER(Handle) ORDER BY CreatedAtUtc, Id is deterministic.
  • Confirmed the model snapshot (OrbitDbContextModelSnapshot.cs) declares no HasIndex on Handle, corroborating the PR's claim the fluent model is intentionally unchanged (raw-SQL migration only).
  • Confirmed the inline // comment in the migration is a URL-linked WHY note, the sanctioned exception under the narration-comment rule.
  • Confirmed FriendGraphService.cs:24 and SetHandleCommand.cs:29 both do Handle.ToLower() ==, matching the index's LOWER(Handle) design — no mismatch between app-level and DB-level case handling.
  • Confirmed the new test Handle_ConcurrentInsertLosesUniqueIndexRace_ReturnsHandleTaken correctly drives DbUpdateException → SQLSTATE 23505DbUniqueViolation.IsUniqueViolationHandleTaken, closing a real untested race-path gap.
  • SQL-injection / PII check: no string interpolation of external input in the migration; all identifiers are static; the dedupe fallback derives only from the row's own DB-generated Id.
  • contract-aligner gate not triggered — no DTO/Controller route or packages/shared type touched by this diff.

Subagents

Agent Verdict
security-reviewer PASS — no findings
contract-aligner N/A — no DTO/Controller route/packages/shared type touched

Validation

Check Result
Build (dotnet) N/A — CI runs Build as a separate required check
Tests (dotnet) N/A — CI runs Unit Tests as a separate required check

Deferred / out of scope

  • The large auto-generated Designer.cs snapshot was spot-checked, not line-by-line verdicted (pure EF boilerplate, confirmed to carry no Handle index of its own).
  • Rubric dimensions not touched by this backend-only migration/test diff (frontend contract, auth surface changes, etc.) are N/A.

@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.

PR Review: fix(api) enforce case-insensitive uniqueness on User.Handle

Recommendation: APPROVE

Severity counts - Critical (incl. breaks old mobile clients): 0 | High: 0 | Medium: 1 (non-blocking, already surfaced in a prior review) | Low/Info: 0

Summary:
This PR was already reviewed multiple times previously (all APPROVE). The only commit since the last review is a no-op merge of main into this branch - confirmed via git diff that the diff against origin/main is unchanged (identical 3 files, identical line counts). No new content to review since the last pass.

Medium (non-blocking, already accepted in a prior review):
CREATE UNIQUE INDEX "IX_Users_Handle_Lower" in src/Orbit.Infrastructure/Migrations/20260712120428_AddUserHandleUniqueIndex.cs:34-37 runs without CONCURRENTLY, taking an ACCESS EXCLUSIVE lock on Users during the migration. Broader blast radius than the cited HabitLogCompletionUniqueIndex precedent since Users sits on every authenticated request, but acceptable while the table is small (CONCURRENTLY cannot run inside EF's migration transaction). No action required.

Independent re-verification performed this pass:

  • Re-diffed via git directly against origin/main to confirm no new content since the last review.
  • Confirmed no SQL injection surface in the raw-SQL migration (static SQL + row-derived Id, no external input interpolated).
  • Confirmed the dedup fallback formula ('user_' || LEFT(REPLACE(u."Id"::text, '-', ''), 12)) is byte-identical to User.SeedDefaultHandle() in src/Orbit.Domain/Entities/User.cs:402.
  • Dispatched a fresh security-reviewer subagent against the migration and test file: PASS, no injection/PII/secrets issues.
  • contract-aligner: N/A - no DTO/Controller/packages-shared surface touched.

Deferred / N/A:
Dimensions covering UI, contract surface, and user-facing feature changes are N/A for this backend migration+test diff. Cross-repo parity is not verifiable in CI (sibling orbit-ui-mobile repo not checked out). Designer.cs (2566 lines, pure EF-generated snapshot) spot-checked only, consistent with all three prior passes.

@sonarqubecloud

Copy link
Copy Markdown

@thomasluizon
thomasluizon merged commit 0cfdc87 into main Jul 12, 2026
19 checks passed
@thomasluizon
thomasluizon deleted the fix/user-handle-unique-index branch July 12, 2026 14:43

@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.

Code Review: PR #338

Scope: PR #338 in thomasluizon/orbit-apifix(api): enforce case-insensitive uniqueness on User.Handle
Recommendation: APPROVE

Summary

Re-diffed the PR's current head (896028d961c035918576b76209c9bc113147290d) against its current base (c6d2bd0588348372ca32d75a79ec81b4831a044c) and confirmed it is byte-identical to what three prior automated reviews already evaluated: same 3 files (migration .cs, migration Designer.cs, SetHandleCommandTests.cs), same +2630/-0 line counts. The only commits added since the original fix commit (be283f1) are three no-op Merge branch 'main' commits — no new content to review.

Independently re-verified the substantive claims rather than just trusting the prior passes:

  • Migration SQL (src/Orbit.Infrastructure/Migrations/20260712120428_AddUserHandleUniqueIndex.cs): dedup CTE partitions by LOWER("Handle") ordered by "CreatedAtUtc", "Id" (both deterministic/non-null tiebreakers), reassigns losers to 'user_' || LEFT(REPLACE(u."Id"::text, '-', ''), 12), then creates IX_Users_Handle_Lower as a partial unique index on LOWER("Handle") WHERE "Handle" IS NOT NULL. Dedup runs before index creation, so the CREATE UNIQUE INDEX can't fail on deploy.
  • Fallback formula parity: confirmed the migration's SQL formula is character-identical to User.SeedDefaultHandle() (src/Orbit.Domain/Entities/User.cs:405, $"user_{Id:N}"[..17]) — Id::text (D-format) minus hyphens yields the same hex sequence as Id:N, and LEFT(...,12) + 'user_' prefix (17 chars total) matches [..17] exactly.
  • Model snapshot: grepped the Designer.cs snapshot and OrbitDbContextModelSnapshot.cs — neither declares HasIndex on Handle, corroborating the PR's claim that the fluent model is intentionally untouched (Npgsql has no fluent functional-index API).
  • App/DB consistency: SetHandleCommand.cs:29 and FriendGraphService.cs:24 both compare via Handle.ToLower() ==, matching the index's LOWER(Handle) semantics.
  • New test (Handle_ConcurrentInsertLosesUniqueIndexRace_ReturnsHandleTaken): correctly throws a DbUpdateException wrapping a DbException with SqlState = "23505", which DbUniqueViolation.IsUniqueViolation (src/Orbit.Application/Common/DbUniqueViolation.cs) walks via InnerException recursion → true → handler's catch (DbUpdateException when DbUniqueViolation.IsUniqueViolation(...))HandleTaken. This closes a genuinely dead/untested path (the catch existed before this PR but had no index to trigger 23505, and no test).
  • Security: no injection surface — all SQL is static plus a per-row, DB-generated Id; no external input concatenated; no PII in the dedup or index logic.
  • Contract surface: no DTO, Controller route, or packages/shared type touched — cross-repo contract-aligner gate does not fire.

Findings

Critical

None.

High

None.

Medium

CREATE UNIQUE INDEX on Users runs without CONCURRENTLY (already surfaced and accepted in all three prior reviews — carried forward, not re-litigated as new)

  • Location: src/Orbit.Infrastructure/Migrations/20260712120428_AddUserHandleUniqueIndex.cs (the CREATE UNIQUE INDEX "IX_Users_Handle_Lower" block)
  • Issue: takes an ACCESS EXCLUSIVE lock on Users while building; Users sits on every authenticated request's path (JWT auth), so blast radius is broader than the cited HabitLogCompletionUniqueIndex precedent.
  • Non-blocking: acceptable as-is while Users is small — CONCURRENTLY can't run inside EF's migration transaction anyway. Worth a non-transactional split later if the table grows large enough for the lock window to matter.

Low / Info

None beyond the accepted Medium above.

Subagents

Agent Verdict
security-reviewer PASS — no injection surface in raw migrationBuilder.Sql() calls, dedup formula is deterministic and derives only from the row's own Id, SetHandleCommandHandler's catch maps to a generic HandleTaken with no internal detail leak.
contract-aligner N/A — no DTO, Controller route, or packages/shared type/endpoints.ts touched by this diff.

Validation

Skipped per task instructions — Build/Unit Tests/SonarCloud run as separate required CI checks on this PR.

Deferred — N/A dimensions

  • DESIGN.md/AI-slop, web↔mobile parity, i18n, FEATURES.md parity: N/A — pure backend bugfix, no UI or user-facing feature-surface change.
  • Contract drift / backward-compat guard: N/A — no DTO, Zod schema, or endpoint change.
  • orbit-ui-mobile repo not checked out in this job — cross-repo dimensions marked not verifiable rather than guessed.

What's good

  • Root-causes the actual bug (missing DB-level enforcement for an app-wide case-insensitive comparison) rather than adding more application-level checking on top of an already-racy pre-check.
  • Dedup fallback formula is provably identical to the existing User.SeedDefaultHandle() domain formula — reassigned handles are indistinguishable from normally-seeded ones.
  • Closes a previously dead, untested code path (DbUpdateExceptionHandleTaken catch) with a real regression test.
  • PR body is unusually clear about the raw-SQL rationale, the intentionally-unchanged model snapshot, and the precedent it mirrors.
  • Down() migration is correctly scoped (only drops the index).

Recommendation

APPROVE. The diff is identical to what's already been independently verified across three prior automated review passes plus this one. The one Medium (non-CONCURRENTLY index build) is an accepted, precedented tradeoff and does not block merge.

thomasluizon added a commit that referenced this pull request Jul 12, 2026
…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 added a commit that referenced this pull request Jul 12, 2026
…ploy (#243) (#345)

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 added a commit that referenced this pull request Jul 12, 2026
#352)

GetChangesV2 fetches all rows changed since a timestamp for seven synced
entities, filtering each by its owner key plus UpdatedAtUtc. None of those
predicates had a supporting composite index, so every sync fell back to a
scan filtered on UpdatedAtUtc.

Add leading-column btree indexes matching each query's exact predicate:
- Habits, Goals, Tags, Notifications, ChecklistTemplates: (UserId, UpdatedAtUtc)
- HabitLogs: (HabitId, UpdatedAtUtc)   -- filtered by HabitId IN (...), no UserId column
- GoalProgressLogs: (GoalId, UpdatedAtUtc)  -- filtered by GoalId IN (...), no UserId column

ProcessedRequests.UserId needs no dedicated FK index: the composite unique
index (UserId, IdempotencyKey, RequestType) already leads with UserId and
serves the cascade, which is why EF's convention suppressed a duplicate.

Migration uses CREATE INDEX IF NOT EXISTS / DROP INDEX IF EXISTS to stay
idempotent across deploys (avoids the #338 42P07 class of failure).
Behavior-preserving; performance only.

Refs thomasluizon/orbit-ui-mobile#243

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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