gitea
Зеркало из https://github.com/go-gitea/gitea
1// Copyright 2014 The Gogs Authors. All rights reserved.
2// Copyright 2018 The Gitea Authors. All rights reserved.
3// SPDX-License-Identifier: MIT
4
5package db
6
7import (
8"context"
9"database/sql"
10"fmt"
11"io"
12"reflect"
13"strings"
14"time"
15
16"code.gitea.io/gitea/modules/log"
17"code.gitea.io/gitea/modules/setting"
18
19"xorm.io/xorm"
20"xorm.io/xorm/contexts"
21"xorm.io/xorm/names"
22"xorm.io/xorm/schemas"
23
24_ "github.com/go-sql-driver/mysql" // Needed for the MySQL driver
25_ "github.com/lib/pq" // Needed for the Postgresql driver
26_ "github.com/microsoft/go-mssqldb" // Needed for the MSSQL driver
27)
28
29var (
30x *xorm.Engine
31tables []any
32initFuncs []func() error
33)
34
35// Engine represents a xorm engine or session.
36type Engine interface {
37Table(tableNameOrBean any) *xorm.Session
38Count(...any) (int64, error)
39Decr(column string, arg ...any) *xorm.Session
40Delete(...any) (int64, error)
41Truncate(...any) (int64, error)
42Exec(...any) (sql.Result, error)
43Find(any, ...any) error
44Get(beans ...any) (bool, error)
45ID(any) *xorm.Session
46In(string, ...any) *xorm.Session
47Incr(column string, arg ...any) *xorm.Session
48Insert(...any) (int64, error)
49Iterate(any, xorm.IterFunc) error
50Join(joinOperator string, tablename, condition any, args ...any) *xorm.Session
51SQL(any, ...any) *xorm.Session
52Where(any, ...any) *xorm.Session
53Asc(colNames ...string) *xorm.Session
54Desc(colNames ...string) *xorm.Session
55Limit(limit int, start ...int) *xorm.Session
56NoAutoTime() *xorm.Session
57SumInt(bean any, columnName string) (res int64, err error)
58Sync(...any) error
59Select(string) *xorm.Session
60SetExpr(string, any) *xorm.Session
61NotIn(string, ...any) *xorm.Session
62OrderBy(any, ...any) *xorm.Session
63Exist(...any) (bool, error)
64Distinct(...string) *xorm.Session
65Query(...any) ([]map[string][]byte, error)
66Cols(...string) *xorm.Session
67Context(ctx context.Context) *xorm.Session
68Ping() error
69}
70
71// TableInfo returns table's information via an object
72func TableInfo(v any) (*schemas.Table, error) {
73return x.TableInfo(v)
74}
75
76// DumpTables dump tables information
77func DumpTables(tables []*schemas.Table, w io.Writer, tp ...schemas.DBType) error {
78return x.DumpTables(tables, w, tp...)
79}
80
81// RegisterModel registers model, if initfunc provided, it will be invoked after data model sync
82func RegisterModel(bean any, initFunc ...func() error) {
83tables = append(tables, bean)
84if len(initFuncs) > 0 && initFunc[0] != nil {
85initFuncs = append(initFuncs, initFunc[0])
86}
87}
88
89func init() {
90gonicNames := []string{"SSL", "UID"}
91for _, name := range gonicNames {
92names.LintGonicMapper[name] = true
93}
94}
95
96// newXORMEngine returns a new XORM engine from the configuration
97func newXORMEngine() (*xorm.Engine, error) {
98connStr, err := setting.DBConnStr()
99if err != nil {
100return nil, err
101}
102
103var engine *xorm.Engine
104
105if setting.Database.Type.IsPostgreSQL() && len(setting.Database.Schema) > 0 {
106// OK whilst we sort out our schema issues - create a schema aware postgres
107registerPostgresSchemaDriver()
108engine, err = xorm.NewEngine("postgresschema", connStr)
109} else {
110engine, err = xorm.NewEngine(setting.Database.Type.String(), connStr)
111}
112
113if err != nil {
114return nil, err
115}
116if setting.Database.Type == "mysql" {
117engine.Dialect().SetParams(map[string]string{"rowFormat": "DYNAMIC"})
118} else if setting.Database.Type == "mssql" {
119engine.Dialect().SetParams(map[string]string{"DEFAULT_VARCHAR": "nvarchar"})
120}
121engine.SetSchema(setting.Database.Schema)
122return engine, nil
123}
124
125// SyncAllTables sync the schemas of all tables, is required by unit test code
126func SyncAllTables() error {
127_, err := x.StoreEngine("InnoDB").SyncWithOptions(xorm.SyncOptions{
128WarnIfDatabaseColumnMissed: true,
129}, tables...)
130return err
131}
132
133// InitEngine initializes the xorm.Engine and sets it as db.DefaultContext
134func InitEngine(ctx context.Context) error {
135xormEngine, err := newXORMEngine()
136if err != nil {
137return fmt.Errorf("failed to connect to database: %w", err)
138}
139
140xormEngine.SetMapper(names.GonicMapper{})
141// WARNING: for serv command, MUST remove the output to os.stdout,
142// so use log file to instead print to stdout.
143xormEngine.SetLogger(NewXORMLogger(setting.Database.LogSQL))
144xormEngine.ShowSQL(setting.Database.LogSQL)
145xormEngine.SetMaxOpenConns(setting.Database.MaxOpenConns)
146xormEngine.SetMaxIdleConns(setting.Database.MaxIdleConns)
147xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
148xormEngine.SetDefaultContext(ctx)
149
150if setting.Database.SlowQueryThreshold > 0 {
151xormEngine.AddHook(&SlowQueryHook{
152Threshold: setting.Database.SlowQueryThreshold,
153Logger: log.GetLogger("xorm"),
154})
155}
156
157SetDefaultEngine(ctx, xormEngine)
158return nil
159}
160
161// SetDefaultEngine sets the default engine for db
162func SetDefaultEngine(ctx context.Context, eng *xorm.Engine) {
163x = eng
164DefaultContext = &Context{
165Context: ctx,
166e: x,
167}
168}
169
170// UnsetDefaultEngine closes and unsets the default engine
171// We hope the SetDefaultEngine and UnsetDefaultEngine can be paired, but it's impossible now,
172// there are many calls to InitEngine -> SetDefaultEngine directly to overwrite the `x` and DefaultContext without close
173// Global database engine related functions are all racy and there is no graceful close right now.
174func UnsetDefaultEngine() {
175if x != nil {
176_ = x.Close()
177x = nil
178}
179DefaultContext = nil
180}
181
182// InitEngineWithMigration initializes a new xorm.Engine and sets it as the db.DefaultContext
183// This function must never call .Sync() if the provided migration function fails.
184// When called from the "doctor" command, the migration function is a version check
185// that prevents the doctor from fixing anything in the database if the migration level
186// is different from the expected value.
187func InitEngineWithMigration(ctx context.Context, migrateFunc func(*xorm.Engine) error) (err error) {
188if err = InitEngine(ctx); err != nil {
189return err
190}
191
192if err = x.Ping(); err != nil {
193return err
194}
195
196preprocessDatabaseCollation(x)
197
198// We have to run migrateFunc here in case the user is re-running installation on a previously created DB.
199// If we do not then table schemas will be changed and there will be conflicts when the migrations run properly.
200//
201// Installation should only be being re-run if users want to recover an old database.
202// However, we should think carefully about should we support re-install on an installed instance,
203// as there may be other problems due to secret reinitialization.
204if err = migrateFunc(x); err != nil {
205return fmt.Errorf("migrate: %w", err)
206}
207
208if err = SyncAllTables(); err != nil {
209return fmt.Errorf("sync database struct error: %w", err)
210}
211
212for _, initFunc := range initFuncs {
213if err := initFunc(); err != nil {
214return fmt.Errorf("initFunc failed: %w", err)
215}
216}
217
218return nil
219}
220
221// NamesToBean return a list of beans or an error
222func NamesToBean(names ...string) ([]any, error) {
223beans := []any{}
224if len(names) == 0 {
225beans = append(beans, tables...)
226return beans, nil
227}
228// Need to map provided names to beans...
229beanMap := make(map[string]any)
230for _, bean := range tables {
231beanMap[strings.ToLower(reflect.Indirect(reflect.ValueOf(bean)).Type().Name())] = bean
232beanMap[strings.ToLower(x.TableName(bean))] = bean
233beanMap[strings.ToLower(x.TableName(bean, true))] = bean
234}
235
236gotBean := make(map[any]bool)
237for _, name := range names {
238bean, ok := beanMap[strings.ToLower(strings.TrimSpace(name))]
239if !ok {
240return nil, fmt.Errorf("no table found that matches: %s", name)
241}
242if !gotBean[bean] {
243beans = append(beans, bean)
244gotBean[bean] = true
245}
246}
247return beans, nil
248}
249
250// DumpDatabase dumps all data from database according the special database SQL syntax to file system.
251func DumpDatabase(filePath, dbType string) error {
252var tbs []*schemas.Table
253for _, t := range tables {
254t, err := x.TableInfo(t)
255if err != nil {
256return err
257}
258tbs = append(tbs, t)
259}
260
261type Version struct {
262ID int64 `xorm:"pk autoincr"`
263Version int64
264}
265t, err := x.TableInfo(&Version{})
266if err != nil {
267return err
268}
269tbs = append(tbs, t)
270
271if len(dbType) > 0 {
272return x.DumpTablesToFile(tbs, filePath, schemas.DBType(dbType))
273}
274return x.DumpTablesToFile(tbs, filePath)
275}
276
277// MaxBatchInsertSize returns the table's max batch insert size
278func MaxBatchInsertSize(bean any) int {
279t, err := x.TableInfo(bean)
280if err != nil {
281return 50
282}
283return 999 / len(t.ColumnsSeq())
284}
285
286// IsTableNotEmpty returns true if table has at least one record
287func IsTableNotEmpty(beanOrTableName any) (bool, error) {
288return x.Table(beanOrTableName).Exist()
289}
290
291// DeleteAllRecords will delete all the records of this table
292func DeleteAllRecords(tableName string) error {
293_, err := x.Exec(fmt.Sprintf("DELETE FROM %s", tableName))
294return err
295}
296
297// GetMaxID will return max id of the table
298func GetMaxID(beanOrTableName any) (maxID int64, err error) {
299_, err = x.Select("MAX(id)").Table(beanOrTableName).Get(&maxID)
300return maxID, err
301}
302
303func SetLogSQL(ctx context.Context, on bool) {
304e := GetEngine(ctx)
305if x, ok := e.(*xorm.Engine); ok {
306x.ShowSQL(on)
307} else if sess, ok := e.(*xorm.Session); ok {
308sess.Engine().ShowSQL(on)
309}
310}
311
312type SlowQueryHook struct {
313Threshold time.Duration
314Logger log.Logger
315}
316
317var _ contexts.Hook = &SlowQueryHook{}
318
319func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
320return c.Ctx, nil
321}
322
323func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
324if c.ExecuteTime >= h.Threshold {
325// 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function
326// is being displayed (the function that ultimately wants to execute the query in the code)
327// instead of the function of the slow query hook being called.
328h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
329}
330return nil
331}
332