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
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
@@ -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;
@@ -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;