refactor: ♻️ change resolvspec types to build in sqltypes
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
# 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`](../writers/bun/README.md) and
|
||||
[`pkg/writers/gorm`](../writers/gorm/README.md)). It can also be imported
|
||||
directly in hand-written models.
|
||||
|
||||
## Import
|
||||
|
||||
```go
|
||||
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](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.
|
||||
Reference in New Issue
Block a user