package main import ( "database/sql" "fmt" "time" ) // SeedSocial creates follows, track_likes, track_reposts, comments, and user_blocks. func SeedSocial(db *sql.DB, cfg Config, users []SeededUser, tracks []SeededTrack) error { fmt.Println("\n═══ SOCIAL ═══") artists := GetArtists(users) allUsers := users // ── 1. Follows (scale-free distribution) ───────────────────────────────── p := NewProgress("follows", cfg.Follows) followRows := make([][]interface{}, 0, cfg.Follows) followSeen := make(map[string]bool) // Build popularity weights: artists are much more likely to be followed artistSet := make(map[string]bool) for _, a := range artists { artistSet[a.ID] = true } for i := 0; i < cfg.Follows*2 && len(followRows) < cfg.Follows; i++ { follower := allUsers[rng.Intn(len(allUsers))] // 70% chance to follow an artist, 30% random user var followed SeededUser if randChance(70) && len(artists) > 0 { // Power-law: top artists get followed more idx := PowerLaw(0, len(artists)-1, 1.5) followed = artists[idx] } else { followed = allUsers[rng.Intn(len(allUsers))] } if follower.ID == followed.ID { continue } key := follower.ID + ":" + followed.ID if followSeen[key] { continue } followSeen[key] = true createdAt := RandomTimeAfter(follower.CreatedAt) if followed.CreatedAt.After(follower.CreatedAt) { createdAt = RandomTimeAfter(followed.CreatedAt) } followRows = append(followRows, []interface{}{ newUUID(), follower.ID, followed.ID, createdAt, createdAt, }) } // Ensure test artist@veza.music has many followers testArtist := findUser(users, "artist@veza.music") if testArtist != nil { for _, u := range users[:min(200, len(users))] { if u.ID == testArtist.ID { continue } key := u.ID + ":" + testArtist.ID if followSeen[key] { continue } followSeen[key] = true followRows = append(followRows, []interface{}{ newUUID(), u.ID, testArtist.ID, RandomTimeAfter(u.CreatedAt), time.Now(), }) } } _, err := BulkInsert(db, "follows", "id, follower_id, followed_id, created_at, updated_at", followRows) if err != nil { return fmt.Errorf("insert follows: %w", err) } p.Update(len(followRows)) p.Done() // ── 2. Track likes ─────────────────────────────────────────────────────── if len(tracks) == 0 { return nil } p = NewProgress("track_likes", cfg.TrackLikes) likeRows := make([][]interface{}, 0, cfg.TrackLikes) likeSeen := make(map[string]bool) for i := 0; i < cfg.TrackLikes*2 && len(likeRows) < cfg.TrackLikes; i++ { user := allUsers[rng.Intn(len(allUsers))] // Power-law: popular tracks get more likes track := tracks[PowerLaw(0, len(tracks)-1, 1.2)] key := user.ID + ":" + track.ID if likeSeen[key] { continue } likeSeen[key] = true likeRows = append(likeRows, []interface{}{ newUUID(), user.ID, track.ID, time.Now(), }) } _, err = BulkInsert(db, "track_likes", "id, user_id, track_id, created_at", likeRows) if err != nil { return fmt.Errorf("insert track_likes: %w", err) } p.Update(len(likeRows)) p.Done() // ── 3. Track reposts ───────────────────────────────────────────────────── p = NewProgress("track_reposts", cfg.TrackReposts) repostRows := make([][]interface{}, 0, cfg.TrackReposts) repostSeen := make(map[string]bool) for i := 0; i < cfg.TrackReposts*2 && len(repostRows) < cfg.TrackReposts; i++ { user := allUsers[rng.Intn(len(allUsers))] track := tracks[PowerLaw(0, len(tracks)-1, 1.2)] if user.ID == track.CreatorID { continue // Don't repost own tracks } key := user.ID + ":" + track.ID if repostSeen[key] { continue } repostSeen[key] = true repostRows = append(repostRows, []interface{}{ newUUID(), user.ID, track.ID, time.Now(), }) } _, err = BulkInsert(db, "track_reposts", "id, user_id, track_id, created_at", repostRows) if err != nil { return fmt.Errorf("insert track_reposts: %w", err) } p.Update(len(repostRows)) p.Done() // ── 4. Comments (track_comments + generic comments) ────────────────────── p = NewProgress("track_comments", cfg.Comments) commentRows := make([][]interface{}, 0, cfg.Comments) for i := 0; i < cfg.Comments; i++ { user := allUsers[rng.Intn(len(allUsers))] track := tracks[PowerLaw(0, len(tracks)-1, 1.0)] content := GenComment() createdAt := RandomTimeAfter(user.CreatedAt) // Timed comment: position within the track var timestampSec *int if randChance(40) { ts := randInt(0, track.Duration) timestampSec = &ts } commentRows = append(commentRows, []interface{}{ newUUID(), track.ID, user.ID, content, nil, // parent_comment_id timestampSec, false, false, nil, createdAt, createdAt, nil, }) } _, err = BulkInsert(db, "track_comments", "id, track_id, user_id, content, parent_comment_id, timestamp_seconds, is_edited, is_deleted, parent_id, created_at, updated_at, deleted_at", commentRows) if err != nil { return fmt.Errorf("insert track_comments: %w", err) } p.Update(len(commentRows)) p.Done() // Also populate the generic 'comments' table (used for track comments via target_type) p = NewProgress("comments (generic)", cfg.Comments/2) genCommentRows := make([][]interface{}, 0, cfg.Comments/2) for i := 0; i < cfg.Comments/2; i++ { user := allUsers[rng.Intn(len(allUsers))] track := tracks[rng.Intn(len(tracks))] content := GenComment() createdAt := RandomTimeAfter(user.CreatedAt) genCommentRows = append(genCommentRows, []interface{}{ newUUID(), user.ID, track.ID, "track", content, createdAt, createdAt, }) } _, _ = BulkInsert(db, "comments", "id, user_id, target_id, target_type, content, created_at, updated_at", genCommentRows) p.Update(len(genCommentRows)) p.Done() // ── 5. User blocks (small number) ──────────────────────────────────────── blockCount := len(users) / 50 // ~2% of users have blocked someone if blockCount < 5 { blockCount = 5 } p = NewProgress("user_blocks", blockCount) blockRows := make([][]interface{}, 0, blockCount) blockSeen := make(map[string]bool) for i := 0; i < blockCount; i++ { blocker := allUsers[rng.Intn(len(allUsers))] blocked := allUsers[rng.Intn(len(allUsers))] if blocker.ID == blocked.ID { continue } key := blocker.ID + ":" + blocked.ID if blockSeen[key] { continue } blockSeen[key] = true blockRows = append(blockRows, []interface{}{ newUUID(), blocker.ID, blocked.ID, nil, time.Now(), time.Now(), }) } _, _ = BulkInsert(db, "user_blocks", "id, blocker_id, blocked_id, reason, created_at, updated_at", blockRows) p.Update(len(blockRows)) p.Done() return nil } func findUser(users []SeededUser, email string) *SeededUser { for i := range users { if users[i].Email == email { return &users[i] } } return nil } func min(a, b int) int { if a < b { return a } return b }