5d9ff5df03
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
142 lines
4.5 KiB
Go
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 := ¬NullArrayRow{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 := ¬NullArrayRow{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)
|
|
}
|
|
})
|
|
}
|