diff options
author | Daniel Baumann <daniel@debian.org> | 2024-10-18 20:33:49 +0200 |
---|---|---|
committer | Daniel Baumann <daniel@debian.org> | 2024-12-12 23:57:56 +0100 |
commit | e68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch) | |
tree | 97775d6c13b0f416af55314eb6a89ef792474615 /models/db/context.go | |
parent | Initial commit. (diff) | |
download | forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip |
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'models/db/context.go')
-rw-r--r-- | models/db/context.go | 331 |
1 files changed, 331 insertions, 0 deletions
diff --git a/models/db/context.go b/models/db/context.go new file mode 100644 index 0000000..43f6125 --- /dev/null +++ b/models/db/context.go @@ -0,0 +1,331 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package db + +import ( + "context" + "database/sql" + + "xorm.io/builder" + "xorm.io/xorm" +) + +// DefaultContext is the default context to run xorm queries in +// will be overwritten by Init with HammerContext +var DefaultContext context.Context + +// contextKey is a value for use with context.WithValue. +type contextKey struct { + name string +} + +// enginedContextKey is a context key. It is used with context.Value() to get the current Engined for the context +var ( + enginedContextKey = &contextKey{"engined"} + _ Engined = &Context{} +) + +// Context represents a db context +type Context struct { + context.Context + e Engine + transaction bool +} + +func newContext(ctx context.Context, e Engine, transaction bool) *Context { + return &Context{ + Context: ctx, + e: e, + transaction: transaction, + } +} + +// InTransaction if context is in a transaction +func (ctx *Context) InTransaction() bool { + return ctx.transaction +} + +// Engine returns db engine +func (ctx *Context) Engine() Engine { + return ctx.e +} + +// Value shadows Value for context.Context but allows us to get ourselves and an Engined object +func (ctx *Context) Value(key any) any { + if key == enginedContextKey { + return ctx + } + return ctx.Context.Value(key) +} + +// WithContext returns this engine tied to this context +func (ctx *Context) WithContext(other context.Context) *Context { + return newContext(ctx, ctx.e.Context(other), ctx.transaction) +} + +// Engined structs provide an Engine +type Engined interface { + Engine() Engine +} + +// GetEngine will get a db Engine from this context or return an Engine restricted to this context +func GetEngine(ctx context.Context) Engine { + if e := getEngine(ctx); e != nil { + return e + } + return x.Context(ctx) +} + +// getEngine will get a db Engine from this context or return nil +func getEngine(ctx context.Context) Engine { + if engined, ok := ctx.(Engined); ok { + return engined.Engine() + } + enginedInterface := ctx.Value(enginedContextKey) + if enginedInterface != nil { + return enginedInterface.(Engined).Engine() + } + return nil +} + +// Committer represents an interface to Commit or Close the Context +type Committer interface { + Commit() error + Close() error +} + +// halfCommitter is a wrapper of Committer. +// It can be closed early, but can't be committed early, it is useful for reusing a transaction. +type halfCommitter struct { + committer Committer + committed bool +} + +func (c *halfCommitter) Commit() error { + c.committed = true + // should do nothing, and the parent committer will commit later + return nil +} + +func (c *halfCommitter) Close() error { + if c.committed { + // it's "commit and close", should do nothing, and the parent committer will commit later + return nil + } + + // it's "rollback and close", let the parent committer rollback right now + return c.committer.Close() +} + +// TxContext represents a transaction Context, +// it will reuse the existing transaction in the parent context or create a new one. +// Some tips to use: +// +// 1 It's always recommended to use `WithTx` in new code instead of `TxContext`, since `WithTx` will handle the transaction automatically. +// 2. To maintain the old code which uses `TxContext`: +// a. Always call `Close()` before returning regardless of whether `Commit()` has been called. +// b. Always call `Commit()` before returning if there are no errors, even if the code did not change any data. +// c. Remember the `Committer` will be a halfCommitter when a transaction is being reused. +// So calling `Commit()` will do nothing, but calling `Close()` without calling `Commit()` will rollback the transaction. +// And all operations submitted by the caller stack will be rollbacked as well, not only the operations in the current function. +// d. It doesn't mean rollback is forbidden, but always do it only when there is an error, and you do want to rollback. +func TxContext(parentCtx context.Context) (*Context, Committer, error) { + if sess, ok := inTransaction(parentCtx); ok { + return newContext(parentCtx, sess, true), &halfCommitter{committer: sess}, nil + } + + sess := x.NewSession() + if err := sess.Begin(); err != nil { + sess.Close() + return nil, nil, err + } + + return newContext(DefaultContext, sess, true), sess, nil +} + +// WithTx represents executing database operations on a transaction, if the transaction exist, +// this function will reuse it otherwise will create a new one and close it when finished. +func WithTx(parentCtx context.Context, f func(ctx context.Context) error) error { + if sess, ok := inTransaction(parentCtx); ok { + err := f(newContext(parentCtx, sess, true)) + if err != nil { + // rollback immediately, in case the caller ignores returned error and tries to commit the transaction. + _ = sess.Close() + } + return err + } + return txWithNoCheck(parentCtx, f) +} + +func txWithNoCheck(parentCtx context.Context, f func(ctx context.Context) error) error { + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + if err := f(newContext(parentCtx, sess, true)); err != nil { + return err + } + + return sess.Commit() +} + +// Insert inserts records into database +func Insert(ctx context.Context, beans ...any) error { + _, err := GetEngine(ctx).Insert(beans...) + return err +} + +// Exec executes a sql with args +func Exec(ctx context.Context, sqlAndArgs ...any) (sql.Result, error) { + return GetEngine(ctx).Exec(sqlAndArgs...) +} + +func Get[T any](ctx context.Context, cond builder.Cond) (object *T, exist bool, err error) { + if !cond.IsValid() { + panic("cond is invalid in db.Get(ctx, cond). This should not be possible.") + } + + var bean T + has, err := GetEngine(ctx).Where(cond).NoAutoCondition().Get(&bean) + if err != nil { + return nil, false, err + } else if !has { + return nil, false, nil + } + return &bean, true, nil +} + +func GetByID[T any](ctx context.Context, id int64) (object *T, exist bool, err error) { + var bean T + has, err := GetEngine(ctx).ID(id).NoAutoCondition().Get(&bean) + if err != nil { + return nil, false, err + } else if !has { + return nil, false, nil + } + return &bean, true, nil +} + +func Exist[T any](ctx context.Context, cond builder.Cond) (bool, error) { + if !cond.IsValid() { + panic("cond is invalid in db.Exist(ctx, cond). This should not be possible.") + } + + var bean T + return GetEngine(ctx).Where(cond).NoAutoCondition().Exist(&bean) +} + +func ExistByID[T any](ctx context.Context, id int64) (bool, error) { + var bean T + return GetEngine(ctx).ID(id).NoAutoCondition().Exist(&bean) +} + +// DeleteByID deletes the given bean with the given ID +func DeleteByID[T any](ctx context.Context, id int64) (int64, error) { + var bean T + return GetEngine(ctx).ID(id).NoAutoCondition().NoAutoTime().Delete(&bean) +} + +func DeleteByIDs[T any](ctx context.Context, ids ...int64) error { + if len(ids) == 0 { + return nil + } + + var bean T + _, err := GetEngine(ctx).In("id", ids).NoAutoCondition().NoAutoTime().Delete(&bean) + return err +} + +func Delete[T any](ctx context.Context, opts FindOptions) (int64, error) { + if opts == nil || !opts.ToConds().IsValid() { + panic("opts are empty or invalid in db.Delete(ctx, opts). This should not be possible.") + } + + var bean T + return GetEngine(ctx).Where(opts.ToConds()).NoAutoCondition().NoAutoTime().Delete(&bean) +} + +// DeleteByBean deletes all records according non-empty fields of the bean as conditions. +func DeleteByBean(ctx context.Context, bean any) (int64, error) { + return GetEngine(ctx).Delete(bean) +} + +// FindIDs finds the IDs for the given table name satisfying the given condition +// By passing a different value than "id" for "idCol", you can query for foreign IDs, i.e. the repo IDs which satisfy the condition +func FindIDs(ctx context.Context, tableName, idCol string, cond builder.Cond) ([]int64, error) { + ids := make([]int64, 0, 10) + if err := GetEngine(ctx).Table(tableName). + Cols(idCol). + Where(cond). + Find(&ids); err != nil { + return nil, err + } + return ids, nil +} + +// DecrByIDs decreases the given column for entities of the "bean" type with one of the given ids by one +// Timestamps of the entities won't be updated +func DecrByIDs(ctx context.Context, ids []int64, decrCol string, bean any) error { + _, err := GetEngine(ctx).Decr(decrCol).In("id", ids).NoAutoCondition().NoAutoTime().Update(bean) + return err +} + +// DeleteBeans deletes all given beans, beans must contain delete conditions. +func DeleteBeans(ctx context.Context, beans ...any) (err error) { + e := GetEngine(ctx) + for i := range beans { + if _, err = e.Delete(beans[i]); err != nil { + return err + } + } + return nil +} + +// TruncateBeans deletes all given beans, beans may contain delete conditions. +func TruncateBeans(ctx context.Context, beans ...any) (err error) { + e := GetEngine(ctx) + for i := range beans { + if _, err = e.Truncate(beans[i]); err != nil { + return err + } + } + return nil +} + +// CountByBean counts the number of database records according non-empty fields of the bean as conditions. +func CountByBean(ctx context.Context, bean any) (int64, error) { + return GetEngine(ctx).Count(bean) +} + +// TableName returns the table name according a bean object +func TableName(bean any) string { + return x.TableName(bean) +} + +// InTransaction returns true if the engine is in a transaction otherwise return false +func InTransaction(ctx context.Context) bool { + _, ok := inTransaction(ctx) + return ok +} + +func inTransaction(ctx context.Context) (*xorm.Session, bool) { + e := getEngine(ctx) + if e == nil { + return nil, false + } + + switch t := e.(type) { + case *xorm.Engine: + return nil, false + case *xorm.Session: + if t.IsInTx() { + return t, true + } + return nil, false + default: + return nil, false + } +} |