clearLegacyColumnUniqueness removes any single-column UNIQUE constraint or index on the email / phone_number columns of authorizer_users and authorizer_otps, regardless of its name. See the call site in NewProvider for why this must run before AutoMigrate. Best-effort and non-fatal: fresh installs,
(db *gorm.DB, log *zerolog.Logger)
| 148 | // installs, and catalogs without information_schema (sqlite), simply find |
| 149 | // nothing to drop. |
| 150 | func clearLegacyColumnUniqueness(db *gorm.DB, log *zerolog.Logger) { |
| 151 | // Run through a discarding GORM logger. The information_schema probe below |
| 152 | // intentionally errors on databases without that catalog (sqlite); GORM's |
| 153 | // default logger writes such errors to os.Stdout, which corrupts the MCP |
| 154 | // server's stdio JSON-RPC stream. We already surface anything actionable via |
| 155 | // the zerolog `log` (stderr), so GORM's own logging here is redundant. |
| 156 | db = db.Session(&gorm.Session{Logger: logger.Discard}) |
| 157 | |
| 158 | targets := map[string]bool{"email": true, "phone_number": true} |
| 159 | tables := []struct { |
| 160 | model any |
| 161 | name string |
| 162 | }{ |
| 163 | {&schemas.User{}, "authorizer_users"}, |
| 164 | {&schemas.OTP{}, "authorizer_otps"}, |
| 165 | } |
| 166 | |
| 167 | for _, t := range tables { |
| 168 | // 1) Drop single-column UNIQUE *constraints* by their real names, read |
| 169 | // from the same catalog GORM uses (information_schema). This matches |
| 170 | // ANY name — ..._key, uni_..., idx_..., or custom — which is what |
| 171 | // actually prevents the DROP CONSTRAINT "uni_..." 42704 abort. A |
| 172 | // constraint's backing index cannot be removed with DROP INDEX, so it |
| 173 | // must go via DropConstraint. |
| 174 | var rows []struct { |
| 175 | ConstraintName string `gorm:"column:constraint_name"` |
| 176 | ColumnName string `gorm:"column:column_name"` |
| 177 | } |
| 178 | const q = `SELECT tc.constraint_name AS constraint_name, kcu.column_name AS column_name |
| 179 | FROM information_schema.table_constraints tc |
| 180 | JOIN information_schema.key_column_usage kcu |
| 181 | ON tc.constraint_name = kcu.constraint_name |
| 182 | AND tc.table_schema = kcu.table_schema |
| 183 | AND tc.table_name = kcu.table_name |
| 184 | WHERE tc.constraint_type = 'UNIQUE' AND tc.table_name = ?` |
| 185 | if err := db.Raw(q, t.name).Scan(&rows).Error; err != nil { |
| 186 | // sqlite and other catalogs without information_schema land here; they |
| 187 | // are not affected by the GORM unique-reconciliation behaviour. |
| 188 | log.Debug().Err(err).Str("table", t.name).Msg("skip legacy unique-constraint scan") |
| 189 | } else { |
| 190 | columnsByConstraint := map[string][]string{} |
| 191 | for _, r := range rows { |
| 192 | columnsByConstraint[r.ConstraintName] = append(columnsByConstraint[r.ConstraintName], r.ColumnName) |
| 193 | } |
| 194 | for name, cols := range columnsByConstraint { |
| 195 | if len(cols) == 1 && targets[cols[0]] { |
| 196 | if err := db.Migrator().DropConstraint(t.model, name); err != nil { |
| 197 | log.Debug().Err(err).Str("constraint", name).Msg("failed to drop legacy unique constraint") |
| 198 | } |
| 199 | } |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | // 2) Drop any standalone single-column UNIQUE *index* (CREATE UNIQUE |
| 204 | // INDEX, not a constraint) on the same columns. These do not cause the |
| 205 | // abort — GORM reads column-uniqueness from table_constraints only — |
| 206 | // but removing them makes the column genuinely non-unique, matching the |
| 207 | // v2 application-layer model. GetIndexes already excludes |
no test coverage detected