feat: editor write path
This commit is contained in:
1 parent
d9158025d4
commit
a0b3c58c52
88 files changed
+5738
-263
No files matched your search
@@ -2,6 +2,7 @@ package database
|
||||
|
||||
import (
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -9,7 +10,9 @@ import (
|
||||
const (
|
||||
articleStoriesMarker = "article_stories"
|
||||
articleStoryLanguageIndex = "idx_articles_story_lang"
|
||||
articleStoryLanguageIndexSQL = "CREATE UNIQUE INDEX idx_articles_story_lang ON articles (story_id, lang)"
|
||||
articleStoryLanguageIndexSQL = "CREATE UNIQUE INDEX idx_articles_story_lang ON articles (story_id, lang, retired_id)"
|
||||
articleStoryRetiredMarker = "article_story_language_retired"
|
||||
articleRetiredIDsMarker = "article_retired_ids"
|
||||
)
|
||||
|
||||
func BackfillArticleStories(db *gorm.DB) error {
|
||||
@@ -35,9 +38,64 @@ func BackfillArticleStories(db *gorm.DB) error {
|
||||
})
|
||||
}
|
||||
|
||||
func BackfillRetiredArticles(db *gorm.DB) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
applied, err := markerApplied(tx, articleRetiredIDsMarker)
|
||||
if err != nil || applied {
|
||||
return err
|
||||
}
|
||||
if err := tx.Unscoped().Model(&entity.Article{}).Where("deleted_at IS NOT NULL AND retired_id = ?", 0).UpdateColumn("retired_id", gorm.Expr("id")).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return applyMarker(tx, articleRetiredIDsMarker)
|
||||
})
|
||||
}
|
||||
|
||||
type duplicateStoryLanguage struct {
|
||||
StoryID uint32
|
||||
Lang string
|
||||
RetiredID uint32
|
||||
Versions int
|
||||
}
|
||||
|
||||
func checkStoryLanguagesUnique(db *gorm.DB) error {
|
||||
var duplicates []duplicateStoryLanguage
|
||||
err := db.Unscoped().Model(&entity.Article{}).
|
||||
Select("story_id, lang, retired_id, COUNT(*) AS versions").
|
||||
Group("story_id, lang, retired_id").
|
||||
Having("COUNT(*) > 1").
|
||||
Order("story_id ASC, lang ASC").
|
||||
Limit(1).
|
||||
Scan(&duplicates).Error
|
||||
if err != nil || len(duplicates) == 0 {
|
||||
return err
|
||||
}
|
||||
first := duplicates[0]
|
||||
return fmt.Errorf("cannot create the unique index %s: story %d holds %d versions in language %q; merge or delete the extra versions, then start again", articleStoryLanguageIndex, first.StoryID, first.Versions, first.Lang)
|
||||
}
|
||||
|
||||
func ensureArticleStoryLanguageIndex(db *gorm.DB) error {
|
||||
if db.Migrator().HasIndex(&entity.Article{}, articleStoryLanguageIndex) {
|
||||
replaced, err := markerApplied(db, articleStoryRetiredMarker)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
indexed := db.Migrator().HasIndex(&entity.Article{}, articleStoryLanguageIndex)
|
||||
if replaced && indexed {
|
||||
return nil
|
||||
}
|
||||
return db.Exec(articleStoryLanguageIndexSQL).Error
|
||||
if indexed {
|
||||
if err := db.Migrator().DropIndex(&entity.Article{}, articleStoryLanguageIndex); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := checkStoryLanguagesUnique(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.Exec(articleStoryLanguageIndexSQL).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if replaced {
|
||||
return nil
|
||||
}
|
||||
return applyMarker(db, articleStoryRetiredMarker)
|
||||
}
|
||||
@@ -41,6 +41,9 @@ func Migrate(db *gorm.DB, publicURL string) error {
|
||||
if err := SeedSiteLanguageSuggestions(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := BackfillRetiredArticles(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureArticleStoryLanguageIndex(db); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -25,6 +26,8 @@ const (
|
||||
redactedPassword = "***"
|
||||
mysqlLockNowait = 3572
|
||||
mysqlLockWaitTimeout = 1205
|
||||
mysqlDeadlock = 1213
|
||||
mysqlSnapshotChanged = 1020
|
||||
)
|
||||
|
||||
type mysqlDriver struct{}
|
||||
@@ -88,7 +91,9 @@ func tableOptionsFor(db *gorm.DB) *gorm.DB {
|
||||
return db
|
||||
}
|
||||
|
||||
var mysqlContentionCodes = []uint16{mysqlLockNowait, mysqlLockWaitTimeout, mysqlDeadlock, mysqlSnapshotChanged}
|
||||
|
||||
func (mysqlDriver) IsLockContention(err error) bool {
|
||||
var failure *mysqldriver.MySQLError
|
||||
return errors.As(err, &failure) && (failure.Number == mysqlLockNowait || failure.Number == mysqlLockWaitTimeout)
|
||||
return errors.As(err, &failure) && slices.Contains(mysqlContentionCodes, failure.Number)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
@@ -23,6 +24,8 @@ const (
|
||||
postgresUTCCallback = "renewcms:postgres_utc"
|
||||
gormQueryCallback = "gorm:query"
|
||||
postgresLockNotAvailable = "55P03"
|
||||
postgresDeadlockDetected = "40P01"
|
||||
postgresSerializationFailure = "40001"
|
||||
)
|
||||
|
||||
var timeType = reflect.TypeOf(time.Time{})
|
||||
@@ -109,7 +112,9 @@ func (postgresDriver) NewBackup(db *gorm.DB, directory string, now func() time.T
|
||||
return NewPostgresBackup(db, directory, now)
|
||||
}
|
||||
|
||||
var postgresContentionCodes = []string{postgresLockNotAvailable, postgresDeadlockDetected, postgresSerializationFailure}
|
||||
|
||||
func (postgresDriver) IsLockContention(err error) bool {
|
||||
var failure *pgconn.PgError
|
||||
return errors.As(err, &failure) && failure.Code == postgresLockNotAvailable
|
||||
return errors.As(err, &failure) && slices.Contains(postgresContentionCodes, failure.Code)
|
||||
}
|
||||
Reference in new issue
Block a user