package database import ( "RenewCMS/internal/domain/article" entity "RenewCMS/internal/infrastructure/persistence/models" "slices" "gorm.io/gorm" ) const ( articleSlugIndexName = "idx_articles_site_slug" articleSlugBackfillBatchSize = 500 articleSlugIndexStatement = "CREATE UNIQUE INDEX idx_articles_site_slug ON articles (site_id, lang, slug)" articleSlugLanguageMarker = "article_slug_index_per_language" ) type slugOwner struct { siteID uint32 lang string slug string } type slugAssignment struct { id uint32 slug string } func BackfillArticleSlugs(db *gorm.DB) error { var rows []entity.Article if err := db.Unscoped().Select("id", "site_id", "lang", "title", "slug", "deleted_at").Order("id ASC").Find(&rows).Error; err != nil { return err } assignments := planSlugAssignments(rows) for batch := range slices.Chunk(assignments, articleSlugBackfillBatchSize) { if err := writeSlugAssignments(db, batch); err != nil { return err } } return nil } func writeSlugAssignments(db *gorm.DB, batch []slugAssignment) error { return db.Transaction(func(tx *gorm.DB) error { for _, assignment := range batch { if err := tx.Unscoped().Model(&entity.Article{}).Where("id = ?", assignment.id).UpdateColumn("slug", assignment.slug).Error; err != nil { return err } } return nil }) } func planSlugAssignments(rows []entity.Article) []slugAssignment { assignments := []slugAssignment{} claimed := map[slugOwner]bool{} pending := []entity.Article{} for _, row := range rows { if row.DeletedAt.Valid { if tombstone := article.Tombstone(row.ID); row.Slug != tombstone { assignments = append(assignments, slugAssignment{row.ID, tombstone}) } continue } owner := slugOwner{row.SiteID, row.Lang, row.Slug} if row.Slug == "" || claimed[owner] { pending = append(pending, row) continue } claimed[owner] = true } for _, row := range pending { base := row.Slug attempt := 2 if base == "" { base = snapshotSlug(row.Title) attempt = 1 } slug := firstFreeSnapshotSlug(claimed, row.SiteID, row.Lang, base, attempt) claimed[slugOwner{row.SiteID, row.Lang, slug}] = true assignments = append(assignments, slugAssignment{row.ID, slug}) } return assignments } func firstFreeSnapshotSlug(claimed map[slugOwner]bool, siteID uint32, lang string, base string, attempt int) string { for ; ; attempt++ { candidate := snapshotSlugWithSuffix(base, attempt) if !claimed[slugOwner{siteID, lang, candidate}] { return candidate } } } func ensureArticleSlugIndex(db *gorm.DB) error { replaced, err := markerApplied(db, articleSlugLanguageMarker) if err != nil { return err } indexed := db.Migrator().HasIndex(&entity.Article{}, articleSlugIndexName) if replaced && indexed { return nil } if indexed { if err := db.Migrator().DropIndex(&entity.Article{}, articleSlugIndexName); err != nil { return err } } if err := db.Exec(articleSlugIndexStatement).Error; err != nil { return err } if replaced { return nil } return applyMarker(db, articleSlugLanguageMarker) }