Files
dbx/utility.go
Elijah Duffy 14686a83cb move utilities out of db-specific packages
DestName & NormalCols now live in root dbx package utilizing union
interfaces. SoftDelete helper has been dropped.
2026-01-28 11:57:50 -08:00

278 lines
7.8 KiB
Go

package dbx
import (
"fmt"
"reflect"
"strings"
"time"
"gitea.auvem.com/go-toolkit/dbx/internal/dbxshared"
"github.com/go-jet/jet/v2/mysql"
"github.com/go-jet/jet/v2/postgres"
"golang.org/x/exp/constraints"
)
// Column is a union type for mysql.Column and postgres.Column
type Column interface {
mysql.Column
postgres.Column
}
// ColumnList is a union type for mysql.ColumnList and postgres.ColumnList
type ColumnList interface {
mysql.ColumnList
postgres.ColumnList
}
// BoolExpression is a union type for mysql.BoolExpression and postgres.BoolExpression
type BoolExpression interface {
mysql.BoolExpression
postgres.BoolExpression
}
// StringToFilter processes a string to be used as a filter in an SQL LIKE
// statement. It replaces all spaces with % and adds % to the beginning and
// end of the string.
func StringToFilter(str string) string {
// Remove any existing leading or trailing % characters
str = strings.Trim(str, "%")
// Replace all spaces with % and add % to the beginning and end of the string
str = strings.ReplaceAll(str, " ", "%")
str = "%" + str + "%"
return str
}
// DestName returns the name of the type passed as `destTypeStruct` as a string,
// normalized for compatibility with the Jet QRM.
func DestName(destTypeStruct any, path ...string) string {
v := reflect.ValueOf(destTypeStruct)
for v.Kind() == reflect.Pointer {
v = v.Elem()
}
destIdent := v.Type().String()
destIdent = destIdent[strings.LastIndex(destIdent, ".")+1:]
for i, p := range path {
if v.Kind() != reflect.Struct {
dbxshared.DBModule.Logger().Error("DestName: path parent is not a struct", "path", destIdent+"."+strings.Join(path[:i+1], "."))
return ""
}
v = v.FieldByName(p)
if !v.IsValid() {
dbxshared.DBModule.Logger().Error("DestName: field does not exist", "path", destIdent+"."+strings.Join(path[:i+1], "."))
return ""
}
destIdent += "." + p
}
return destIdent
}
// NormalCols processes a list of columns and strips out any that implement any of
// ColumnTimestamp, ColumnTime, or ColumnDate.
func NormalCols[CL ColumnList](cols ...Column) CL {
res := make(CL, 0)
for _, col := range cols {
switch col.(type) {
case mysql.ColumnTimestamp, // = postgres.ColumnTimestamp
mysql.ColumnTime, // = postgres.ColumnTime
mysql.ColumnDate: // = postgres.ColumnDate
// skip time/date/timestamp columns
default:
res = append(res, col)
}
}
return res
}
// ExprValues converts a list of values to a list of mysql.Expression values using
// function f to transform the values (mysql.String for strings, mysql.Uint64, etc).
func ExprValues[T any](values []T, f func(T) mysql.Expression) []mysql.Expression {
expressions := make([]mysql.Expression, len(values))
for i, v := range values {
expressions[i] = f(v)
}
return expressions
}
// ExprStringers converts a list of fmt.Stringers to a list of mysql.Expression values.
func ExprStringers(values []fmt.Stringer) []mysql.Expression {
expressions := make([]mysql.Expression, len(values))
for i, v := range values {
expressions[i] = mysql.String(v.String())
}
return expressions
}
// NowPtr returns a pointer to the current time.
func NowPtr() *time.Time {
now := time.Now()
return &now
}
// Ptr returns a pointer to the given value of any scalar type. Returns nil if the value is a zero value.
func Ptr[T any](val T) *T {
if reflect.ValueOf(val).IsZero() {
return nil
}
return &val
}
// Val returns the value of the pointer to a scalar type, or the zero value if the pointer is nil.
func Val[T any](ptr *T) T {
if ptr == nil {
var zero T
return zero
}
return *ptr
}
// TrimPtr trims the whitespace from a pointer to a string and returns nil only if the pointer is nil.
func TrimPtr(s *string) *string {
if s == nil {
return nil
}
trimmed := strings.TrimSpace(*s)
return &trimmed
}
// TrimPtrToNil trims the whitespace from a pointer to a string and returns nil
// if the resulting string is empty or if the pointer is nil.
func TrimPtrToNil(s *string) *string {
if s == nil {
return nil
}
trimmed := strings.TrimSpace(*s)
if trimmed == "" {
return nil
}
return &trimmed
}
// IsZero checks if a pointer references the zero value of a given type and
// returns an error if this condition is met, otherwise returns nil if the
// pointer is nil or the value is not zero.
func IsZero[T any](ptr *T) error {
if ptr == nil {
return nil
}
if reflect.ValueOf(*ptr).IsZero() {
return ErrValueIsZero
}
return nil
}
// ApplyPtr compares the existing value with a new value and returns the updated value if they differ.
// If the new value is nil, the existing value is retained. If the new value is a zero-value, the
// existing value is NOT retained, it will be set to nil. If the value is changed, targetColumn is pushed
// to updatedColumns.
func ApplyPtr[T constraints.Float | constraints.Integer | string | bool](
existing *T,
newVal *T,
updatedColumns *mysql.ColumnList,
targetColumn mysql.Column,
) *T {
if newVal == nil {
return existing
}
if reflect.ValueOf(*newVal).IsZero() {
newVal = nil
}
if newVal == nil && existing == nil || newVal != nil && existing != nil && *existing == *newVal {
return existing
}
*updatedColumns = append(*updatedColumns, targetColumn)
return newVal
}
// ApplyComplexPtr compares the existing value with a new value and returns the updated value if they differ.
// The new value may be of a different type (e.g. existing is uint16 and new is uint64), but it will be
// converted to match the current type resulting in potential loss of data. If the new value is nil, the
// existing value is retained. If the new value is a zero-value, the existing value is NOT retained, it
// will be set to nil. If the value is changed, targetColumn is pushed to updatedColumns.
func ApplyComplexPtr[
Existing constraints.Float | constraints.Integer,
New constraints.Float | constraints.Integer,
](
existing *Existing,
newVal *New,
updatedColumns *mysql.ColumnList,
targetColumn mysql.Column,
) *Existing {
if newVal == nil {
return existing
}
cast := Existing(*newVal) // Convert new value to existing type
if existing != nil && *existing == cast {
return existing
}
if reflect.ValueOf(cast).IsZero() {
if existing != nil {
*updatedColumns = append(*updatedColumns, targetColumn)
}
return nil
} else {
*updatedColumns = append(*updatedColumns, targetColumn)
return &cast
}
}
type ApplyInterface[T any] interface {
Equal(T) bool
IsZero() bool
}
// ApplyInterfacePtr compares the existing value with a new value and returns the updated value if
// they differ. Comparable types must have IsZero and Equal methods. If the new value is nil, the
// existing value is retained. If the new value is a zero-value, the existing value is NOT retained,
// it will be set to nil. If the value is changed, targetColumn is pushed to updatedColumns.
func ApplyInterfacePtr[T ApplyInterface[T]](
existing *T,
newVal *T,
updatedColumns *mysql.ColumnList,
targetColumn mysql.Column,
) *T {
if newVal == nil {
return existing
}
if existing != nil && (*existing).Equal(*newVal) {
return existing
}
if (*newVal).IsZero() {
if existing != nil {
*updatedColumns = append(*updatedColumns, targetColumn)
}
return nil
} else {
*updatedColumns = append(*updatedColumns, targetColumn)
return newVal
}
}
// ApplyVal compares the existing value with a pointer to a new value and returns the updated value if they
// differ. If the new value is nil, the existing value is retained. If the value is changed, targetColumn
// is pushed to updatedColumns
func ApplyVal[T constraints.Float | constraints.Integer | string | bool](
existing T,
newVal *T,
updatedColumns *mysql.ColumnList,
targetColumn mysql.Column,
) T {
if newVal == nil {
return existing
}
if existing == *newVal {
return existing
}
*updatedColumns = append(*updatedColumns, targetColumn)
return *newVal
}