Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 1m40s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 42m16s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
186 lines
6.2 KiB
Go
186 lines
6.2 KiB
Go
package model
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
const prefillGroupNameIndex = "uk_prefill_name"
|
|
|
|
type conflictingPrefillGroupUniqueness struct {
|
|
constraints []string
|
|
indexes []string
|
|
}
|
|
|
|
type prefillGroupNameIndexState struct {
|
|
exists bool
|
|
valid bool
|
|
}
|
|
|
|
func (conflicts conflictingPrefillGroupUniqueness) empty() bool {
|
|
return len(conflicts.constraints) == 0 && len(conflicts.indexes) == 0
|
|
}
|
|
|
|
func inspectConflictingPrefillGroupUniqueness(db *gorm.DB, tableName string) (conflictingPrefillGroupUniqueness, error) {
|
|
var conflicts conflictingPrefillGroupUniqueness
|
|
if err := db.Raw(`
|
|
SELECT constraint_meta.conname
|
|
FROM pg_catalog.pg_constraint AS constraint_meta
|
|
WHERE constraint_meta.conrelid = to_regclass(?)
|
|
AND constraint_meta.contype = 'u'
|
|
AND cardinality(constraint_meta.conkey) = 1
|
|
AND EXISTS (
|
|
SELECT 1
|
|
FROM pg_catalog.pg_attribute AS attribute_meta
|
|
WHERE attribute_meta.attrelid = constraint_meta.conrelid
|
|
AND attribute_meta.attnum = constraint_meta.conkey[1]
|
|
AND attribute_meta.attname = ?
|
|
)
|
|
ORDER BY constraint_meta.conname`, tableName, "name").Scan(&conflicts.constraints).Error; err != nil {
|
|
return conflicts, fmt.Errorf("inspect conflicting prefill group unique constraints: %w", err)
|
|
}
|
|
|
|
if err := db.Raw(`
|
|
SELECT index_class.relname
|
|
FROM pg_catalog.pg_index AS index_meta
|
|
JOIN pg_catalog.pg_class AS index_class
|
|
ON index_class.oid = index_meta.indexrelid
|
|
JOIN pg_catalog.pg_attribute AS attribute_meta
|
|
ON attribute_meta.attrelid = index_meta.indrelid
|
|
AND attribute_meta.attnum = index_meta.indkey[0]
|
|
WHERE index_meta.indrelid = to_regclass(?)
|
|
AND index_meta.indisunique
|
|
AND NOT index_meta.indisprimary
|
|
AND index_meta.indpred IS NULL
|
|
AND index_meta.indexprs IS NULL
|
|
AND index_meta.indnatts = 1
|
|
AND attribute_meta.attname = ?
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM pg_catalog.pg_constraint AS constraint_meta
|
|
WHERE constraint_meta.conindid = index_meta.indexrelid
|
|
)
|
|
ORDER BY index_class.relname`, tableName, "name").Scan(&conflicts.indexes).Error; err != nil {
|
|
return conflicts, fmt.Errorf("inspect conflicting prefill group unique indexes: %w", err)
|
|
}
|
|
|
|
return conflicts, nil
|
|
}
|
|
|
|
func inspectPrefillGroupNameIndex(db *gorm.DB, tableName string) (prefillGroupNameIndexState, error) {
|
|
var state struct {
|
|
Exists bool `gorm:"column:index_exists"`
|
|
Valid bool `gorm:"column:index_valid"`
|
|
}
|
|
if err := db.Raw(`
|
|
SELECT count(*) > 0 AS index_exists,
|
|
COALESCE(bool_or(
|
|
index_meta.indisunique
|
|
AND index_meta.indisvalid
|
|
AND index_meta.indisready
|
|
AND NOT index_meta.indisprimary
|
|
AND index_meta.indexprs IS NULL
|
|
AND index_meta.indnatts = 1
|
|
AND attribute_meta.attname = ?
|
|
AND pg_get_expr(index_meta.indpred, index_meta.indrelid) = '(deleted_at IS NULL)'
|
|
), false) AS index_valid
|
|
FROM pg_catalog.pg_index AS index_meta
|
|
JOIN pg_catalog.pg_class AS index_class
|
|
ON index_class.oid = index_meta.indexrelid
|
|
LEFT JOIN pg_catalog.pg_attribute AS attribute_meta
|
|
ON attribute_meta.attrelid = index_meta.indrelid
|
|
AND attribute_meta.attnum = index_meta.indkey[0]
|
|
WHERE index_meta.indrelid = to_regclass(?)
|
|
AND index_class.relname = ?`, "name", tableName, prefillGroupNameIndex).Scan(&state).Error; err != nil {
|
|
return prefillGroupNameIndexState{}, fmt.Errorf("inspect prefill group partial unique index: %w", err)
|
|
}
|
|
return prefillGroupNameIndexState{exists: state.Exists, valid: state.Valid}, nil
|
|
}
|
|
|
|
// migratePrefillGroupUniqueness replaces global PostgreSQL uniqueness on name
|
|
// before AutoMigrate inspects the column. Match the definition rather than the
|
|
// object name, which can change across older schemas and database imports.
|
|
// Composite, expression and partial indexes retain their separate semantics.
|
|
func migratePrefillGroupUniqueness(db *gorm.DB) error {
|
|
if db == nil {
|
|
return fmt.Errorf("migrate prefill group uniqueness: database is nil")
|
|
}
|
|
if db.Dialector.Name() != "postgres" {
|
|
return nil
|
|
}
|
|
|
|
statement := &gorm.Statement{DB: db}
|
|
if err := statement.Parse(&PrefillGroup{}); err != nil {
|
|
return fmt.Errorf("parse prefill group schema: %w", err)
|
|
}
|
|
tableName := statement.Schema.Table
|
|
conflicts, err := inspectConflictingPrefillGroupUniqueness(db, tableName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if conflicts.empty() {
|
|
return nil
|
|
}
|
|
return db.Transaction(func(tx *gorm.DB) error {
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasTable(&PrefillGroup{}) {
|
|
return nil
|
|
}
|
|
|
|
if err := tx.Exec(
|
|
"LOCK TABLE ? IN ACCESS EXCLUSIVE MODE",
|
|
clause.Table{Name: tableName},
|
|
).Error; err != nil {
|
|
return fmt.Errorf("lock prefill groups for uniqueness migration: %w", err)
|
|
}
|
|
|
|
conflicts, err := inspectConflictingPrefillGroupUniqueness(tx, tableName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if conflicts.empty() {
|
|
return nil
|
|
}
|
|
if !migrator.HasColumn(&PrefillGroup{}, "DeletedAt") {
|
|
if err := migrator.AddColumn(&PrefillGroup{}, "DeletedAt"); err != nil {
|
|
return fmt.Errorf("add prefill groups deleted_at column: %w", err)
|
|
}
|
|
}
|
|
|
|
// A global index or constraint may already use the target index name.
|
|
// Drop it before creating the replacement under the exclusive table lock;
|
|
// the transaction restores all old objects if any migration step fails.
|
|
for _, constraintName := range conflicts.constraints {
|
|
if err := migrator.DropConstraint(&PrefillGroup{}, constraintName); err != nil {
|
|
return fmt.Errorf("drop conflicting prefill group constraint %q: %w", constraintName, err)
|
|
}
|
|
}
|
|
for _, indexName := range conflicts.indexes {
|
|
if err := migrator.DropIndex(&PrefillGroup{}, indexName); err != nil {
|
|
return fmt.Errorf("drop conflicting prefill group index %q: %w", indexName, err)
|
|
}
|
|
}
|
|
|
|
targetIndex, err := inspectPrefillGroupNameIndex(tx, tableName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !targetIndex.exists {
|
|
if err := migrator.CreateIndex(&PrefillGroup{}, prefillGroupNameIndex); err != nil {
|
|
return fmt.Errorf("create prefill group partial unique index: %w", err)
|
|
}
|
|
targetIndex, err = inspectPrefillGroupNameIndex(tx, tableName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !targetIndex.valid {
|
|
return fmt.Errorf("prefill group index %q has an unexpected definition", prefillGroupNameIndex)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|