Files
relspecgo/pkg/writers/bun/array_integration_test.go
T
Hein 5d9ff5df03 feat(bun): generate native Go array slices for PostgreSQL array columns
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
2026-07-21 12:41:38 +02:00

142 lines
4.5 KiB
Go

package bun_test
import (
"context"
"database/sql"
"os"
"testing"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
)
// TestBunArrayColumns_NativeSlice verifies against a live PostgreSQL
// database (issue #13) that RelSpec's generated Bun array tags round-trip
// correctly through Bun's own pgdialect for both NOT NULL and nullable
// columns: NULL, '{}', and populated arrays must all scan and insert
// without a "bun: Scan(unsupported ...)" error.
//
// Requires the RELSPEC_TEST_PG_CONN environment variable, e.g.:
//
// RELSPEC_TEST_PG_CONN="postgres://postgres:postgres@localhost:5432/relspec_test?sslmode=disable"
func TestBunArrayColumns_NativeSlice(t *testing.T) {
connStr := os.Getenv("RELSPEC_TEST_PG_CONN")
if connStr == "" {
t.Skip("Skipping Bun array integration test: RELSPEC_TEST_PG_CONN environment variable not set")
}
sqldb, err := sql.Open("pgx", connStr)
if err != nil {
t.Fatalf("open connection: %v", err)
}
defer sqldb.Close()
db := bun.NewDB(sqldb, pgdialect.New())
ctx := context.Background()
type notNullArrayRow struct {
bun.BaseModel `bun:"table:relspec_test_arr_notnull"`
ID int64 `bun:"id,pk,autoincrement"`
Tags []string `bun:"tags,type:text[],notnull"`
}
type nullableArrayRow struct {
bun.BaseModel `bun:"table:relspec_test_arr_nullable"`
ID int64 `bun:"id,pk,autoincrement"`
Tags *[]string `bun:"tags,type:text[]"`
}
if _, err := db.NewDropTable().Model((*notNullArrayRow)(nil)).IfExists().Exec(ctx); err != nil {
t.Fatalf("drop table: %v", err)
}
if _, err := db.NewDropTable().Model((*nullableArrayRow)(nil)).IfExists().Exec(ctx); err != nil {
t.Fatalf("drop table: %v", err)
}
if _, err := db.NewCreateTable().Model((*notNullArrayRow)(nil)).Exec(ctx); err != nil {
t.Fatalf("create not-null table: %v", err)
}
if _, err := db.NewCreateTable().Model((*nullableArrayRow)(nil)).Exec(ctx); err != nil {
t.Fatalf("create nullable table: %v", err)
}
t.Cleanup(func() {
db.NewDropTable().Model((*notNullArrayRow)(nil)).IfExists().Exec(ctx)
db.NewDropTable().Model((*nullableArrayRow)(nil)).IfExists().Exec(ctx)
})
t.Run("not null populated array", func(t *testing.T) {
row := &notNullArrayRow{Tags: []string{"a", "b", "c"}}
if _, err := db.NewInsert().Model(row).Exec(ctx); err != nil {
t.Fatalf("insert: %v", err)
}
var out notNullArrayRow
if err := db.NewSelect().Model(&out).Where("id = ?", row.ID).Scan(ctx); err != nil {
t.Fatalf("select: %v", err)
}
if len(out.Tags) != 3 || out.Tags[0] != "a" || out.Tags[2] != "c" {
t.Errorf("Tags = %v, want [a b c]", out.Tags)
}
})
t.Run("not null empty array", func(t *testing.T) {
row := &notNullArrayRow{Tags: []string{}}
if _, err := db.NewInsert().Model(row).Exec(ctx); err != nil {
t.Fatalf("insert: %v", err)
}
var out notNullArrayRow
if err := db.NewSelect().Model(&out).Where("id = ?", row.ID).Scan(ctx); err != nil {
t.Fatalf("select: %v", err)
}
if len(out.Tags) != 0 {
t.Errorf("Tags = %v, want empty", out.Tags)
}
})
t.Run("nullable NULL", func(t *testing.T) {
row := &nullableArrayRow{Tags: nil}
if _, err := db.NewInsert().Model(row).Exec(ctx); err != nil {
t.Fatalf("insert: %v", err)
}
var out nullableArrayRow
if err := db.NewSelect().Model(&out).Where("id = ?", row.ID).Scan(ctx); err != nil {
t.Fatalf("select: %v", err)
}
if out.Tags != nil {
t.Errorf("Tags = %v, want nil (SQL NULL)", *out.Tags)
}
})
t.Run("nullable empty array", func(t *testing.T) {
empty := []string{}
row := &nullableArrayRow{Tags: &empty}
if _, err := db.NewInsert().Model(row).Exec(ctx); err != nil {
t.Fatalf("insert: %v", err)
}
var out nullableArrayRow
if err := db.NewSelect().Model(&out).Where("id = ?", row.ID).Scan(ctx); err != nil {
t.Fatalf("select: %v", err)
}
if out.Tags == nil {
t.Fatalf("Tags = nil, want non-nil pointer to empty slice")
}
if len(*out.Tags) != 0 {
t.Errorf("Tags = %v, want empty slice", *out.Tags)
}
})
t.Run("nullable populated array", func(t *testing.T) {
tags := []string{"x", "y"}
row := &nullableArrayRow{Tags: &tags}
if _, err := db.NewInsert().Model(row).Exec(ctx); err != nil {
t.Fatalf("insert: %v", err)
}
var out nullableArrayRow
if err := db.NewSelect().Model(&out).Where("id = ?", row.ID).Scan(ctx); err != nil {
t.Fatalf("select: %v", err)
}
if out.Tags == nil || len(*out.Tags) != 2 || (*out.Tags)[0] != "x" {
t.Errorf("Tags = %v, want [x y]", out.Tags)
}
})
}