Bun's pgdialect scans/appends native slices directly, so array columns
(text[], integer[], uuid[], ...) always generate as plain []string,
[]int32, etc. with an explicit "array" bun tag, regardless of --types
(sqltypes/stdlib/baselib). The SqlXxxArray wrapper types are no longer
used for Bun array columns (gorm is unaffected and keeps using them).
Adds --array-nullable pointer_slice to represent nullable array columns
as *[]T instead of []T, so callers can distinguish SQL NULL (nil) from
'{}' (pointer to an empty slice). Verified end-to-end against a live
PostgreSQL instance for NULL/{}/populated arrays in every --types mode.
Closes #13
Bun Writer
Generates Go source files with Bun model definitions from database schema information.
Overview
The Bun Writer converts RelSpec's internal database model representation into Go source code with Bun struct definitions, complete with proper tags, relationships, and table configuration.
With --types sqltypes, nullable fields use the pkg/sqltypes package.
Features
- Generates Bun-compatible Go structs
- Creates proper
bunstruct tags - Adds relationship fields
- Supports both single-file and multi-file output
- Maps SQL types to Go types
- Handles nullable fields with sql.Null* types
- Generates table aliases
Usage
Basic Example
package main
import (
"git.warky.dev/wdevs/relspecgo/pkg/models"
"git.warky.dev/wdevs/relspecgo/pkg/writers"
"git.warky.dev/wdevs/relspecgo/pkg/writers/bun"
)
func main() {
options := &writers.WriterOptions{
OutputPath: "models.go",
PackageName: "models",
}
writer := bun.NewWriter(options)
err := writer.WriteDatabase(db)
if err != nil {
panic(err)
}
}
CLI Examples
# Generate Bun models from a DBML schema (default: baselib pointer types)
relspec convert --from dbml --from-path schema.dbml \
--to bun --to-path models.go --package models
# Use standard library database/sql nullable types instead
relspec convert --from dbml --from-path schema.dbml \
--to bun --to-path models.go --package models \
--types stdlib
# Select sqltypes package types (git.warky.dev/wdevs/relspecgo/pkg/sqltypes)
relspec convert --from pgsql --from-conn "postgres://localhost/mydb" \
--to bun --to-path models.go --package models \
--types sqltypes
# Multi-file output (one file per table)
relspec convert --from json --from-path schema.json \
--to bun --to-path models/ --package models
# Inject computed/scan-only fields that are not present in the database schema
# (extra-fields.json contains the JSON array shown below)
relspec convert --from dbml --from-path schema.dbml \
--to bun --to-path models.go --package models \
--extra-fields extra-fields.json
Generated Code Examples
sqltypes package types (--types sqltypes)
package models
import (
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type User struct {
bun.BaseModel `bun:"table:users,alias:u"`
ID int64 `bun:"id,type:uuid,pk," json:"id"`
Username string `bun:"username,type:text,notnull," json:"username"`
Email sql_types.SqlString `bun:"email,type:text,nullzero," json:"email"`
Tags []string `bun:"tags,type:text[],array,default:'{}',notnull," json:"tags"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
}
Standard library — --types stdlib
package models
import (
"database/sql"
"time"
"github.com/uptrace/bun"
)
type User struct {
bun.BaseModel `bun:"table:users,alias:u"`
ID string `bun:"id,type:uuid,pk," json:"id"`
Username string `bun:"username,type:text,notnull," json:"username"`
Email sql.NullString `bun:"email,type:text,nullzero," json:"email"`
Tags []string `bun:"tags,type:text[],array,default:'{}',notnull," json:"tags"`
CreatedAt time.Time `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
}
Supported Bun Tags
table- Table name and aliascolumn- Column name (auto-derived if not specified)pk- Primary keyautoincrement- Auto-incrementnotnull- NOT NULL constraintunique- Unique constraintdefault- Default valuerel- Relationship definitiontype- Explicit SQL type
Type Mapping
The nullable type package is selected with --types (or WriterOptions.NullableTypes).
| SQL Type | NOT NULL (both) | Nullable — sqltypes | Nullable — stdlib |
|---|---|---|---|
bigint |
int64 |
SqlInt64 |
sql.NullInt64 |
integer |
int32 |
SqlInt32 |
sql.NullInt32 |
smallint |
int16 |
SqlInt16 |
sql.NullInt16 |
text, varchar |
string |
SqlString |
sql.NullString |
boolean |
bool |
SqlBool |
sql.NullBool |
timestamp, timestamptz |
time.Time* |
SqlTimeStamp |
sql.NullTime |
numeric, decimal |
float64 |
SqlFloat64 |
sql.NullFloat64 |
uuid |
string |
SqlUUID |
sql.NullString |
jsonb |
string |
SqlJSONB |
sql.NullString |
text[] |
[]string† |
[]string† |
[]string† |
integer[] |
[]int32† |
[]int32† |
[]int32† |
uuid[] |
[]string† |
[]string† |
[]string† |
vector |
SqlVector |
SqlVector |
[]float32 |
† Array columns always use a plain native Go slice with an explicit array
bun tag (bun:"tags,type:text[],array,..."), in every --types mode — the
SqlXxxArray wrapper types are never used, since bun's pgdialect scans/appends
native slices directly. Regardless of NOT NULL, unless --array-nullable pointer_slice is set — see NullableArrays below.
* In sqltypes mode, NOT NULL timestamps use SqlTimeStamp (not time.Time) unless the base type is a simple integer or boolean. In stdlib mode, NOT NULL timestamps use time.Time.
Writer Options
NullableTypes
Controls which Go package is used for nullable column types. Set via the --types CLI flag or WriterOptions.NullableTypes:
// Use sqltypes package types
options := &writers.WriterOptions{
OutputPath: "models.go",
PackageName: "models",
NullableTypes: writers.NullableTypeSqlTypes,
}
// Use standard library database/sql types
options := &writers.WriterOptions{
OutputPath: "models.go",
PackageName: "models",
NullableTypes: writers.NullableTypeStdlib,
}
NullableArrays
Controls how nullable PostgreSQL array columns are represented in stdlib/baselib
--types mode (no effect in sqltypes mode, which always uses the SqlXxxArray
wrapper types). Set via the --array-nullable CLI flag or WriterOptions.NullableArrays:
// Default: every array column is a plain slice, e.g. []string.
// SQL NULL and '{}' both scan into a nil/zero-length slice, so callers
// cannot distinguish them.
options := &writers.WriterOptions{
NullableArrays: writers.NullableArraysSlice,
}
// Nullable array columns become a pointer to a slice, e.g. *[]string.
// A nil pointer means SQL NULL; a non-nil pointer to an empty slice
// means '{}'. NOT NULL array columns are unaffected and stay plain slices.
options := &writers.WriterOptions{
NullableArrays: writers.NullableArraysPointerSlice,
}
tags []string `bun:"tags,type:text[],notnull,"` // NOT NULL, either mode
tags []string `bun:"tags,type:text[],nullzero,"` // nullable, default (slice)
tags *[]string `bun:"tags,type:text[],nullzero,"` // nullable, pointer_slice
Metadata Options
options := &writers.WriterOptions{
OutputPath: "models.go",
PackageName: "models",
Metadata: map[string]any{
"multi_file": true, // Enable multi-file mode
"populate_refs": true, // Populate RefDatabase/RefSchema
"generate_get_id_str": true, // Generate GetIDStr() methods
},
}
Extra fields
Use Metadata["extra_fields"] (or the CLI --extra-fields <path> flag) to inject
custom fields into generated Bun models without editing generated files. This is
intended for computed columns and scan-only query fields that Bun must scan into
but that are not real database table columns.
The CLI flag expects a path to a JSON file, not inline JSON. This avoids shell quoting problems with struct tags and complex type names.
Each entry supports:
| Field | Required | Description |
|---|---|---|
target_table |
No | Optional model scope. Accepts table name (projects), qualified table (public.projects), or model name (ModelPublicProjects). If omitted, the field is added to every generated model. |
name |
Yes | Go struct field name. |
type |
Yes | Go type to emit. |
bun_tag |
No | Raw Bun tag contents, e.g. thought_count,scanonly. |
json_tag |
No | Raw JSON tag contents. |
comment |
No | Optional line comment. |
options := &writers.WriterOptions{
OutputPath: "models.go",
PackageName: "models",
Metadata: map[string]any{
"extra_fields": `[
{
"target_table": "projects",
"name": "ThoughtCount",
"type": "sql_types.SqlInt64",
"bun_tag": "thought_count,scanonly",
"json_tag": "thought_count",
"comment": "Computed by ResolveSpec queries"
}
]`,
},
}
Example extra-fields.json:
[
{
"target_table": "projects",
"name": "ThoughtCount",
"type": "sql_types.SqlInt64",
"bun_tag": "thought_count,scanonly",
"json_tag": "thought_count",
"comment": "Computed by ResolveSpec queries"
}
]
Notes
- Model names are derived from table names (singularized, PascalCase)
- Table aliases are auto-generated from table names
- Nullable columns use plain Go pointer types (
*string,*time.Time, …) by default; pass--types sqltypesto usesql_types.SqlString,sql_types.SqlTimeStamp, etc., or--types stdlibto usesql.NullString,sql.NullTime, etc. - Array columns always use plain Go slices (
[]string,[]int32, …) with an explicitarraybun tag, regardless of--types; pass--array-nullable pointer_sliceto use*[]stringetc. for nullable array columns. - Multi-file mode: one file per table named
sql_{schema}_{table}.go - Generated code is auto-formatted
- JSON tags are automatically added