From 2aa3abf0354d76b4364de5e3c685c79cf9df57a9 Mon Sep 17 00:00:00 2001 From: Ryan Chen Date: Wed, 10 Jun 2026 19:13:37 -0400 Subject: [PATCH] Rebuild legacy users table to add email and drop phone NOT NULL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- migrate.go | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/migrate.go b/migrate.go index e7119c7..89bec4b 100644 --- a/migrate.go +++ b/migrate.go @@ -10,9 +10,6 @@ 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)`, - // Users may have email, phone, or both. Add whichever column is missing. - `ALTER TABLE users ADD COLUMN phone TEXT UNIQUE`, - `ALTER TABLE users ADD COLUMN email TEXT UNIQUE`, // 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). @@ -47,4 +44,56 @@ func runMigrations(database *sql.DB) { 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) + } }