982 lines
22 KiB
Go
982 lines
22 KiB
Go
// 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 with custom formatting (YYYY-MM-DDTHH:MM:SS).
|
|
type SqlTimeStamp struct{ SqlNull[time.Time] }
|
|
|
|
func (t SqlTimeStamp) MarshalJSON() ([]byte, error) {
|
|
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
|
return []byte("null"), nil
|
|
}
|
|
return fmt.Appendf(nil, `"%s"`, t.Val.Format("2006-01-02T15:04:05")), 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("2006-01-02T15:04:05") == "0001-01-01T00:00:00") {
|
|
t.Valid = false
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t SqlTimeStamp) Value() (driver.Value, error) {
|
|
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
|
return nil, nil
|
|
}
|
|
return t.Val.Format("2006-01-02T15:04:05"), nil
|
|
}
|
|
|
|
func (t SqlTimeStamp) MarshalYAML() (any, error) {
|
|
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
|
return nil, nil
|
|
}
|
|
return t.Val.Format("2006-01-02T15:04:05"), 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("2006-01-02T15:04:05") == "0001-01-01T00:00:00") {
|
|
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(0002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
|
return e.EncodeElement("", start)
|
|
}
|
|
return e.EncodeElement(t.Val.Format("2006-01-02T15:04:05"), 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("2006-01-02T15:04:05") != "0001-01-01T00:00:00"
|
|
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}}
|
|
}
|