// Package sqltypes provides nullable SQL types with automatic casting and conversion methods. package sqltypes import ( "database/sql" "database/sql/driver" "encoding/base64" "encoding/json" "encoding/xml" "fmt" "reflect" "strconv" "strings" "time" "github.com/google/uuid" "gopkg.in/yaml.v3" ) // tryParseDT attempts to parse a string into a time.Time using various formats. func tryParseDT(str string) (time.Time, error) { var lasterror error tryFormats := []string{ time.RFC3339, "2006-01-02T15:04:05.000-0700", "2006-01-02T15:04:05.000", "06-01-02T15:04:05.000", "2006-01-02T15:04:05", "2006-01-02 15:04:05", "02/01/2006", "02-01-2006", "2006-01-02", "15:04:05.000", "15:04:05", "15:04", } for _, f := range tryFormats { tx, err := time.Parse(f, str) if err == nil { return tx, nil } lasterror = err } return time.Time{}, lasterror // Return zero time on failure } // ToJSONDT formats a time.Time to RFC3339 string. func ToJSONDT(dt time.Time) string { return dt.Format(time.RFC3339) } // SqlNull is a generic nullable type that behaves like sql.NullXXX with auto-casting. type SqlNull[T any] struct { Val T Valid bool } // Scan implements sql.Scanner. func (n *SqlNull[T]) Scan(value any) error { if value == nil { n.Valid = false n.Val = *new(T) return nil } // Check if T is []byte, and decode base64 if applicable // Do this BEFORE trying sql.Null to ensure base64 is handled var zero T if _, ok := any(zero).([]byte); ok { // For []byte types, try to decode from base64 var strVal string switch v := value.(type) { case string: strVal = v case []byte: strVal = string(v) default: strVal = fmt.Sprintf("%v", value) } // Try base64 decode if decoded, err := base64.StdEncoding.DecodeString(strVal); err == nil { n.Val = any(decoded).(T) n.Valid = true return nil } // Fallback to raw bytes n.Val = any([]byte(strVal)).(T) n.Valid = true return nil } // Try standard sql.Null[T] for other types. var sqlNull sql.Null[T] if err := sqlNull.Scan(value); err == nil { n.Val = sqlNull.V n.Valid = sqlNull.Valid return nil } // Fallback: parse from string/bytes. switch v := value.(type) { case string: return n.FromString(v) case []byte: return n.FromString(string(v)) case float32, float64: return n.FromString(fmt.Sprintf("%f", value)) case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: return n.FromString(fmt.Sprintf("%d", value)) default: return n.FromString(fmt.Sprintf("%v", value)) } } func (n *SqlNull[T]) FromString(s string) error { s = strings.TrimSpace(s) n.Valid = false n.Val = *new(T) if s == "" || strings.EqualFold(s, "null") { return nil } var zero T switch any(zero).(type) { case int, int8, int16, int32, int64: if i, err := strconv.ParseInt(s, 10, 64); err == nil { reflect.ValueOf(&n.Val).Elem().SetInt(i) n.Valid = true } else if f, err := strconv.ParseFloat(s, 64); err == nil { reflect.ValueOf(&n.Val).Elem().SetInt(int64(f)) n.Valid = true } case uint, uint8, uint16, uint32, uint64: if u, err := strconv.ParseUint(s, 10, 64); err == nil { reflect.ValueOf(&n.Val).Elem().SetUint(u) n.Valid = true } else if f, err := strconv.ParseFloat(s, 64); err == nil && f >= 0 { reflect.ValueOf(&n.Val).Elem().SetUint(uint64(f)) n.Valid = true } case float32, float64: if f, err := strconv.ParseFloat(s, 64); err == nil { reflect.ValueOf(&n.Val).Elem().SetFloat(f) n.Valid = true } case bool: if b, err := strconv.ParseBool(s); err == nil { n.Val = any(b).(T) n.Valid = true } case time.Time: if t, err := tryParseDT(s); err == nil && !t.IsZero() { n.Val = any(t).(T) n.Valid = true } case uuid.UUID: if u, err := uuid.Parse(s); err == nil { n.Val = any(u).(T) n.Valid = true } case []byte: n.Val = any([]byte(s)).(T) n.Valid = true case string: n.Val = any(s).(T) n.Valid = true } return nil } // Value implements driver.Valuer. func (n SqlNull[T]) Value() (driver.Value, error) { if !n.Valid { return nil, nil } // Check if the type implements fmt.Stringer (e.g., uuid.UUID, custom types) // Convert to string for driver compatibility if stringer, ok := any(n.Val).(fmt.Stringer); ok { return stringer.String(), nil } return any(n.Val), nil } // MarshalJSON implements json.Marshaler. func (n SqlNull[T]) MarshalJSON() ([]byte, error) { if !n.Valid { return []byte("null"), nil } // Check if T is []byte, and encode to base64 if _, ok := any(n.Val).([]byte); ok { // Encode []byte as base64 encoded := base64.StdEncoding.EncodeToString(any(n.Val).([]byte)) return json.Marshal(encoded) } return json.Marshal(n.Val) } // UnmarshalJSON implements json.Unmarshaler. func (n *SqlNull[T]) UnmarshalJSON(b []byte) error { if len(b) == 0 || string(b) == "null" || strings.TrimSpace(string(b)) == "" { n.Valid = false n.Val = *new(T) return nil } // Check if T is []byte, and decode from base64 var val T if _, ok := any(val).([]byte); ok { // Unmarshal as string first (JSON representation) var s string if err := json.Unmarshal(b, &s); err == nil { // Decode from base64 if decoded, err := base64.StdEncoding.DecodeString(s); err == nil { n.Val = any(decoded).(T) n.Valid = true return nil } // Fallback to raw string as bytes n.Val = any([]byte(s)).(T) n.Valid = true return nil } } if err := json.Unmarshal(b, &val); err == nil { n.Val = val n.Valid = true return nil } // Fallback: unmarshal as string and parse. var s string if err := json.Unmarshal(b, &s); err == nil { return n.FromString(s) } return fmt.Errorf("cannot unmarshal %s into SqlNull[%T]", b, n.Val) } // MarshalYAML implements yaml.Marshaler. func (n SqlNull[T]) MarshalYAML() (any, error) { if !n.Valid { return nil, nil } // Check if T is []byte, and encode to base64 (mirrors MarshalJSON). if b, ok := any(n.Val).([]byte); ok { return base64.StdEncoding.EncodeToString(b), nil } return n.Val, nil } // UnmarshalYAML implements yaml.Unmarshaler. func (n *SqlNull[T]) UnmarshalYAML(value *yaml.Node) error { if value == nil || value.Tag == "!!null" { n.Valid = false n.Val = *new(T) return nil } // Check if T is []byte, and decode from base64. var zero T if _, ok := any(zero).([]byte); ok { var s string if err := value.Decode(&s); err == nil { if decoded, err := base64.StdEncoding.DecodeString(s); err == nil { n.Val = any(decoded).(T) n.Valid = true return nil } n.Val = any([]byte(s)).(T) n.Valid = true return nil } } var val T if err := value.Decode(&val); err == nil { n.Val = val n.Valid = true return nil } // Fallback: decode as string and parse. var s string if err := value.Decode(&s); err == nil { return n.FromString(s) } return fmt.Errorf("cannot unmarshal %q into SqlNull[%T]", value.Value, n.Val) } // MarshalXML implements xml.Marshaler. func (n SqlNull[T]) MarshalXML(e *xml.Encoder, start xml.StartElement) error { if !n.Valid { return e.EncodeElement("", start) } // Check if T is []byte, and encode to base64 (mirrors MarshalJSON). if b, ok := any(n.Val).([]byte); ok { return e.EncodeElement(base64.StdEncoding.EncodeToString(b), start) } return e.EncodeElement(n.Val, start) } // UnmarshalXML implements xml.Unmarshaler. // // XML has no native null representation, so an empty element unmarshals to // an invalid (null) value rather than a zero-value-but-valid one. func (n *SqlNull[T]) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var s string if err := d.DecodeElement(&s, &start); err != nil { return err } if s == "" { n.Valid = false n.Val = *new(T) return nil } var zero T if _, ok := any(zero).([]byte); ok { if decoded, err := base64.StdEncoding.DecodeString(s); err == nil { n.Val = any(decoded).(T) n.Valid = true return nil } n.Val = any([]byte(s)).(T) n.Valid = true return nil } return n.FromString(s) } // String implements fmt.Stringer. func (n SqlNull[T]) String() string { if !n.Valid { return "" } // Check if the type implements fmt.Stringer for better string representation if stringer, ok := any(n.Val).(fmt.Stringer); ok { return stringer.String() } return fmt.Sprintf("%v", n.Val) } // Int64 converts to int64 or 0 if invalid. func (n SqlNull[T]) Int64() int64 { if !n.Valid { return 0 } v := reflect.ValueOf(any(n.Val)) switch v.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return v.Int() case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return int64(v.Uint()) case reflect.Float32, reflect.Float64: return int64(v.Float()) case reflect.String: i, _ := strconv.ParseInt(v.String(), 10, 64) return i case reflect.Bool: if v.Bool() { return 1 } return 0 } return 0 } // Float64 converts to float64 or 0.0 if invalid. func (n SqlNull[T]) Float64() float64 { if !n.Valid { return 0.0 } v := reflect.ValueOf(any(n.Val)) switch v.Kind() { case reflect.Float32, reflect.Float64: return v.Float() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return float64(v.Int()) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return float64(v.Uint()) case reflect.String: f, _ := strconv.ParseFloat(v.String(), 64) return f } return 0.0 } // Bool converts to bool or false if invalid. func (n SqlNull[T]) Bool() bool { if !n.Valid { return false } v := reflect.ValueOf(any(n.Val)) if v.Kind() == reflect.Bool { return v.Bool() } s := strings.ToLower(strings.TrimSpace(fmt.Sprint(n.Val))) return s == "true" || s == "t" || s == "1" || s == "yes" || s == "on" } // Time converts to time.Time or zero if invalid. func (n SqlNull[T]) Time() time.Time { if !n.Valid { return time.Time{} } if t, ok := any(n.Val).(time.Time); ok { return t } return time.Time{} } // UUID converts to uuid.UUID or Nil if invalid. func (n SqlNull[T]) UUID() uuid.UUID { if !n.Valid { return uuid.Nil } if u, ok := any(n.Val).(uuid.UUID); ok { return u } return uuid.Nil } // Type aliases for common types. type ( SqlInt16 = SqlNull[int16] SqlInt32 = SqlNull[int32] SqlInt64 = SqlNull[int64] SqlFloat32 = SqlNull[float32] SqlFloat64 = SqlNull[float64] SqlBool = SqlNull[bool] SqlString = SqlNull[string] SqlByteArray = SqlNull[[]byte] SqlUUID = SqlNull[uuid.UUID] ) // SqlTimeStamp - Timestamp serialized as RFC3339. type SqlTimeStamp struct{ SqlNull[time.Time] } func (t SqlTimeStamp) MarshalJSON() ([]byte, error) { if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0o002, 1, 1, 0, 0, 0, 0, time.UTC)) { return []byte("null"), nil } return fmt.Appendf(nil, `"%s"`, t.Val.Format(time.RFC3339)), nil } func (t *SqlTimeStamp) UnmarshalJSON(b []byte) error { if err := t.SqlNull.UnmarshalJSON(b); err != nil { return err } if t.Valid && (t.Val.IsZero() || t.Val.Format(time.RFC3339) == "0001-01-01T00:00:00Z") { t.Valid = false } return nil } func (t SqlTimeStamp) Value() (driver.Value, error) { if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0o002, 1, 1, 0, 0, 0, 0, time.UTC)) { return nil, nil } return t.Val.Format(time.RFC3339), nil } func (t SqlTimeStamp) MarshalYAML() (any, error) { if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0o002, 1, 1, 0, 0, 0, 0, time.UTC)) { return nil, nil } return t.Val.Format(time.RFC3339), nil } func (t *SqlTimeStamp) UnmarshalYAML(value *yaml.Node) error { if err := t.SqlNull.UnmarshalYAML(value); err != nil { return err } if t.Valid && (t.Val.IsZero() || t.Val.Format(time.RFC3339) == "0001-01-01T00:00:00Z") { t.Valid = false } return nil } func (t SqlTimeStamp) MarshalXML(e *xml.Encoder, start xml.StartElement) error { if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0o002, 1, 1, 0, 0, 0, 0, time.UTC)) { return e.EncodeElement("", start) } return e.EncodeElement(t.Val.Format(time.RFC3339), start) } func (t *SqlTimeStamp) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var s string if err := d.DecodeElement(&s, &start); err != nil { return err } if s == "" { t.Valid = false t.Val = time.Time{} return nil } tm, err := tryParseDT(s) if err != nil { return err } t.Val = tm t.Valid = !tm.IsZero() && tm.Format(time.RFC3339) != "0001-01-01T00:00:00Z" return nil } func SqlTimeStampNow() SqlTimeStamp { return SqlTimeStamp{SqlNull: SqlNull[time.Time]{Val: time.Now(), Valid: true}} } // SqlDate - Date only (YYYY-MM-DD). type SqlDate struct{ SqlNull[time.Time] } func (d SqlDate) MarshalJSON() ([]byte, error) { if !d.Valid || d.Val.IsZero() { return []byte("null"), nil } s := d.Val.Format("2006-01-02") if strings.HasPrefix(s, "0001-01-01") { return []byte("null"), nil } return fmt.Appendf(nil, `"%s"`, s), nil } func (d *SqlDate) UnmarshalJSON(b []byte) error { if err := d.SqlNull.UnmarshalJSON(b); err != nil { return err } if d.Valid && d.Val.Format("2006-01-02") <= "0001-01-01" { d.Valid = false } return nil } func (d SqlDate) Value() (driver.Value, error) { if !d.Valid || d.Val.IsZero() { return nil, nil } s := d.Val.Format("2006-01-02") if s <= "0001-01-01" { return nil, nil } return s, nil } func (d SqlDate) String() string { if !d.Valid { return "" } s := d.Val.Format("2006-01-02") if strings.HasPrefix(s, "0001-01-01") || strings.HasPrefix(s, "1800-12-31") { return "" } return s } func (d SqlDate) MarshalYAML() (any, error) { if !d.Valid || d.Val.IsZero() { return nil, nil } s := d.Val.Format("2006-01-02") if strings.HasPrefix(s, "0001-01-01") { return nil, nil } return s, nil } func (d *SqlDate) UnmarshalYAML(value *yaml.Node) error { if err := d.SqlNull.UnmarshalYAML(value); err != nil { return err } if d.Valid && d.Val.Format("2006-01-02") <= "0001-01-01" { d.Valid = false } return nil } func (d SqlDate) MarshalXML(e *xml.Encoder, start xml.StartElement) error { if !d.Valid || d.Val.IsZero() { return e.EncodeElement("", start) } s := d.Val.Format("2006-01-02") if strings.HasPrefix(s, "0001-01-01") { return e.EncodeElement("", start) } return e.EncodeElement(s, start) } func (d *SqlDate) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error { var s string if err := dec.DecodeElement(&s, &start); err != nil { return err } if s == "" { d.Valid = false d.Val = time.Time{} return nil } tm, err := tryParseDT(s) if err != nil { return err } d.Val = tm d.Valid = !tm.IsZero() && tm.Format("2006-01-02") > "0001-01-01" return nil } func SqlDateNow() SqlDate { return SqlDate{SqlNull: SqlNull[time.Time]{Val: time.Now(), Valid: true}} } // SqlTime - Time only (HH:MM:SS). type SqlTime struct{ SqlNull[time.Time] } func (t SqlTime) MarshalJSON() ([]byte, error) { if !t.Valid || t.Val.IsZero() { return []byte("null"), nil } s := t.Val.Format("15:04:05") if s == "00:00:00" { return []byte("null"), nil } return fmt.Appendf(nil, `"%s"`, s), nil } func (t *SqlTime) UnmarshalJSON(b []byte) error { if err := t.SqlNull.UnmarshalJSON(b); err != nil { return err } if t.Valid && t.Val.Format("15:04:05") == "00:00:00" { t.Valid = false } return nil } func (t SqlTime) Value() (driver.Value, error) { if !t.Valid || t.Val.IsZero() { return nil, nil } return t.Val.Format("15:04:05"), nil } func (t SqlTime) String() string { if !t.Valid { return "" } return t.Val.Format("15:04:05") } func (t SqlTime) MarshalYAML() (any, error) { if !t.Valid || t.Val.IsZero() { return nil, nil } s := t.Val.Format("15:04:05") if s == "00:00:00" { return nil, nil } return s, nil } func (t *SqlTime) UnmarshalYAML(value *yaml.Node) error { if err := t.SqlNull.UnmarshalYAML(value); err != nil { return err } if t.Valid && t.Val.Format("15:04:05") == "00:00:00" { t.Valid = false } return nil } func (t SqlTime) MarshalXML(e *xml.Encoder, start xml.StartElement) error { if !t.Valid || t.Val.IsZero() { return e.EncodeElement("", start) } s := t.Val.Format("15:04:05") if s == "00:00:00" { return e.EncodeElement("", start) } return e.EncodeElement(s, start) } func (t *SqlTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var s string if err := d.DecodeElement(&s, &start); err != nil { return err } if s == "" { t.Valid = false t.Val = time.Time{} return nil } tm, err := tryParseDT(s) if err != nil { return err } t.Val = tm t.Valid = !tm.IsZero() && tm.Format("15:04:05") != "00:00:00" return nil } func SqlTimeNow() SqlTime { return SqlTime{SqlNull: SqlNull[time.Time]{Val: time.Now(), Valid: true}} } // SqlJSONB - Nullable JSONB as []byte. type SqlJSONB []byte // SqlJSON - Nullable JSON as []byte. PostgreSQL's json and jsonb types share // the same textual representation and Go marshalling behavior, differing only // in server-side storage, so SqlJSON is an alias of SqlJSONB. type SqlJSON = SqlJSONB // Scan implements sql.Scanner. func (n *SqlJSONB) Scan(value any) error { if value == nil { *n = nil return nil } switch v := value.(type) { case string: *n = []byte(v) case []byte: *n = v default: dat, err := json.Marshal(value) if err != nil { return fmt.Errorf("failed to marshal value to JSON: %v", err) } *n = dat } return nil } // Value implements driver.Valuer. func (n SqlJSONB) Value() (driver.Value, error) { if len(n) == 0 { return nil, nil } var js any if err := json.Unmarshal(n, &js); err != nil { return nil, fmt.Errorf("invalid JSON: %v", err) } return string(n), nil } // MarshalJSON implements json.Marshaler. func (n SqlJSONB) MarshalJSON() ([]byte, error) { if len(n) == 0 { return []byte("null"), nil } var obj any if err := json.Unmarshal(n, &obj); err != nil { return []byte("null"), nil } return n, nil } // UnmarshalJSON implements json.Unmarshaler. func (n *SqlJSONB) UnmarshalJSON(b []byte) error { s := strings.TrimSpace(string(b)) if s == "null" || s == "" || (!strings.HasPrefix(s, "{") && !strings.HasPrefix(s, "[")) { *n = nil return nil } *n = b return nil } // MarshalYAML implements yaml.Marshaler. The underlying JSON is decoded into // a generic value first so it renders as native YAML mappings/sequences // rather than an embedded JSON string. func (n SqlJSONB) MarshalYAML() (any, error) { if len(n) == 0 { return nil, nil } var v any if err := json.Unmarshal(n, &v); err != nil { return nil, nil } return v, nil } // UnmarshalYAML implements yaml.Unmarshaler. func (n *SqlJSONB) UnmarshalYAML(value *yaml.Node) error { if value == nil || value.Tag == "!!null" { *n = nil return nil } var v any if err := value.Decode(&v); err != nil { return err } b, err := json.Marshal(v) if err != nil { return err } *n = b return nil } // MarshalXML implements xml.Marshaler. JSON has no clean structural mapping // to XML, so the raw JSON text is emitted as the element's text content. func (n SqlJSONB) MarshalXML(e *xml.Encoder, start xml.StartElement) error { if len(n) == 0 { return e.EncodeElement("", start) } var obj any if err := json.Unmarshal(n, &obj); err != nil { return e.EncodeElement("", start) } return e.EncodeElement(string(n), start) } // UnmarshalXML implements xml.Unmarshaler, reading back the raw JSON text // written by MarshalXML. func (n *SqlJSONB) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var s string if err := d.DecodeElement(&s, &start); err != nil { return err } s = strings.TrimSpace(s) if s == "" { *n = nil return nil } *n = []byte(s) return nil } func (n SqlJSONB) AsMap() (map[string]any, error) { if len(n) == 0 { return nil, nil } js := make(map[string]any) if err := json.Unmarshal(n, &js); err != nil { return nil, fmt.Errorf("invalid JSON: %v", err) } return js, nil } func (n SqlJSONB) AsSlice() ([]any, error) { if len(n) == 0 { return nil, nil } js := make([]any, 0) if err := json.Unmarshal(n, &js); err != nil { return nil, fmt.Errorf("invalid JSON: %v", err) } return js, nil } // TryIfInt64 tries to parse any value to int64 with default. func TryIfInt64(v any, def int64) int64 { switch val := v.(type) { case string: i, err := strconv.ParseInt(val, 10, 64) if err != nil { return def } return i case int: return int64(val) case int8: return int64(val) case int16: return int64(val) case int32: return int64(val) case int64: return val case uint: return int64(val) case uint8: return int64(val) case uint16: return int64(val) case uint32: return int64(val) case uint64: return int64(val) case float32: return int64(val) case float64: return int64(val) case []byte: i, err := strconv.ParseInt(string(val), 10, 64) if err != nil { return def } return i default: return def } } // Constructor helpers - clean and fast value creation func Null[T any](v T, valid bool) SqlNull[T] { return SqlNull[T]{Val: v, Valid: valid} } func NewSql[T any](value any) SqlNull[T] { n := SqlNull[T]{} if value == nil { return n } // Fast path: exact match if v, ok := value.(T); ok { n.Val = v n.Valid = true return n } // Try from another SqlNull if sn, ok := value.(SqlNull[T]); ok { return sn } // Convert via string _ = n.FromString(fmt.Sprintf("%v", value)) return n } func NewSqlInt16(v int16) SqlInt16 { return SqlInt16{Val: v, Valid: true} } func NewSqlInt32(v int32) SqlInt32 { return SqlInt32{Val: v, Valid: true} } func NewSqlInt64(v int64) SqlInt64 { return SqlInt64{Val: v, Valid: true} } func NewSqlFloat32(v float32) SqlFloat32 { return SqlFloat32{Val: v, Valid: true} } func NewSqlFloat64(v float64) SqlFloat64 { return SqlFloat64{Val: v, Valid: true} } func NewSqlBool(v bool) SqlBool { return SqlBool{Val: v, Valid: true} } func NewSqlString(v string) SqlString { return SqlString{Val: v, Valid: true} } func NewSqlByteArray(v []byte) SqlByteArray { return SqlByteArray{Val: v, Valid: true} } func NewSqlUUID(v uuid.UUID) SqlUUID { return SqlUUID{Val: v, Valid: true} } func NewSqlTimeStamp(v time.Time) SqlTimeStamp { return SqlTimeStamp{SqlNull: SqlNull[time.Time]{Val: v, Valid: true}} } func NewSqlDate(v time.Time) SqlDate { return SqlDate{SqlNull: SqlNull[time.Time]{Val: v, Valid: true}} } func NewSqlTime(v time.Time) SqlTime { return SqlTime{SqlNull: SqlNull[time.Time]{Val: v, Valid: true}} }