code.gitea.io/gitea@v1.22.3/models/db/context.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package db 5 6 import ( 7 "context" 8 "database/sql" 9 10 "xorm.io/builder" 11 "xorm.io/xorm" 12 ) 13 14 // DefaultContext is the default context to run xorm queries in 15 // will be overwritten by Init with HammerContext 16 var DefaultContext context.Context 17 18 // contextKey is a value for use with context.WithValue. 19 type contextKey struct { 20 name string 21 } 22 23 // enginedContextKey is a context key. It is used with context.Value() to get the current Engined for the context 24 var ( 25 enginedContextKey = &contextKey{"engined"} 26 _ Engined = &Context{} 27 ) 28 29 // Context represents a db context 30 type Context struct { 31 context.Context 32 e Engine 33 transaction bool 34 } 35 36 func newContext(ctx context.Context, e Engine, transaction bool) *Context { 37 return &Context{ 38 Context: ctx, 39 e: e, 40 transaction: transaction, 41 } 42 } 43 44 // InTransaction if context is in a transaction 45 func (ctx *Context) InTransaction() bool { 46 return ctx.transaction 47 } 48 49 // Engine returns db engine 50 func (ctx *Context) Engine() Engine { 51 return ctx.e 52 } 53 54 // Value shadows Value for context.Context but allows us to get ourselves and an Engined object 55 func (ctx *Context) Value(key any) any { 56 if key == enginedContextKey { 57 return ctx 58 } 59 return ctx.Context.Value(key) 60 } 61 62 // WithContext returns this engine tied to this context 63 func (ctx *Context) WithContext(other context.Context) *Context { 64 return newContext(ctx, ctx.e.Context(other), ctx.transaction) 65 } 66 67 // Engined structs provide an Engine 68 type Engined interface { 69 Engine() Engine 70 } 71 72 // GetEngine will get a db Engine from this context or return an Engine restricted to this context 73 func GetEngine(ctx context.Context) Engine { 74 if e := getEngine(ctx); e != nil { 75 return e 76 } 77 return x.Context(ctx) 78 } 79 80 // getEngine will get a db Engine from this context or return nil 81 func getEngine(ctx context.Context) Engine { 82 if engined, ok := ctx.(Engined); ok { 83 return engined.Engine() 84 } 85 enginedInterface := ctx.Value(enginedContextKey) 86 if enginedInterface != nil { 87 return enginedInterface.(Engined).Engine() 88 } 89 return nil 90 } 91 92 // Committer represents an interface to Commit or Close the Context 93 type Committer interface { 94 Commit() error 95 Close() error 96 } 97 98 // halfCommitter is a wrapper of Committer. 99 // It can be closed early, but can't be committed early, it is useful for reusing a transaction. 100 type halfCommitter struct { 101 committer Committer 102 committed bool 103 } 104 105 func (c *halfCommitter) Commit() error { 106 c.committed = true 107 // should do nothing, and the parent committer will commit later 108 return nil 109 } 110 111 func (c *halfCommitter) Close() error { 112 if c.committed { 113 // it's "commit and close", should do nothing, and the parent committer will commit later 114 return nil 115 } 116 117 // it's "rollback and close", let the parent committer rollback right now 118 return c.committer.Close() 119 } 120 121 // TxContext represents a transaction Context, 122 // it will reuse the existing transaction in the parent context or create a new one. 123 // Some tips to use: 124 // 125 // 1 It's always recommended to use `WithTx` in new code instead of `TxContext`, since `WithTx` will handle the transaction automatically. 126 // 2. To maintain the old code which uses `TxContext`: 127 // a. Always call `Close()` before returning regardless of whether `Commit()` has been called. 128 // b. Always call `Commit()` before returning if there are no errors, even if the code did not change any data. 129 // c. Remember the `Committer` will be a halfCommitter when a transaction is being reused. 130 // So calling `Commit()` will do nothing, but calling `Close()` without calling `Commit()` will rollback the transaction. 131 // And all operations submitted by the caller stack will be rollbacked as well, not only the operations in the current function. 132 // d. It doesn't mean rollback is forbidden, but always do it only when there is an error, and you do want to rollback. 133 func TxContext(parentCtx context.Context) (*Context, Committer, error) { 134 if sess, ok := inTransaction(parentCtx); ok { 135 return newContext(parentCtx, sess, true), &halfCommitter{committer: sess}, nil 136 } 137 138 sess := x.NewSession() 139 if err := sess.Begin(); err != nil { 140 sess.Close() 141 return nil, nil, err 142 } 143 144 return newContext(DefaultContext, sess, true), sess, nil 145 } 146 147 // WithTx represents executing database operations on a transaction, if the transaction exist, 148 // this function will reuse it otherwise will create a new one and close it when finished. 149 func WithTx(parentCtx context.Context, f func(ctx context.Context) error) error { 150 if sess, ok := inTransaction(parentCtx); ok { 151 err := f(newContext(parentCtx, sess, true)) 152 if err != nil { 153 // rollback immediately, in case the caller ignores returned error and tries to commit the transaction. 154 _ = sess.Close() 155 } 156 return err 157 } 158 return txWithNoCheck(parentCtx, f) 159 } 160 161 func txWithNoCheck(parentCtx context.Context, f func(ctx context.Context) error) error { 162 sess := x.NewSession() 163 defer sess.Close() 164 if err := sess.Begin(); err != nil { 165 return err 166 } 167 168 if err := f(newContext(parentCtx, sess, true)); err != nil { 169 return err 170 } 171 172 return sess.Commit() 173 } 174 175 // Insert inserts records into database 176 func Insert(ctx context.Context, beans ...any) error { 177 _, err := GetEngine(ctx).Insert(beans...) 178 return err 179 } 180 181 // Exec executes a sql with args 182 func Exec(ctx context.Context, sqlAndArgs ...any) (sql.Result, error) { 183 return GetEngine(ctx).Exec(sqlAndArgs...) 184 } 185 186 func Get[T any](ctx context.Context, cond builder.Cond) (object *T, exist bool, err error) { 187 if !cond.IsValid() { 188 panic("cond is invalid in db.Get(ctx, cond). This should not be possible.") 189 } 190 191 var bean T 192 has, err := GetEngine(ctx).Where(cond).NoAutoCondition().Get(&bean) 193 if err != nil { 194 return nil, false, err 195 } else if !has { 196 return nil, false, nil 197 } 198 return &bean, true, nil 199 } 200 201 func GetByID[T any](ctx context.Context, id int64) (object *T, exist bool, err error) { 202 var bean T 203 has, err := GetEngine(ctx).ID(id).NoAutoCondition().Get(&bean) 204 if err != nil { 205 return nil, false, err 206 } else if !has { 207 return nil, false, nil 208 } 209 return &bean, true, nil 210 } 211 212 func Exist[T any](ctx context.Context, cond builder.Cond) (bool, error) { 213 if !cond.IsValid() { 214 panic("cond is invalid in db.Exist(ctx, cond). This should not be possible.") 215 } 216 217 var bean T 218 return GetEngine(ctx).Where(cond).NoAutoCondition().Exist(&bean) 219 } 220 221 func ExistByID[T any](ctx context.Context, id int64) (bool, error) { 222 var bean T 223 return GetEngine(ctx).ID(id).NoAutoCondition().Exist(&bean) 224 } 225 226 // DeleteByID deletes the given bean with the given ID 227 func DeleteByID[T any](ctx context.Context, id int64) (int64, error) { 228 var bean T 229 return GetEngine(ctx).ID(id).NoAutoCondition().NoAutoTime().Delete(&bean) 230 } 231 232 func DeleteByIDs[T any](ctx context.Context, ids ...int64) error { 233 if len(ids) == 0 { 234 return nil 235 } 236 237 var bean T 238 _, err := GetEngine(ctx).In("id", ids).NoAutoCondition().NoAutoTime().Delete(&bean) 239 return err 240 } 241 242 func Delete[T any](ctx context.Context, opts FindOptions) (int64, error) { 243 if opts == nil || !opts.ToConds().IsValid() { 244 panic("opts are empty or invalid in db.Delete(ctx, opts). This should not be possible.") 245 } 246 247 var bean T 248 return GetEngine(ctx).Where(opts.ToConds()).NoAutoCondition().NoAutoTime().Delete(&bean) 249 } 250 251 // DeleteByBean deletes all records according non-empty fields of the bean as conditions. 252 func DeleteByBean(ctx context.Context, bean any) (int64, error) { 253 return GetEngine(ctx).Delete(bean) 254 } 255 256 // FindIDs finds the IDs for the given table name satisfying the given condition 257 // By passing a different value than "id" for "idCol", you can query for foreign IDs, i.e. the repo IDs which satisfy the condition 258 func FindIDs(ctx context.Context, tableName, idCol string, cond builder.Cond) ([]int64, error) { 259 ids := make([]int64, 0, 10) 260 if err := GetEngine(ctx).Table(tableName). 261 Cols(idCol). 262 Where(cond). 263 Find(&ids); err != nil { 264 return nil, err 265 } 266 return ids, nil 267 } 268 269 // DecrByIDs decreases the given column for entities of the "bean" type with one of the given ids by one 270 // Timestamps of the entities won't be updated 271 func DecrByIDs(ctx context.Context, ids []int64, decrCol string, bean any) error { 272 _, err := GetEngine(ctx).Decr(decrCol).In("id", ids).NoAutoCondition().NoAutoTime().Update(bean) 273 return err 274 } 275 276 // DeleteBeans deletes all given beans, beans must contain delete conditions. 277 func DeleteBeans(ctx context.Context, beans ...any) (err error) { 278 e := GetEngine(ctx) 279 for i := range beans { 280 if _, err = e.Delete(beans[i]); err != nil { 281 return err 282 } 283 } 284 return nil 285 } 286 287 // TruncateBeans deletes all given beans, beans may contain delete conditions. 288 func TruncateBeans(ctx context.Context, beans ...any) (err error) { 289 e := GetEngine(ctx) 290 for i := range beans { 291 if _, err = e.Truncate(beans[i]); err != nil { 292 return err 293 } 294 } 295 return nil 296 } 297 298 // CountByBean counts the number of database records according non-empty fields of the bean as conditions. 299 func CountByBean(ctx context.Context, bean any) (int64, error) { 300 return GetEngine(ctx).Count(bean) 301 } 302 303 // TableName returns the table name according a bean object 304 func TableName(bean any) string { 305 return x.TableName(bean) 306 } 307 308 // InTransaction returns true if the engine is in a transaction otherwise return false 309 func InTransaction(ctx context.Context) bool { 310 _, ok := inTransaction(ctx) 311 return ok 312 } 313 314 func inTransaction(ctx context.Context) (*xorm.Session, bool) { 315 e := getEngine(ctx) 316 if e == nil { 317 return nil, false 318 } 319 320 switch t := e.(type) { 321 case *xorm.Engine: 322 return nil, false 323 case *xorm.Session: 324 if t.IsInTx() { 325 return t, true 326 } 327 return nil, false 328 default: 329 return nil, false 330 } 331 }