Prep for the upcoming E2E Playwright CI workflow. The full seed (1200 users, 5000 tracks, 100k play events, 10k messages, etc.) takes ~60s and produces a lot of fixture data the suite never reads. A CI run just needs the 5 test accounts the auth fixture logs in as (admin/artist/user/mod/new) plus a small content set so player / playlist tests have something to render. New flag: go run ./cmd/tools/seed --ci CIConfig (cmd/tools/seed/config.go): - TotalUsers = 5 (== len(testAccounts), so SeedUsers' "remaining" branch is a no-op — only the 5 hardcoded accounts get inserted). - Tracks = 10, Playlists = 3 (covers player + playlist suites). - Albums = 0, all social/chat/live/marketplace/analytics/etc. = 0. main.go gates the heavy seeders (Social / Chat / Live / Marketplace / Analytics / Content / Moderation / Notifications / Misc) behind `if !cfg.CIMode`, prints a one-line "skipping ..." banner so the run log makes the choice obvious. The Users / Tracks / Playlists path is unchanged — same code, same validation pass at the end. Time: ~5s in CI mode (bcrypt cost 12 × 5 + a handful of bulk inserts) vs the ~60s minimal mode and ~5min full mode, measured locally against a tmpfs Postgres. Validate() and the SUMMARY printout work unchanged — empty tables just show "0 rows", and the orphan-FK checks remain useful (and pass trivially when the heavy seeders are skipped). modeName() returns "CI" so the boot banner reflects the choice. go build ./... clean. Help output: -ci Bare-minimum seed for E2E CI (...) -minimal Use reduced volumes (50 users, 200 tracks) for fast dev Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
224 lines
7.7 KiB
Go
224 lines
7.7 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/joho/godotenv"
|
|
_ "github.com/lib/pq"
|
|
)
|
|
|
|
func main() {
|
|
_ = godotenv.Load()
|
|
|
|
cfg := ParseFlags()
|
|
|
|
// Deterministic seed for reproducibility
|
|
InitRNG(42)
|
|
|
|
dbURL := os.Getenv("DATABASE_URL")
|
|
if dbURL == "" {
|
|
log.Fatal("DATABASE_URL not set")
|
|
}
|
|
|
|
db, err := sql.Open("postgres", dbURL)
|
|
if err != nil {
|
|
log.Fatalf("DB connect: %v", err)
|
|
}
|
|
defer db.Close()
|
|
if err := db.Ping(); err != nil {
|
|
log.Fatalf("DB ping: %v", err)
|
|
}
|
|
|
|
// Increase connection pool for bulk operations
|
|
db.SetMaxOpenConns(10)
|
|
db.SetMaxIdleConns(5)
|
|
|
|
startTime := time.Now()
|
|
|
|
fmt.Println("╔═══════════════════════════════════════════════════════════╗")
|
|
fmt.Println("║ VEZA — Realistic Database Seeder v2 ║")
|
|
fmt.Println("╚═══════════════════════════════════════════════════════════╝")
|
|
fmt.Printf("\n Mode: %s\n", modeName(cfg))
|
|
fmt.Printf(" Users: %d | Tracks: %d | Plays: %d\n\n", cfg.TotalUsers, cfg.Tracks, cfg.PlayEvents)
|
|
|
|
// ── TRUNCATE all tables ──────────────────────────────────────────────────
|
|
fmt.Print("Truncating all tables... ")
|
|
if err := TruncateAll(db); err != nil {
|
|
log.Fatalf("truncate: %v", err)
|
|
}
|
|
fmt.Println("done")
|
|
|
|
// ── SEED in dependency order ─────────────────────────────────────────────
|
|
|
|
// Level 0: Users (no FK dependencies)
|
|
users, err := SeedUsers(db, cfg)
|
|
if err != nil {
|
|
log.Fatalf("seed users: %v", err)
|
|
}
|
|
|
|
// Level 1: Tracks (depends on users)
|
|
tracks, err := SeedTracks(db, cfg, users)
|
|
if err != nil {
|
|
log.Fatalf("seed tracks: %v", err)
|
|
}
|
|
|
|
// Level 2: Playlists (depends on users, tracks)
|
|
_, err = SeedPlaylists(db, cfg, users, tracks)
|
|
if err != nil {
|
|
log.Fatalf("seed playlists: %v", err)
|
|
}
|
|
|
|
// CI mode stops here — the E2E suite only needs the test accounts
|
|
// + a fixture set of tracks/playlists. Skipping the remaining
|
|
// seeders shaves ~50s off the typical CI seed time.
|
|
if cfg.CIMode {
|
|
fmt.Println("\n═══ CI MODE: skipping social/chat/live/marketplace/analytics/content/moderation/notifications/misc ═══")
|
|
} else {
|
|
// Level 2: Social (depends on users, tracks)
|
|
if err := SeedSocial(db, cfg, users, tracks); err != nil {
|
|
log.Fatalf("seed social: %v", err)
|
|
}
|
|
|
|
// Level 2: Chat (depends on users)
|
|
_, err = SeedChat(db, cfg, users)
|
|
if err != nil {
|
|
log.Fatalf("seed chat: %v", err)
|
|
}
|
|
|
|
// Level 2: Live streams (depends on users)
|
|
if err := SeedLive(db, cfg, users, tracks); err != nil {
|
|
log.Fatalf("seed live: %v", err)
|
|
}
|
|
|
|
// Level 2: Marketplace (depends on users, tracks)
|
|
_, err = SeedMarketplace(db, cfg, users, tracks)
|
|
if err != nil {
|
|
log.Fatalf("seed marketplace: %v", err)
|
|
}
|
|
|
|
// Level 3: Analytics (depends on users, tracks — heaviest step)
|
|
if err := SeedAnalytics(db, cfg, users, tracks); err != nil {
|
|
log.Fatalf("seed analytics: %v", err)
|
|
}
|
|
|
|
// Level 2: Content (depends on users, tracks)
|
|
if err := SeedContent(db, cfg, users, tracks); err != nil {
|
|
log.Fatalf("seed content: %v", err)
|
|
}
|
|
|
|
// Level 2: Moderation (depends on users, tracks)
|
|
if err := SeedModeration(db, cfg, users, tracks); err != nil {
|
|
log.Fatalf("seed moderation: %v", err)
|
|
}
|
|
|
|
// Level 2: Notifications (depends on users)
|
|
if err := SeedNotifications(db, cfg, users); err != nil {
|
|
log.Fatalf("seed notifications: %v", err)
|
|
}
|
|
|
|
// Level 2: Misc (depends on users)
|
|
if err := SeedMisc(db, cfg, users); err != nil {
|
|
log.Fatalf("seed misc: %v", err)
|
|
}
|
|
}
|
|
|
|
// ── VALIDATION ───────────────────────────────────────────────────────────
|
|
fmt.Println("\n═══ VALIDATION ═══")
|
|
validate(db)
|
|
|
|
// ── SUMMARY ──────────────────────────────────────────────────────────────
|
|
elapsed := time.Since(startTime)
|
|
fmt.Println()
|
|
fmt.Println("╔═══════════════════════════════════════════════════════════╗")
|
|
fmt.Println("║ Seed Complete! ║")
|
|
fmt.Println("╚═══════════════════════════════════════════════════════════╝")
|
|
fmt.Println()
|
|
|
|
tables := []string{
|
|
"users", "user_profiles", "user_settings", "user_roles",
|
|
"tracks", "track_genres", "track_tags",
|
|
"playlists", "playlist_tracks", "playlist_follows",
|
|
"follows", "track_likes", "track_reposts", "track_comments", "comments", "user_blocks",
|
|
"rooms", "room_members", "messages",
|
|
"live_streams", "co_listening_sessions",
|
|
"products", "orders", "order_items", "product_reviews",
|
|
"seller_stripe_accounts", "seller_balances",
|
|
"track_plays", "playback_history", "daily_track_stats",
|
|
"geographic_play_stats", "analytics_events",
|
|
"files", "user_storage_quotas",
|
|
"courses", "lessons", "course_enrollments",
|
|
"gear_items",
|
|
"groups", "group_members",
|
|
"reports", "moderation_actions", "user_strikes", "user_suspensions",
|
|
"notifications", "notification_preferences",
|
|
"support_tickets", "api_keys", "announcements",
|
|
"data_exports", "login_history", "user_preferences",
|
|
}
|
|
|
|
totalRows := 0
|
|
for _, t := range tables {
|
|
n := CountRows(db, t)
|
|
totalRows += n
|
|
fmt.Printf(" %-30s %7d rows\n", t, n)
|
|
}
|
|
|
|
fmt.Printf("\n %-30s %7d rows\n", "TOTAL", totalRows)
|
|
fmt.Printf(" %-30s %s\n", "Duration", elapsed.Round(time.Millisecond))
|
|
|
|
fmt.Println("\n--- Test Accounts ---")
|
|
for _, ta := range testAccounts {
|
|
fmt.Printf(" %-12s %-25s password: %s\n", ta.Role, ta.Email, ta.Password)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
func modeName(cfg Config) string {
|
|
if cfg.CIMode {
|
|
return "CI"
|
|
}
|
|
if cfg.TotalUsers <= 100 {
|
|
return "MINIMAL"
|
|
}
|
|
return "FULL"
|
|
}
|
|
|
|
// validate checks data integrity.
|
|
func validate(db *sql.DB) {
|
|
checks := []struct {
|
|
name string
|
|
query string
|
|
}{
|
|
{"test accounts exist", "SELECT COUNT(*) FROM users WHERE email IN ('admin@veza.music','artist@veza.music','user@veza.music','mod@veza.music','new@veza.music')"},
|
|
{"no orphan track_plays", "SELECT COUNT(*) FROM track_plays tp LEFT JOIN tracks t ON tp.track_id = t.id WHERE t.id IS NULL"},
|
|
{"no orphan follows", "SELECT COUNT(*) FROM follows f LEFT JOIN users u ON f.follower_id = u.id WHERE u.id IS NULL"},
|
|
{"no orphan playlist_tracks", "SELECT COUNT(*) FROM playlist_tracks pt LEFT JOIN playlists p ON pt.playlist_id = p.id WHERE p.id IS NULL"},
|
|
{"no orphan messages", "SELECT COUNT(*) FROM messages m LEFT JOIN rooms r ON m.room_id = r.id WHERE r.id IS NULL"},
|
|
{"no orphan order_items", "SELECT COUNT(*) FROM order_items oi LEFT JOIN orders o ON oi.order_id = o.id WHERE o.id IS NULL"},
|
|
}
|
|
|
|
for _, c := range checks {
|
|
var n int
|
|
err := db.QueryRow(c.query).Scan(&n)
|
|
if err != nil {
|
|
fmt.Printf(" ⚠ %s: query error: %v\n", c.name, err)
|
|
continue
|
|
}
|
|
if c.name == "test accounts exist" {
|
|
if n == 5 {
|
|
fmt.Printf(" ✓ %s: %d/5\n", c.name, n)
|
|
} else {
|
|
fmt.Printf(" ✗ %s: only %d/5 found!\n", c.name, n)
|
|
}
|
|
} else {
|
|
if n == 0 {
|
|
fmt.Printf(" ✓ %s: OK\n", c.name)
|
|
} else {
|
|
fmt.Printf(" ✗ %s: %d orphans found!\n", c.name, n)
|
|
}
|
|
}
|
|
}
|
|
}
|