mirror of
https://codeberg.org/forgejo/forgejo
synced 2025-10-19 10:50:52 +02:00
131 lines
4.5 KiB
Go
131 lines
4.5 KiB
Go
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package db
|
|
|
|
import (
|
|
"cmp"
|
|
"fmt"
|
|
"slices"
|
|
"sync"
|
|
|
|
"xorm.io/xorm/schemas"
|
|
)
|
|
|
|
var (
|
|
cachedForeignKeyOrderedTables = sync.OnceValues(foreignKeyOrderedTables)
|
|
cachedTableNameLookupOrder = sync.OnceValues(tableNameLookupOrder)
|
|
)
|
|
|
|
// Create a list of database tables in their "foreign key order". This order specifies the safe insertion order for
|
|
// records into tables, where earlier tables in the list are referenced by foreign keys that exist in tables later in
|
|
// the list. This order can be used in reverse as a safe deletion order as well.
|
|
//
|
|
// An ordered list of tables is incompatible with tables that have self-referencing foreign keys and circular referenced
|
|
// foreign keys; however neither of those cases are in-use in Forgejo.
|
|
func calculateTableForeignKeyOrder(tables []*schemas.Table) ([]*schemas.Table, error) {
|
|
remainingTables := slices.Clone(tables)
|
|
|
|
// Create a lookup for each table that has a foreign key, and a slice of the tables that it references it.
|
|
referencingTables := make(map[string][]string)
|
|
for _, table := range remainingTables {
|
|
tableName := table.Name
|
|
for _, fk := range table.ForeignKeys {
|
|
referencingTables[tableName] = append(referencingTables[tableName], fk.TargetTableName)
|
|
}
|
|
}
|
|
|
|
orderedTables := make([]*schemas.Table, 0, len(remainingTables))
|
|
|
|
for len(remainingTables) > 0 {
|
|
nextGroup := make([]*schemas.Table, 0, len(remainingTables))
|
|
|
|
for _, targetTable := range remainingTables {
|
|
// Skip if this targetTable has foreign keys and the target table hasn't been created.
|
|
slice, ok := referencingTables[targetTable.Name]
|
|
if ok && len(slice) > 1 { // This table is still referencing an uncreated table
|
|
continue
|
|
}
|
|
// This table's references are satisfied or it had none
|
|
nextGroup = append(nextGroup, targetTable)
|
|
}
|
|
|
|
if len(nextGroup) == 0 {
|
|
return nil, fmt.Errorf("calculateTableForeignKeyOrder: unable to figure out next table from remainingTables = %#v", remainingTables)
|
|
}
|
|
|
|
orderedTables = append(orderedTables, nextGroup...)
|
|
|
|
// Cleanup between loops: remove each table in nextGroup from remainingTables, and remove their table names from
|
|
// referencingTables as well.
|
|
for _, doneTable := range nextGroup {
|
|
remainingTables = slices.DeleteFunc(remainingTables, func(remainingTable *schemas.Table) bool {
|
|
return remainingTable.Name == doneTable.Name
|
|
})
|
|
for referencingTable, referencedTables := range referencingTables {
|
|
referencingTables[referencingTable] = slices.DeleteFunc(referencedTables, func(tableName string) bool {
|
|
return tableName == doneTable.Name
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return orderedTables, nil
|
|
}
|
|
|
|
// Create a list of registered database tables in their "foreign key order", per calculateTableForeignKeyOrder.
|
|
func foreignKeyOrderedTables() ([]*schemas.Table, error) {
|
|
schemaTables := make([]*schemas.Table, 0, len(tables))
|
|
for _, tbl := range tables {
|
|
table, err := TableInfo(tbl)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("foreignKeyOrderedTables: failure to fetch schema table for bean %#v: %w", tbl, err)
|
|
}
|
|
schemaTables = append(schemaTables, table)
|
|
}
|
|
|
|
orderedTables, err := calculateTableForeignKeyOrder(schemaTables)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return orderedTables, nil
|
|
}
|
|
|
|
// Create a map from each registered database table's name to its order in "foreign key order", per
|
|
// calculateTableForeignKeyOrder.
|
|
func tableNameLookupOrder() (map[string]int, error) {
|
|
tables, err := cachedForeignKeyOrderedTables()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
lookupMap := make(map[string]int, len(tables))
|
|
for i, table := range tables {
|
|
lookupMap[table.Name] = i
|
|
}
|
|
|
|
return lookupMap, nil
|
|
}
|
|
|
|
// When used as a comparator function in `slices.SortFunc`, can sort a slice into the safe insertion order for records
|
|
// in tables, where earlier tables in the list are referenced by foreign keys that exist in tables later in the list.
|
|
func TableNameInsertionOrderSortFunc(table1, table2 string) int {
|
|
lookupMap, err := cachedTableNameLookupOrder()
|
|
if err != nil {
|
|
panic(fmt.Sprintf("cachedTableNameLookupOrder failed: %#v", err))
|
|
}
|
|
|
|
// Since this is typically used by `slices.SortFunc` it can't return an error. If a table is referenced that isn't
|
|
// a registered model then it will be sorted at the beginning -- this case is used in models/gitea_migrations/test.
|
|
val1, ok := lookupMap[table1]
|
|
if !ok {
|
|
val1 = -1
|
|
}
|
|
val2, ok := lookupMap[table2]
|
|
if !ok {
|
|
val2 = -1
|
|
}
|
|
|
|
return cmp.Compare(val1, val2)
|
|
}
|