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  }