137 lines
4.8 KiB
Markdown
137 lines
4.8 KiB
Markdown
# gosqltypes
|
|
|
|
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 the `bun` and
|
|
`gorm` writers in [relspecgo](https://git.warky.dev/wdevs/relspecgo)). It can also be imported
|
|
directly in hand-written models.
|
|
|
|
## Import
|
|
|
|
```go
|
|
import sql_types "git.warky.dev/wdevs/gosqltypes"
|
|
```
|
|
|
|
## 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](https://github.com/pgvector/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`:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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.
|