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.