Skip to content

feat(migrations): UNIQUE constraints on account_artist_ids + artist_organization_ids pairs#46

Merged
sweetmantech merged 1 commit into
mainfrom
feat/join-row-unique-constraints
Jul 9, 2026
Merged

feat(migrations): UNIQUE constraints on account_artist_ids + artist_organization_ids pairs#46
sweetmantech merged 1 commit into
mainfrom
feat/join-row-unique-constraints

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

What

Adds supabase/migrations/20260708200000_join_row_unique_constraints.sql:

  1. account_artist_idsUNIQUE (account_id, artist_id). The table has only a bigint PK on id today, so nothing prevents duplicate pairs and the join row can't serve as a deterministic key.
  2. artist_organization_idsUNIQUE (artist_id, organization_id).

Each constraint is preceded by a defensive dedupe (keep the lowest id per pair, delete the rest) so the migration applies cleanly against prod data even if duplicate pairs exist — none are known today. Constraint adds are guarded by pg_constraint existence checks, so the migration is idempotent.

Why

P1 prerequisite for re-keying Composio connection ownership on the join row (connector-connection scoping): recoupable/chat#1860.

Cross-reference: this closes the same check-then-insert race class tracked in recoupable/chat#1844 for these two sibling tables — concurrent duplicate inserts now fail at the database instead of minting duplicate rows. It does not fix #1844 itself (different tables).

Status

Not yet applied to any environment — migration apply + verification pending, tracked in chat#1860.

🤖 Generated with Claude Code


Summary by cubic

Add UNIQUE constraints to account_artist_ids and artist_organization_ids so each pair is stored once and the join row can serve as a deterministic key. This prevents duplicate inserts and enables connector-connection scoping needed for chat#1860.

  • Migration
    • Dedupes existing rows first (keeps lowest id per pair).
    • Adds UNIQUE(account_id, artist_id) and UNIQUE(artist_id, organization_id) if missing (idempotent).
    • Safe to run on prod; no app changes required.

Written for commit 0ca5636. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Prevented duplicate link records in artist/account and artist/organization relationships.
    • Improved data consistency by cleaning up existing duplicates before enforcing uniqueness.
    • Reduced the chance of duplicate entries appearing from repeated or concurrent actions.

…rganization_ids pairs

Dedupe existing violating rows (keep lowest id per pair), then add
UNIQUE(account_id, artist_id) on account_artist_ids and
UNIQUE(artist_id, organization_id) on artist_organization_ids so
connector-connection scoping can key on the join row (chat#1860 P1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@supabase

supabase Bot commented Jul 8, 2026

Copy link
Copy Markdown

Updates to Preview Branch (feat/join-row-unique-constraints) ↗︎

Deployments Status Updated
Database Wed, 08 Jul 2026 21:38:22 UTC
Services Wed, 08 Jul 2026 21:38:22 UTC
APIs Wed, 08 Jul 2026 21:38:22 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations Wed, 08 Jul 2026 21:38:28 UTC
Migrations Wed, 08 Jul 2026 21:38:31 UTC
Seeding Wed, 08 Jul 2026 21:38:34 UTC
Edge Functions Wed, 08 Jul 2026 21:38:34 UTC

View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a SQL migration that deduplicates rows and adds idempotent composite UNIQUE constraints to the account_artist_ids table on (account_id, artist_id) and the artist_organization_ids table on (artist_id, organization_id).

Changes

Join Row Unique Constraints

Layer / File(s) Summary
Migration header and strategy
supabase/migrations/20260708200000_join_row_unique_constraints.sql
Header comments document the purpose and idempotent deduplication approach for applying constraints safely to existing data.
Deduplication and constraint enforcement
supabase/migrations/20260708200000_join_row_unique_constraints.sql
Deletes duplicate rows keeping the lowest id per pair, then conditionally adds composite UNIQUE constraints for account_artist_ids (account_id, artist_id) and artist_organization_ids (artist_id, organization_id) if they don't already exist.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding unique constraints to the two join-table ID pairs.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/join-row-unique-constraints

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.

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

🧹 Nitpick comments (1)
supabase/migrations/20260708200000_join_row_unique_constraints.sql (1)

44-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider using IS NOT DISTINCT FROM consistently across both dedupe blocks.

The account_artist_ids dedupe (lines 25-26) uses IS NOT DISTINCT FROM while this block uses =. While = is correct here because artist_organization_ids columns are NOT NULL, using IS NOT DISTINCT FROM in both blocks would be more defensive against future schema changes and makes the intent explicitly clear.

♻️ Consistency suggestion
 DELETE FROM public.artist_organization_ids a
 USING public.artist_organization_ids b
-WHERE a.artist_id = b.artist_id
-  AND a.organization_id = b.organization_id
+WHERE a.artist_id IS NOT DISTINCT FROM b.artist_id
+  AND a.organization_id IS NOT DISTINCT FROM b.organization_id
   AND a.id > b.id;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260708200000_join_row_unique_constraints.sql` around
lines 44 - 48, The dedupe logic in the `artist_organization_ids` cleanup block
is inconsistent with the earlier `account_artist_ids` block because it uses `=`
instead of `IS NOT DISTINCT FROM`. Update the `DELETE ... USING` join condition
in this migration so the `a.artist_id`/`b.artist_id` and
`a.organization_id`/`b.organization_id` comparisons match the more defensive
pattern used in the other dedupe block, keeping the `a.id > b.id` tie-breaker
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@supabase/migrations/20260708200000_join_row_unique_constraints.sql`:
- Around line 44-48: The dedupe logic in the `artist_organization_ids` cleanup
block is inconsistent with the earlier `account_artist_ids` block because it
uses `=` instead of `IS NOT DISTINCT FROM`. Update the `DELETE ... USING` join
condition in this migration so the `a.artist_id`/`b.artist_id` and
`a.organization_id`/`b.organization_id` comparisons match the more defensive
pattern used in the other dedupe block, keeping the `a.id > b.id` tie-breaker
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: afb4e307-a3d1-44ce-9b83-626c057a6db3

📥 Commits

Reviewing files that changed from the base of the PR and between 8188eed and 0ca5636.

📒 Files selected for processing (1)
  • supabase/migrations/20260708200000_join_row_unique_constraints.sql

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="supabase/migrations/20260708200000_join_row_unique_constraints.sql">

<violation number="1" location="supabase/migrations/20260708200000_join_row_unique_constraints.sql:37">
P2: Rows with a NULL `account_id` or `artist_id` can still be duplicated after this migration, because PostgreSQL's default `UNIQUE (account_id, artist_id)` treats NULLs as distinct. Since the dedupe is already using `IS NOT DISTINCT FROM`, it looks like NULL-equivalent pairs are intended to collapse too; consider making these columns `NOT NULL` before adding the constraint, or using a NULL-aware uniqueness strategy such as `UNIQUE NULLS NOT DISTINCT` if the target Postgres version supports it.</violation>

<violation number="2" location="supabase/migrations/20260708200000_join_row_unique_constraints.sql:58">
P3: `artist_organization_ids` already has a unique index on `(artist_id, organization_id)`, so this block will add a redundant second unique index rather than closing a missing race for that table. If the constraint object is needed for metadata, consider reusing/renaming the existing unique index or otherwise accounting for `artist_organization_ids_unique`; if the database-enforced uniqueness is all that is needed, this part of the migration can be skipped.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

) THEN
ALTER TABLE "public"."account_artist_ids"
ADD CONSTRAINT "account_artist_ids_account_id_artist_id_key"
UNIQUE (account_id, artist_id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Rows with a NULL account_id or artist_id can still be duplicated after this migration, because PostgreSQL's default UNIQUE (account_id, artist_id) treats NULLs as distinct. Since the dedupe is already using IS NOT DISTINCT FROM, it looks like NULL-equivalent pairs are intended to collapse too; consider making these columns NOT NULL before adding the constraint, or using a NULL-aware uniqueness strategy such as UNIQUE NULLS NOT DISTINCT if the target Postgres version supports it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/migrations/20260708200000_join_row_unique_constraints.sql, line 37:

<comment>Rows with a NULL `account_id` or `artist_id` can still be duplicated after this migration, because PostgreSQL's default `UNIQUE (account_id, artist_id)` treats NULLs as distinct. Since the dedupe is already using `IS NOT DISTINCT FROM`, it looks like NULL-equivalent pairs are intended to collapse too; consider making these columns `NOT NULL` before adding the constraint, or using a NULL-aware uniqueness strategy such as `UNIQUE NULLS NOT DISTINCT` if the target Postgres version supports it.</comment>

<file context>
@@ -0,0 +1,60 @@
+    ) THEN
+        ALTER TABLE "public"."account_artist_ids"
+            ADD CONSTRAINT "account_artist_ids_account_id_artist_id_key"
+            UNIQUE (account_id, artist_id);
+    END IF;
+END $$;
</file context>

) THEN
ALTER TABLE "public"."artist_organization_ids"
ADD CONSTRAINT "artist_organization_ids_artist_id_organization_id_key"
UNIQUE (artist_id, organization_id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: artist_organization_ids already has a unique index on (artist_id, organization_id), so this block will add a redundant second unique index rather than closing a missing race for that table. If the constraint object is needed for metadata, consider reusing/renaming the existing unique index or otherwise accounting for artist_organization_ids_unique; if the database-enforced uniqueness is all that is needed, this part of the migration can be skipped.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/migrations/20260708200000_join_row_unique_constraints.sql, line 58:

<comment>`artist_organization_ids` already has a unique index on `(artist_id, organization_id)`, so this block will add a redundant second unique index rather than closing a missing race for that table. If the constraint object is needed for metadata, consider reusing/renaming the existing unique index or otherwise accounting for `artist_organization_ids_unique`; if the database-enforced uniqueness is all that is needed, this part of the migration can be skipped.</comment>

<file context>
@@ -0,0 +1,60 @@
+    ) THEN
+        ALTER TABLE "public"."artist_organization_ids"
+            ADD CONSTRAINT "artist_organization_ids_artist_id_organization_id_key"
+            UNIQUE (artist_id, organization_id);
+    END IF;
+END $$;
</file context>

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Pre-apply verification (read-only against prod godremdqwajrwazhbrue)

Migrations auto-apply on merge, so this is a read-only pre-check of the current prod schema to confirm the migration is safe and additive before it runs. No schema was modified.

Current state matches the PR's premise

Constraints on the two target tables today (pg_constraint):

Table Existing constraints Target UNIQUE present?
account_artist_ids PK (id); FK account_idaccounts(id) ON DELETE CASCADE; FK artist_idaccounts(id) ON DELETE CASCADE ❌ no UNIQUE(account_id, artist_id)
artist_organization_ids PK (id); FK artist_idaccounts(id) ON DELETE CASCADE; FK organization_idaccounts(id) ON DELETE CASCADE ❌ no UNIQUE(artist_id, organization_id)

So the join row genuinely can't serve as a deterministic key yet — exactly the gap this migration closes.

Dedupe blast radius: zero

Counted the rows the defensive DELETE step would remove:

Table Duplicate pairs Rows the dedupe would delete
account_artist_ids 0 0
artist_organization_ids 0 0

Confirms the PR's "none known today." The migration is purely additive on current prod data — it adds two constraints and deletes nothing.

Idempotency

Both constraint adds are pg_constraint-guarded and the target names (account_artist_ids_account_id_artist_id_key, artist_organization_ids_artist_id_organization_id_key) are absent, so the first run adds them and any re-run is a no-op. Dedupe DELETEs are no-ops at 0 duplicates.

Verdict

Safe to merge/apply: additive, zero row loss, idempotent, and it unblocks the join-row re-key (chat#1860 P1). Post-merge I can re-query to confirm both constraints landed and that a duplicate insert is rejected — read-only, on request.

🤖 Generated with Claude Code

@sweetmantech sweetmantech merged commit 71dede1 into main Jul 9, 2026
3 checks passed
@sweetmantech

Copy link
Copy Markdown
Contributor Author

Post-apply verification — migration executed in prod ✅

Ran the AFTER pass against prod (godremdqwajrwazhbrue) once the auto-apply completed, comparing to the pre-merge baseline.

Check Before (pre-merge) After (post-apply)
UNIQUE(account_id, artist_id) on account_artist_ids absent presentUNIQUE (account_id, artist_id)
UNIQUE(artist_id, organization_id) on artist_organization_ids absent presentUNIQUE (artist_id, organization_id)
Migration recorded in schema_migrations 0 20260708200000 / join_row_unique_constraints ✅ no version drift
Duplicate pairs (both tables) 0 / 0 0 / 0
artist_organization_ids rows 82 82 ✅ unchanged
account_artist_ids rows 1883 1882 ⓘ see note

Functional test (live): inserting a duplicate of an existing (account_id, artist_id) pair is now rejected with unique_violation (SQLSTATE 23505)duplicate_rejected=t. The test ran inside a DO block that always RAISEs at the end, so the transaction aborts and no row was ever committed.

Note on the account_artist_ids count (1883 → 1882)

This is not attributable to the migration's dedupe. The dedupe only removes exact-duplicate (account_id, artist_id) pairs, and both the pre-merge baseline and the post-apply state show 0 duplicate pairs — so it deleted nothing. The −1 over the ~1h window is ordinary roster churn (deleteArtist / roster edits run continuously in prod). artist_organization_ids was unchanged (82 → 82).

Verdict

Migration applied cleanly and behaves exactly as specified: two UNIQUE constraints added, zero rows removed by the dedupe, recorded under the correct version, and the constraints actively reject duplicates. The join row can now serve as a deterministic key — unblocks the api re-key (chat#1860 P1).

🤖 Generated with Claude Code

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