From 2277f4375dede7c76d45a458d9ee65f7d7b1b685 Mon Sep 17 00:00:00 2001 From: Mlcarvalho1 Date: Tue, 26 May 2026 22:01:48 -0300 Subject: [PATCH] fix: migration runner idempotente e seeds com UNIQUE constraint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/api/internal/migration/migration.go | 18 +++++++++++++++++- apps/api/internal/migration/sql/006_quests.sql | 5 +++-- .../internal/migration/sql/008_cosmetics.sql | 18 +++++++++--------- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/apps/api/internal/migration/migration.go b/apps/api/internal/migration/migration.go index cc0e635..2adcf0f 100644 --- a/apps/api/internal/migration/migration.go +++ b/apps/api/internal/migration/migration.go @@ -33,10 +33,26 @@ var m007 string var m008 string 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} 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 { - return fmt.Errorf("migration %03d: %w", i+1, err) + return fmt.Errorf("migration %03d: %w", version, err) } } return nil diff --git a/apps/api/internal/migration/sql/006_quests.sql b/apps/api/internal/migration/sql/006_quests.sql index a1c1467..4dbe6d6 100644 --- a/apps/api/internal/migration/sql/006_quests.sql +++ b/apps/api/internal/migration/sql/006_quests.sql @@ -6,7 +6,8 @@ CREATE TABLE IF NOT EXISTS quests ( target_count INTEGER, target_pct NUMERIC(5,2), 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 ( @@ -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), ('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) -ON CONFLICT DO NOTHING; +ON CONFLICT (title, quest_type) DO NOTHING; INSERT INTO schema_migrations (version) VALUES (6) ON CONFLICT DO NOTHING; diff --git a/apps/api/internal/migration/sql/008_cosmetics.sql b/apps/api/internal/migration/sql/008_cosmetics.sql index e48efa8..aae41e8 100644 --- a/apps/api/internal/migration/sql/008_cosmetics.sql +++ b/apps/api/internal/migration/sql/008_cosmetics.sql @@ -1,7 +1,7 @@ CREATE TABLE IF NOT EXISTS cosmetics ( id SERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL, - type VARCHAR(20) NOT NULL CHECK (type IN ('hat','outfit','accessory')), + name VARCHAR(100) NOT NULL UNIQUE, + type VARCHAR(20) NOT NULL CHECK (type IN ('hair','shirt','pants','shoes','hat','accessory')), unlock_level INTEGER NOT NULL, 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 - ('Chapéu de Aventureiro', 'hat', 2, '{"color":"#8B4513","accent":"#d4a85a"}'), - ('Roupa de Marinheiro', 'outfit', 3, '{"primary":"#1e3a5f","accent":"#ffffff"}'), - ('Óculos de Sol', 'accessory', 4, '{"color":"#1a1a1a"}'), - ('Chapéu de Capitão', 'hat', 5, '{"color":"#1e293b","badge":"#ffd700"}'), - ('Roupa Dourada', 'outfit', 7, '{"primary":"#d4af37","accent":"#ffd700"}'), - ('Coroa de Campeão', 'hat', 10, '{"color":"#ffd700","gems":"#e11d48"}') -ON CONFLICT DO NOTHING; + ('Cabelo Ruivo', 'hair', 2, '{"color":"#8B1a0a"}'), + ('Camisa Marinheiro', 'shirt', 3, '{"color":"#1e3a5f"}'), + ('Calças Aventureiro', 'pants', 4, '{"color":"#2d4a1e"}'), + ('Botas de Capitão', 'shoes', 5, '{"color":"#8B4513"}'), + ('Camisa Dourada', 'shirt', 7, '{"color":"#d4af37"}'), + ('Cabelo Platinado', 'hair', 10, '{"color":"#e8e8e8"}') +ON CONFLICT (name) DO NOTHING; INSERT INTO schema_migrations (version) VALUES (8) ON CONFLICT DO NOTHING;