Skip to content

fix(migration): prevent UNIQUE constraint crash in migration 103 when user has permissions on both dup and keeper channel#3805

Merged
Yeraze merged 1 commit into
mainfrom
claude/determined-planck-oc1qlm
Jun 27, 2026
Merged

fix(migration): prevent UNIQUE constraint crash in migration 103 when user has permissions on both dup and keeper channel#3805
Yeraze merged 1 commit into
mainfrom
claude/determined-planck-oc1qlm

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 27, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #3804 — migration 103 (consolidate_mqtt_channels) crashes with SqliteError: UNIQUE constraint failed: channel_database_permissions.user_id, channel_database_permissions.channel_database_id when upgrading to 4.12.0 if a user holds permissions on both the duplicate and the keeper channel_database row.

Root Cause

Part A of migration 103 merges duplicate channel_database rows and then reassigns permissions:

UPDATE channel_database_permissions SET channel_database_id = keep WHERE channel_database_id = dup

If a user already has a permission row on keep, this UPDATE creates a second (user_id, channel_database_id=keep) row, violating the unique constraint. The existing test only seeded a user with permissions on the dup — not on both — so the collision case was never caught.

Changes

  • src/server/migrations/103_consolidate_mqtt_channels.ts — Before the UPDATE, add a DELETE that removes the dup-side row for any user who already holds a row on keep. Applied to all three backends:

    • SQLite: direct sub-select on user_id
    • PostgreSQL: same pattern with quoted "userId" / "channelDatabaseId" column names
    • MySQL: wraps the sub-select in a derived table (AS tmp) to avoid MySQL Error 1093 (cannot reference the target table of DELETE in a sub-select)
  • src/server/migrations/103_consolidate_mqtt_channels.test.ts — New test case seeds user 99 with permissions on both the dup (id=2) and the keeper (id=1), then asserts the migration completes without throwing and the user retains exactly one permission row on the keeper.

Test Results

All 9 tests pass (8 original + 1 new collision test).

Workaround for Existing Broken Installs

Users who are already unable to start after upgrading to 4.12.0 can apply the same patch manually to /opt/meshmonitor/dist/server/migrations/103_consolidate_mqtt_channels.js, then run systemctl start meshmonitor.


Generated by Claude Code

…tion 103

When a user already has a `channel_database_permissions` row on both the
keeper and the duplicate channel, the `UPDATE channel_database_permissions
SET channel_database_id = keep WHERE channel_database_id = dup` violated
the UNIQUE(user_id, channel_database_id) constraint, crashing migration 103
and making the app unbootable (issue #3804).

Fix: add a DELETE that removes the dup-side row for any user who already
holds the keep-side row before the UPDATE, for all three backends (SQLite,
PostgreSQL, MySQL). The MySQL version wraps the subquery in a derived table
to avoid MySQL's Error 1093 restriction on self-referencing DELETE.

Also add a test case seeding the exact collision scenario to prevent
regressions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNiMxHa13x5DpyjGqZNtSP
@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: fix(migration): prevent UNIQUE constraint crash in migration 103

Summary: This is a targeted, well-scoped fix for a real crash in migration 103 (consolidate_mqtt_channels) affecting users upgrading to 4.12.0 with specific permission configurations.


Code Quality ✅

The fix is clean and idiomatic for each database backend:

  • SQLite (103_consolidate_mqtt_channels.ts:68-72): Uses a sub-select within the DELETE to remove conflicting dup-side permission rows before the UPDATE. Correct approach.
  • PostgreSQL (lines 163-166): Mirrors the SQLite pattern with correct quoted "userId" / "channelDatabaseId" camelCase column names. Consistent with PG dialect requirements.
  • MySQL (lines 249-252): Uses the derived table workaround (AS tmp) to circumvent MySQL Error 1093. This is the standard and correct fix for this MySQL limitation.

The comments clearly explain why the DELETE is needed before the UPDATE, which is important for future maintainers.


Correctness ✅

The fix is logically sound:

  1. DELETE the dup-side permission row for any user who already has a row on keep
  2. UPDATE remaining dup rows (users who only had dup access) to point to keep
  3. DELETE the dup channel row

This preserves the invariant that users who had access to either duplicate retain access to the canonical keeper. No permission data is lost — users on keep already have access, and users on dup (not keep) are correctly migrated.


Test Coverage ✅

The new test (lines 135-168) correctly covers the collision scenario:

  • Seeds user 99 with permissions on both id=1 (keeper) and id=2 (dup) — exactly the bug scenario
  • Asserts no UNIQUE constraint exception is thrown
  • Asserts exactly one permission row remains, pointing to the keeper

One minor observation: the collision test creates a new in-memory DB (db2) without calling seedDb, which means it only tests the minimal schema needed for the collision. This is intentional and reasonable — it isolates the fix without depending on the broader fixture. The test is clearly documented and works correctly.


Potential Issues

Minor: The test at line 138 uses const db2 = new Database(':memory:') outside of beforeEach, so db2 won't be automatically closed/cleaned up if the test suite grows. In practice this is fine for in-memory SQLite (it's garbage collected), but it's worth noting for consistency.

No security concerns. The DELETE uses parameterized queries consistently across all three backends. No raw string concatenation or user-controlled input.

No performance concerns. The DELETE sub-select runs once per (dup, keep) pair, which is the same cardinality as the existing permission UPDATE. No new full-table scans are introduced.


Verdict

LGTM. The fix is minimal, correct, well-tested for all three database backends, and properly handles the edge case. The MySQL derived-table workaround is the right approach. The PR description also helpfully includes a manual workaround for users already stuck.

@Yeraze Yeraze merged commit bafa3a9 into main Jun 27, 2026
19 checks passed
@Yeraze Yeraze deleted the claude/determined-planck-oc1qlm branch June 27, 2026 01:39
Yeraze added a commit that referenced this pull request Jun 27, 2026
Hotfix for #3804: migration 103 UNIQUE-constraint startup crash when upgrading
to 4.12.0 with MQTT channel permissions (fix merged in #3805). Bumps all five
version files and adds the [4.12.1] CHANGELOG entry.


Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4

Co-authored-by: Claude Opus 4.8 (1M context) <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.

[BUG] Migration 103 crashes with UNIQUE constraint on channel_database_permissions when upgrading to 4.12.0

2 participants