code.gitea.io/gitea@v1.22.3/models/db/engine.go (about) 1 // Copyright 2014 The Gogs Authors. All rights reserved. 2 // Copyright 2018 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package db 6 7 import ( 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 29 var ( 30 x *xorm.Engine 31 tables []any 32 initFuncs []func() error 33 ) 34 35 // Engine represents a xorm engine or session. 36 type Engine interface { 37 Table(tableNameOrBean any) *xorm.Session 38 Count(...any) (int64, error) 39 Decr(column string, arg ...any) *xorm.Session 40 Delete(...any) (int64, error) 41 Truncate(...any) (int64, error) 42 Exec(...any) (sql.Result, error) 43 Find(any, ...any) error 44 Get(beans ...any) (bool, error) 45 ID(any) *xorm.Session 46 In(string, ...any) *xorm.Session 47 Incr(column string, arg ...any) *xorm.Session 48 Insert(...any) (int64, error) 49 Iterate(any, xorm.IterFunc) error 50 Join(joinOperator string, tablename, condition any, args ...any) *xorm.Session 51 SQL(any, ...any) *xorm.Session 52 Where(any, ...any) *xorm.Session 53 Asc(colNames ...string) *xorm.Session 54 Desc(colNames ...string) *xorm.Session 55 Limit(limit int, start ...int) *xorm.Session 56 NoAutoTime() *xorm.Session 57 SumInt(bean any, columnName string) (res int64, err error) 58 Sync(...any) error 59 Select(string) *xorm.Session 60 SetExpr(string, any) *xorm.Session 61 NotIn(string, ...any) *xorm.Session 62 OrderBy(any, ...any) *xorm.Session 63 Exist(...any) (bool, error) 64 Distinct(...string) *xorm.Session 65 Query(...any) ([]map[string][]byte, error) 66 Cols(...string) *xorm.Session 67 Context(ctx context.Context) *xorm.Session 68 Ping() error 69 } 70 71 // TableInfo returns table's information via an object 72 func TableInfo(v any) (*schemas.Table, error) { 73 return x.TableInfo(v) 74 } 75 76 // DumpTables dump tables information 77 func DumpTables(tables []*schemas.Table, w io.Writer, tp ...schemas.DBType) error { 78 return x.DumpTables(tables, w, tp...) 79 } 80 81 // RegisterModel registers model, if initfunc provided, it will be invoked after data model sync 82 func RegisterModel(bean any, initFunc ...func() error) { 83 tables = append(tables, bean) 84 if len(initFuncs) > 0 && initFunc[0] != nil { 85 initFuncs = append(initFuncs, initFunc[0]) 86 } 87 } 88 89 func init() { 90 gonicNames := []string{"SSL", "UID"} 91 for _, name := range gonicNames { 92 names.LintGonicMapper[name] = true 93 } 94 } 95 96 // newXORMEngine returns a new XORM engine from the configuration 97 func newXORMEngine() (*xorm.Engine, error) { 98 connStr, err := setting.DBConnStr() 99 if err != nil { 100 return nil, err 101 } 102 103 var engine *xorm.Engine 104 105 if setting.Database.Type.IsPostgreSQL() && len(setting.Database.Schema) > 0 { 106 // OK whilst we sort out our schema issues - create a schema aware postgres 107 registerPostgresSchemaDriver() 108 engine, err = xorm.NewEngine("postgresschema", connStr) 109 } else { 110 engine, err = xorm.NewEngine(setting.Database.Type.String(), connStr) 111 } 112 113 if err != nil { 114 return nil, err 115 } 116 if setting.Database.Type == "mysql" { 117 engine.Dialect().SetParams(map[string]string{"rowFormat": "DYNAMIC"}) 118 } else if setting.Database.Type == "mssql" { 119 engine.Dialect().SetParams(map[string]string{"DEFAULT_VARCHAR": "nvarchar"}) 120 } 121 engine.SetSchema(setting.Database.Schema) 122 return engine, nil 123 } 124 125 // SyncAllTables sync the schemas of all tables, is required by unit test code 126 func SyncAllTables() error { 127 _, err := x.StoreEngine("InnoDB").SyncWithOptions(xorm.SyncOptions{ 128 WarnIfDatabaseColumnMissed: true, 129 }, tables...) 130 return err 131 } 132 133 // InitEngine initializes the xorm.Engine and sets it as db.DefaultContext 134 func InitEngine(ctx context.Context) error { 135 xormEngine, err := newXORMEngine() 136 if err != nil { 137 return fmt.Errorf("failed to connect to database: %w", err) 138 } 139 140 xormEngine.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. 143 xormEngine.SetLogger(NewXORMLogger(setting.Database.LogSQL)) 144 xormEngine.ShowSQL(setting.Database.LogSQL) 145 xormEngine.SetMaxOpenConns(setting.Database.MaxOpenConns) 146 xormEngine.SetMaxIdleConns(setting.Database.MaxIdleConns) 147 xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime) 148 xormEngine.SetDefaultContext(ctx) 149 150 if setting.Database.SlowQueryThreshold > 0 { 151 xormEngine.AddHook(&SlowQueryHook{ 152 Threshold: setting.Database.SlowQueryThreshold, 153 Logger: log.GetLogger("xorm"), 154 }) 155 } 156 157 SetDefaultEngine(ctx, xormEngine) 158 return nil 159 } 160 161 // SetDefaultEngine sets the default engine for db 162 func SetDefaultEngine(ctx context.Context, eng *xorm.Engine) { 163 x = eng 164 DefaultContext = &Context{ 165 Context: ctx, 166 e: 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. 174 func UnsetDefaultEngine() { 175 if x != nil { 176 _ = x.Close() 177 x = nil 178 } 179 DefaultContext = 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. 187 func InitEngineWithMigration(ctx context.Context, migrateFunc func(*xorm.Engine) error) (err error) { 188 if err = InitEngine(ctx); err != nil { 189 return err 190 } 191 192 if err = x.Ping(); err != nil { 193 return err 194 } 195 196 preprocessDatabaseCollation(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. 204 if err = migrateFunc(x); err != nil { 205 return fmt.Errorf("migrate: %w", err) 206 } 207 208 if err = SyncAllTables(); err != nil { 209 return fmt.Errorf("sync database struct error: %w", err) 210 } 211 212 for _, initFunc := range initFuncs { 213 if err := initFunc(); err != nil { 214 return fmt.Errorf("initFunc failed: %w", err) 215 } 216 } 217 218 return nil 219 } 220 221 // NamesToBean return a list of beans or an error 222 func NamesToBean(names ...string) ([]any, error) { 223 beans := []any{} 224 if len(names) == 0 { 225 beans = append(beans, tables...) 226 return beans, nil 227 } 228 // Need to map provided names to beans... 229 beanMap := make(map[string]any) 230 for _, bean := range tables { 231 beanMap[strings.ToLower(reflect.Indirect(reflect.ValueOf(bean)).Type().Name())] = bean 232 beanMap[strings.ToLower(x.TableName(bean))] = bean 233 beanMap[strings.ToLower(x.TableName(bean, true))] = bean 234 } 235 236 gotBean := make(map[any]bool) 237 for _, name := range names { 238 bean, ok := beanMap[strings.ToLower(strings.TrimSpace(name))] 239 if !ok { 240 return nil, fmt.Errorf("no table found that matches: %s", name) 241 } 242 if !gotBean[bean] { 243 beans = append(beans, bean) 244 gotBean[bean] = true 245 } 246 } 247 return beans, nil 248 } 249 250 // DumpDatabase dumps all data from database according the special database SQL syntax to file system. 251 func DumpDatabase(filePath, dbType string) error { 252 var tbs []*schemas.Table 253 for _, t := range tables { 254 t, err := x.TableInfo(t) 255 if err != nil { 256 return err 257 } 258 tbs = append(tbs, t) 259 } 260 261 type Version struct { 262 ID int64 `xorm:"pk autoincr"` 263 Version int64 264 } 265 t, err := x.TableInfo(&Version{}) 266 if err != nil { 267 return err 268 } 269 tbs = append(tbs, t) 270 271 if len(dbType) > 0 { 272 return x.DumpTablesToFile(tbs, filePath, schemas.DBType(dbType)) 273 } 274 return x.DumpTablesToFile(tbs, filePath) 275 } 276 277 // MaxBatchInsertSize returns the table's max batch insert size 278 func MaxBatchInsertSize(bean any) int { 279 t, err := x.TableInfo(bean) 280 if err != nil { 281 return 50 282 } 283 return 999 / len(t.ColumnsSeq()) 284 } 285 286 // IsTableNotEmpty returns true if table has at least one record 287 func IsTableNotEmpty(beanOrTableName any) (bool, error) { 288 return x.Table(beanOrTableName).Exist() 289 } 290 291 // DeleteAllRecords will delete all the records of this table 292 func DeleteAllRecords(tableName string) error { 293 _, err := x.Exec(fmt.Sprintf("DELETE FROM %s", tableName)) 294 return err 295 } 296 297 // GetMaxID will return max id of the table 298 func GetMaxID(beanOrTableName any) (maxID int64, err error) { 299 _, err = x.Select("MAX(id)").Table(beanOrTableName).Get(&maxID) 300 return maxID, err 301 } 302 303 func SetLogSQL(ctx context.Context, on bool) { 304 e := GetEngine(ctx) 305 if x, ok := e.(*xorm.Engine); ok { 306 x.ShowSQL(on) 307 } else if sess, ok := e.(*xorm.Session); ok { 308 sess.Engine().ShowSQL(on) 309 } 310 } 311 312 type SlowQueryHook struct { 313 Threshold time.Duration 314 Logger log.Logger 315 } 316 317 var _ contexts.Hook = &SlowQueryHook{} 318 319 func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) { 320 return c.Ctx, nil 321 } 322 323 func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error { 324 if 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. 328 h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime) 329 } 330 return nil 331 }