diff --git a/backend/cmd/manual_migrate/main.go b/backend/cmd/manual_migrate/main.go index eafa9bf..2c19e1d 100644 --- a/backend/cmd/manual_migrate/main.go +++ b/backend/cmd/manual_migrate/main.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "os" + "sort" "strings" "github.com/joho/godotenv" @@ -44,60 +45,69 @@ func main() { } defer db.Close() - // List of migrations to run (in order) - migrations := []string{ - "024_create_external_services_credentials.sql", - "025_create_chat_tables.sql", - "026_create_system_settings.sql", - "027_create_email_system.sql", - "028_add_avatar_url_to_users.sql", + // Discover migrations directory from several probable locations + possibleDirs := []string{ + "migrations", + "backend/migrations", + "../migrations", + "/home/yamamoto/lab/gohorsejobs/backend/migrations", } - for _, migFile := range migrations { - log.Printf("Processing migration: %s", migFile) + var migrationsDir string + for _, d := range possibleDirs { + if fi, err := os.Stat(d); err == nil && fi.IsDir() { + migrationsDir = d + break + } + } - // Try multiple paths - paths := []string{ - "migrations/" + migFile, - "backend/migrations/" + migFile, - "../migrations/" + migFile, - "/home/yamamoto/lab/gohorsejobs/backend/migrations/" + migFile, + if migrationsDir == "" { + log.Fatal("Could not find migrations directory; looked in common locations") + } + + entries, err := os.ReadDir(migrationsDir) + if err != nil { + log.Fatalf("Failed reading migrations dir %s: %v", migrationsDir, err) + } + + var files []string + for _, e := range entries { + if e.IsDir() { + continue + } + name := e.Name() + if strings.HasSuffix(name, ".sql") { + files = append(files, name) + } + } + + // Sort filenames to ensure chronological order + sort.Strings(files) + + for _, migFile := range files { + fullPath := migrationsDir + "/" + migFile + log.Printf("Processing migration: %s (from %s)", migFile, fullPath) + + content, err := os.ReadFile(fullPath) + if err != nil { + log.Fatalf("Could not read migration %s: %v", fullPath, err) } - var content []byte - var readErr error - - for _, p := range paths { - content, readErr = os.ReadFile(p) - if readErr == nil { - log.Printf("Found migration at: %s", p) - break - } - } - - if content == nil { - log.Fatalf("Could not find migration file %s. Last error: %v", migFile, readErr) - } - - statements := strings.Split(string(content), ";") - for _, stmt := range statements { - trimmed := strings.TrimSpace(stmt) - if trimmed == "" { + // Execute the whole file. lib/pq supports multi-statement Exec. + sqlText := string(content) + log.Printf("Executing migration file %s", fullPath) + _, err = db.Exec(sqlText) + if err != nil { + errStr := err.Error() + // Tolerable errors: object already exists, column doesn't exist in some contexts, + // or duplicate key when updates are guarded by intent. Log and continue. + if strings.Contains(errStr, "already exists") || strings.Contains(errStr, "column") && strings.Contains(errStr, "does not exist") || strings.Contains(errStr, "duplicate key value violates unique constraint") { + log.Printf("Warning while applying %s: %v", migFile, err) continue } - log.Printf("Executing: %s", trimmed) - _, err = db.Exec(trimmed) - if err != nil { - if strings.Contains(err.Error(), "already exists") { - log.Printf("Warning (ignored): %v", err) - } else { - log.Printf("FAILED executing: %s\nError: %v", trimmed, err) - // Verify if we should stop. For now, continue best effort or fail? - // Use fatal for critical schema errors not "already exists" - log.Fatal(err) - } - } + log.Fatalf("Failed applying migration %s: %v", migFile, err) } + log.Printf("Migration %s applied successfully", migFile) } diff --git a/backend/migrations/004_create_prefectures_cities_tables.sql b/backend/migrations/004_create_prefectures_cities_tables.sql index 749a9ee..a7d322c 100755 --- a/backend/migrations/004_create_prefectures_cities_tables.sql +++ b/backend/migrations/004_create_prefectures_cities_tables.sql @@ -11,17 +11,29 @@ CREATE TABLE IF NOT EXISTS regions ( CREATE TABLE IF NOT EXISTS cities ( id SERIAL PRIMARY KEY, - region_id INT NOT NULL, + region_id INT, name VARCHAR(100) NOT NULL, - - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - - FOREIGN KEY (region_id) REFERENCES regions(id) ON DELETE CASCADE + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); --- Indexes -CREATE INDEX idx_regions_country ON regions(country_code); -CREATE INDEX idx_cities_region ON cities(region_id); +-- Ensure column and constraints exist when table already existed without them +ALTER TABLE cities + ADD COLUMN IF NOT EXISTS region_id INT; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name + WHERE tc.table_name = 'cities' AND tc.constraint_type = 'FOREIGN KEY' AND kcu.column_name = 'region_id' + ) THEN + ALTER TABLE cities ADD CONSTRAINT fk_cities_region FOREIGN KEY (region_id) REFERENCES regions(id) ON DELETE CASCADE; + END IF; +END$$; + +-- Indexes (safe if table/column already existed) +CREATE INDEX IF NOT EXISTS idx_regions_country ON regions(country_code); +CREATE INDEX IF NOT EXISTS idx_cities_region ON cities(region_id); -- Comments COMMENT ON TABLE regions IS 'Global Regions (States, Provinces, Prefectures)'; diff --git a/backend/migrations/032_update_superadmin_lol.sql b/backend/migrations/032_update_superadmin_lol.sql index efdedf9..d08cdf9 100644 --- a/backend/migrations/032_update_superadmin_lol.sql +++ b/backend/migrations/032_update_superadmin_lol.sql @@ -3,7 +3,7 @@ -- Increase status column length to support 'force_change_password' (21 chars) ALTER TABLE users ALTER COLUMN status TYPE VARCHAR(50); - +-- Update only the intended superadmin identifier to avoid unique constraint conflicts. UPDATE users SET identifier = 'lol', @@ -12,4 +12,14 @@ SET name = 'Dr. Horse Expert', status = 'force_change_password', updated_at = CURRENT_TIMESTAMP -WHERE identifier = 'superadmin' OR email = 'admin@gohorsejobs.com'; +WHERE identifier = 'superadmin'; + +-- If there is a separate user with email 'admin@gohorsejobs.com', update non-identifier fields only +UPDATE users +SET + email = 'lol@gohorsejobs.com', + full_name = 'Dr. Horse Expert', + name = 'Dr. Horse Expert', + status = 'force_change_password', + updated_at = CURRENT_TIMESTAMP +WHERE email = 'admin@gohorsejobs.com' AND identifier <> 'lol';