Files
bbq/migrate.go
T
ryan 2aa3abf035 Rebuild legacy users table to add email and drop phone NOT NULL
The migration ALTER TABLE users ADD COLUMN email TEXT UNIQUE always
failed ("Cannot add a UNIQUE column" — SQLite doesn't support it), so
phone-era DBs never got an email column and verify 500'd with "no such
column: email". Even with the column added, phone NOT NULL would reject
email-only users. SQLite can't ALTER either change, so detect the legacy
shape (missing email column) and rebuild the table, preserving user ids
and sessions (FKs off so DROP TABLE doesn't cascade).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 19:13:37 -04:00

100 lines
3.4 KiB
Go

package main
import (
"database/sql"
"log"
"strings"
)
func runMigrations(database *sql.DB) {
migrations := []string{
`ALTER TABLE events ADD COLUMN description TEXT DEFAULT ''`,
`ALTER TABLE events ADD COLUMN user_id INTEGER REFERENCES users(id)`,
// Verification codes use a generic identifier column.
`ALTER TABLE verification_codes ADD COLUMN identifier TEXT NOT NULL DEFAULT ''`,
// Indexes for auth tables (created here so they run after column migrations).
`CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id)`,
`CREATE INDEX IF NOT EXISTS idx_verification_codes_identifier ON verification_codes(identifier)`,
// Dedupe RSVPs: keep earliest per (event_id, name).
`DELETE FROM rsvps WHERE id NOT IN (SELECT MIN(id) FROM rsvps GROUP BY event_id, name COLLATE NOCASE)`,
// Plus-one tracking for RSVPs.
`ALTER TABLE rsvps ADD COLUMN plus_one INTEGER NOT NULL DEFAULT 0`,
// Drop legacy phone/email columns from verification_codes. They were
// NOT NULL with no default, so inserts using only identifier failed.
`DROP INDEX IF EXISTS idx_verification_codes_phone`,
`DROP INDEX IF EXISTS idx_verification_codes_email`,
`ALTER TABLE verification_codes DROP COLUMN phone`,
`ALTER TABLE verification_codes DROP COLUMN email`,
// SMS consent logging for Twilio compliance.
`CREATE TABLE IF NOT EXISTS sms_consents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
phone TEXT NOT NULL,
ip_address TEXT NOT NULL DEFAULT '',
user_agent TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)`,
}
for _, m := range migrations {
_, err := database.Exec(m)
if err != nil {
if strings.Contains(err.Error(), "duplicate column name") ||
strings.Contains(err.Error(), "no such column") {
continue
}
log.Printf("migration warning: %v", err)
}
}
rebuildLegacyUsersTable(database)
}
// rebuildLegacyUsersTable migrates users tables from the phone-auth era
// (phone NOT NULL, no email column). SQLite can't add a UNIQUE column or
// drop NOT NULL via ALTER TABLE, so the table has to be rebuilt. Detected
// by the missing email column.
func rebuildLegacyUsersTable(database *sql.DB) {
var hasEmail int
err := database.QueryRow(
`SELECT COUNT(*) FROM pragma_table_info('users') WHERE name = 'email'`,
).Scan(&hasEmail)
if err != nil || hasEmail > 0 {
return
}
// Disable FK enforcement so DROP TABLE users doesn't cascade-delete
// sessions. main.go sets MaxOpenConns(1), so these pragmas apply to
// the same connection the transaction runs on.
database.Exec(`PRAGMA foreign_keys=OFF`)
defer database.Exec(`PRAGMA foreign_keys=ON`)
tx, err := database.Begin()
if err != nil {
log.Printf("users rebuild: %v", err)
return
}
defer tx.Rollback()
stmts := []string{
`CREATE TABLE users_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
phone TEXT UNIQUE,
email TEXT UNIQUE,
name TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)`,
`INSERT INTO users_new (id, phone, name, created_at)
SELECT id, phone, name, created_at FROM users`,
`DROP TABLE users`,
`ALTER TABLE users_new RENAME TO users`,
}
for _, stmt := range stmts {
if _, err := tx.Exec(stmt); err != nil {
log.Printf("users rebuild: %v", err)
return
}
}
if err := tx.Commit(); err != nil {
log.Printf("users rebuild: %v", err)
}
}