test(drawdb): add test for converting column types with modifiers
* Implement tests to ensure explicit type modifiers are preserved during conversion. * Validate behavior for varchar, numeric, and custom vector types.
This commit is contained in:
246
pkg/pgsql/types_registry.go
Normal file
246
pkg/pgsql/types_registry.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package pgsql
|
||||
|
||||
import "strings"
|
||||
|
||||
// TypeSpec describes PostgreSQL type capabilities used by parsers/writers.
|
||||
type TypeSpec struct {
|
||||
SupportsLength bool
|
||||
SupportsPrecision bool
|
||||
}
|
||||
|
||||
var postgresBaseTypes = map[string]TypeSpec{
|
||||
// Numeric types
|
||||
"smallint": {},
|
||||
"integer": {},
|
||||
"bigint": {},
|
||||
"decimal": {SupportsPrecision: true},
|
||||
"numeric": {SupportsPrecision: true},
|
||||
"real": {},
|
||||
"double precision": {},
|
||||
"smallserial": {},
|
||||
"serial": {},
|
||||
"bigserial": {},
|
||||
"money": {},
|
||||
|
||||
// Character types
|
||||
"char": {SupportsLength: true},
|
||||
"character": {SupportsLength: true},
|
||||
"varchar": {SupportsLength: true},
|
||||
"character varying": {SupportsLength: true},
|
||||
"text": {},
|
||||
"name": {},
|
||||
|
||||
// Binary
|
||||
"bytea": {},
|
||||
|
||||
// Date/time
|
||||
"timestamp": {SupportsPrecision: true},
|
||||
"timestamp without time zone": {SupportsPrecision: true},
|
||||
"timestamp with time zone": {SupportsPrecision: true},
|
||||
"time": {SupportsPrecision: true},
|
||||
"time without time zone": {SupportsPrecision: true},
|
||||
"time with time zone": {SupportsPrecision: true},
|
||||
"date": {},
|
||||
"interval": {SupportsPrecision: true},
|
||||
|
||||
// Boolean
|
||||
"boolean": {},
|
||||
|
||||
// Geometric
|
||||
"point": {},
|
||||
"line": {},
|
||||
"lseg": {},
|
||||
"box": {},
|
||||
"path": {},
|
||||
"polygon": {},
|
||||
"circle": {},
|
||||
|
||||
// Network
|
||||
"cidr": {},
|
||||
"inet": {},
|
||||
"macaddr": {},
|
||||
"macaddr8": {},
|
||||
|
||||
// Bit string
|
||||
"bit": {SupportsLength: true},
|
||||
"bit varying": {SupportsLength: true},
|
||||
"varbit": {SupportsLength: true},
|
||||
|
||||
// Text search
|
||||
"tsvector": {},
|
||||
"tsquery": {},
|
||||
|
||||
// UUID/XML/JSON
|
||||
"uuid": {},
|
||||
"xml": {},
|
||||
"json": {},
|
||||
"jsonb": {},
|
||||
|
||||
// Range
|
||||
"int4range": {},
|
||||
"int8range": {},
|
||||
"numrange": {},
|
||||
"tsrange": {},
|
||||
"tstzrange": {},
|
||||
"daterange": {},
|
||||
"int4multirange": {},
|
||||
"int8multirange": {},
|
||||
"nummultirange": {},
|
||||
"tsmultirange": {},
|
||||
"tstzmultirange": {},
|
||||
"datemultirange": {},
|
||||
|
||||
// Object identifier
|
||||
"oid": {},
|
||||
"regclass": {},
|
||||
"regproc": {},
|
||||
"regtype": {},
|
||||
|
||||
// Pseudo-ish/common built-ins seen in schemas
|
||||
"record": {},
|
||||
"void": {},
|
||||
|
||||
// Common extensions
|
||||
"citext": {},
|
||||
"hstore": {},
|
||||
"ltree": {},
|
||||
"lquery": {},
|
||||
"ltxtquery": {},
|
||||
"vector": {SupportsLength: true}, // pgvector: vector(dim)
|
||||
"halfvec": {SupportsLength: true}, // pgvector: halfvec(dim)
|
||||
"sparsevec": {SupportsLength: true}, // pgvector: sparsevec(dim)
|
||||
}
|
||||
|
||||
var postgresTypeAliases = map[string]string{
|
||||
// Integer aliases
|
||||
"int2": "smallint",
|
||||
"int4": "integer",
|
||||
"int8": "bigint",
|
||||
"int": "integer",
|
||||
|
||||
// Serial aliases
|
||||
"serial2": "smallserial",
|
||||
"serial4": "serial",
|
||||
"serial8": "bigserial",
|
||||
|
||||
// Character aliases
|
||||
"bpchar": "char",
|
||||
|
||||
// Float aliases
|
||||
"float4": "real",
|
||||
"float8": "double precision",
|
||||
"float": "double precision",
|
||||
|
||||
// Time aliases
|
||||
"timestamptz": "timestamp with time zone",
|
||||
"timetz": "time with time zone",
|
||||
|
||||
// Bit alias
|
||||
"varbit": "bit varying",
|
||||
|
||||
// Boolean alias
|
||||
"bool": "boolean",
|
||||
}
|
||||
|
||||
// GetPostgresBaseTypes returns a sorted-ish stable list of registered base type names.
|
||||
func GetPostgresBaseTypes() []string {
|
||||
result := make([]string, 0, len(postgresBaseTypes))
|
||||
for t := range postgresBaseTypes {
|
||||
result = append(result, t)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetPostgresTypes returns the registered PostgreSQL types.
|
||||
// When includeArrays is true, each base type also includes an array variant ("type[]").
|
||||
func GetPostgresTypes(includeArrays bool) []string {
|
||||
base := GetPostgresBaseTypes()
|
||||
if !includeArrays {
|
||||
return base
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(base)*2)
|
||||
result = append(result, base...)
|
||||
for _, t := range base {
|
||||
result = append(result, t+"[]")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ExtractBaseType returns the type without outer array suffixes and modifiers.
|
||||
// Examples:
|
||||
// - varchar(255) -> varchar
|
||||
// - text[] -> text
|
||||
// - numeric(10,2)[] -> numeric
|
||||
func ExtractBaseType(sqlType string) string {
|
||||
t := normalizeTypeToken(sqlType)
|
||||
t = strings.TrimSpace(stripArraySuffixes(t))
|
||||
if idx := strings.Index(t, "("); idx > 0 {
|
||||
t = strings.TrimSpace(t[:idx])
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// ExtractBaseTypeLower is ExtractBaseType with lowercase normalization.
|
||||
func ExtractBaseTypeLower(sqlType string) string {
|
||||
return strings.ToLower(ExtractBaseType(sqlType))
|
||||
}
|
||||
|
||||
// IsArrayType reports whether the SQL type has one or more [] suffixes.
|
||||
func IsArrayType(sqlType string) bool {
|
||||
t := normalizeTypeToken(sqlType)
|
||||
return strings.HasSuffix(t, "[]")
|
||||
}
|
||||
|
||||
// ElementType returns the underlying element type for array types.
|
||||
// For non-array types, it returns the input unchanged.
|
||||
func ElementType(sqlType string) string {
|
||||
t := normalizeTypeToken(sqlType)
|
||||
return stripArraySuffixes(t)
|
||||
}
|
||||
|
||||
// CanonicalizeBaseType resolves aliases to canonical PostgreSQL type names.
|
||||
func CanonicalizeBaseType(baseType string) string {
|
||||
base := strings.ToLower(normalizeTypeToken(baseType))
|
||||
if canonical, ok := postgresTypeAliases[base]; ok {
|
||||
return canonical
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
// IsKnownPostgresType reports whether a type (including array forms) exists in the registry.
|
||||
func IsKnownPostgresType(sqlType string) bool {
|
||||
base := CanonicalizeBaseType(ExtractBaseTypeLower(sqlType))
|
||||
_, ok := postgresBaseTypes[base]
|
||||
return ok
|
||||
}
|
||||
|
||||
// SupportsLength reports if this SQL type accepts a single length/dimension modifier.
|
||||
func SupportsLength(sqlType string) bool {
|
||||
base := CanonicalizeBaseType(ExtractBaseTypeLower(sqlType))
|
||||
spec, ok := postgresBaseTypes[base]
|
||||
return ok && spec.SupportsLength
|
||||
}
|
||||
|
||||
// SupportsPrecision reports if this SQL type accepts precision (and possibly scale).
|
||||
func SupportsPrecision(sqlType string) bool {
|
||||
base := CanonicalizeBaseType(ExtractBaseTypeLower(sqlType))
|
||||
spec, ok := postgresBaseTypes[base]
|
||||
return ok && spec.SupportsPrecision
|
||||
}
|
||||
|
||||
// HasExplicitTypeModifier reports if the type already includes "(...)".
|
||||
func HasExplicitTypeModifier(sqlType string) bool {
|
||||
return strings.Contains(sqlType, "(")
|
||||
}
|
||||
|
||||
func stripArraySuffixes(t string) string {
|
||||
for strings.HasSuffix(t, "[]") {
|
||||
t = strings.TrimSpace(strings.TrimSuffix(t, "[]"))
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func normalizeTypeToken(t string) string {
|
||||
return strings.Join(strings.Fields(strings.TrimSpace(t)), " ")
|
||||
}
|
||||
99
pkg/pgsql/types_registry_test.go
Normal file
99
pkg/pgsql/types_registry_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package pgsql
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPostgresTypeRegistry_MasterListIncludesRequestedTypes(t *testing.T) {
|
||||
required := []string{
|
||||
"vector",
|
||||
"integer",
|
||||
"citext",
|
||||
}
|
||||
|
||||
types := make(map[string]bool)
|
||||
for _, typ := range GetPostgresTypes(true) {
|
||||
types[typ] = true
|
||||
}
|
||||
|
||||
for _, typ := range required {
|
||||
if !types[typ] {
|
||||
t.Fatalf("master type list missing %q", typ)
|
||||
}
|
||||
if !types[typ+"[]"] {
|
||||
t.Fatalf("master type list missing array variant %q", typ+"[]")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgresTypeRegistry_TypeParsingAndCapabilities(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
wantBase string
|
||||
wantCanonicalBase string
|
||||
wantArray bool
|
||||
wantKnown bool
|
||||
wantLength bool
|
||||
wantPrecision bool
|
||||
}{
|
||||
{
|
||||
input: "integer[]",
|
||||
wantBase: "integer",
|
||||
wantCanonicalBase: "integer",
|
||||
wantArray: true,
|
||||
wantKnown: true,
|
||||
},
|
||||
{
|
||||
input: "citext[]",
|
||||
wantBase: "citext",
|
||||
wantCanonicalBase: "citext",
|
||||
wantArray: true,
|
||||
wantKnown: true,
|
||||
},
|
||||
{
|
||||
input: "vector(1536)",
|
||||
wantBase: "vector",
|
||||
wantCanonicalBase: "vector",
|
||||
wantKnown: true,
|
||||
wantLength: true,
|
||||
},
|
||||
{
|
||||
input: "numeric(10,2)",
|
||||
wantBase: "numeric",
|
||||
wantCanonicalBase: "numeric",
|
||||
wantKnown: true,
|
||||
wantPrecision: true,
|
||||
},
|
||||
{
|
||||
input: "int4",
|
||||
wantBase: "int4",
|
||||
wantCanonicalBase: "integer",
|
||||
wantKnown: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
base := ExtractBaseTypeLower(tt.input)
|
||||
if base != tt.wantBase {
|
||||
t.Fatalf("ExtractBaseTypeLower(%q) = %q, want %q", tt.input, base, tt.wantBase)
|
||||
}
|
||||
|
||||
canonical := CanonicalizeBaseType(base)
|
||||
if canonical != tt.wantCanonicalBase {
|
||||
t.Fatalf("CanonicalizeBaseType(%q) = %q, want %q", base, canonical, tt.wantCanonicalBase)
|
||||
}
|
||||
|
||||
if IsArrayType(tt.input) != tt.wantArray {
|
||||
t.Fatalf("IsArrayType(%q) = %v, want %v", tt.input, IsArrayType(tt.input), tt.wantArray)
|
||||
}
|
||||
if IsKnownPostgresType(tt.input) != tt.wantKnown {
|
||||
t.Fatalf("IsKnownPostgresType(%q) = %v, want %v", tt.input, IsKnownPostgresType(tt.input), tt.wantKnown)
|
||||
}
|
||||
if SupportsLength(tt.input) != tt.wantLength {
|
||||
t.Fatalf("SupportsLength(%q) = %v, want %v", tt.input, SupportsLength(tt.input), tt.wantLength)
|
||||
}
|
||||
if SupportsPrecision(tt.input) != tt.wantPrecision {
|
||||
t.Fatalf("SupportsPrecision(%q) = %v, want %v", tt.input, SupportsPrecision(tt.input), tt.wantPrecision)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user