Skip to content

fix(categories): add unique index on family_id and name - #2981

Open
bittensorrider wants to merge 7 commits into
we-promise:mainfrom
bittensorrider:fix/929-category-uniqueness
Open

fix(categories): add unique index on family_id and name#2981
bittensorrider wants to merge 7 commits into
we-promise:mainfrom
bittensorrider:fix/929-category-uniqueness

Conversation

@bittensorrider

@bittensorrider bittensorrider commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #929: Family#investment_contributions_category (and other find_or_create_by! category paths) rescue ActiveRecord::RecordNotUnique, but the DB only indexed categories.family_id, so races could still insert duplicates.

  • Migration dedupes existing (family_id, name) collisions (keeping the oldest row) and reassigns transactions, budget_categories, subcategory parent_id, import_mappings, and rule action/condition values before deleting extras
  • Adds unique index index_categories_on_family_id_and_name
  • Test asserts the DB raises RecordNotUnique when validations are bypassed

Test plan

  • bin/rails db:migrate on a DB that may already have duplicate category names
  • bin/rails test test/models/category_test.rb
  • Trigger parallel investment contribution categorization / category import and confirm no duplicate names per family

Closes #929

Summary by CodeRabbit

  • Bug Fixes

    • Prevented duplicate category names within the same family.
    • Consolidated existing duplicates while preserving related records, budget amounts, and import mappings.
    • Resolved conflicting budget associations during cleanup.
  • Data Integrity

    • Added database enforcement to allow only one category with a given name per family.
    • Preserved category relationships when duplicates are consolidated.
  • Tests

    • Added coverage for duplicate prevention, consolidation, budget handling, and relationship preservation.

find_or_create_by! race rescues only work when the DB enforces uniqueness.
Deduplicate existing collisions, then add the unique index so parallel
sync/import jobs cannot create duplicate category names per family.

Closes we-promise#929

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The migration consolidates duplicate categories by family and name, remaps related records, removes duplicates, and adds a unique database index. Tests cover budget associations, import mappings, index restoration, and database-level uniqueness.

Changes

Category uniqueness

Layer / File(s) Summary
Deduplication plan
db/migrate/20260808120000_add_unique_index_on_categories_family_id_and_name.rb
The migration skips processing when the index exists and ranks duplicate categories by creation time and ID.
Relationship remapping
db/migrate/20260808120000_add_unique_index_on_categories_family_id_and_name.rb
Related categories, transactions, budget associations, import mappings, and rule references are remapped. Duplicate categories are deleted after cleanup.
Uniqueness enforcement and validation
db/schema.rb, test/migrations/add_unique_index_on_categories_family_id_and_name_test.rb, test/models/category_test.rb
The schema adds the unique index. Tests cover budget cleanup, import-source remapping, index restoration, and database-level rejection of duplicate categories.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Migration
  participant Categories
  participant RelatedRecords
  participant Database

  Migration->>Categories: Build duplicate-category map
  Migration->>RelatedRecords: Reparent and remap references
  Migration->>Categories: Delete duplicate categories
  Migration->>Database: Create unique index on family_id and name
  Database-->>Migration: Enforce uniqueness
Loading

Suggested reviewers: jjmata

🚥 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 and concisely describes adding the unique index on category family_id and name.
Linked Issues check ✅ Passed The migration adds database uniqueness for categories on family_id and name, directly satisfying issue #929.
Out of Scope Changes check ✅ Passed The migration, schema update, and tests support the uniqueness objective and contain no unrelated changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a595147844

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
db/migrate/20260808120000_add_unique_index_on_categories_family_id_and_name.rb (1)

24-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use two-space indentation for dedupe_categories!.

Move def dedupe_categories! to the same class indentation as def up. De-indent its body by two spaces.

As per coding guidelines, Ruby code should use 2-space indent.

🤖 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
`@db/migrate/20260808120000_add_unique_index_on_categories_family_id_and_name.rb`
around lines 24 - 25, Adjust the indentation of the private method
dedupe_categories! to match the class-level indentation of up, and de-indent the
entire method body by two spaces while preserving its implementation.

Source: Coding guidelines

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

Inline comments:
In
`@db/migrate/20260808120000_add_unique_index_on_categories_family_id_and_name.rb`:
- Around line 64-73: Update the migration’s duplicate-row deletion flow using
category_dedupe_map so each collision reconciles the duplicate
budget_categories.budgeted_spending into the keeper row before deletion,
following an explicit preservation policy. Ensure the policy handles differing
amounts without changing the budget total, and add migration coverage for
colliding rows with different spending values.
- Around line 82-88: Extend the category deduplication remapping around the
existing import_mappings UPDATE to also update import_source_mappings targets,
replacing duplicate category IDs with category_dedupe_map.keeper_id where
target_type is 'Category'. Run this remapping before the duplicate categories
are deleted, preserving valid non-Category mappings.

---

Nitpick comments:
In
`@db/migrate/20260808120000_add_unique_index_on_categories_family_id_and_name.rb`:
- Around line 24-25: Adjust the indentation of the private method
dedupe_categories! to match the class-level indentation of up, and de-indent the
entire method body by two spaces while preserving its implementation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 59f3de77-71fb-4589-8f54-784b90d7a1ed

📥 Commits

Reviewing files that changed from the base of the PR and between eb928ad and a595147.

📒 Files selected for processing (3)
  • db/migrate/20260808120000_add_unique_index_on_categories_family_id_and_name.rb
  • db/schema.rb
  • test/models/category_test.rb

bittensorrider and others added 3 commits August 8, 2026 23:36
Match Category::Merger by summing duplicate budget_categories amounts
into the keeper row before deleting (budget_id, category_id) collisions.

Co-authored-by: Cursor <cursoragent@cursor.com>
Document the additive collision policy and add migration tests for
differing keeper/duplicate amounts plus non-colliding reassignment.

Co-authored-by: Cursor <cursoragent@cursor.com>
Update target_id via category_dedupe_map before deleting duplicate
categories so session import mappings stay valid; leave non-Category
targets untouched.

Co-authored-by: Cursor <cursoragent@cursor.com>

@jjmata jjmata left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict: Well thought-out migration overall, but there's a plausible data-migration failure edge case in the budget_categories reassignment step.

  • db/migrate/20260808120000_add_unique_index_on_categories_family_id_and_name.rb (final UPDATE budget_categories SET category_id = map.keeper_id ... block, the "reassign remaining non-colliding duplicate rows" step): if a family has two or more duplicate-named categories that each have a budget_categories row for the same budget, and the keeper category has no row for that budget, neither duplicate collides with an existing keeper row, so both are skipped by the earlier "sum into keeper" and "delete colliding" steps. The final blanket reassignment then tries to give both rows (budget_id, category_id=keeper_id) in a single UPDATE, which violates the very unique index this migration is adding (index_budget_categories_on_budget_id_and_category_id) and would fail the migration for that family. The test suite only covers the single-duplicate-per-budget case, so this wouldn't be caught. Consider extending the "sum into keeper" logic to also aggregate duplicate-vs-duplicate collisions (not just duplicate-vs-keeper) before the final reassignment.
  • Everything else — the dedup ordering (oldest row kept via created_at ASC, id ASC), remapping of transactions, subcategory parent_id, import_mappings/import_source_mappings, and rule_actions/rule_conditions — looks correct, and the migration test coverage for the budget_categories sum/reassign/import-mapping paths is solid (Minitest, no RSpec, per convention).

Generated by Claude Code

When several duplicate-named categories each have a budget_categories
row for the same budget and the keeper has none, the final UPDATE would
violate the (budget_id, category_id) unique index. Sum extras into one
survivor and delete the rest first.

Co-authored-by: Cursor <cursoragent@cursor.com>
@bittensorrider

Copy link
Copy Markdown
Contributor Author

@jjmata Good catch — that multi-duplicate / no-keeper-row path would have failed the migration on index_budget_categories_on_budget_id_and_category_id.

Fixed by collapsing duplicate-vs-duplicate budget_categories for the same (budget_id, keeper_id) before the final remap: pick one survivor (id ASC), sum the others’ budgeted_spending into it, delete the extras, then reassign. Added a regression test covering that case (bin/rails test test/migrations/add_unique_index_on_categories_family_id_and_name_test.rb — 4 runs, 0 failures).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@test/migrations/add_unique_index_on_categories_family_id_and_name_test.rb`:
- Around line 70-104: Add an assertion immediately after `@migration.up` in the
test “collapses multiple duplicate budget_categories for the same budget when
keeper has none” that verifies the unique index has been restored, using the
existing index-checking helper or established schema assertion. Keep
ensure_unique_index! only as cleanup and preserve the existing deletion and
consolidation assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 52c1a21a-23c7-43a4-a426-e2bc965c3929

📥 Commits

Reviewing files that changed from the base of the PR and between e74d920 and 8b5d5d9.

📒 Files selected for processing (2)
  • db/migrate/20260808120000_add_unique_index_on_categories_family_id_and_name.rb
  • test/migrations/add_unique_index_on_categories_family_id_and_name_test.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • db/migrate/20260808120000_add_unique_index_on_categories_family_id_and_name.rb

Copy link
Copy Markdown
Collaborator

Bot triage: this is a DB-level unique constraint per convention, and the migration is safe on data that already has duplicates — it dedupes (family_id, name) collisions (keeping the oldest row by created_at, id) and remaps transactions, subcategory parent_id, import_mappings, import_source_mappings, rule_actions/rule_conditions before deleting extras, all inside the up migration, before adding the index.

The one real edge case — two-or-more duplicate categories sharing a budget_categories row for the same budget when the keeper has none, which would've violated index_budget_categories_on_budget_id_and_category_id mid-migration — was caught by @jjmata's review and fixed (collapses duplicate-vs-duplicate rows into one survivor before the final reassignment), with a regression test (collapses multiple duplicate budget_categories...) confirming it. CI is green. Looks safe to merge.


Generated by Claude Code

Resolve db/schema.rb version conflict by keeping main's latest
migration stamp (2026_08_09_000000) while retaining the categories
unique index from this branch.

Co-authored-by: Cursor <cursoragent@cursor.com>
@bittensorrider

Copy link
Copy Markdown
Contributor Author

Thanks @sure-design — agreed on the merge readiness (including the multi-dup budget_categories collapse + regression test from @jjmata’s review).

Resolved the db/schema.rb conflict against latest main: kept schema version 2026_08_09_000000 (Wise import_all_history migration) and retained index_categories_on_family_id_and_name. Ready for approve/merge.

Copy link
Copy Markdown
Collaborator

Update: new commit since the last triage (cef11d96, merge from main resolving the db/schema.rb version conflict while keeping index_categories_on_family_id_and_name). CI on this head is green (lint/test_unit/test_system/scan_js/scan_ruby all success, 05:45–05:52 UTC). No open review comments remain unaddressed. Looks ready to merge.


Generated by 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.

Migration needed?

3 participants