mirror of
https://gitee.com/johng/gf
synced 2026-07-05 05:13:14 +08:00
Merge branch 'master' into copilot/fix-3931
This commit is contained in:
@ -294,6 +294,9 @@ type DB interface {
|
||||
// SetMaxConnLifeTime sets the maximum amount of time a connection may be reused.
|
||||
SetMaxConnLifeTime(d time.Duration)
|
||||
|
||||
// SetMaxIdleConnTime sets the maximum amount of time a connection may be idle before being closed.
|
||||
SetMaxIdleConnTime(d time.Duration)
|
||||
|
||||
// ===========================================================================
|
||||
// Utility methods.
|
||||
// ===========================================================================
|
||||
@ -510,24 +513,25 @@ type StatsItem interface {
|
||||
|
||||
// Core is the base struct for database management.
|
||||
type Core struct {
|
||||
db DB // DB interface object.
|
||||
ctx context.Context // Context for chaining operation only. Do not set a default value in Core initialization.
|
||||
group string // Configuration group name.
|
||||
schema string // Custom schema for this object.
|
||||
debug *gtype.Bool // Enable debug mode for the database, which can be changed in runtime.
|
||||
cache *gcache.Cache // Cache manager, SQL result cache only.
|
||||
links *gmap.Map // links caches all created links by node.
|
||||
logger glog.ILogger // Logger for logging functionality.
|
||||
config *ConfigNode // Current config node.
|
||||
localTypeMap *gmap.StrAnyMap // Local type map for database field type conversion.
|
||||
dynamicConfig dynamicConfig // Dynamic configurations, which can be changed in runtime.
|
||||
innerMemCache *gcache.Cache // Internal memory cache for storing temporary data.
|
||||
db DB // DB interface object.
|
||||
ctx context.Context // Context for chaining operation only. Do not set a default value in Core initialization.
|
||||
group string // Configuration group name.
|
||||
schema string // Custom schema for this object.
|
||||
debug *gtype.Bool // Enable debug mode for the database, which can be changed in runtime.
|
||||
cache *gcache.Cache // Cache manager, SQL result cache only.
|
||||
links *gmap.KVMap[ConfigNode, *sql.DB] // links caches all created links by node.
|
||||
logger glog.ILogger // Logger for logging functionality.
|
||||
config *ConfigNode // Current config node.
|
||||
localTypeMap *gmap.StrAnyMap // Local type map for database field type conversion.
|
||||
dynamicConfig dynamicConfig // Dynamic configurations, which can be changed in runtime.
|
||||
innerMemCache *gcache.Cache // Internal memory cache for storing temporary data.
|
||||
}
|
||||
|
||||
type dynamicConfig struct {
|
||||
MaxIdleConnCount int
|
||||
MaxOpenConnCount int
|
||||
MaxConnLifeTime time.Duration
|
||||
MaxIdleConnTime time.Duration
|
||||
}
|
||||
|
||||
// DoCommitInput is the input parameters for function DoCommit.
|
||||
@ -787,28 +791,39 @@ const (
|
||||
type LocalType string
|
||||
|
||||
const (
|
||||
LocalTypeUndefined LocalType = ""
|
||||
LocalTypeString LocalType = "string"
|
||||
LocalTypeTime LocalType = "time"
|
||||
LocalTypeDate LocalType = "date"
|
||||
LocalTypeDatetime LocalType = "datetime"
|
||||
LocalTypeInt LocalType = "int"
|
||||
LocalTypeUint LocalType = "uint"
|
||||
LocalTypeInt64 LocalType = "int64"
|
||||
LocalTypeUint64 LocalType = "uint64"
|
||||
LocalTypeBigInt LocalType = "bigint"
|
||||
LocalTypeIntSlice LocalType = "[]int"
|
||||
LocalTypeInt64Slice LocalType = "[]int64"
|
||||
LocalTypeUint64Slice LocalType = "[]uint64"
|
||||
LocalTypeStringSlice LocalType = "[]string"
|
||||
LocalTypeInt64Bytes LocalType = "int64-bytes"
|
||||
LocalTypeUint64Bytes LocalType = "uint64-bytes"
|
||||
LocalTypeFloat32 LocalType = "float32"
|
||||
LocalTypeFloat64 LocalType = "float64"
|
||||
LocalTypeBytes LocalType = "[]byte"
|
||||
LocalTypeBool LocalType = "bool"
|
||||
LocalTypeJson LocalType = "json"
|
||||
LocalTypeJsonb LocalType = "jsonb"
|
||||
LocalTypeUndefined LocalType = ""
|
||||
LocalTypeString LocalType = "string"
|
||||
LocalTypeTime LocalType = "time"
|
||||
LocalTypeDate LocalType = "date"
|
||||
LocalTypeDatetime LocalType = "datetime"
|
||||
LocalTypeInt LocalType = "int"
|
||||
LocalTypeUint LocalType = "uint"
|
||||
LocalTypeInt32 LocalType = "int32"
|
||||
LocalTypeUint32 LocalType = "uint32"
|
||||
LocalTypeInt64 LocalType = "int64"
|
||||
LocalTypeUint64 LocalType = "uint64"
|
||||
LocalTypeBigInt LocalType = "bigint"
|
||||
LocalTypeIntSlice LocalType = "[]int"
|
||||
LocalTypeUintSlice LocalType = "[]uint"
|
||||
LocalTypeInt32Slice LocalType = "[]int32"
|
||||
LocalTypeUint32Slice LocalType = "[]uint32"
|
||||
LocalTypeInt64Slice LocalType = "[]int64"
|
||||
LocalTypeUint64Slice LocalType = "[]uint64"
|
||||
LocalTypeStringSlice LocalType = "[]string"
|
||||
LocalTypeInt64Bytes LocalType = "int64-bytes"
|
||||
LocalTypeUint64Bytes LocalType = "uint64-bytes"
|
||||
LocalTypeFloat32 LocalType = "float32"
|
||||
LocalTypeFloat64 LocalType = "float64"
|
||||
LocalTypeFloat32Slice LocalType = "[]float32"
|
||||
LocalTypeFloat64Slice LocalType = "[]float64"
|
||||
LocalTypeBytes LocalType = "[]byte"
|
||||
LocalTypeBytesSlice LocalType = "[][]byte"
|
||||
LocalTypeBool LocalType = "bool"
|
||||
LocalTypeBoolSlice LocalType = "[]bool"
|
||||
LocalTypeJson LocalType = "json"
|
||||
LocalTypeJsonb LocalType = "jsonb"
|
||||
LocalTypeUUID LocalType = "uuid.UUID"
|
||||
LocalTypeUUIDSlice LocalType = "[]uuid.UUID"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -852,8 +867,10 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
// checker is the checker function for instances map.
|
||||
checker = func(v DB) bool { return v == nil }
|
||||
// instances is the management map for instances.
|
||||
instances = gmap.NewStrAnyMap(true)
|
||||
instances = gmap.NewKVMapWithChecker[string, DB](checker, true)
|
||||
|
||||
// driverMap manages all custom registered driver.
|
||||
driverMap = map[string]Driver{}
|
||||
@ -927,6 +944,9 @@ func NewByGroup(group ...string) (db DB, err error) {
|
||||
)
|
||||
}
|
||||
|
||||
// linksChecker is the checker function for links map.
|
||||
var linksChecker = func(v *sql.DB) bool { return v == nil }
|
||||
|
||||
// newDBByConfigNode creates and returns an ORM object with given configuration node and group name.
|
||||
//
|
||||
// Very Note:
|
||||
@ -943,7 +963,7 @@ func newDBByConfigNode(node *ConfigNode, group string) (db DB, err error) {
|
||||
group: group,
|
||||
debug: gtype.NewBool(),
|
||||
cache: gcache.New(),
|
||||
links: gmap.New(true),
|
||||
links: gmap.NewKVMapWithChecker[ConfigNode, *sql.DB](linksChecker, true),
|
||||
logger: glog.New(),
|
||||
config: node,
|
||||
localTypeMap: gmap.NewStrAnyMap(true),
|
||||
@ -952,6 +972,7 @@ func newDBByConfigNode(node *ConfigNode, group string) (db DB, err error) {
|
||||
MaxIdleConnCount: node.MaxIdleConnCount,
|
||||
MaxOpenConnCount: node.MaxOpenConnCount,
|
||||
MaxConnLifeTime: node.MaxConnLifeTime,
|
||||
MaxIdleConnTime: node.MaxIdleConnTime,
|
||||
},
|
||||
}
|
||||
if v, ok := driverMap[node.Type]; ok {
|
||||
@ -974,14 +995,14 @@ func Instance(name ...string) (db DB, err error) {
|
||||
if len(name) > 0 && name[0] != "" {
|
||||
group = name[0]
|
||||
}
|
||||
v := instances.GetOrSetFuncLock(group, func() any {
|
||||
v := instances.GetOrSetFuncLock(group, func() DB {
|
||||
db, err = NewByGroup(group)
|
||||
return db
|
||||
})
|
||||
if v != nil {
|
||||
return v.(DB), nil
|
||||
return v, nil
|
||||
}
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// getConfigNodeByGroup calculates and returns a configuration node of given group. It
|
||||
@ -1109,7 +1130,7 @@ func (c *Core) getSqlDb(master bool, schema ...string) (sqlDb *sql.DB, err error
|
||||
|
||||
// Cache the underlying connection pool object by node.
|
||||
var (
|
||||
instanceCacheFunc = func() any {
|
||||
instanceCacheFunc = func() *sql.DB {
|
||||
if sqlDb, err = c.db.Open(node); err != nil {
|
||||
return nil
|
||||
}
|
||||
@ -1131,6 +1152,9 @@ func (c *Core) getSqlDb(master bool, schema ...string) (sqlDb *sql.DB, err error
|
||||
} else {
|
||||
sqlDb.SetConnMaxLifetime(defaultMaxConnLifeTime)
|
||||
}
|
||||
if c.dynamicConfig.MaxIdleConnTime > 0 {
|
||||
sqlDb.SetConnMaxIdleTime(c.dynamicConfig.MaxIdleConnTime)
|
||||
}
|
||||
return sqlDb
|
||||
}
|
||||
// it here uses NODE VALUE not pointer as the cache key, in case of oracle ORA-12516 error.
|
||||
@ -1138,7 +1162,7 @@ func (c *Core) getSqlDb(master bool, schema ...string) (sqlDb *sql.DB, err error
|
||||
)
|
||||
if instanceValue != nil && sqlDb == nil {
|
||||
// It reads from instance map.
|
||||
sqlDb = instanceValue.(*sql.DB)
|
||||
sqlDb = instanceValue
|
||||
}
|
||||
if node.Debug {
|
||||
c.db.SetDebug(node.Debug)
|
||||
|
||||
@ -113,19 +113,17 @@ func (c *Core) Close(ctx context.Context) (err error) {
|
||||
if err = c.cache.Close(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
c.links.LockFunc(func(m map[any]any) {
|
||||
c.links.LockFunc(func(m map[ConfigNode]*sql.DB) {
|
||||
for k, v := range m {
|
||||
if db, ok := v.(*sql.DB); ok {
|
||||
err = db.Close()
|
||||
if err != nil {
|
||||
err = gerror.WrapCode(gcode.CodeDbOperationError, err, `db.Close failed`)
|
||||
}
|
||||
intlog.Printf(ctx, `close link: %s, err: %v`, k, err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
delete(m, k)
|
||||
err = v.Close()
|
||||
if err != nil {
|
||||
err = gerror.WrapCode(gcode.CodeDbOperationError, err, `db.Close failed`)
|
||||
}
|
||||
intlog.Printf(ctx, `close link: %s, err: %v`, gconv.String(k), err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
delete(m, k)
|
||||
}
|
||||
})
|
||||
return
|
||||
@ -446,8 +444,10 @@ func (c *Core) DoInsert(ctx context.Context, link Link, table string, list List,
|
||||
// Group the list by fields. Different fields to different list.
|
||||
// It here uses ListMap to keep sequence for data inserting.
|
||||
// ============================================================================================
|
||||
var keyListMap = gmap.NewListMap()
|
||||
var tmpkeyListMap = make(map[string]List)
|
||||
var (
|
||||
keyListMap = gmap.NewListMap()
|
||||
tmpKeyListMap = make(map[string]List)
|
||||
)
|
||||
for _, item := range list {
|
||||
mapLen := len(item)
|
||||
if mapLen == 0 {
|
||||
@ -463,13 +463,13 @@ func (c *Core) DoInsert(ctx context.Context, link Link, table string, list List,
|
||||
keys = tmpKeys // for fieldsToSequence
|
||||
|
||||
tmpKeysInSequenceStr := gstr.Join(tmpKeys, ",")
|
||||
if tmpkeyListMapItem, ok := tmpkeyListMap[tmpKeysInSequenceStr]; ok {
|
||||
tmpkeyListMap[tmpKeysInSequenceStr] = append(tmpkeyListMapItem, item)
|
||||
if tmpKeyListMapItem, ok := tmpKeyListMap[tmpKeysInSequenceStr]; ok {
|
||||
tmpKeyListMap[tmpKeysInSequenceStr] = append(tmpKeyListMapItem, item)
|
||||
} else {
|
||||
tmpkeyListMap[tmpKeysInSequenceStr] = List{item}
|
||||
tmpKeyListMap[tmpKeysInSequenceStr] = List{item}
|
||||
}
|
||||
}
|
||||
for tmpKeysInSequenceStr, itemList := range tmpkeyListMap {
|
||||
for tmpKeysInSequenceStr, itemList := range tmpKeyListMap {
|
||||
keyListMap.Set(tmpKeysInSequenceStr, itemList)
|
||||
}
|
||||
if keyListMap.Size() > 1 {
|
||||
@ -757,11 +757,35 @@ func (c *Core) GetInnerMemCache() *gcache.Cache {
|
||||
return c.innerMemCache
|
||||
}
|
||||
|
||||
func (c *Core) SetTableFields(ctx context.Context, table string, fields map[string]*TableField, schema ...string) error {
|
||||
if table == "" {
|
||||
return gerror.NewCode(gcode.CodeInvalidParameter, "table name cannot be empty")
|
||||
}
|
||||
charL, charR := c.db.GetChars()
|
||||
table = gstr.Trim(table, charL+charR)
|
||||
if gstr.Contains(table, " ") {
|
||||
return gerror.NewCode(
|
||||
gcode.CodeInvalidParameter,
|
||||
"function TableFields supports only single table operations",
|
||||
)
|
||||
}
|
||||
var (
|
||||
innerMemCache = c.GetInnerMemCache()
|
||||
// prefix:group@schema#table
|
||||
cacheKey = genTableFieldsCacheKey(
|
||||
c.db.GetGroup(),
|
||||
gutil.GetOrDefaultStr(c.db.GetSchema(), schema...),
|
||||
table,
|
||||
)
|
||||
)
|
||||
return innerMemCache.Set(ctx, cacheKey, fields, gcache.DurationNoExpire)
|
||||
}
|
||||
|
||||
// GetTablesWithCache retrieves and returns the table names of current database with cache.
|
||||
func (c *Core) GetTablesWithCache() ([]string, error) {
|
||||
var (
|
||||
ctx = c.db.GetCtx()
|
||||
cacheKey = fmt.Sprintf(`Tables:%s`, c.db.GetGroup())
|
||||
cacheKey = genTableNamesCacheKey(c.db.GetGroup())
|
||||
cacheDuration = gcache.DurationNoExpire
|
||||
innerMemCache = c.GetInnerMemCache()
|
||||
)
|
||||
|
||||
@ -118,6 +118,11 @@ type ConfigNode struct {
|
||||
// Optional field
|
||||
MaxConnLifeTime time.Duration `json:"maxLifeTime"`
|
||||
|
||||
// MaxIdleConnTime specifies the maximum idle time of a connection before being closed
|
||||
// This is Go 1.15+ feature: sql.DB.SetConnMaxIdleTime
|
||||
// Optional field
|
||||
MaxIdleConnTime time.Duration `json:"maxIdleTime"`
|
||||
|
||||
// QueryTimeout specifies the maximum execution time for DQL operations
|
||||
// Optional field
|
||||
QueryTimeout time.Duration `json:"queryTimeout"`
|
||||
@ -363,6 +368,16 @@ func (c *Core) SetMaxConnLifeTime(d time.Duration) {
|
||||
c.dynamicConfig.MaxConnLifeTime = d
|
||||
}
|
||||
|
||||
// SetMaxIdleConnTime sets the maximum amount of time a connection may be idle before being closed.
|
||||
//
|
||||
// Idle connections may be closed lazily before reuse.
|
||||
//
|
||||
// If d <= 0, connections are not closed due to a connection's idle time.
|
||||
// This is Go 1.15+ feature: sql.DB.SetConnMaxIdleTime.
|
||||
func (c *Core) SetMaxIdleConnTime(d time.Duration) {
|
||||
c.dynamicConfig.MaxIdleConnTime = d
|
||||
}
|
||||
|
||||
// GetConfig returns the current used node configuration.
|
||||
func (c *Core) GetConfig() *ConfigNode {
|
||||
var configNode = c.getConfigNodeFromCtx(c.db.GetCtx())
|
||||
|
||||
@ -30,14 +30,14 @@ func (item *localStatsItem) Stats() sql.DBStats {
|
||||
// Stats retrieves and returns the pool stat for all nodes that have been established.
|
||||
func (c *Core) Stats(ctx context.Context) []StatsItem {
|
||||
var items = make([]StatsItem, 0)
|
||||
c.links.Iterator(func(k, v any) bool {
|
||||
var (
|
||||
node = k.(ConfigNode)
|
||||
sqlDB = v.(*sql.DB)
|
||||
)
|
||||
c.links.Iterator(func(k ConfigNode, v *sql.DB) bool {
|
||||
// Create a local copy of k to avoid loop variable address re-use issue
|
||||
// In Go, loop variables are re-used with the same memory address across iterations,
|
||||
// directly using &k would cause all localStatsItem instances to share the same address
|
||||
node := k
|
||||
items = append(items, &localStatsItem{
|
||||
node: &node,
|
||||
stats: sqlDB.Stats(),
|
||||
stats: v.Stats(),
|
||||
})
|
||||
return true
|
||||
})
|
||||
|
||||
@ -166,6 +166,19 @@ func (c *Core) DoCommit(ctx context.Context, in DoCommitInput) (out DoCommitOutp
|
||||
timestampMilli1 = gtime.TimestampMilli()
|
||||
)
|
||||
|
||||
// Panic recovery to handle panics from underlying database drivers
|
||||
defer func() {
|
||||
if exception := recover(); exception != nil {
|
||||
if err == nil {
|
||||
if v, ok := exception.(error); ok && gerror.HasStack(v) {
|
||||
err = v
|
||||
} else {
|
||||
err = gerror.WrapCodef(gcode.CodeDbOperationError, gerror.NewCodef(gcode.CodeInternalPanic, "%+v", exception), FormatSqlWithArgs(in.Sql, in.Args))
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Trace span start.
|
||||
tr := otel.GetTracerProvider().Tracer(traceInstrumentName, trace.WithInstrumentationVersion(gf.VERSION))
|
||||
ctx, span := tr.Start(ctx, string(in.Type), trace.WithSpanKind(trace.SpanKindClient))
|
||||
@ -501,9 +514,7 @@ func (c *Core) OrderRandomFunction() string {
|
||||
return "RAND()"
|
||||
}
|
||||
|
||||
func (c *Core) columnValueToLocalValue(
|
||||
ctx context.Context, value any, columnType *sql.ColumnType,
|
||||
) (any, error) {
|
||||
func (c *Core) columnValueToLocalValue(ctx context.Context, value any, columnType *sql.ColumnType) (any, error) {
|
||||
var scanType = columnType.ScanType()
|
||||
if scanType != nil {
|
||||
// Common basic builtin types.
|
||||
@ -513,10 +524,7 @@ func (c *Core) columnValueToLocalValue(
|
||||
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||
reflect.Float32, reflect.Float64:
|
||||
return gconv.Convert(
|
||||
gconv.String(value),
|
||||
columnType.ScanType().String(),
|
||||
), nil
|
||||
return gconv.Convert(gconv.String(value), scanType.String()), nil
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ package gdb
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
@ -251,3 +252,22 @@ func (c *Core) guessPrimaryTableName(tableStr string) string {
|
||||
}
|
||||
return guessedTableName
|
||||
}
|
||||
|
||||
// GetPrimaryKeys retrieves and returns the primary key field names of the specified table.
|
||||
// This method extracts primary key information from TableFields.
|
||||
// The parameter `schema` is optional, if not specified it uses the default schema.
|
||||
func (c *Core) GetPrimaryKeys(ctx context.Context, table string, schema ...string) ([]string, error) {
|
||||
tableFields, err := c.db.TableFields(ctx, table, schema...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var primaryKeys []string
|
||||
for _, field := range tableFields {
|
||||
if strings.EqualFold(field.Key, "pri") {
|
||||
primaryKeys = append(primaryKeys, field.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return primaryKeys, nil
|
||||
}
|
||||
|
||||
@ -109,7 +109,17 @@ func (d *DriverWrapperDB) TableFields(
|
||||
// InsertOptionReplace: if there's unique/primary key in the data, it deletes it from table and inserts a new one;
|
||||
// InsertOptionSave: if there's unique/primary key in the data, it updates it or else inserts a new one;
|
||||
// InsertOptionIgnore: if there's unique/primary key in the data, it ignores the inserting;
|
||||
func (d *DriverWrapperDB) DoInsert(ctx context.Context, link Link, table string, list List, option DoInsertOption) (result sql.Result, err error) {
|
||||
func (d *DriverWrapperDB) DoInsert(
|
||||
ctx context.Context, link Link, table string, list List, option DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
if len(list) == 0 {
|
||||
return nil, gerror.NewCodef(
|
||||
gcode.CodeInvalidRequest,
|
||||
`data list is empty for %s operation`,
|
||||
GetInsertOperationByOption(option.InsertOption),
|
||||
)
|
||||
}
|
||||
|
||||
// Convert data type before commit it to underlying db driver.
|
||||
for i, item := range list {
|
||||
list[i], err = d.GetCore().ConvertDataForRecord(ctx, item, table)
|
||||
|
||||
@ -151,13 +151,26 @@ func isDoStruct(object any) bool {
|
||||
// getTableNameFromOrmTag retrieves and returns the table name from struct object.
|
||||
func getTableNameFromOrmTag(object any) string {
|
||||
var tableName string
|
||||
// Use the interface value.
|
||||
if r, ok := object.(iTableName); ok {
|
||||
tableName = r.TableName()
|
||||
var actualObj = object
|
||||
|
||||
if rv, ok := object.(reflect.Value); ok {
|
||||
// Check if reflect.Value is valid
|
||||
if rv.IsValid() && rv.CanInterface() {
|
||||
actualObj = rv.Interface()
|
||||
} else {
|
||||
// If reflect.Value is invalid, we cannot proceed with interface checks
|
||||
return ""
|
||||
}
|
||||
}
|
||||
// User meta data tag "orm".
|
||||
if tableName == "" {
|
||||
if ormTag := gmeta.Get(object, OrmTagForStruct); !ormTag.IsEmpty() {
|
||||
|
||||
// Check iTableName interface
|
||||
if actualObj != nil {
|
||||
if r, ok := actualObj.(iTableName); ok {
|
||||
return r.TableName()
|
||||
}
|
||||
|
||||
// User meta data tag "orm".
|
||||
if ormTag := gmeta.Get(actualObj, OrmTagForStruct); !ormTag.IsEmpty() {
|
||||
match, _ := gregex.MatchString(
|
||||
fmt.Sprintf(`%s\s*:\s*([^,]+)`, OrmTagForTable),
|
||||
ormTag.String(),
|
||||
@ -166,17 +179,19 @@ func getTableNameFromOrmTag(object any) string {
|
||||
tableName = match[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use the struct name of snake case.
|
||||
if tableName == "" {
|
||||
if t, err := gstructs.StructType(object); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
tableName = gstr.CaseSnakeFirstUpper(
|
||||
gstr.StrEx(t.String(), "."),
|
||||
)
|
||||
|
||||
// Use the struct name of snake case.
|
||||
if tableName == "" {
|
||||
if t, err := gstructs.StructType(actualObj); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
tableName = gstr.CaseSnakeFirstUpper(
|
||||
gstr.StrEx(t.String(), "."),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tableName
|
||||
}
|
||||
|
||||
@ -719,6 +734,14 @@ func formatWhereKeyValue(in formatWhereKeyValueInput) (newArgs []any) {
|
||||
reflectValue = reflect.ValueOf(in.Value)
|
||||
reflectKind = reflectValue.Kind()
|
||||
)
|
||||
// Check if the value implements iString interface (like uuid.UUID).
|
||||
// These types should be treated as single values, not arrays.
|
||||
if reflectKind == reflect.Array {
|
||||
if v, ok := in.Value.(iString); ok {
|
||||
in.Value = v.String()
|
||||
reflectKind = reflect.String
|
||||
}
|
||||
}
|
||||
switch reflectKind {
|
||||
// Slice argument.
|
||||
case reflect.Slice, reflect.Array:
|
||||
@ -780,9 +803,7 @@ func formatWhereKeyValue(in formatWhereKeyValueInput) (newArgs []any) {
|
||||
|
||||
// handleSliceAndStructArgsForSql is an important function, which handles the sql and all its arguments
|
||||
// before committing them to underlying driver.
|
||||
func handleSliceAndStructArgsForSql(
|
||||
oldSql string, oldArgs []any,
|
||||
) (newSql string, newArgs []any) {
|
||||
func handleSliceAndStructArgsForSql(oldSql string, oldArgs []any) (newSql string, newArgs []any) {
|
||||
newSql = oldSql
|
||||
if len(oldArgs) == 0 {
|
||||
return
|
||||
@ -800,6 +821,13 @@ func handleSliceAndStructArgsForSql(
|
||||
newArgs = append(newArgs, oldArg)
|
||||
continue
|
||||
}
|
||||
// It does not split types that implement fmt.Stringer interface (like uuid.UUID).
|
||||
// These types should be converted to string instead of being expanded as arrays.
|
||||
// Eg: table.Where("uuid = ?", uuid.UUID{...})
|
||||
if v, ok := oldArg.(iString); ok {
|
||||
newArgs = append(newArgs, v.String())
|
||||
continue
|
||||
}
|
||||
var (
|
||||
valueHolderCount = gstr.Count(newSql, "?")
|
||||
argSliceLength = argReflectInfo.OriginValue.Len()
|
||||
@ -953,6 +981,7 @@ func FormatMultiLineSqlToSingle(sql string) (string, error) {
|
||||
return sql, nil
|
||||
}
|
||||
|
||||
// genTableFieldsCacheKey generates cache key for table fields.
|
||||
func genTableFieldsCacheKey(group, schema, table string) string {
|
||||
return fmt.Sprintf(
|
||||
`%s%s@%s#%s`,
|
||||
@ -963,6 +992,7 @@ func genTableFieldsCacheKey(group, schema, table string) string {
|
||||
)
|
||||
}
|
||||
|
||||
// genSelectCacheKey generates cache key for select.
|
||||
func genSelectCacheKey(table, group, schema, name, sql string, args ...any) string {
|
||||
if name == "" {
|
||||
name = fmt.Sprintf(
|
||||
@ -975,3 +1005,13 @@ func genSelectCacheKey(table, group, schema, name, sql string, args ...any) stri
|
||||
}
|
||||
return fmt.Sprintf(`%s%s`, cachePrefixSelectCache, name)
|
||||
}
|
||||
|
||||
// genTableNamesCacheKey generates cache key for table names.
|
||||
func genTableNamesCacheKey(group string) string {
|
||||
return fmt.Sprintf(`Tables:%s`, group)
|
||||
}
|
||||
|
||||
// genSoftTimeFieldNameTypeCacheKey generates cache key for soft time field name and type.
|
||||
func genSoftTimeFieldNameTypeCacheKey(schema, table string, candidateFields []string) string {
|
||||
return fmt.Sprintf(`getSoftFieldNameAndType:%s#%s#%s`, schema, table, strings.Join(candidateFields, "_"))
|
||||
}
|
||||
|
||||
@ -17,44 +17,45 @@ import (
|
||||
|
||||
// Model is core struct implementing the DAO for ORM.
|
||||
type Model struct {
|
||||
db DB // Underlying DB interface.
|
||||
tx TX // Underlying TX interface.
|
||||
rawSql string // rawSql is the raw SQL string which marks a raw SQL based Model not a table based Model.
|
||||
schema string // Custom database schema.
|
||||
linkType int // Mark for operation on master or slave.
|
||||
tablesInit string // Table names when model initialization.
|
||||
tables string // Operation table names, which can be more than one table names and aliases, like: "user", "user u", "user u, user_detail ud".
|
||||
fields []any // Operation fields, multiple fields joined using char ','.
|
||||
fieldsEx []any // Excluded operation fields, it here uses slice instead of string type for quick filtering.
|
||||
withArray []any // Arguments for With feature.
|
||||
withAll bool // Enable model association operations on all objects that have "with" tag in the struct.
|
||||
extraArgs []any // Extra custom arguments for sql, which are prepended to the arguments before sql committed to underlying driver.
|
||||
whereBuilder *WhereBuilder // Condition builder for where operation.
|
||||
groupBy string // Used for "group by" statement.
|
||||
orderBy string // Used for "order by" statement.
|
||||
having []any // Used for "having..." statement.
|
||||
start int // Used for "select ... start, limit ..." statement.
|
||||
limit int // Used for "select ... start, limit ..." statement.
|
||||
option int // Option for extra operation features.
|
||||
offset int // Offset statement for some databases grammar.
|
||||
partition string // Partition table partition name.
|
||||
data any // Data for operation, which can be type of map/[]map/struct/*struct/string, etc.
|
||||
batch int // Batch number for batch Insert/Replace/Save operations.
|
||||
filter bool // Filter data and where key-value pairs according to the fields of the table.
|
||||
distinct string // Force the query to only return distinct results.
|
||||
lockInfo string // Lock for update or in shared lock.
|
||||
cacheEnabled bool // Enable sql result cache feature, which is mainly for indicating cache duration(especially 0) usage.
|
||||
cacheOption CacheOption // Cache option for query statement.
|
||||
hookHandler HookHandler // Hook functions for model hook feature.
|
||||
unscoped bool // Disables soft deleting features when select/delete operations.
|
||||
safe bool // If true, it clones and returns a new model object whenever operation done; or else it changes the attribute of current model.
|
||||
onDuplicate any // onDuplicate is used for on Upsert clause.
|
||||
onDuplicateEx any // onDuplicateEx is used for excluding some columns on Upsert clause.
|
||||
onConflict any // onConflict is used for conflict keys on Upsert clause.
|
||||
tableAliasMap map[string]string // Table alias to true table name, usually used in join statements.
|
||||
softTimeOption SoftTimeOption // SoftTimeOption is the option to customize soft time feature for Model.
|
||||
shardingConfig ShardingConfig // ShardingConfig for database/table sharding feature.
|
||||
shardingValue any // Sharding value for sharding feature.
|
||||
db DB // Underlying DB interface.
|
||||
tx TX // Underlying TX interface.
|
||||
rawSql string // rawSql is the raw SQL string which marks a raw SQL based Model not a table based Model.
|
||||
schema string // Custom database schema.
|
||||
linkType int // Mark for operation on master or slave.
|
||||
tablesInit string // Table names when model initialization.
|
||||
tables string // Operation table names, which can be more than one table names and aliases, like: "user", "user u", "user u, user_detail ud".
|
||||
fields []any // Operation fields, multiple fields joined using char ','.
|
||||
fieldsEx []any // Excluded operation fields, it here uses slice instead of string type for quick filtering.
|
||||
withArray []any // Arguments for With feature.
|
||||
withAll bool // Enable model association operations on all objects that have "with" tag in the struct.
|
||||
extraArgs []any // Extra custom arguments for sql, which are prepended to the arguments before sql committed to underlying driver.
|
||||
whereBuilder *WhereBuilder // Condition builder for where operation.
|
||||
groupBy string // Used for "group by" statement.
|
||||
orderBy string // Used for "order by" statement.
|
||||
having []any // Used for "having..." statement.
|
||||
start int // Used for "select ... start, limit ..." statement.
|
||||
limit int // Used for "select ... start, limit ..." statement.
|
||||
option int // Option for extra operation features.
|
||||
offset int // Offset statement for some databases grammar.
|
||||
partition string // Partition table partition name.
|
||||
data any // Data for operation, which can be type of map/[]map/struct/*struct/string, etc.
|
||||
batch int // Batch number for batch Insert/Replace/Save operations.
|
||||
filter bool // Filter data and where key-value pairs according to the fields of the table.
|
||||
distinct string // Force the query to only return distinct results.
|
||||
lockInfo string // Lock for update or in shared lock.
|
||||
cacheEnabled bool // Enable sql result cache feature, which is mainly for indicating cache duration(especially 0) usage.
|
||||
cacheOption CacheOption // Cache option for query statement.
|
||||
pageCacheOption []CacheOption // Cache option for paging query statement.
|
||||
hookHandler HookHandler // Hook functions for model hook feature.
|
||||
unscoped bool // Disables soft deleting features when select/delete operations.
|
||||
safe bool // If true, it clones and returns a new model object whenever operation done; or else it changes the attribute of current model.
|
||||
onDuplicate any // onDuplicate is used for on Upsert clause.
|
||||
onDuplicateEx any // onDuplicateEx is used for excluding some columns on Upsert clause.
|
||||
onConflict any // onConflict is used for conflict keys on Upsert clause.
|
||||
tableAliasMap map[string]string // Table alias to true table name, usually used in join statements.
|
||||
softTimeOption SoftTimeOption // SoftTimeOption is the option to customize soft time feature for Model.
|
||||
shardingConfig ShardingConfig // ShardingConfig for database/table sharding feature.
|
||||
shardingValue any // Sharding value for sharding feature.
|
||||
}
|
||||
|
||||
// ModelHandler is a function that handles given Model and returns a new Model that is custom modified.
|
||||
|
||||
@ -50,6 +50,18 @@ func (m *Model) Cache(option CacheOption) *Model {
|
||||
return model
|
||||
}
|
||||
|
||||
// PageCache sets the cache feature for pagination queries. It allows to configure
|
||||
// separate cache options for count query and data query in pagination.
|
||||
//
|
||||
// Note that, the cache feature is disabled if the model is performing select statement
|
||||
// on a transaction.
|
||||
func (m *Model) PageCache(countOption CacheOption, dataOption CacheOption) *Model {
|
||||
model := m.getModel()
|
||||
model.pageCacheOption = []CacheOption{countOption, dataOption}
|
||||
model.cacheEnabled = true
|
||||
return model
|
||||
}
|
||||
|
||||
// checkAndRemoveSelectCache checks and removes the cache in insert/update/delete statement if
|
||||
// cache feature is enabled.
|
||||
func (m *Model) checkAndRemoveSelectCache(ctx context.Context) {
|
||||
|
||||
@ -31,9 +31,7 @@ func (m *Model) Delete(where ...any) (result sql.Result, err error) {
|
||||
var (
|
||||
conditionWhere, conditionExtra, conditionArgs = m.formatCondition(ctx, false, false)
|
||||
conditionStr = conditionWhere + conditionExtra
|
||||
fieldNameDelete, fieldTypeDelete = m.softTimeMaintainer().GetFieldNameAndTypeForDelete(
|
||||
ctx, "", m.tablesInit,
|
||||
)
|
||||
fieldNameDelete, fieldTypeDelete = m.softTimeMaintainer().GetFieldInfo(ctx, "", m.tablesInit, SoftTimeFieldDelete)
|
||||
)
|
||||
if m.unscoped {
|
||||
fieldNameDelete = ""
|
||||
@ -52,7 +50,7 @@ func (m *Model) Delete(where ...any) (result sql.Result, err error) {
|
||||
|
||||
// Soft deleting.
|
||||
if fieldNameDelete != "" {
|
||||
dataHolder, dataValue := m.softTimeMaintainer().GetDataByFieldNameAndTypeForDelete(
|
||||
dataHolder, dataValue := m.softTimeMaintainer().GetDeleteData(
|
||||
ctx, "", fieldNameDelete, fieldTypeDelete,
|
||||
)
|
||||
in := &HookUpdateInput{
|
||||
|
||||
@ -45,8 +45,9 @@ func (m *Model) FieldsPrefix(prefixOrAlias string, fieldNamesOrMapStruct ...any)
|
||||
if len(fields) == 0 {
|
||||
return m
|
||||
}
|
||||
prefixOrAlias = m.QuoteWord(prefixOrAlias)
|
||||
for i, field := range fields {
|
||||
fields[i] = prefixOrAlias + "." + gconv.String(field)
|
||||
fields[i] = fmt.Sprintf("%s.%s", prefixOrAlias, m.QuoteWord(gconv.String(field)))
|
||||
}
|
||||
model := m.getModel()
|
||||
return model.appendToFields(fields...)
|
||||
@ -81,14 +82,21 @@ func (m *Model) doFieldsEx(table string, fieldNamesOrMapStruct ...any) *Model {
|
||||
}
|
||||
|
||||
// FieldsExPrefix performs as function FieldsEx but add extra prefix for each field.
|
||||
// Note that this function must be used together with FieldsPrefix, otherwise it will be invalid.
|
||||
func (m *Model) FieldsExPrefix(prefixOrAlias string, fieldNamesOrMapStruct ...any) *Model {
|
||||
model := m.doFieldsEx(
|
||||
fields := m.filterFieldsFrom(
|
||||
m.getTableNameByPrefixOrAlias(prefixOrAlias),
|
||||
fieldNamesOrMapStruct...,
|
||||
)
|
||||
for i, field := range model.fieldsEx {
|
||||
model.fieldsEx[i] = prefixOrAlias + "." + gconv.String(field)
|
||||
if len(fields) == 0 {
|
||||
return m
|
||||
}
|
||||
prefixOrAlias = m.QuoteWord(prefixOrAlias)
|
||||
for i, field := range fields {
|
||||
fields[i] = fmt.Sprintf("%s.%s", prefixOrAlias, m.QuoteWord(gconv.String(field)))
|
||||
}
|
||||
model := m.getModel()
|
||||
model.fieldsEx = append(model.fieldsEx, fields...)
|
||||
return model
|
||||
}
|
||||
|
||||
@ -96,7 +104,7 @@ func (m *Model) FieldsExPrefix(prefixOrAlias string, fieldNamesOrMapStruct ...an
|
||||
func (m *Model) FieldCount(column string, as ...string) *Model {
|
||||
asStr := ""
|
||||
if len(as) > 0 && as[0] != "" {
|
||||
asStr = fmt.Sprintf(` AS %s`, m.db.GetCore().QuoteWord(as[0]))
|
||||
asStr = fmt.Sprintf(` AS %s`, m.QuoteWord(as[0]))
|
||||
}
|
||||
model := m.getModel()
|
||||
return model.appendToFields(
|
||||
@ -108,7 +116,7 @@ func (m *Model) FieldCount(column string, as ...string) *Model {
|
||||
func (m *Model) FieldSum(column string, as ...string) *Model {
|
||||
asStr := ""
|
||||
if len(as) > 0 && as[0] != "" {
|
||||
asStr = fmt.Sprintf(` AS %s`, m.db.GetCore().QuoteWord(as[0]))
|
||||
asStr = fmt.Sprintf(` AS %s`, m.QuoteWord(as[0]))
|
||||
}
|
||||
model := m.getModel()
|
||||
return model.appendToFields(
|
||||
@ -120,7 +128,7 @@ func (m *Model) FieldSum(column string, as ...string) *Model {
|
||||
func (m *Model) FieldMin(column string, as ...string) *Model {
|
||||
asStr := ""
|
||||
if len(as) > 0 && as[0] != "" {
|
||||
asStr = fmt.Sprintf(` AS %s`, m.db.GetCore().QuoteWord(as[0]))
|
||||
asStr = fmt.Sprintf(` AS %s`, m.QuoteWord(as[0]))
|
||||
}
|
||||
model := m.getModel()
|
||||
return model.appendToFields(
|
||||
@ -132,7 +140,7 @@ func (m *Model) FieldMin(column string, as ...string) *Model {
|
||||
func (m *Model) FieldMax(column string, as ...string) *Model {
|
||||
asStr := ""
|
||||
if len(as) > 0 && as[0] != "" {
|
||||
asStr = fmt.Sprintf(` AS %s`, m.db.GetCore().QuoteWord(as[0]))
|
||||
asStr = fmt.Sprintf(` AS %s`, m.QuoteWord(as[0]))
|
||||
}
|
||||
model := m.getModel()
|
||||
return model.appendToFields(
|
||||
@ -144,7 +152,7 @@ func (m *Model) FieldMax(column string, as ...string) *Model {
|
||||
func (m *Model) FieldAvg(column string, as ...string) *Model {
|
||||
asStr := ""
|
||||
if len(as) > 0 && as[0] != "" {
|
||||
asStr = fmt.Sprintf(` AS %s`, m.db.GetCore().QuoteWord(as[0]))
|
||||
asStr = fmt.Sprintf(` AS %s`, m.QuoteWord(as[0]))
|
||||
}
|
||||
model := m.getModel()
|
||||
return model.appendToFields(
|
||||
|
||||
@ -262,9 +262,9 @@ func (m *Model) doInsertWithOption(ctx context.Context, insertOption InsertOptio
|
||||
var (
|
||||
list List
|
||||
stm = m.softTimeMaintainer()
|
||||
fieldNameCreate, fieldTypeCreate = stm.GetFieldNameAndTypeForCreate(ctx, "", m.tablesInit)
|
||||
fieldNameUpdate, fieldTypeUpdate = stm.GetFieldNameAndTypeForUpdate(ctx, "", m.tablesInit)
|
||||
fieldNameDelete, fieldTypeDelete = stm.GetFieldNameAndTypeForDelete(ctx, "", m.tablesInit)
|
||||
fieldNameCreate, fieldTypeCreate = stm.GetFieldInfo(ctx, "", m.tablesInit, SoftTimeFieldCreate)
|
||||
fieldNameUpdate, fieldTypeUpdate = stm.GetFieldInfo(ctx, "", m.tablesInit, SoftTimeFieldUpdate)
|
||||
fieldNameDelete, fieldTypeDelete = stm.GetFieldInfo(ctx, "", m.tablesInit, SoftTimeFieldDelete)
|
||||
)
|
||||
// m.data was already converted to type List/Map by function Data
|
||||
newData, err := m.filterDataForInsertOrUpdate(m.data)
|
||||
@ -295,20 +295,20 @@ func (m *Model) doInsertWithOption(ctx context.Context, insertOption InsertOptio
|
||||
if !m.unscoped && isSoftTimeFeatureEnabled {
|
||||
for k, v := range list {
|
||||
if fieldNameCreate != "" && empty.IsNil(v[fieldNameCreate]) {
|
||||
fieldCreateValue := stm.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeCreate, false)
|
||||
fieldCreateValue := stm.GetFieldValue(ctx, fieldTypeCreate, false)
|
||||
if fieldCreateValue != nil {
|
||||
v[fieldNameCreate] = fieldCreateValue
|
||||
}
|
||||
}
|
||||
if fieldNameUpdate != "" && empty.IsNil(v[fieldNameUpdate]) {
|
||||
fieldUpdateValue := stm.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeUpdate, false)
|
||||
fieldUpdateValue := stm.GetFieldValue(ctx, fieldTypeUpdate, false)
|
||||
if fieldUpdateValue != nil {
|
||||
v[fieldNameUpdate] = fieldUpdateValue
|
||||
}
|
||||
}
|
||||
// for timestamp field that should initialize the delete_at field with value, for example 0.
|
||||
if fieldNameDelete != "" && empty.IsNil(v[fieldNameDelete]) {
|
||||
fieldDeleteValue := stm.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeDelete, true)
|
||||
fieldDeleteValue := stm.GetFieldValue(ctx, fieldTypeDelete, true)
|
||||
if fieldDeleteValue != nil {
|
||||
v[fieldNameDelete] = fieldDeleteValue
|
||||
}
|
||||
|
||||
@ -6,16 +6,124 @@
|
||||
|
||||
package gdb
|
||||
|
||||
// Lock clause constants for different databases.
|
||||
// These constants provide type-safe and IDE-friendly access to various lock syntaxes.
|
||||
const (
|
||||
// Common lock clauses (supported by most databases)
|
||||
LockForUpdate = "FOR UPDATE"
|
||||
LockForUpdateSkipLocked = "FOR UPDATE SKIP LOCKED"
|
||||
|
||||
// MySQL lock clauses
|
||||
LockInShareMode = "LOCK IN SHARE MODE" // MySQL legacy syntax
|
||||
LockForShare = "FOR SHARE" // MySQL 8.0+ and PostgreSQL
|
||||
LockForUpdateNowait = "FOR UPDATE NOWAIT" // MySQL 8.0+ and Oracle
|
||||
|
||||
// PostgreSQL specific lock clauses
|
||||
LockForNoKeyUpdate = "FOR NO KEY UPDATE"
|
||||
LockForKeyShare = "FOR KEY SHARE"
|
||||
LockForShareNowait = "FOR SHARE NOWAIT"
|
||||
LockForShareSkipLocked = "FOR SHARE SKIP LOCKED"
|
||||
LockForNoKeyUpdateNowait = "FOR NO KEY UPDATE NOWAIT"
|
||||
LockForNoKeyUpdateSkipLocked = "FOR NO KEY UPDATE SKIP LOCKED"
|
||||
LockForKeyShareNowait = "FOR KEY SHARE NOWAIT"
|
||||
LockForKeyShareSkipLocked = "FOR KEY SHARE SKIP LOCKED"
|
||||
|
||||
// Oracle specific lock clauses
|
||||
LockForUpdateWait5 = "FOR UPDATE WAIT 5"
|
||||
LockForUpdateWait10 = "FOR UPDATE WAIT 10"
|
||||
LockForUpdateWait30 = "FOR UPDATE WAIT 30"
|
||||
|
||||
// SQL Server lock hints (use with WITH clause)
|
||||
LockWithUpdLock = "WITH (UPDLOCK)"
|
||||
LockWithHoldLock = "WITH (HOLDLOCK)"
|
||||
LockWithXLock = "WITH (XLOCK)"
|
||||
LockWithTabLock = "WITH (TABLOCK)"
|
||||
LockWithNoLock = "WITH (NOLOCK)"
|
||||
LockWithUpdLockHoldLock = "WITH (UPDLOCK, HOLDLOCK)"
|
||||
)
|
||||
|
||||
// Lock sets a custom lock clause for the current operation.
|
||||
// This is a generic method that allows you to specify any lock syntax supported by your database.
|
||||
// You can use predefined constants or custom strings.
|
||||
//
|
||||
// Database-specific lock syntax support:
|
||||
//
|
||||
// PostgreSQL (most comprehensive):
|
||||
// - "FOR UPDATE" - Exclusive lock, blocks all access
|
||||
// - "FOR NO KEY UPDATE" - Weaker exclusive lock, doesn't block FOR KEY SHARE
|
||||
// - "FOR SHARE" - Shared lock, allows reads but blocks writes
|
||||
// - "FOR KEY SHARE" - Weakest lock, only locks key values
|
||||
// - All above can be combined with:
|
||||
// - "NOWAIT" - Return immediately if lock cannot be acquired
|
||||
// - "SKIP LOCKED" - Skip locked rows instead of waiting
|
||||
//
|
||||
// MySQL:
|
||||
// - "FOR UPDATE" - Exclusive lock (all versions)
|
||||
// - "LOCK IN SHARE MODE" - Shared lock (legacy syntax)
|
||||
// - "FOR SHARE" - Shared lock (MySQL 8.0+)
|
||||
// - "FOR UPDATE NOWAIT" - MySQL 8.0+ only
|
||||
// - "FOR UPDATE SKIP LOCKED" - MySQL 8.0+ only
|
||||
//
|
||||
// Oracle:
|
||||
// - "FOR UPDATE" - Exclusive lock
|
||||
// - "FOR UPDATE NOWAIT" - Exclusive lock, no wait
|
||||
// - "FOR UPDATE SKIP LOCKED" - Exclusive lock, skip locked rows
|
||||
// - "FOR UPDATE WAIT n" - Exclusive lock, wait n seconds
|
||||
// - "FOR UPDATE OF column_list" - Lock specific columns
|
||||
//
|
||||
// SQL Server (uses WITH hints):
|
||||
// - "WITH (UPDLOCK)" - Update lock
|
||||
// - "WITH (HOLDLOCK)" - Hold lock until transaction end
|
||||
// - "WITH (XLOCK)" - Exclusive lock
|
||||
// - "WITH (TABLOCK)" - Table lock
|
||||
// - "WITH (NOLOCK)" - No lock (dirty read)
|
||||
// - "WITH (UPDLOCK, HOLDLOCK)" - Combined update and hold lock
|
||||
//
|
||||
// SQLite:
|
||||
// - Limited locking support, database-level locks only
|
||||
// - No row-level lock syntax supported
|
||||
//
|
||||
// Usage examples:
|
||||
//
|
||||
// db.Model("users").Lock("FOR UPDATE NOWAIT").Where("id", 1).One()
|
||||
// db.Model("users").Lock("FOR SHARE SKIP LOCKED").Where("status", "active").All()
|
||||
// db.Model("users").Lock("WITH (UPDLOCK)").Where("id", 1).One() // SQL Server
|
||||
// db.Model("users").Lock("FOR UPDATE OF name, email").Where("id", 1).One() // Oracle
|
||||
// db.Model("users").Lock("FOR UPDATE WAIT 15").Where("id", 1).One() // Oracle custom wait
|
||||
//
|
||||
// Or use predefined constants for better IDE support:
|
||||
//
|
||||
// db.Model("users").Lock(gdb.LockForUpdateNowait).Where("id", 1).One()
|
||||
// db.Model("users").Lock(gdb.LockForShareSkipLocked).Where("status", "active").All()
|
||||
func (m *Model) Lock(lockClause string) *Model {
|
||||
model := m.getModel()
|
||||
model.lockInfo = lockClause
|
||||
return model
|
||||
}
|
||||
|
||||
// LockUpdate sets the lock for update for current operation.
|
||||
// This is equivalent to Lock("FOR UPDATE").
|
||||
func (m *Model) LockUpdate() *Model {
|
||||
model := m.getModel()
|
||||
model.lockInfo = "FOR UPDATE"
|
||||
model.lockInfo = LockForUpdate
|
||||
return model
|
||||
}
|
||||
|
||||
// LockUpdateSkipLocked sets the lock for update with skip locked behavior for current operation.
|
||||
// It skips the locked rows.
|
||||
// This is equivalent to Lock("FOR UPDATE SKIP LOCKED").
|
||||
// Note: Supported by PostgreSQL, Oracle, and MySQL 8.0+.
|
||||
func (m *Model) LockUpdateSkipLocked() *Model {
|
||||
model := m.getModel()
|
||||
model.lockInfo = LockForUpdateSkipLocked
|
||||
return model
|
||||
}
|
||||
|
||||
// LockShared sets the lock in share mode for current operation.
|
||||
// This is equivalent to Lock("LOCK IN SHARE MODE") for MySQL or Lock("FOR SHARE") for PostgreSQL.
|
||||
// Note: For maximum compatibility, this uses MySQL's legacy syntax.
|
||||
func (m *Model) LockShared() *Model {
|
||||
model := m.getModel()
|
||||
model.lockInfo = "LOCK IN SHARE MODE"
|
||||
model.lockInfo = LockInShareMode
|
||||
return model
|
||||
}
|
||||
|
||||
@ -56,6 +56,9 @@ func (m *Model) AllAndCount(useFieldForCount bool) (result Result, totalCount in
|
||||
if !useFieldForCount {
|
||||
countModel.fields = []any{Raw("1")}
|
||||
}
|
||||
if len(m.pageCacheOption) > 0 {
|
||||
countModel = countModel.Cache(m.pageCacheOption[0])
|
||||
}
|
||||
|
||||
// Get the total count of records
|
||||
totalCount, err = countModel.Count()
|
||||
@ -68,8 +71,13 @@ func (m *Model) AllAndCount(useFieldForCount bool) (result Result, totalCount in
|
||||
return
|
||||
}
|
||||
|
||||
resultModel := m.Clone()
|
||||
if len(m.pageCacheOption) > 1 {
|
||||
resultModel = resultModel.Cache(m.pageCacheOption[1])
|
||||
}
|
||||
|
||||
// Retrieve all records
|
||||
result, err = m.doGetAll(m.GetCtx(), SelectTypeDefault, false)
|
||||
result, err = resultModel.doGetAll(m.GetCtx(), SelectTypeDefault, false)
|
||||
return
|
||||
}
|
||||
|
||||
@ -337,7 +345,9 @@ func (m *Model) ScanAndCount(pointer any, totalCount *int, useFieldForCount bool
|
||||
if !useFieldForCount {
|
||||
countModel.fields = []any{Raw("1")}
|
||||
}
|
||||
|
||||
if len(m.pageCacheOption) > 0 {
|
||||
countModel = countModel.Cache(m.pageCacheOption[0])
|
||||
}
|
||||
// Get the total count of records
|
||||
*totalCount, err = countModel.Count()
|
||||
if err != nil {
|
||||
@ -348,7 +358,11 @@ func (m *Model) ScanAndCount(pointer any, totalCount *int, useFieldForCount bool
|
||||
if *totalCount == 0 {
|
||||
return
|
||||
}
|
||||
err = m.Scan(pointer)
|
||||
scanModel := m.Clone()
|
||||
if len(m.pageCacheOption) > 1 {
|
||||
scanModel = scanModel.Cache(m.pageCacheOption[1])
|
||||
}
|
||||
err = scanModel.Scan(pointer)
|
||||
return
|
||||
}
|
||||
|
||||
@ -710,8 +724,12 @@ func (m *Model) getFormattedSqlAndArgs(
|
||||
}
|
||||
// Raw SQL Model.
|
||||
if m.rawSql != "" {
|
||||
sqlWithHolder = fmt.Sprintf("SELECT %s FROM (%s) AS T", queryFields, m.rawSql)
|
||||
return sqlWithHolder, nil
|
||||
conditionWhere, conditionExtra, conditionArgs := m.formatCondition(ctx, false, true)
|
||||
sqlWithHolder = fmt.Sprintf(
|
||||
"SELECT %s FROM (%s%s) AS T",
|
||||
queryFields, m.rawSql, conditionWhere+conditionExtra,
|
||||
)
|
||||
return sqlWithHolder, conditionArgs
|
||||
}
|
||||
conditionWhere, conditionExtra, conditionArgs := m.formatCondition(ctx, false, true)
|
||||
sqlWithHolder = fmt.Sprintf("SELECT %s FROM %s%s", queryFields, m.tables, conditionWhere+conditionExtra)
|
||||
@ -752,7 +770,7 @@ func (m *Model) getHolderAndArgsAsSubModel(ctx context.Context) (holder string,
|
||||
func (m *Model) getAutoPrefix() string {
|
||||
autoPrefix := ""
|
||||
if gstr.Contains(m.tables, " JOIN ") {
|
||||
autoPrefix = m.db.GetCore().QuoteWord(
|
||||
autoPrefix = m.QuoteWord(
|
||||
m.db.GetCore().guessPrimaryTableName(m.tablesInit),
|
||||
)
|
||||
}
|
||||
@ -762,7 +780,6 @@ func (m *Model) getAutoPrefix() string {
|
||||
func (m *Model) getFieldsAsStr() string {
|
||||
var (
|
||||
fieldsStr string
|
||||
core = m.db.GetCore()
|
||||
)
|
||||
for _, v := range m.fields {
|
||||
field := gconv.String(v)
|
||||
@ -773,7 +790,7 @@ func (m *Model) getFieldsAsStr() string {
|
||||
switch v.(type) {
|
||||
case Raw, *Raw:
|
||||
default:
|
||||
field = core.QuoteString(field)
|
||||
field = m.QuoteWord(field)
|
||||
}
|
||||
}
|
||||
if fieldsStr != "" {
|
||||
@ -829,7 +846,7 @@ func (m *Model) getFieldsFiltered() string {
|
||||
if len(newFields) > 0 {
|
||||
newFields += ","
|
||||
}
|
||||
newFields += m.db.GetCore().QuoteWord(k)
|
||||
newFields += m.QuoteWord(k)
|
||||
}
|
||||
return newFields
|
||||
}
|
||||
@ -848,7 +865,7 @@ func (m *Model) formatCondition(
|
||||
}
|
||||
// WHERE
|
||||
conditionWhere, conditionArgs = m.whereBuilder.Build()
|
||||
softDeletingCondition := m.softTimeMaintainer().GetWhereConditionForDelete(ctx)
|
||||
softDeletingCondition := m.softTimeMaintainer().GetDeleteCondition(ctx)
|
||||
if m.rawSql != "" && conditionWhere != "" {
|
||||
if gstr.ContainsI(m.rawSql, " WHERE ") {
|
||||
conditionWhere = " AND " + conditionWhere
|
||||
|
||||
@ -43,28 +43,27 @@ type softTimeMaintainer struct {
|
||||
*Model
|
||||
}
|
||||
|
||||
// SoftTimeFieldType represents different soft time field purposes.
|
||||
type SoftTimeFieldType int
|
||||
|
||||
const (
|
||||
SoftTimeFieldCreate SoftTimeFieldType = iota
|
||||
SoftTimeFieldUpdate
|
||||
SoftTimeFieldDelete
|
||||
)
|
||||
|
||||
type iSoftTimeMaintainer interface {
|
||||
GetFieldNameAndTypeForCreate(
|
||||
ctx context.Context, schema string, table string,
|
||||
) (fieldName string, fieldType LocalType)
|
||||
// GetFieldInfo returns field name and type for specified field purpose.
|
||||
GetFieldInfo(ctx context.Context, schema, table string, fieldPurpose SoftTimeFieldType) (fieldName string, localType LocalType)
|
||||
|
||||
GetFieldNameAndTypeForUpdate(
|
||||
ctx context.Context, schema string, table string,
|
||||
) (fieldName string, fieldType LocalType)
|
||||
// GetFieldValue generates value for create/update/delete operations.
|
||||
GetFieldValue(ctx context.Context, localType LocalType, isDeleted bool) any
|
||||
|
||||
GetFieldNameAndTypeForDelete(
|
||||
ctx context.Context, schema string, table string,
|
||||
) (fieldName string, fieldType LocalType)
|
||||
// GetDeleteCondition returns WHERE condition for soft delete query.
|
||||
GetDeleteCondition(ctx context.Context) string
|
||||
|
||||
GetValueByFieldTypeForCreateOrUpdate(
|
||||
ctx context.Context, fieldType LocalType, isDeletedField bool,
|
||||
) (dataValue any)
|
||||
|
||||
GetDataByFieldNameAndTypeForDelete(
|
||||
ctx context.Context, fieldPrefix, fieldName string, fieldType LocalType,
|
||||
) (dataHolder string, dataValue any)
|
||||
|
||||
GetWhereConditionForDelete(ctx context.Context) string
|
||||
// GetDeleteData returns UPDATE statement data for soft delete.
|
||||
GetDeleteData(ctx context.Context, prefix, fieldName string, localType LocalType) (holder string, value any)
|
||||
}
|
||||
|
||||
// getSoftFieldNameAndTypeCacheItem is the internal struct for storing create/update/delete fields.
|
||||
@ -102,137 +101,83 @@ func (m *Model) softTimeMaintainer() iSoftTimeMaintainer {
|
||||
}
|
||||
}
|
||||
|
||||
// GetFieldNameAndTypeForCreate checks and returns the field name for record creating time.
|
||||
// If there's no field name for storing creating time, it returns an empty string.
|
||||
// GetFieldInfo returns field name and type for specified field purpose.
|
||||
// It checks the key with or without cases or chars '-'/'_'/'.'/' '.
|
||||
func (m *softTimeMaintainer) GetFieldNameAndTypeForCreate(
|
||||
ctx context.Context, schema string, table string,
|
||||
) (fieldName string, fieldType LocalType) {
|
||||
// It checks whether this feature disabled.
|
||||
func (m *softTimeMaintainer) GetFieldInfo(
|
||||
ctx context.Context, schema, table string, fieldPurpose SoftTimeFieldType,
|
||||
) (fieldName string, localType LocalType) {
|
||||
// Check if feature is disabled
|
||||
if m.db.GetConfig().TimeMaintainDisabled {
|
||||
return "", LocalTypeUndefined
|
||||
}
|
||||
tableName := ""
|
||||
if table != "" {
|
||||
tableName = table
|
||||
} else {
|
||||
tableName = m.tablesInit
|
||||
}
|
||||
config := m.db.GetConfig()
|
||||
if config.CreatedAt != "" {
|
||||
return m.getSoftFieldNameAndType(
|
||||
ctx, schema, tableName, []string{config.CreatedAt},
|
||||
)
|
||||
}
|
||||
return m.getSoftFieldNameAndType(
|
||||
ctx, schema, tableName, createdFieldNames,
|
||||
)
|
||||
}
|
||||
|
||||
// GetFieldNameAndTypeForUpdate checks and returns the field name for record updating time.
|
||||
// If there's no field name for storing updating time, it returns an empty string.
|
||||
// It checks the key with or without cases or chars '-'/'_'/'.'/' '.
|
||||
func (m *softTimeMaintainer) GetFieldNameAndTypeForUpdate(
|
||||
ctx context.Context, schema string, table string,
|
||||
) (fieldName string, fieldType LocalType) {
|
||||
// It checks whether this feature disabled.
|
||||
if m.db.GetConfig().TimeMaintainDisabled {
|
||||
return "", LocalTypeUndefined
|
||||
}
|
||||
tableName := ""
|
||||
if table != "" {
|
||||
tableName = table
|
||||
} else {
|
||||
// Determine table name
|
||||
tableName := table
|
||||
if tableName == "" {
|
||||
tableName = m.tablesInit
|
||||
}
|
||||
config := m.db.GetConfig()
|
||||
if config.UpdatedAt != "" {
|
||||
return m.getSoftFieldNameAndType(
|
||||
ctx, schema, tableName, []string{config.UpdatedAt},
|
||||
)
|
||||
}
|
||||
return m.getSoftFieldNameAndType(
|
||||
ctx, schema, tableName, updatedFieldNames,
|
||||
)
|
||||
}
|
||||
|
||||
// GetFieldNameAndTypeForDelete checks and returns the field name for record deleting time.
|
||||
// If there's no field name for storing deleting time, it returns an empty string.
|
||||
// It checks the key with or without cases or chars '-'/'_'/'.'/' '.
|
||||
func (m *softTimeMaintainer) GetFieldNameAndTypeForDelete(
|
||||
ctx context.Context, schema string, table string,
|
||||
) (fieldName string, fieldType LocalType) {
|
||||
// It checks whether this feature disabled.
|
||||
if m.db.GetConfig().TimeMaintainDisabled {
|
||||
return "", LocalTypeUndefined
|
||||
}
|
||||
tableName := ""
|
||||
if table != "" {
|
||||
tableName = table
|
||||
} else {
|
||||
tableName = m.tablesInit
|
||||
}
|
||||
// Get config and field candidates
|
||||
config := m.db.GetConfig()
|
||||
if config.DeletedAt != "" {
|
||||
return m.getSoftFieldNameAndType(
|
||||
ctx, schema, tableName, []string{config.DeletedAt},
|
||||
)
|
||||
}
|
||||
return m.getSoftFieldNameAndType(
|
||||
ctx, schema, tableName, deletedFieldNames,
|
||||
var (
|
||||
configField string
|
||||
defaultFields []string
|
||||
)
|
||||
|
||||
switch fieldPurpose {
|
||||
case SoftTimeFieldCreate:
|
||||
configField = config.CreatedAt
|
||||
defaultFields = createdFieldNames
|
||||
case SoftTimeFieldUpdate:
|
||||
configField = config.UpdatedAt
|
||||
defaultFields = updatedFieldNames
|
||||
case SoftTimeFieldDelete:
|
||||
configField = config.DeletedAt
|
||||
defaultFields = deletedFieldNames
|
||||
}
|
||||
|
||||
// Use config field if specified, otherwise use defaults
|
||||
if configField != "" {
|
||||
return m.getSoftFieldNameAndType(ctx, schema, tableName, []string{configField})
|
||||
}
|
||||
return m.getSoftFieldNameAndType(ctx, schema, tableName, defaultFields)
|
||||
}
|
||||
|
||||
// getSoftFieldNameAndType retrieves and returns the field name of the table for possible key.
|
||||
func (m *softTimeMaintainer) getSoftFieldNameAndType(
|
||||
ctx context.Context,
|
||||
schema string, table string, checkFiledNames []string,
|
||||
ctx context.Context, schema, table string, candidateFields []string,
|
||||
) (fieldName string, fieldType LocalType) {
|
||||
var (
|
||||
innerMemCache = m.db.GetCore().GetInnerMemCache()
|
||||
cacheKey = fmt.Sprintf(
|
||||
`getSoftFieldNameAndType:%s#%s#%s`,
|
||||
schema, table, strings.Join(checkFiledNames, "_"),
|
||||
)
|
||||
cacheDuration = gcache.DurationNoExpire
|
||||
cacheFunc = func(ctx context.Context) (value any, err error) {
|
||||
// Ignore the error from TableFields.
|
||||
fieldsMap, err := m.TableFields(table, schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(fieldsMap) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
for _, checkFiledName := range checkFiledNames {
|
||||
fieldName = searchFieldNameFromMap(fieldsMap, checkFiledName)
|
||||
if fieldName != "" {
|
||||
fieldType, _ = m.db.CheckLocalTypeForField(
|
||||
ctx, fieldsMap[fieldName].Type, nil,
|
||||
)
|
||||
var cacheItem = getSoftFieldNameAndTypeCacheItem{
|
||||
FieldName: fieldName,
|
||||
FieldType: fieldType,
|
||||
}
|
||||
return cacheItem, nil
|
||||
}
|
||||
}
|
||||
return
|
||||
// Build cache key
|
||||
cacheKey := genSoftTimeFieldNameTypeCacheKey(schema, table, candidateFields)
|
||||
|
||||
// Try to get from cache
|
||||
cache := m.db.GetCore().GetInnerMemCache()
|
||||
result, err := cache.GetOrSetFunc(ctx, cacheKey, func(ctx context.Context) (any, error) {
|
||||
// Get table fields
|
||||
fieldsMap, err := m.TableFields(table, schema)
|
||||
if err != nil || len(fieldsMap) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
)
|
||||
result, err := innerMemCache.GetOrSetFunc(
|
||||
ctx, cacheKey, cacheFunc, cacheDuration,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
// Search for matching field
|
||||
for _, field := range candidateFields {
|
||||
if name := searchFieldNameFromMap(fieldsMap, field); name != "" {
|
||||
fType, _ := m.db.CheckLocalTypeForField(ctx, fieldsMap[name].Type, nil)
|
||||
return getSoftFieldNameAndTypeCacheItem{
|
||||
FieldName: name,
|
||||
FieldType: fType,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}, gcache.DurationNoExpire)
|
||||
|
||||
if err != nil || result == nil {
|
||||
return "", LocalTypeUndefined
|
||||
}
|
||||
if result == nil {
|
||||
return
|
||||
}
|
||||
cacheItem := result.Val().(getSoftFieldNameAndTypeCacheItem)
|
||||
fieldName = cacheItem.FieldName
|
||||
fieldType = cacheItem.FieldType
|
||||
return
|
||||
|
||||
item := result.Val().(getSoftFieldNameAndTypeCacheItem)
|
||||
return item.FieldName, item.FieldType
|
||||
}
|
||||
|
||||
func searchFieldNameFromMap(fieldsMap map[string]*TableField, key string) string {
|
||||
@ -252,13 +197,13 @@ func searchFieldNameFromMap(fieldsMap map[string]*TableField, key string) string
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetWhereConditionForDelete retrieves and returns the condition string for soft deleting.
|
||||
// GetDeleteCondition returns WHERE condition for soft delete query.
|
||||
// It supports multiple tables string like:
|
||||
// "user u, user_detail ud"
|
||||
// "user u LEFT JOIN user_detail ud ON(ud.uid=u.uid)"
|
||||
// "user LEFT JOIN user_detail ON(user_detail.uid=user.uid)"
|
||||
// "user u LEFT JOIN user_detail ud ON(ud.uid=u.uid) LEFT JOIN user_stats us ON(us.uid=u.uid)".
|
||||
func (m *softTimeMaintainer) GetWhereConditionForDelete(ctx context.Context) string {
|
||||
func (m *softTimeMaintainer) GetDeleteCondition(ctx context.Context) string {
|
||||
if m.unscoped {
|
||||
return ""
|
||||
}
|
||||
@ -284,9 +229,9 @@ func (m *softTimeMaintainer) GetWhereConditionForDelete(ctx context.Context) str
|
||||
return conditionArray.Join(" AND ")
|
||||
}
|
||||
// Only one table.
|
||||
fieldName, fieldType := m.GetFieldNameAndTypeForDelete(ctx, "", m.tablesInit)
|
||||
fieldName, fieldType := m.GetFieldInfo(ctx, "", m.tablesInit, SoftTimeFieldDelete)
|
||||
if fieldName != "" {
|
||||
return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, "", fieldName, fieldType)
|
||||
return m.buildDeleteCondition(ctx, "", fieldName, fieldType)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@ -310,140 +255,130 @@ func (m *softTimeMaintainer) getConditionOfTableStringForSoftDeleting(ctx contex
|
||||
} else {
|
||||
table = array2[0]
|
||||
}
|
||||
fieldName, fieldType := m.GetFieldNameAndTypeForDelete(ctx, schema, table)
|
||||
fieldName, fieldType := m.GetFieldInfo(ctx, schema, table, SoftTimeFieldDelete)
|
||||
if fieldName == "" {
|
||||
return ""
|
||||
}
|
||||
if len(array1) >= 3 {
|
||||
return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, array1[2], fieldName, fieldType)
|
||||
return m.buildDeleteCondition(ctx, array1[2], fieldName, fieldType)
|
||||
}
|
||||
if len(array1) >= 2 {
|
||||
return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, array1[1], fieldName, fieldType)
|
||||
return m.buildDeleteCondition(ctx, array1[1], fieldName, fieldType)
|
||||
}
|
||||
return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, table, fieldName, fieldType)
|
||||
return m.buildDeleteCondition(ctx, table, fieldName, fieldType)
|
||||
}
|
||||
|
||||
// GetDataByFieldNameAndTypeForDelete creates and returns the placeholder and value for
|
||||
// specified field name and type in soft-deleting scenario.
|
||||
func (m *softTimeMaintainer) GetDataByFieldNameAndTypeForDelete(
|
||||
ctx context.Context, fieldPrefix, fieldName string, fieldType LocalType,
|
||||
) (dataHolder string, dataValue any) {
|
||||
var (
|
||||
quotedFieldPrefix = m.db.GetCore().QuoteWord(fieldPrefix)
|
||||
quotedFieldName = m.db.GetCore().QuoteWord(fieldName)
|
||||
)
|
||||
if quotedFieldPrefix != "" {
|
||||
quotedFieldName = fmt.Sprintf(`%s.%s`, quotedFieldPrefix, quotedFieldName)
|
||||
// GetDeleteData returns UPDATE statement data for soft delete.
|
||||
func (m *softTimeMaintainer) GetDeleteData(
|
||||
ctx context.Context, prefix, fieldName string, fieldType LocalType,
|
||||
) (holder string, value any) {
|
||||
core := m.db.GetCore()
|
||||
quotedName := core.QuoteWord(fieldName)
|
||||
|
||||
if prefix != "" {
|
||||
quotedName = fmt.Sprintf(`%s.%s`, core.QuoteWord(prefix), quotedName)
|
||||
}
|
||||
dataHolder = fmt.Sprintf(`%s=?`, quotedFieldName)
|
||||
dataValue = m.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldType, false)
|
||||
|
||||
holder = fmt.Sprintf(`%s=?`, quotedName)
|
||||
value = m.GetFieldValue(ctx, fieldType, false)
|
||||
return
|
||||
}
|
||||
|
||||
func (m *softTimeMaintainer) getConditionByFieldNameAndTypeForSoftDeleting(
|
||||
ctx context.Context, fieldPrefix, fieldName string, fieldType LocalType,
|
||||
// buildDeleteCondition builds WHERE condition for soft delete filtering.
|
||||
func (m *softTimeMaintainer) buildDeleteCondition(
|
||||
ctx context.Context, prefix, fieldName string, fieldType LocalType,
|
||||
) string {
|
||||
var (
|
||||
quotedFieldPrefix = m.db.GetCore().QuoteWord(fieldPrefix)
|
||||
quotedFieldName = m.db.GetCore().QuoteWord(fieldName)
|
||||
)
|
||||
if quotedFieldPrefix != "" {
|
||||
quotedFieldName = fmt.Sprintf(`%s.%s`, quotedFieldPrefix, quotedFieldName)
|
||||
core := m.db.GetCore()
|
||||
quotedName := core.QuoteWord(fieldName)
|
||||
|
||||
if prefix != "" {
|
||||
quotedName = fmt.Sprintf(`%s.%s`, core.QuoteWord(prefix), quotedName)
|
||||
}
|
||||
switch m.softTimeOption.SoftTimeType {
|
||||
case SoftTimeTypeAuto:
|
||||
switch fieldType {
|
||||
case LocalTypeDate, LocalTypeTime, LocalTypeDatetime:
|
||||
return fmt.Sprintf(`%s IS NULL`, quotedFieldName)
|
||||
return fmt.Sprintf(`%s IS NULL`, quotedName)
|
||||
case LocalTypeInt, LocalTypeUint, LocalTypeInt64, LocalTypeUint64, LocalTypeBool:
|
||||
return fmt.Sprintf(`%s=0`, quotedFieldName)
|
||||
return fmt.Sprintf(`%s=0`, quotedName)
|
||||
default:
|
||||
intlog.Errorf(
|
||||
ctx,
|
||||
`invalid field type "%s" of field name "%s" with prefix "%s" for soft deleting condition`,
|
||||
fieldType, fieldName, fieldPrefix,
|
||||
)
|
||||
intlog.Errorf(ctx, `invalid field type "%s" for soft delete condition: prefix=%s, field=%s`, fieldType, prefix, fieldName)
|
||||
return ""
|
||||
}
|
||||
|
||||
case SoftTimeTypeTime:
|
||||
return fmt.Sprintf(`%s IS NULL`, quotedFieldName)
|
||||
return fmt.Sprintf(`%s IS NULL`, quotedName)
|
||||
|
||||
default:
|
||||
return fmt.Sprintf(`%s=0`, quotedFieldName)
|
||||
return fmt.Sprintf(`%s=0`, quotedName)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetValueByFieldTypeForCreateOrUpdate creates and returns the value for specified field type,
|
||||
// usually for creating or updating operations.
|
||||
func (m *softTimeMaintainer) GetValueByFieldTypeForCreateOrUpdate(
|
||||
ctx context.Context, fieldType LocalType, isDeletedField bool,
|
||||
// GetFieldValue generates value for create/update/delete operations.
|
||||
func (m *softTimeMaintainer) GetFieldValue(
|
||||
ctx context.Context, fieldType LocalType, isDeleted bool,
|
||||
) any {
|
||||
var value any
|
||||
if isDeletedField {
|
||||
switch fieldType {
|
||||
case LocalTypeDate, LocalTypeTime, LocalTypeDatetime:
|
||||
value = nil
|
||||
default:
|
||||
value = 0
|
||||
}
|
||||
return value
|
||||
// For deleted field, return "empty" value
|
||||
if isDeleted {
|
||||
return m.getEmptyValue(fieldType)
|
||||
}
|
||||
|
||||
// For create/update/delete, return current time value
|
||||
switch m.softTimeOption.SoftTimeType {
|
||||
case SoftTimeTypeAuto:
|
||||
switch fieldType {
|
||||
case LocalTypeDate, LocalTypeTime, LocalTypeDatetime:
|
||||
value = gtime.Now()
|
||||
case LocalTypeInt, LocalTypeUint, LocalTypeInt64, LocalTypeUint64:
|
||||
value = gtime.Timestamp()
|
||||
case LocalTypeBool:
|
||||
value = 1
|
||||
default:
|
||||
intlog.Errorf(
|
||||
ctx,
|
||||
`invalid field type "%s" for soft deleting data`,
|
||||
fieldType,
|
||||
)
|
||||
}
|
||||
|
||||
return m.getAutoValue(ctx, fieldType)
|
||||
default:
|
||||
switch fieldType {
|
||||
case LocalTypeBool:
|
||||
value = 1
|
||||
return 1
|
||||
default:
|
||||
value = m.createValueBySoftTimeOption(isDeletedField)
|
||||
return m.getTimestampValue()
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (m *softTimeMaintainer) createValueBySoftTimeOption(isDeletedField bool) any {
|
||||
var value any
|
||||
if isDeletedField {
|
||||
switch m.softTimeOption.SoftTimeType {
|
||||
case SoftTimeTypeTime:
|
||||
value = nil
|
||||
default:
|
||||
value = 0
|
||||
}
|
||||
return value
|
||||
}
|
||||
// getTimestampValue returns timestamp value for soft time.
|
||||
func (m *softTimeMaintainer) getTimestampValue() any {
|
||||
switch m.softTimeOption.SoftTimeType {
|
||||
case SoftTimeTypeTime:
|
||||
value = gtime.Now()
|
||||
return gtime.Now()
|
||||
case SoftTimeTypeTimestamp:
|
||||
value = gtime.Timestamp()
|
||||
return gtime.Timestamp()
|
||||
case SoftTimeTypeTimestampMilli:
|
||||
value = gtime.TimestampMilli()
|
||||
return gtime.TimestampMilli()
|
||||
case SoftTimeTypeTimestampMicro:
|
||||
value = gtime.TimestampMicro()
|
||||
return gtime.TimestampMicro()
|
||||
case SoftTimeTypeTimestampNano:
|
||||
value = gtime.TimestampNano()
|
||||
return gtime.TimestampNano()
|
||||
default:
|
||||
panic(gerror.NewCodef(
|
||||
gcode.CodeInternalPanic,
|
||||
`unrecognized SoftTimeType "%d"`, m.softTimeOption.SoftTimeType,
|
||||
))
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// getEmptyValue returns "empty" value for deleted field.
|
||||
func (m *softTimeMaintainer) getEmptyValue(fieldType LocalType) any {
|
||||
switch fieldType {
|
||||
case LocalTypeDate, LocalTypeTime, LocalTypeDatetime:
|
||||
return nil
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// getAutoValue returns auto-detected value based on field type.
|
||||
func (m *softTimeMaintainer) getAutoValue(ctx context.Context, fieldType LocalType) any {
|
||||
switch fieldType {
|
||||
case LocalTypeDate, LocalTypeTime, LocalTypeDatetime:
|
||||
return gtime.Now()
|
||||
case LocalTypeInt, LocalTypeUint, LocalTypeInt64, LocalTypeUint64:
|
||||
return gtime.Timestamp()
|
||||
case LocalTypeBool:
|
||||
return 1
|
||||
default:
|
||||
intlog.Errorf(ctx, `invalid field type "%s" for soft time auto value`, fieldType)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,9 +50,7 @@ func (m *Model) Update(dataAndWhere ...any) (result sql.Result, err error) {
|
||||
reflectInfo = reflection.OriginTypeAndKind(m.data)
|
||||
conditionWhere, conditionExtra, conditionArgs = m.formatCondition(ctx, false, false)
|
||||
conditionStr = conditionWhere + conditionExtra
|
||||
fieldNameUpdate, fieldTypeUpdate = stm.GetFieldNameAndTypeForUpdate(
|
||||
ctx, "", m.tablesInit,
|
||||
)
|
||||
fieldNameUpdate, fieldTypeUpdate = stm.GetFieldInfo(ctx, "", m.tablesInit, SoftTimeFieldUpdate)
|
||||
)
|
||||
if fieldNameUpdate != "" && (m.unscoped || m.isFieldInFieldsEx(fieldNameUpdate)) {
|
||||
fieldNameUpdate = ""
|
||||
@ -68,7 +66,7 @@ func (m *Model) Update(dataAndWhere ...any) (result sql.Result, err error) {
|
||||
var dataMap = anyValueToMapBeforeToRecord(newData)
|
||||
// Automatically update the record updating time.
|
||||
if fieldNameUpdate != "" && empty.IsNil(dataMap[fieldNameUpdate]) {
|
||||
dataValue := stm.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeUpdate, false)
|
||||
dataValue := stm.GetFieldValue(ctx, fieldTypeUpdate, false)
|
||||
dataMap[fieldNameUpdate] = dataValue
|
||||
}
|
||||
newData = dataMap
|
||||
@ -77,7 +75,7 @@ func (m *Model) Update(dataAndWhere ...any) (result sql.Result, err error) {
|
||||
var updateStr = gconv.String(newData)
|
||||
// Automatically update the record updating time.
|
||||
if fieldNameUpdate != "" && !gstr.Contains(updateStr, fieldNameUpdate) {
|
||||
dataValue := stm.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldTypeUpdate, false)
|
||||
dataValue := stm.GetFieldValue(ctx, fieldTypeUpdate, false)
|
||||
updateStr += fmt.Sprintf(`,%s=?`, fieldNameUpdate)
|
||||
conditionArgs = append([]any{dataValue}, conditionArgs...)
|
||||
}
|
||||
|
||||
@ -68,6 +68,11 @@ func (m *Model) mappingAndFilterToTableFields(table string, fields []any, filter
|
||||
if fieldsTable != "" {
|
||||
hasTable, _ := m.db.GetCore().HasTable(fieldsTable)
|
||||
if !hasTable {
|
||||
if fieldsTable != m.tablesInit {
|
||||
// Table/alias unknown (e.g., FieldsPrefix called before LeftJoin), skip filtering.
|
||||
return fields
|
||||
}
|
||||
// HasTable cache miss for main table, fallback to use main table for field mapping.
|
||||
fieldsTable = m.tablesInit
|
||||
}
|
||||
}
|
||||
|
||||
159
database/gdb/gdb_panic_recovery_test.go
Normal file
159
database/gdb/gdb_panic_recovery_test.go
Normal file
@ -0,0 +1,159 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
// mockPanicStmt simulates a prepared statement that panics during execution
|
||||
type mockPanicStmt struct {
|
||||
panicMessage string
|
||||
}
|
||||
|
||||
func (m *mockPanicStmt) ExecContext(ctx context.Context, args ...any) (sql.Result, error) {
|
||||
if m.panicMessage != "" {
|
||||
panic(m.panicMessage)
|
||||
}
|
||||
panic("math/big: buffer too small to fit value")
|
||||
}
|
||||
|
||||
func (m *mockPanicStmt) QueryContext(ctx context.Context, args ...any) (*sql.Rows, error) {
|
||||
if m.panicMessage != "" {
|
||||
panic(m.panicMessage)
|
||||
}
|
||||
panic("math/big: buffer too small to fit value")
|
||||
}
|
||||
|
||||
func (m *mockPanicStmt) QueryRowContext(ctx context.Context, args ...any) *sql.Row {
|
||||
if m.panicMessage != "" {
|
||||
panic(m.panicMessage)
|
||||
}
|
||||
panic("math/big: buffer too small to fit value")
|
||||
}
|
||||
|
||||
func (m *mockPanicStmt) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Test_PanicRecoveryErrorWrapping tests that the panic recovery properly wraps errors
|
||||
// with correct error codes and messages
|
||||
func Test_PanicRecoveryErrorWrapping(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test creating an error from a string panic value
|
||||
defer func() {
|
||||
if exception := recover(); exception != nil {
|
||||
var err error
|
||||
if v, ok := exception.(error); ok && gerror.HasStack(v) {
|
||||
err = v
|
||||
} else {
|
||||
err = gerror.WrapCodef(gcode.CodeDbOperationError, gerror.NewCodef(gcode.CodeInternalPanic, "%+v", exception), "test SQL")
|
||||
}
|
||||
|
||||
t.AssertNE(err, nil)
|
||||
t.Assert(strings.Contains(err.Error(), "buffer too small"), true)
|
||||
t.Assert(strings.Contains(err.Error(), "test SQL"), true)
|
||||
}
|
||||
}()
|
||||
|
||||
// Simulate the panic that would occur in database operations
|
||||
panic("math/big: buffer too small to fit value")
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test creating an error from an error panic value with stack
|
||||
defer func() {
|
||||
if exception := recover(); exception != nil {
|
||||
var err error
|
||||
if v, ok := exception.(error); ok && gerror.HasStack(v) {
|
||||
err = v
|
||||
} else {
|
||||
err = gerror.WrapCodef(gcode.CodeDbOperationError, gerror.NewCodef(gcode.CodeInternalPanic, "%+v", exception), "test SQL")
|
||||
}
|
||||
|
||||
t.AssertNE(err, nil)
|
||||
// Since gerror has stack, it should preserve the original error
|
||||
t.Assert(strings.Contains(err.Error(), "custom database error"), true)
|
||||
}
|
||||
}()
|
||||
|
||||
// Simulate a panic with a custom error that has stack
|
||||
customErr := gerror.New("custom database error")
|
||||
panic(customErr)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DoCommit_StmtPanicRecovery simulates the scenario from the issue where
|
||||
// statement execution causes a panic during DoCommit operations
|
||||
func Test_DoCommit_StmtPanicRecovery(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// We'll test the panic recovery by triggering it in the defer function
|
||||
// Since we can't easily mock sql.Stmt, we'll test the panic recovery mechanism directly
|
||||
|
||||
testPanicRecovery := func(panicValue any, sqlText string) (err error) {
|
||||
defer func() {
|
||||
if exception := recover(); exception != nil {
|
||||
if err == nil {
|
||||
if v, ok := exception.(error); ok && gerror.HasStack(v) {
|
||||
err = v
|
||||
} else {
|
||||
err = gerror.WrapCodef(gcode.CodeDbOperationError, gerror.NewCodef(gcode.CodeInternalPanic, "%+v", exception), FormatSqlWithArgs(sqlText, []any{123}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Simulate the panic that would occur in database operations
|
||||
panic(panicValue)
|
||||
}
|
||||
|
||||
// Test different panic scenarios
|
||||
testCases := []struct {
|
||||
name string
|
||||
panicValue any
|
||||
sqlText string
|
||||
}{
|
||||
{
|
||||
name: "String panic from math/big",
|
||||
panicValue: "math/big: buffer too small to fit value",
|
||||
sqlText: "INSERT INTO test VALUES (?)",
|
||||
},
|
||||
{
|
||||
name: "Custom error panic",
|
||||
panicValue: gerror.New("clickhouse driver panic"),
|
||||
sqlText: "SELECT * FROM test WHERE id = ?",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Log("Testing:", tc.name)
|
||||
|
||||
// Test the panic recovery mechanism
|
||||
err := testPanicRecovery(tc.panicValue, tc.sqlText)
|
||||
|
||||
// After our fix, these should return errors instead of panicking
|
||||
t.AssertNE(err, nil)
|
||||
|
||||
// Verify the error contains information about the panic
|
||||
errorMsg := err.Error()
|
||||
|
||||
if tc.name == "String panic from math/big" {
|
||||
t.Assert(strings.Contains(errorMsg, "buffer too small"), true)
|
||||
t.Assert(strings.Contains(errorMsg, "INSERT INTO test VALUES"), true)
|
||||
} else {
|
||||
t.Assert(strings.Contains(errorMsg, "clickhouse driver panic"), true)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -8,6 +8,7 @@ package gdb_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
@ -1189,3 +1190,40 @@ func Test_IsConfigured(t *testing.T) {
|
||||
t.Assert(result, true)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_ConfigNode_ConnectionPoolSettings(t *testing.T) {
|
||||
// Test connection pool configuration fields
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Save original config and restore after test
|
||||
originalConfig := gdb.GetAllConfig()
|
||||
defer func() {
|
||||
gdb.SetConfig(originalConfig)
|
||||
}()
|
||||
|
||||
// Reset config
|
||||
gdb.SetConfig(make(gdb.Config))
|
||||
|
||||
testNode := gdb.ConfigNode{
|
||||
Host: "127.0.0.1",
|
||||
Port: "3306",
|
||||
User: "root",
|
||||
Pass: "123456",
|
||||
Name: "test_db",
|
||||
Type: "mysql",
|
||||
MaxIdleConnCount: 10,
|
||||
MaxOpenConnCount: 100,
|
||||
MaxConnLifeTime: 30 * time.Second,
|
||||
MaxIdleConnTime: 10 * time.Second,
|
||||
}
|
||||
|
||||
err := gdb.AddConfigNode("pool_test", testNode)
|
||||
t.AssertNil(err)
|
||||
|
||||
result := gdb.GetAllConfig()
|
||||
t.Assert(len(result), 1)
|
||||
t.Assert(result["pool_test"][0].MaxIdleConnCount, 10)
|
||||
t.Assert(result["pool_test"][0].MaxOpenConnCount, 100)
|
||||
t.Assert(result["pool_test"][0].MaxConnLifeTime, 30*time.Second)
|
||||
t.Assert(result["pool_test"][0].MaxIdleConnTime, 10*time.Second)
|
||||
})
|
||||
}
|
||||
|
||||
@ -142,6 +142,11 @@ func Test_Core_SetMaxConnections(t *testing.T) {
|
||||
testDuration := time.Hour
|
||||
core.SetMaxConnLifeTime(testDuration)
|
||||
t.Assert(core.dynamicConfig.MaxConnLifeTime, testDuration)
|
||||
|
||||
// Test SetMaxIdleConnTime
|
||||
idleTimeDuration := 30 * time.Minute
|
||||
core.SetMaxIdleConnTime(idleTimeDuration)
|
||||
t.Assert(core.dynamicConfig.MaxIdleConnTime, idleTimeDuration)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -50,8 +50,10 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
// configChecker checks whether the *Config is nil.
|
||||
configChecker = func(v *Config) bool { return v == nil }
|
||||
// Configuration groups.
|
||||
localConfigMap = gmap.NewStrAnyMap(true)
|
||||
localConfigMap = gmap.NewKVMapWithChecker[string, *Config](configChecker, true)
|
||||
)
|
||||
|
||||
// SetConfig sets the global configuration for specified group.
|
||||
@ -119,7 +121,7 @@ func GetConfig(name ...string) (config *Config, ok bool) {
|
||||
group = name[0]
|
||||
}
|
||||
if v := localConfigMap.Get(group); v != nil {
|
||||
return v.(*Config), true
|
||||
return v, true
|
||||
}
|
||||
return &Config{}, false
|
||||
}
|
||||
|
||||
@ -14,8 +14,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// localInstances for instance management of redis client.
|
||||
localInstances = gmap.NewStrAnyMap(true)
|
||||
// checker is the checker function for instances map.
|
||||
checker = func(v *Redis) bool { return v == nil }
|
||||
localInstances = gmap.NewKVMapWithChecker[string, *Redis](checker, true)
|
||||
)
|
||||
|
||||
// Instance returns an instance of redis client with specified group.
|
||||
@ -26,7 +27,7 @@ func Instance(name ...string) *Redis {
|
||||
if len(name) > 0 && name[0] != "" {
|
||||
group = name[0]
|
||||
}
|
||||
v := localInstances.GetOrSetFuncLock(group, func() any {
|
||||
return localInstances.GetOrSetFuncLock(group, func() *Redis {
|
||||
if config, ok := GetConfig(group); ok {
|
||||
r, err := New(config)
|
||||
if err != nil {
|
||||
@ -37,8 +38,4 @@ func Instance(name ...string) *Redis {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if v != nil {
|
||||
return v.(*Redis)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user