Files
phishingclub/backend/vendor/github.com/enetx/g/option_sql.go
T
RonniSkansing 2028391c3c vendor backend
Signed-off-by: RonniSkansing <rskansing@gmail.com>
2026-09-16 23:17:35 +02:00

240 lines
6.4 KiB
Go

package g
import (
"database/sql"
"database/sql/driver"
"fmt"
"math"
"time"
)
// Scan implements the database/sql.Scanner interface for Option[T].
//
// Behavior:
// - If src is nil, the Option is set to None (SQL NULL).
// - If T implements sql.Scanner, its Scan method is used.
// - If src can be directly assigned to T, it is assigned as-is.
// - Otherwise, common database type conversions are attempted (e.g., int64 → int, []byte → string).
//
// Supported conversions (common SQL types):
// - INTEGER → int, int8, int16, int32, int64, uint*
// - REAL → float32, float64
// - TEXT → string, []byte
// - BLOB → []byte
// - BOOLEAN → bool
// - TIMESTAMP → time.Time
//
// Driver-owned []byte buffers (BLOB/TEXT) are copied before being stored, so the
// scanned Option keeps a value that is safe to retain across subsequent rows
// (per the database/sql Scanner contract).
//
// Returns an error if the value cannot be converted to T.
func (o *Option[T]) Scan(src any) error {
if src == nil {
*o = None[T]()
return nil
}
var v T
if scanner, ok := any(&v).(sql.Scanner); ok {
if err := scanner.Scan(src); err != nil {
return err
}
*o = Some(v)
return nil
}
if val, ok := src.(T); ok {
// Copy driver-owned []byte buffers before storing: database/sql may
// overwrite the backing array on the next row (database/sql contract).
if b, isBytes := any(val).([]byte); isBytes {
val = any(append([]byte(nil), b...)).(T)
}
*o = Some(val)
return nil
}
if converted, ok := convertToT[T](src); ok {
*o = Some(converted)
return nil
}
return fmt.Errorf("Option.Scan: cannot scan %T into %T", src, v)
}
// Value implements the database/sql/driver.Valuer interface for Option[T].
//
// Behavior:
// - If the Option is None, returns nil (SQL NULL).
// - If T implements driver.Valuer, its Value method is used.
// - If the underlying value is already a valid driver.Value type (int64, float64, bool, []byte, string, time.Time), it is returned directly.
// - Otherwise, safe conversions are applied (int → int64, uint → int64, float32 → float64).
//
// Returns an error if the value cannot be converted to a driver.Value.
func (o Option[T]) Value() (driver.Value, error) {
if o.IsNone() {
return nil, nil
}
if valuer, ok := any(o.v).(driver.Valuer); ok {
return valuer.Value()
}
switch val := any(o.v).(type) {
case int64, float64, bool, []byte, string, time.Time:
return val, nil
default:
if converted, ok := convertToDriverValue(val); ok {
return converted, nil
}
}
return nil, fmt.Errorf("Option.Value: unsupported type %T", o.v)
}
// convertToT attempts to safely convert a source value from database/sql
// into type T, supporting common database types without using reflection.
//
// Only standard Go primitive types are supported:
// - int64 → int, int8, int16, int32, uint*, uint64 (if fits)
// - float64 → float32, float64
// - string / []byte → string
// - []byte / string → []byte
// - bool → bool
// - time.Time → time.Time
//
// Returns the converted value and true on success, otherwise the zero value of T and false.
func convertToT[T any](src any) (T, bool) {
var zero T
switch any(zero).(type) {
case int:
if i64, ok := src.(int64); ok && fitsInt(i64) {
return any(int(i64)).(T), true
}
case int8:
if i64, ok := src.(int64); ok && fitsInt8(i64) {
return any(int8(i64)).(T), true
}
case int16:
if i64, ok := src.(int64); ok && fitsInt16(i64) {
return any(int16(i64)).(T), true
}
case int32:
if i64, ok := src.(int64); ok && fitsInt32(i64) {
return any(int32(i64)).(T), true
}
case int64:
if i64, ok := src.(int64); ok {
return any(i64).(T), true
}
case uint:
if i64, ok := src.(int64); ok && i64 >= 0 {
return any(uint(i64)).(T), true
}
case uint8:
if i64, ok := src.(int64); ok && i64 >= 0 && i64 <= math.MaxUint8 {
return any(uint8(i64)).(T), true
}
case uint16:
if i64, ok := src.(int64); ok && i64 >= 0 && i64 <= math.MaxUint16 {
return any(uint16(i64)).(T), true
}
case uint32:
if i64, ok := src.(int64); ok && i64 >= 0 && i64 <= math.MaxUint32 {
return any(uint32(i64)).(T), true
}
case uint64:
if i64, ok := src.(int64); ok && i64 >= 0 {
return any(uint64(i64)).(T), true
}
case float32:
if f64, ok := src.(float64); ok {
return any(float32(f64)).(T), true
}
case float64:
if f64, ok := src.(float64); ok {
return any(f64).(T), true
}
case string:
switch v := src.(type) {
case string:
return any(v).(T), true
case []byte:
return any(string(v)).(T), true
}
case []byte:
switch v := src.(type) {
case []byte:
// Copy the driver-owned buffer: database/sql may reuse it on the next row.
return any(append([]byte(nil), v...)).(T), true
case string:
return any([]byte(v)).(T), true
}
case bool:
if b, ok := src.(bool); ok {
return any(b).(T), true
}
case time.Time:
if t, ok := src.(time.Time); ok {
return any(t).(T), true
}
}
return zero, false
}
// convertToDriverValue safely converts primitive Go types to a value
// compatible with database/sql driver.Value.
//
// Supported conversions:
// - int, int8, int16, int32 → int64
// - uint8, uint16, uint32 → int64
// - uint, uint64 → int64 (only if <= math.MaxInt64)
// - float32 → float64
//
// Returns the converted value and true on success, otherwise nil and false.
func convertToDriverValue(val any) (driver.Value, bool) {
switch v := val.(type) {
case int:
return int64(v), true
case int8:
return int64(v), true
case int16:
return int64(v), true
case int32:
return int64(v), true
case uint:
if uint64(v) <= math.MaxInt64 {
return int64(v), true
}
case uint8:
return int64(v), true
case uint16:
return int64(v), true
case uint32:
return int64(v), true
case uint64:
if v <= math.MaxInt64 {
return int64(v), true
}
case float32:
return float64(v), true
}
return nil, false
}
// fitsInt returns true if i fits in a Go int.
func fitsInt(i int64) bool { return i >= math.MinInt && i <= math.MaxInt }
// fitsInt8 returns true if i fits in an int8.
func fitsInt8(i int64) bool { return i >= math.MinInt8 && i <= math.MaxInt8 }
// fitsInt16 returns true if i fits in an int16.
func fitsInt16(i int64) bool { return i >= math.MinInt16 && i <= math.MaxInt16 }
// fitsInt32 returns true if i fits in an int32.
func fitsInt32(i int64) bool { return i >= math.MinInt32 && i <= math.MaxInt32 }