99 lines
2.5 KiB
Go
99 lines
2.5 KiB
Go
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, slug)"
|
|
)
|
|
|
|
type slugOwner struct {
|
|
siteID uint32
|
|
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", "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.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, base, attempt)
|
|
claimed[slugOwner{row.SiteID, slug}] = true
|
|
assignments = append(assignments, slugAssignment{row.ID, slug})
|
|
}
|
|
return assignments
|
|
}
|
|
|
|
func firstFreeSnapshotSlug(claimed map[slugOwner]bool, siteID uint32, base string, attempt int) string {
|
|
for ; ; attempt++ {
|
|
candidate := snapshotSlugWithSuffix(base, attempt)
|
|
if !claimed[slugOwner{siteID, candidate}] {
|
|
return candidate
|
|
}
|
|
}
|
|
}
|
|
|
|
func ensureArticleSlugIndex(db *gorm.DB) error {
|
|
if db.Migrator().HasIndex(&entity.Article{}, articleSlugIndexName) {
|
|
return nil
|
|
}
|
|
return db.Exec(articleSlugIndexStatement).Error
|
|
}
|