// 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" "fmt" "strings" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/internal/intlog" "github.com/gogf/gf/v2/os/gcache" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/text/gregex" "github.com/gogf/gf/v2/text/gstr" "github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/util/gutil" ) // SoftTimeType custom defines the soft time field type. type SoftTimeType int const ( SoftTimeTypeAuto SoftTimeType = 0 // (Default)Auto detect the field type by table field type. SoftTimeTypeTime SoftTimeType = 1 // Using datetime as the field value. SoftTimeTypeTimestamp SoftTimeType = 2 // In unix seconds. SoftTimeTypeTimestampMilli SoftTimeType = 3 // In unix milliseconds. SoftTimeTypeTimestampMicro SoftTimeType = 4 // In unix microseconds. SoftTimeTypeTimestampNano SoftTimeType = 5 // In unix nanoseconds. ) // SoftTimeOption is the option to customize soft time feature for Model. type SoftTimeOption struct { SoftTimeType SoftTimeType // The value type for soft time field. } type softTimeMaintainer struct { *Model } type iSoftTimeMaintainer interface { GetFieldNameAndTypeForCreate( ctx context.Context, schema string, table string, ) (fieldName string, fieldType LocalType) GetFieldNameAndTypeForUpdate( ctx context.Context, schema string, table string, ) (fieldName string, fieldType LocalType) GetFieldNameAndTypeForDelete( ctx context.Context, schema string, table string, ) (fieldName string, fieldType LocalType) 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 } // getSoftFieldNameAndTypeCacheItem is the internal struct for storing create/update/delete fields. type getSoftFieldNameAndTypeCacheItem struct { FieldName string FieldType LocalType } var ( // Default field names of table for automatic-filled for record creating. createdFieldNames = []string{"created_at", "create_at"} // Default field names of table for automatic-filled for record updating. updatedFieldNames = []string{"updated_at", "update_at"} // Default field names of table for automatic-filled for record deleting. deletedFieldNames = []string{"deleted_at", "delete_at"} ) // SoftTime sets the SoftTimeOption to customize soft time feature for Model. func (m *Model) SoftTime(option SoftTimeOption) *Model { model := m.getModel() model.softTimeOption = option return model } // Unscoped disables the soft time feature for insert, update and delete operations. func (m *Model) Unscoped() *Model { model := m.getModel() model.unscoped = true return model } func (m *Model) softTimeMaintainer() iSoftTimeMaintainer { return &softTimeMaintainer{ m, } } // 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. // 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. 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 { 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 } config := m.db.GetConfig() if config.DeletedAt != "" { return m.getSoftFieldNameAndType( ctx, schema, tableName, []string{config.DeletedAt}, ) } return m.getSoftFieldNameAndType( ctx, schema, tableName, deletedFieldNames, ) } // getSoftFieldName 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, ) (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 interface{}, 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, _ = gutil.MapPossibleItemByKey( gconv.Map(fieldsMap), checkFiledName, ) if fieldName != "" { fieldType, _ = m.db.CheckLocalTypeForField( ctx, fieldsMap[fieldName].Type, nil, ) var cacheItem = getSoftFieldNameAndTypeCacheItem{ FieldName: fieldName, FieldType: fieldType, } return cacheItem, nil } } return } ) result, err := innerMemCache.GetOrSetFunc( ctx, cacheKey, cacheFunc, cacheDuration, ) if err != nil { return } if result == nil { return } cacheItem := result.Val().(getSoftFieldNameAndTypeCacheItem) fieldName = cacheItem.FieldName fieldType = cacheItem.FieldType return } // GetWhereConditionForDelete retrieves and returns the condition string for soft deleting. // 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 { if m.unscoped { return "" } conditionArray := garray.NewStrArray() if gstr.Contains(m.tables, " JOIN ") { // Base table. tableMatch, _ := gregex.MatchString(`(.+?) [A-Z]+ JOIN`, m.tables) conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(ctx, tableMatch[1])) // Multiple joined tables, exclude the sub query sql which contains char '(' and ')'. tableMatches, _ := gregex.MatchAllString(`JOIN ([^()]+?) ON`, m.tables) for _, match := range tableMatches { conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(ctx, match[1])) } } if conditionArray.Len() == 0 && gstr.Contains(m.tables, ",") { // Multiple base tables. for _, s := range gstr.SplitAndTrim(m.tables, ",") { conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(ctx, s)) } } conditionArray.FilterEmpty() if conditionArray.Len() > 0 { return conditionArray.Join(" AND ") } // Only one table. fieldName, fieldType := m.GetFieldNameAndTypeForDelete(ctx, "", m.tablesInit) if fieldName != "" { return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, "", fieldName, fieldType) } return "" } // getConditionOfTableStringForSoftDeleting does something as its name describes. // Examples for `s`: // - `test`.`demo` as b // - `test`.`demo` b // - `demo` // - demo func (m *softTimeMaintainer) getConditionOfTableStringForSoftDeleting(ctx context.Context, s string) string { var ( table string schema string array1 = gstr.SplitAndTrim(s, " ") array2 = gstr.SplitAndTrim(array1[0], ".") ) if len(array2) >= 2 { table = array2[1] schema = array2[0] } else { table = array2[0] } fieldName, fieldType := m.GetFieldNameAndTypeForDelete(ctx, schema, table) if fieldName == "" { return "" } if len(array1) >= 3 { return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, array1[2], fieldName, fieldType) } if len(array1) >= 2 { return m.getConditionByFieldNameAndTypeForSoftDeleting(ctx, array1[1], fieldName, fieldType) } return m.getConditionByFieldNameAndTypeForSoftDeleting(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) } dataHolder = fmt.Sprintf(`%s=?`, quotedFieldName) dataValue = m.GetValueByFieldTypeForCreateOrUpdate(ctx, fieldType, false) return } func (m *softTimeMaintainer) getConditionByFieldNameAndTypeForSoftDeleting( ctx context.Context, fieldPrefix, 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) } switch m.softTimeOption.SoftTimeType { case SoftTimeTypeAuto: switch fieldType { case LocalTypeDate, LocalTypeDatetime: return fmt.Sprintf(`%s IS NULL`, quotedFieldName) case LocalTypeInt, LocalTypeUint, LocalTypeInt64, LocalTypeUint64, LocalTypeBool: return fmt.Sprintf(`%s=0`, quotedFieldName) default: intlog.Errorf( ctx, `invalid field type "%s" of field name "%s" with prefix "%s" for soft deleting condition`, fieldType, fieldName, fieldPrefix, ) } case SoftTimeTypeTime: return fmt.Sprintf(`%s IS NULL`, quotedFieldName) default: return fmt.Sprintf(`%s=0`, quotedFieldName) } 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, ) any { var value any if isDeletedField { switch fieldType { case LocalTypeDate, LocalTypeDatetime: value = nil default: value = 0 } return value } switch m.softTimeOption.SoftTimeType { case SoftTimeTypeAuto: switch fieldType { case LocalTypeDate, 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, ) } default: switch fieldType { case LocalTypeBool: value = 1 default: value = m.createValueBySoftTimeOption(isDeletedField) } } 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 } switch m.softTimeOption.SoftTimeType { case SoftTimeTypeTime: value = gtime.Now() case SoftTimeTypeTimestamp: value = gtime.Timestamp() case SoftTimeTypeTimestampMilli: value = gtime.TimestampMilli() case SoftTimeTypeTimestampMicro: value = gtime.TimestampMicro() case SoftTimeTypeTimestampNano: value = gtime.TimestampNano() default: panic(gerror.NewCodef( gcode.CodeInternalPanic, `unrecognized SoftTimeType "%d"`, m.softTimeOption.SoftTimeType, )) } return value }