fix: migration runner idempotente e seeds com UNIQUE constraint

Runner agora checa schema_migrations antes de rodar cada SQL — sem
reexecução de seeds a cada startup. Quests: UNIQUE(title, quest_type).
Cosmetics: UNIQUE(name), tipos corrigidos para hair/shirt/pants/shoes.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
2026-05-26 22:01:48 -03:00
co-authored by Claude Sonnet 4.6
parent c1964ea036
commit 2277f4375d
3 changed files with 29 additions and 12 deletions
+17 -1
View File
@@ -33,10 +33,26 @@ var m007 string
var m008 string var m008 string
func Run(ctx context.Context, pool *pgxpool.Pool) error { func Run(ctx context.Context, pool *pgxpool.Pool) error {
// Bootstrap: ensure schema_migrations table exists before checking versions.
if _, err := pool.Exec(ctx, `
CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
applied_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
)
`); err != nil {
return fmt.Errorf("bootstrap schema_migrations: %w", err)
}
migrations := []string{m001, m002, m003, m004, m005, m006, m007, m008} migrations := []string{m001, m002, m003, m004, m005, m006, m007, m008}
for i, sql := range migrations { for i, sql := range migrations {
version := i + 1
var applied bool
_ = pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE version=$1)`, version).Scan(&applied)
if applied {
continue
}
if _, err := pool.Exec(ctx, sql); err != nil { if _, err := pool.Exec(ctx, sql); err != nil {
return fmt.Errorf("migration %03d: %w", i+1, err) return fmt.Errorf("migration %03d: %w", version, err)
} }
} }
return nil return nil
@@ -6,7 +6,8 @@ CREATE TABLE IF NOT EXISTS quests (
target_count INTEGER, target_count INTEGER,
target_pct NUMERIC(5,2), target_pct NUMERIC(5,2),
xp_reward INTEGER NOT NULL DEFAULT 50, xp_reward INTEGER NOT NULL DEFAULT 50,
active BOOLEAN NOT NULL DEFAULT TRUE active BOOLEAN NOT NULL DEFAULT TRUE,
UNIQUE (title, quest_type)
); );
CREATE TABLE IF NOT EXISTS player_quests ( CREATE TABLE IF NOT EXISTS player_quests (
@@ -27,6 +28,6 @@ INSERT INTO quests (title, description, quest_type, target_count, target_pct, xp
('Categorizador Ativo', 'Categorize 5 transações hoje', 'daily', 5, NULL, 30), ('Categorizador Ativo', 'Categorize 5 transações hoje', 'daily', 5, NULL, 30),
('Controlador Semanal', 'Registre ao menos 3 transações esta semana', 'weekly', 3, NULL, 40), ('Controlador Semanal', 'Registre ao menos 3 transações esta semana', 'weekly', 3, NULL, 40),
('Mês Sem Surpresas', 'Cubra todas as recorrências do mês', 'monthly', NULL, NULL, 150) ('Mês Sem Surpresas', 'Cubra todas as recorrências do mês', 'monthly', NULL, NULL, 150)
ON CONFLICT DO NOTHING; ON CONFLICT (title, quest_type) DO NOTHING;
INSERT INTO schema_migrations (version) VALUES (6) ON CONFLICT DO NOTHING; INSERT INTO schema_migrations (version) VALUES (6) ON CONFLICT DO NOTHING;
@@ -1,7 +1,7 @@
CREATE TABLE IF NOT EXISTS cosmetics ( CREATE TABLE IF NOT EXISTS cosmetics (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL UNIQUE,
type VARCHAR(20) NOT NULL CHECK (type IN ('hat','outfit','accessory')), type VARCHAR(20) NOT NULL CHECK (type IN ('hair','shirt','pants','shoes','hat','accessory')),
unlock_level INTEGER NOT NULL, unlock_level INTEGER NOT NULL,
css_data JSONB NOT NULL DEFAULT '{}' css_data JSONB NOT NULL DEFAULT '{}'
); );
@@ -14,12 +14,12 @@ CREATE TABLE IF NOT EXISTS player_cosmetics (
); );
INSERT INTO cosmetics (name, type, unlock_level, css_data) VALUES INSERT INTO cosmetics (name, type, unlock_level, css_data) VALUES
('Chapéu de Aventureiro', 'hat', 2, '{"color":"#8B4513","accent":"#d4a85a"}'), ('Cabelo Ruivo', 'hair', 2, '{"color":"#8B1a0a"}'),
('Roupa de Marinheiro', 'outfit', 3, '{"primary":"#1e3a5f","accent":"#ffffff"}'), ('Camisa Marinheiro', 'shirt', 3, '{"color":"#1e3a5f"}'),
('Óculos de Sol', 'accessory', 4, '{"color":"#1a1a1a"}'), ('Calças Aventureiro', 'pants', 4, '{"color":"#2d4a1e"}'),
('Chapéu de Capitão', 'hat', 5, '{"color":"#1e293b","badge":"#ffd700"}'), ('Botas de Capitão', 'shoes', 5, '{"color":"#8B4513"}'),
('Roupa Dourada', 'outfit', 7, '{"primary":"#d4af37","accent":"#ffd700"}'), ('Camisa Dourada', 'shirt', 7, '{"color":"#d4af37"}'),
('Coroa de Campeão', 'hat', 10, '{"color":"#ffd700","gems":"#e11d48"}') ('Cabelo Platinado', 'hair', 10, '{"color":"#e8e8e8"}')
ON CONFLICT DO NOTHING; ON CONFLICT (name) DO NOTHING;
INSERT INTO schema_migrations (version) VALUES (8) ON CONFLICT DO NOTHING; INSERT INTO schema_migrations (version) VALUES (8) ON CONFLICT DO NOTHING;