Drop the leftover per-user unique constraint on SQLite (fixes #133) #134
No reviewers
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
qwc-open/asiakirjat!134
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/sqlite-project-access-unique"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes #133. Independent of #132 — different files, either order merges cleanly.
The bug
Migration
002_auth_groupswidens uniqueness onproject_accessto includesource, so a user can hold a manual grant alongside a synced LDAP or OAuth2 one. On SQLite it never took effect:The constraint actually came from the table-level
UNIQUE(project_id, user_id)declared insideCREATE TABLEin001_initial.DROP INDEXcannot remove the auto-index behind an inline constraint (sqlite_autoindex_project_access_1), so the statement succeeded silently and both constraints stayed live.SQLite is the default driver, so most deployments carry the wrong schema. Any second-source
Grantfails:The
ON CONFLICT(project_id, user_id, source)upsert targets the other index, so the violation is never handled. Group mappings silently fail to apply to users who already hold a manual grant, and granting manual access to an LDAP-granted user fails the same way.Tests first
All three tests were written against
mainand fail there with the exact production error:internal/database/project_access_schema_test.go—TestProjectAccessUniqueOnSourcereadsPRAGMA index_list/index_infoand asserts no unique index constrains(project_id, user_id)withoutsource. Onmain:unique index "sqlite_autoindex_project_access_1" still constrains (project_id, user_id) without source.internal/store/sql/access_source_test.go—TestGrantAllowsOneRowPerSourceandTestRevokeBySourceLeavesOtherSourcesgrant the same user from two sources. Onmain:UNIQUE constraint failed.TestGrantUpdatesRoleWithinSourcepasses both before and after — it guards the upsert path the new key backs, so the rebuild can't silently turn re-grants into duplicates.I re-ran the suite with the new SQLite migration files moved aside to confirm they go red without the fix and green with it.
The fix
011_project_access_source_uniquerebuildsproject_accesswithout the inline constraint — the only way to drop one in SQLite — and recreatesidx_project_access_source. Existing rows are already unique per(project_id, user_id), which is stricter than the target constraint, so every row carries over; no data is dropped or merged on the way up.The down migration collapses multi-source rows back to one per user (keeping the manual grant where there is one) so the old constraint can hold again.
TestProjectAccessSourceUniqueRoundTripmigrates down to 010 and back up, asserting both the collapse and that the constraint doesn't reappear.On the other dialects
Postgres and MySQL dropped their old constraint correctly in 002, so nothing is broken there. Their
011exists only to keep numbering aligned: Postgres runs an idempotentDROP CONSTRAINT IF EXISTS(a no-op on a correct database, a repair on a stale one), MySQL is a bareSELECT 1sinceDROP INDEXhas noIF EXISTSform to make a repair safe.Both are trivial, but neither is exercised locally — the multi-dialect migration test is still blocked on CI Docker access (audit item H-12). If you'd rather not run untested SQL against Postgres/MySQL at all, say so and I'll make 011 SQLite-only and accept the numbering gap.
Assisted by Claude Opus 5.