Files
bbq/migrate.go
T
ryan cf2ef77bb0 Add yes/no RSVP answers with required login
RSVPs now carry a status ('yes'/'no'). When auth is enabled, answering
requires login for all events, not just capped ones. Answers are deduped
one per user and re-answering updates in place. A 'no' zeroes plus_one,
is excluded from Going counts and cap math, frees the person's slot
claims, and shows in a separate "Can't make it" list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 07:59:33 -04:00

105 lines
3.7 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
)`,
// Attendee cap (0 = uncapped); capped events require login to RSVP.
`ALTER TABLE events ADD COLUMN attendee_cap INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE rsvps ADD COLUMN user_id INTEGER REFERENCES users(id)`,
// RSVPs can now be a yes or a no.
`ALTER TABLE rsvps ADD COLUMN status TEXT NOT NULL DEFAULT 'yes'`,
}
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)
}
}