# 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`](../../sqltypes/README.md) package.

## Features

- Generates Bun-compatible Go structs
- Creates proper `bun` struct 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

```go
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

```bash
# 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`)

```go
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`

```go
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 alias
- `column` - Column name (auto-derived if not specified)
- `pk` - Primary key
- `autoincrement` - Auto-increment
- `notnull` - NOT NULL constraint
- `unique` - Unique constraint
- `default` - Default value
- `rel` - Relationship definition
- `type` - 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](#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`:

```go
// 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`:

```go
// 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

```go
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. |

```go
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`:

```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 sqltypes` to use `sql_types.SqlString`, `sql_types.SqlTimeStamp`, etc., or `--types stdlib` to use `sql.NullString`, `sql.NullTime`, etc.
- Array columns always use plain Go slices (`[]string`, `[]int32`, …) with an explicit `array` bun tag, regardless of `--types`; pass `--array-nullable pointer_slice` to use `*[]string` etc. 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
