fix(bun): use PostgreSQL internal array type name for sqltypes array columns
bun's pgdialect overrides Field.Scan/Append with its own slice-only array handling whenever the tag's type: value ends in "[]", clobbering the sql.Scanner/driver.Valuer implemented on SqlXxxArray wrapper types and causing "bun: Scan(unsupported sqltypes.SqlStringArray)" at query time. Emit the underscore-prefixed internal type name (e.g. _text) instead, which is DDL-valid but doesn't end in "[]" so bun leaves our scanner alone.
This commit is contained in:
@@ -186,6 +186,40 @@ func (tm *TypeMapper) bunGoType(sqlType string) string {
|
||||
return tm.sqlTypesAlias + ".SqlString"
|
||||
}
|
||||
|
||||
// pgArrayInternalTypeName returns PostgreSQL's internal array type name
|
||||
// (e.g. "_text" for text[]) for the given canonical base element type.
|
||||
//
|
||||
// This is used instead of the "text[]" spelling in the sqltypes-style bun
|
||||
// tag: bun's pgdialect unconditionally overrides Field.Scan/Append with its
|
||||
// own array handling whenever the tag's "type:" value ends in "[]" (see
|
||||
// pgdialect.Dialect.onField), which clobbers the sql.Scanner/driver.Valuer
|
||||
// implemented on the SqlXxxArray wrapper types and causes
|
||||
// "bun: Scan(unsupported sqltypes.SqlXxxArray)" errors at query time. The
|
||||
// underscore-prefixed internal name is a real, DDL-valid PostgreSQL type
|
||||
// name that doesn't end in "[]", so it sidesteps the override.
|
||||
func (tm *TypeMapper) pgArrayInternalTypeName(baseElemType string) string {
|
||||
typeMap := map[string]string{
|
||||
"text": "_text", "varchar": "_varchar",
|
||||
"char": "_bpchar", "character": "_bpchar", "bpchar": "_bpchar",
|
||||
"citext": "_citext",
|
||||
"inet": "_inet", "cidr": "_cidr", "macaddr": "_macaddr",
|
||||
"json": "_json", "jsonb": "_jsonb",
|
||||
"integer": "_int4", "int": "_int4", "int4": "_int4", "serial": "_int4",
|
||||
"smallint": "_int2", "int2": "_int2", "smallserial": "_int2",
|
||||
"bigint": "_int8", "int8": "_int8", "bigserial": "_int8",
|
||||
"real": "_float4", "float4": "_float4",
|
||||
"double precision": "_float8", "float8": "_float8",
|
||||
"numeric": "_numeric", "decimal": "_numeric",
|
||||
"money": "_money",
|
||||
"boolean": "_bool", "bool": "_bool",
|
||||
"uuid": "_uuid",
|
||||
}
|
||||
if pgType, ok := typeMap[baseElemType]; ok {
|
||||
return pgType
|
||||
}
|
||||
return "_text"
|
||||
}
|
||||
|
||||
// arrayGoType returns the Go type for a PostgreSQL array column.
|
||||
// The baseElemType is the canonical base type (e.g. "text", "integer").
|
||||
func (tm *TypeMapper) arrayGoType(baseElemType string) string {
|
||||
@@ -360,6 +394,9 @@ func (tm *TypeMapper) BuildBunTag(column *models.Column, table *models.Table) st
|
||||
typeStr = fmt.Sprintf("%s(%d)", typeStr, column.Precision)
|
||||
}
|
||||
}
|
||||
if isArray && tm.typeStyle == writers.NullableTypeSqlTypes {
|
||||
typeStr = tm.pgArrayInternalTypeName(tm.extractBaseType(typeStr))
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("type:%s", typeStr))
|
||||
if isArray && tm.typeStyle == writers.NullableTypeStdlib {
|
||||
parts = append(parts, "array")
|
||||
|
||||
@@ -827,9 +827,45 @@ func TestTypeMapper_BuildBunTag(t *testing.T) {
|
||||
t.Errorf("BuildBunTag() = %q, missing %q", result, part)
|
||||
}
|
||||
}
|
||||
// sqltypes mode must NOT add "array" — SqlXxxArray uses sql.Scanner
|
||||
// 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 sqltypes mode", result)
|
||||
t.Errorf("BuildBunTag() = %q, must not contain 'array' in baselib mode", 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)
|
||||
|
||||
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)
|
||||
}
|
||||
if strings.Contains(result, "[]") {
|
||||
t.Errorf("BuildBunTag() = %q, must not use a \"[]\"-suffixed type in sqltypes mode", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user