commit 20681b20347248da28ea815e195ca1e610799b1c Author: upstream Date: Tue Sep 29 11:39:12 2026 +0200 Upstream go-gitea/gitea state of the touched files before 59a6ab1425 diff --git a/models/db/engine.go b/models/db/engine.go new file mode 100644 index 0000000..5cfb201 --- /dev/null +++ b/models/db/engine.go @@ -0,0 +1,193 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2018 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package db + +import ( + "context" + "database/sql" + "fmt" + "reflect" + "strings" + + _ "github.com/go-sql-driver/mysql" // Needed for the MySQL driver + _ "github.com/lib/pq" // Needed for the Postgresql driver + _ "github.com/microsoft/go-mssqldb" // Needed for the MSSQL driver + + "xorm.io/xorm" + "xorm.io/xorm/core" + "xorm.io/xorm/dialects" + "xorm.io/xorm/names" + "xorm.io/xorm/schemas" +) + +var ( + xormEngine *xorm.Engine + registeredModels []any + registeredInitFuncs []func() error +) + +// SQLSession represents a common interface for engine and session to execute SQLs +type SQLSession interface { + Count(...any) (int64, error) + Decr(column string, arg ...any) *xorm.Session + Delete(...any) (int64, error) + Truncate(...any) (int64, error) + Exec(...any) (sql.Result, error) + Find(any, ...any) error + FindAndCount(any, ...any) (int64, error) + Get(beans ...any) (bool, error) + ID(any) *xorm.Session + In(string, ...any) *xorm.Session + Incr(column string, arg ...any) *xorm.Session + Insert(...any) (int64, error) + Iterate(any, xorm.IterFunc) error + Join(joinOperator string, tablename, condition any, args ...any) *xorm.Session + SQL(any, ...any) *xorm.Session + Where(any, ...any) *xorm.Session + Asc(colNames ...string) *xorm.Session + Desc(colNames ...string) *xorm.Session + Limit(limit int, start ...int) *xorm.Session + NoAutoTime() *xorm.Session + SumInt(bean any, columnName string) (res int64, err error) + Select(string) *xorm.Session + SetExpr(string, any) *xorm.Session + NotIn(string, ...any) *xorm.Session + OrderBy(any, ...any) *xorm.Session + Exist(...any) (bool, error) + Distinct(...string) *xorm.Session + Query(...any) ([]map[string][]byte, error) + Cols(...string) *xorm.Session + Table(tableNameOrBean any) *xorm.Session + Context(ctx context.Context) *xorm.Session + QueryInterface(sqlOrArgs ...any) ([]map[string]any, error) + IsTableExist(tableNameOrBean any) (bool, error) +} + +// Engine represents a xorm engine +type Engine interface { + SQLSession + Sync(...any) error + Ping() error +} + +// Session represents a xorm session interface +type Session interface { + Engine + And(query any, args ...any) *xorm.Session + Begin() error + Close() error + Commit() error + IsInTx() bool + Rollback() error + Engine() *xorm.Engine +} + +// EngineMigration is a xorm engine interface used for migrations. +// It extends Engine with additional methods that are only available on the engine (not on the session) +// and are needed by the migration packages. +type EngineMigration interface { + Engine + Close() error + DB() *core.DB + DBMetas() ([]*schemas.Table, error) + Dialect() dialects.Dialect + DropTables(beans ...any) error + NewSession() *xorm.Session + SetMapper(mapper names.Mapper) + SyncWithOptions(opts xorm.SyncOptions, beans ...any) (*xorm.SyncResult, error) + TableInfo(bean any) (*schemas.Table, error) + TableName(bean any, includeSchema ...bool) string +} + +var ( + _ Engine = (*xorm.Engine)(nil) + _ Engine = (*xorm.Session)(nil) + _ Session = (*xorm.Session)(nil) + _ EngineMigration = (*xorm.Engine)(nil) +) + +// RegisterModel registers model, if initFuncs provided, it will be invoked after data model sync +func RegisterModel(bean any, initFunc ...func() error) { + registeredModels = append(registeredModels, bean) + if len(registeredInitFuncs) > 0 && initFunc[0] != nil { + registeredInitFuncs = append(registeredInitFuncs, initFunc[0]) + } +} + +func ResetModels() { + registeredModels, registeredInitFuncs = nil, nil +} + +// SyncAllTables sync the schemas of all tables, is required by unit test code +func SyncAllTables() error { + _, err := xormEngine.StoreEngine("InnoDB").SyncWithOptions(xorm.SyncOptions{ + WarnIfDatabaseColumnMissed: true, + }, registeredModels...) + return err +} + +// NamesToBean return a list of beans or an error +func NamesToBean(names ...string) ([]any, error) { + beans := []any{} + if len(names) == 0 { + beans = append(beans, registeredModels...) + return beans, nil + } + // Need to map provided names to beans... + beanMap := make(map[string]any) + for _, bean := range registeredModels { + beanMap[strings.ToLower(reflect.Indirect(reflect.ValueOf(bean)).Type().Name())] = bean + beanMap[strings.ToLower(xormEngine.TableName(bean))] = bean + beanMap[strings.ToLower(xormEngine.TableName(bean, true))] = bean + } + + gotBean := make(map[any]bool) + for _, name := range names { + bean, ok := beanMap[strings.ToLower(strings.TrimSpace(name))] + if !ok { + return nil, fmt.Errorf("no table found that matches: %s", name) + } + if !gotBean[bean] { + beans = append(beans, bean) + gotBean[bean] = true + } + } + return beans, nil +} + +// MaxBatchInsertSize returns the table's max batch insert size +func MaxBatchInsertSize(bean any) int { + t, err := xormEngine.TableInfo(bean) + if err != nil { + return 50 + } + return 999 / len(t.ColumnsSeq()) +} + +// IsTableNotEmpty returns true if table has at least one record +func IsTableNotEmpty(beanOrTableName any) (bool, error) { + return xormEngine.Table(beanOrTableName).Exist() +} + +// DeleteAllRecords will delete all the records of this table +func DeleteAllRecords(tableName string) error { + _, err := xormEngine.Exec("DELETE FROM " + tableName) + return err +} + +// GetMaxID will return max id of the table +func GetMaxID(beanOrTableName any) (maxID int64, err error) { + _, err = xormEngine.Select("MAX(id)").Table(beanOrTableName).Get(&maxID) + return maxID, err +} + +func SetLogSQL(ctx context.Context, on bool) { + e := GetEngine(ctx) + if x, ok := e.(*xorm.Engine); ok { + x.ShowSQL(on) + } else if sess, ok := e.(*xorm.Session); ok { + sess.Engine().ShowSQL(on) + } +} diff --git a/models/db/engine_test.go b/models/db/engine_test.go new file mode 100644 index 0000000..69e30aa --- /dev/null +++ b/models/db/engine_test.go @@ -0,0 +1,83 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package db_test + +import ( + "path/filepath" + "testing" + + "gitea.dev/models/db" + issues_model "gitea.dev/models/issues" + "gitea.dev/models/unittest" + "gitea.dev/modules/setting" + + _ "gitea.dev/cmd" // for TestPrimaryKeys + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDumpDatabase(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + dir := t.TempDir() + + type Version struct { + ID int64 `xorm:"pk autoincr"` + Version int64 + } + assert.NoError(t, db.GetEngine(t.Context()).Sync(new(Version))) + + for _, dbType := range setting.SupportedDatabaseTypes { + assert.NoError(t, db.DumpDatabase(filepath.Join(dir, dbType+".sql"), setting.DatabaseType(dbType))) + } +} + +func TestDeleteOrphanedObjects(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + countBefore, err := db.GetEngine(t.Context()).Count(&issues_model.PullRequest{}) + assert.NoError(t, err) + + _, err = db.GetEngine(t.Context()).Insert(&issues_model.PullRequest{IssueID: 1000}, &issues_model.PullRequest{IssueID: 1001}, &issues_model.PullRequest{IssueID: 1003}) + assert.NoError(t, err) + + orphaned, err := db.CountOrphanedObjects(t.Context(), "pull_request", "issue", "pull_request.issue_id=issue.id") + assert.NoError(t, err) + assert.EqualValues(t, 3, orphaned) + + err = db.DeleteOrphanedObjects(t.Context(), "pull_request", "issue", "pull_request.issue_id=issue.id") + assert.NoError(t, err) + + countAfter, err := db.GetEngine(t.Context()).Count(&issues_model.PullRequest{}) + assert.NoError(t, err) + assert.Equal(t, countBefore, countAfter) +} + +func TestPrimaryKeys(t *testing.T) { + // Some dbs require that all tables have primary keys, see + // https://github.com/go-gitea/gitea/issues/21086 + // https://github.com/go-gitea/gitea/issues/16802 + // To avoid creating tables without primary key again, this test will check them. + // Import "gitea.dev/cmd" to make sure each db.RegisterModel in init functions has been called. + + beans, err := db.NamesToBean() + require.NoError(t, err) + + whitelist := map[string]string{ + "the_table_name_to_skip_checking": "Write a note here to explain why", + } + + for _, bean := range beans { + table, err := db.GetXORMEngineForTesting().TableInfo(bean) + if err != nil { + t.Fatal(err) + } + if why, ok := whitelist[table.Name]; ok { + t.Logf("ignore %q because %q", table.Name, why) + continue + } + assert.NotEmpty(t, table.PrimaryKeys, "table %q has no primary key", table.Name) + } +}