Files
relspecgo/pkg/sqltypes

sqltypes

Nullable SQL types for hand-written or generated Go models. Each type wraps a value with a Valid flag and implements database/sql.Scanner, driver.Valuer, encoding/json, gopkg.in/yaml.v3, and encoding/xml marshalling — so a single struct field can be scanned from a database row, round-tripped through JSON/YAML/XML, and written back to the database without any per-format glue code.

This package is what the bun and gorm writers emit when generating models with --types sqltypes (see pkg/writers/bun and pkg/writers/gorm). It can also be imported directly in hand-written models.

Import

import sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"

Scalar types

All scalar types are instantiations of the generic SqlNull[T]:

Type Underlying Typical SQL type
SqlInt16 int16 smallint
SqlInt32 int32 integer
SqlInt64 int64 bigint
SqlFloat32 float32 real, float4
SqlFloat64 float64 double precision, numeric, decimal, money
SqlBool bool boolean
SqlString string text, varchar, char, citext, inet, cidr, macaddr
SqlByteArray []byte bytea (base64-encoded in JSON/YAML/XML)
SqlUUID uuid.UUID (github.com/google/uuid) uuid

You can also instantiate SqlNull[T] directly for any type not covered above, e.g. SqlNull[MyEnum].

Date/time types

Plain time.Time doesn't distinguish date-only, time-only, and timestamp semantics, and its zero value marshals to a confusing 0001-01-01T00:00:00Z. These wrapper types fix both problems:

Type Format Notes
SqlTimeStamp 2006-01-02T15:04:05 Full timestamp
SqlDate 2006-01-02 Date only
SqlTime 15:04:05 Time only

Zero/pre-epoch values (time.Time{} or anything before 0002-01-01) marshal to null and Value() returns nil, instead of leaking Go's zero-time sentinel into the database or API responses.

JSON types

Type Underlying Notes
SqlJSONB []byte Raw JSON bytes; MarshalYAML decodes to native YAML mappings/sequences instead of an embedded JSON string
SqlJSON = SqlJSONB Alias — PostgreSQL's json and jsonb share the same Go representation

SqlJSONB has AsMap() / AsSlice() helpers for pulling out map[string]any / []any without a separate json.Unmarshal call.

Vector type (pgvector)

SqlVector wraps []float32 for the vector column type (pgvector), scanning/writing the [1,2,3] literal format pgvector uses over the wire.

Array types

PostgreSQL array columns (text[], integer[], …) map to SqlXxxArray types, each wrapping Val []T + Valid bool and handling PostgreSQL's {a,b,c} array literal format on Scan/Value:

SqlStringArray, SqlInt16Array, SqlInt32Array, SqlInt64Array, SqlFloat32Array, SqlFloat64Array, SqlBoolArray, SqlUUIDArray.

Constructing values

Every type has a NewSqlXxx(v) constructor that sets Valid: true:

name := sql_types.NewSqlString("Ada Lovelace")
age  := sql_types.NewSqlInt32(36)
tags := sql_types.NewSqlStringArray([]string{"engineer", "mathematician"})

The zero value of any type (sql_types.SqlString{}) is null/invalid — use it directly for a NULL field instead of a separate constructor.

Generic helpers:

sql_types.Null(v, valid)      // SqlNull[T]{Val: v, Valid: valid}
sql_types.NewSql[T](anyValue) // best-effort conversion from any Go value

Reading values back

Each scalar type has typed accessors that return the zero value instead of panicking when Valid is false:

n.Int64()   // SqlInt16/32/64, SqlFloat32/64, SqlBool, SqlString → int64
n.Float64() // → float64
n.Bool()    // → bool
n.Time()    // SqlNull[time.Time]-based types → time.Time
n.UUID()    // SqlUUID → uuid.UUID
n.String()  // fmt.Stringer — empty string when invalid

Example

type User struct {
    ID        sql_types.SqlUUID        `json:"id"`
    Name      sql_types.SqlString      `json:"name"`
    Tags      sql_types.SqlStringArray `json:"tags"`
    Metadata  sql_types.SqlJSONB       `json:"metadata"`
    CreatedAt sql_types.SqlTimeStamp   `json:"created_at"`
}

u := User{
    ID:        sql_types.NewSqlUUID(uuid.New()),
    Name:      sql_types.NewSqlString("Ada Lovelace"),
    Tags:      sql_types.NewSqlStringArray([]string{"engineer"}),
    CreatedAt: sql_types.SqlTimeStampNow(),
}
// Metadata left as the zero value → serializes as null, scans as NULL.

Every type implements sql.Scanner and driver.Valuer, so these fields can be used directly as struct fields with database/sql, bun, or gorm without additional tags or hooks.