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
This commit is contained in:
@@ -556,7 +556,7 @@ func TestWriter_FieldNameCollision(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTypeMapper_SQLTypeToGoType_Bun(t *testing.T) {
|
||||
mapper := NewTypeMapper("")
|
||||
mapper := NewTypeMapper("", "")
|
||||
|
||||
tests := []struct {
|
||||
sqlType string
|
||||
@@ -701,7 +701,7 @@ func TestWriter_StringPrimaryKeyHelpers_Bun(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTypeMapper_BuildBunTag(t *testing.T) {
|
||||
mapper := NewTypeMapper("")
|
||||
mapper := NewTypeMapper("", "")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -827,65 +827,78 @@ func TestTypeMapper_BuildBunTag(t *testing.T) {
|
||||
t.Errorf("BuildBunTag() = %q, missing %q", result, part)
|
||||
}
|
||||
}
|
||||
// baselib mode must NOT add "array" — the Go type is already a
|
||||
// real slice ([]string, []int32, ...), which bun's pgdialect
|
||||
// scans natively without the explicit "array" tag option.
|
||||
if strings.Contains(result, ",array,") || strings.HasSuffix(result, ",array,") {
|
||||
t.Errorf("BuildBunTag() = %q, must not contain 'array' in baselib mode", result)
|
||||
// Array columns always carry the "array" tag, telling bun's
|
||||
// pgdialect to scan/append the native Go slice as a PostgreSQL array.
|
||||
if strings.HasSuffix(tt.column.Type, "[]") && !strings.Contains(result, ",array,") {
|
||||
t.Errorf("BuildBunTag() = %q, expected 'array' tag", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTypeMapper_BuildBunTag_SqlTypesArrayUsesInternalTypeName verifies that
|
||||
// array columns in sqltypes mode never produce a "[]"-suffixed "type:" tag.
|
||||
// bun's pgdialect unconditionally overrides Field.Scan/Append with its own
|
||||
// (slice-only) array handling whenever the tag's "type:" value ends in "[]",
|
||||
// which clobbers the sql.Scanner/driver.Valuer implemented on the
|
||||
// SqlXxxArray wrapper types and produces
|
||||
// "bun: Scan(unsupported sqltypes.SqlXxxArray)" at query time.
|
||||
func TestTypeMapper_BuildBunTag_SqlTypesArrayUsesInternalTypeName(t *testing.T) {
|
||||
mapper := NewTypeMapper(writers.NullableTypeSqlTypes)
|
||||
// TestTypeMapper_BuildBunTag_ArraysAreNativeInEveryMode verifies that array
|
||||
// columns always use a plain "text[]"-style type and the native Go slice
|
||||
// type plus an explicit "array" tag, regardless of NullableTypes style
|
||||
// (sqltypes/stdlib/baselib). bun's pgdialect scans/appends native slices
|
||||
// directly; the SqlXxxArray wrapper types are never used for array columns.
|
||||
func TestTypeMapper_BuildBunTag_ArraysAreNativeInEveryMode(t *testing.T) {
|
||||
for _, style := range []string{writers.NullableTypeSqlTypes, writers.NullableTypeStdlib, writers.NullableTypeBaselib} {
|
||||
t.Run(style, func(t *testing.T) {
|
||||
mapper := NewTypeMapper(style, "")
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
column *models.Column
|
||||
wantSubstr string
|
||||
}{
|
||||
{name: "text array", column: &models.Column{Name: "tags", Type: "text[]"}, wantSubstr: "type:_text,"},
|
||||
{name: "varchar array", column: &models.Column{Name: "labels", Type: "varchar[]"}, wantSubstr: "type:_varchar,"},
|
||||
{name: "integer array", column: &models.Column{Name: "scores", Type: "integer[]", NotNull: true}, wantSubstr: "type:_int4,"},
|
||||
{name: "boolean array", column: &models.Column{Name: "flags", Type: "boolean[]"}, wantSubstr: "type:_bool,"},
|
||||
{name: "uuid array", column: &models.Column{Name: "ids", Type: "uuid[]"}, wantSubstr: "type:_uuid,"},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := mapper.BuildBunTag(tt.column, nil)
|
||||
if !strings.Contains(result, tt.wantSubstr) {
|
||||
t.Errorf("BuildBunTag() = %q, missing %q", result, tt.wantSubstr)
|
||||
cases := []struct {
|
||||
name string
|
||||
column *models.Column
|
||||
wantSubstr string
|
||||
}{
|
||||
{name: "text array", column: &models.Column{Name: "tags", Type: "text[]"}, wantSubstr: "type:text[],array,"},
|
||||
{name: "varchar array", column: &models.Column{Name: "labels", Type: "varchar[]"}, wantSubstr: "type:varchar[],array,"},
|
||||
{name: "integer array", column: &models.Column{Name: "scores", Type: "integer[]", NotNull: true}, wantSubstr: "type:integer[],array,"},
|
||||
{name: "boolean array", column: &models.Column{Name: "flags", Type: "boolean[]"}, wantSubstr: "type:boolean[],array,"},
|
||||
{name: "uuid array", column: &models.Column{Name: "ids", Type: "uuid[]"}, wantSubstr: "type:uuid[],array,"},
|
||||
}
|
||||
if strings.Contains(result, "[]") {
|
||||
t.Errorf("BuildBunTag() = %q, must not use a \"[]\"-suffixed type in sqltypes mode", result)
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := mapper.BuildBunTag(tt.column, nil)
|
||||
if !strings.Contains(result, tt.wantSubstr) {
|
||||
t.Errorf("BuildBunTag() = %q, missing %q", result, tt.wantSubstr)
|
||||
}
|
||||
goType := mapper.SQLTypeToGoType(tt.column.Type, tt.column.NotNull)
|
||||
if strings.Contains(goType, "sql_types") {
|
||||
t.Errorf("SQLTypeToGoType() = %q, array columns must use a native Go slice, not an sql_types wrapper", goType)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeMapper_BuildBunTag_StdlibArrayHasArrayTag(t *testing.T) {
|
||||
mapper := NewTypeMapper(writers.NullableTypeStdlib)
|
||||
|
||||
// TestTypeMapper_SQLTypeToGoType_ArrayNullable verifies that nullable array
|
||||
// columns become a pointer-to-slice when NullableArrays is
|
||||
// "pointer_slice", so callers can distinguish SQL NULL (nil pointer) from
|
||||
// '{}' (pointer to an empty slice); NOT NULL columns are unaffected.
|
||||
func TestTypeMapper_SQLTypeToGoType_ArrayNullable(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
column *models.Column
|
||||
name string
|
||||
typeStyle string
|
||||
arrayNullable string
|
||||
sqlType string
|
||||
notNull bool
|
||||
want string
|
||||
}{
|
||||
{name: "text array", column: &models.Column{Name: "tags", Type: "text[]"}},
|
||||
{name: "integer array", column: &models.Column{Name: "scores", Type: "integer[]", NotNull: true}},
|
||||
{name: "baselib nullable slice (default)", typeStyle: writers.NullableTypeBaselib, arrayNullable: "", sqlType: "text[]", notNull: false, want: "[]string"},
|
||||
{name: "baselib nullable pointer_slice", typeStyle: writers.NullableTypeBaselib, arrayNullable: writers.NullableArraysPointerSlice, sqlType: "text[]", notNull: false, want: "*[]string"},
|
||||
{name: "baselib not null pointer_slice unaffected", typeStyle: writers.NullableTypeBaselib, arrayNullable: writers.NullableArraysPointerSlice, sqlType: "text[]", notNull: true, want: "[]string"},
|
||||
{name: "stdlib nullable pointer_slice", typeStyle: writers.NullableTypeStdlib, arrayNullable: writers.NullableArraysPointerSlice, sqlType: "integer[]", notNull: false, want: "*[]int32"},
|
||||
{name: "sqltypes nullable pointer_slice (arrays are always native)", typeStyle: writers.NullableTypeSqlTypes, arrayNullable: writers.NullableArraysPointerSlice, sqlType: "text[]", notNull: false, want: "*[]string"},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := mapper.BuildBunTag(tt.column, nil)
|
||||
if !strings.Contains(result, "array") {
|
||||
t.Errorf("BuildBunTag() = %q, expected 'array' in stdlib mode", result)
|
||||
mapper := NewTypeMapper(tt.typeStyle, tt.arrayNullable)
|
||||
got := mapper.SQLTypeToGoType(tt.sqlType, tt.notNull)
|
||||
if got != tt.want {
|
||||
t.Errorf("SQLTypeToGoType(%q, %v) = %q, want %q", tt.sqlType, tt.notNull, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1052,7 +1065,7 @@ func TestExtraFields_InMultiFile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTypeMapper_BuildBunTag_PreservesExplicitTypeModifiers(t *testing.T) {
|
||||
mapper := NewTypeMapper("")
|
||||
mapper := NewTypeMapper("", "")
|
||||
|
||||
col := &models.Column{
|
||||
Name: "embedding",
|
||||
|
||||
Reference in New Issue
Block a user