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:
2026-04-26 12:35:54 +02:00
parent 535a91d4be
commit 988798998d
24 changed files with 1052 additions and 264 deletions

View File

@@ -206,8 +206,19 @@ func (r *Reader) queryColumns(schemaName string) (map[string]map[string]*models.
c.numeric_precision,
c.numeric_scale,
c.udt_name,
pg_catalog.format_type(a.atttypid, a.atttypmod) as formatted_data_type,
col_description((c.table_schema||'.'||c.table_name)::regclass, c.ordinal_position) as description
FROM information_schema.columns c
JOIN pg_catalog.pg_namespace n
ON n.nspname = c.table_schema
JOIN pg_catalog.pg_class cls
ON cls.relname = c.table_name
AND cls.relnamespace = n.oid
JOIN pg_catalog.pg_attribute a
ON a.attrelid = cls.oid
AND a.attname = c.column_name
AND a.attnum > 0
AND NOT a.attisdropped
WHERE c.table_schema = $1
ORDER BY c.table_schema, c.table_name, c.ordinal_position
`
@@ -221,12 +232,12 @@ func (r *Reader) queryColumns(schemaName string) (map[string]map[string]*models.
columnsMap := make(map[string]map[string]*models.Column)
for rows.Next() {
var schema, tableName, columnName, isNullable, dataType, udtName string
var schema, tableName, columnName, isNullable, dataType, udtName, formattedDataType string
var ordinalPosition int
var columnDefault, description *string
var charMaxLength, numPrecision, numScale *int
if err := rows.Scan(&schema, &tableName, &columnName, &ordinalPosition, &columnDefault, &isNullable, &dataType, &charMaxLength, &numPrecision, &numScale, &udtName, &description); err != nil {
if err := rows.Scan(&schema, &tableName, &columnName, &ordinalPosition, &columnDefault, &isNullable, &dataType, &charMaxLength, &numPrecision, &numScale, &udtName, &formattedDataType, &description); err != nil {
return nil, err
}
@@ -246,7 +257,7 @@ func (r *Reader) queryColumns(schemaName string) (map[string]map[string]*models.
}
// Map data type, preserving serial types when detected
column.Type = r.mapDataType(dataType, udtName, hasNextval)
column.Type = r.mapDataType(dataType, udtName, formattedDataType, hasNextval)
column.NotNull = (isNullable == "NO")
column.Sequence = uint(ordinalPosition)

View File

@@ -259,12 +259,14 @@ func (r *Reader) close() {
}
}
// mapDataType maps PostgreSQL data types to canonical types
func (r *Reader) mapDataType(pgType, udtName string, hasNextval bool) string {
// mapDataType maps PostgreSQL data types while preserving exact type text when available.
func (r *Reader) mapDataType(pgType, udtName, formattedType string, hasNextval bool) string {
normalizedPGType := strings.ToLower(strings.TrimSpace(pgType))
// If the column has a nextval default, it's likely a serial type
// Map to the appropriate serial type instead of the base integer type
if hasNextval {
switch strings.ToLower(pgType) {
switch normalizedPGType {
case "integer", "int", "int4":
return "serial"
case "bigint", "int8":
@@ -274,6 +276,17 @@ func (r *Reader) mapDataType(pgType, udtName string, hasNextval bool) string {
}
}
// Prefer the database-provided formatted type; this preserves arrays/custom
// types/modifiers like text[], vector(1536), numeric(10,2), etc.
if strings.TrimSpace(formattedType) != "" {
return formattedType
}
// information_schema reports arrays generically as "ARRAY" with udt_name like "_text".
if strings.EqualFold(pgType, "ARRAY") && strings.HasPrefix(udtName, "_") && len(udtName) > 1 {
return udtName[1:] + "[]"
}
// Map common PostgreSQL types
typeMap := map[string]string{
"integer": "integer",
@@ -320,7 +333,7 @@ func (r *Reader) mapDataType(pgType, udtName string, hasNextval bool) string {
}
// Try mapped type first
if mapped, exists := typeMap[pgType]; exists {
if mapped, exists := typeMap[normalizedPGType]; exists {
return mapped
}
@@ -329,8 +342,11 @@ func (r *Reader) mapDataType(pgType, udtName string, hasNextval bool) string {
return pgsql.GetSQLType(pgType)
}
// Return UDT name for custom types
// Return UDT name for custom types (including array fallback when needed)
if udtName != "" {
if strings.HasPrefix(udtName, "_") && len(udtName) > 1 {
return udtName[1:] + "[]"
}
return udtName
}

View File

@@ -173,35 +173,39 @@ func TestMapDataType(t *testing.T) {
reader := &Reader{}
tests := []struct {
pgType string
udtName string
expected string
pgType string
udtName string
formattedType string
expected string
}{
{"integer", "int4", "integer"},
{"bigint", "int8", "bigint"},
{"smallint", "int2", "smallint"},
{"character varying", "varchar", "varchar"},
{"text", "text", "text"},
{"boolean", "bool", "boolean"},
{"timestamp without time zone", "timestamp", "timestamp"},
{"timestamp with time zone", "timestamptz", "timestamptz"},
{"json", "json", "json"},
{"jsonb", "jsonb", "jsonb"},
{"uuid", "uuid", "uuid"},
{"numeric", "numeric", "numeric"},
{"real", "float4", "real"},
{"double precision", "float8", "double precision"},
{"date", "date", "date"},
{"time without time zone", "time", "time"},
{"bytea", "bytea", "bytea"},
{"unknown_type", "custom", "custom"}, // Should return UDT name
{"integer", "int4", "", "integer"},
{"bigint", "int8", "", "bigint"},
{"smallint", "int2", "", "smallint"},
{"character varying", "varchar", "", "varchar"},
{"text", "text", "", "text"},
{"boolean", "bool", "", "boolean"},
{"timestamp without time zone", "timestamp", "", "timestamp"},
{"timestamp with time zone", "timestamptz", "", "timestamptz"},
{"json", "json", "", "json"},
{"jsonb", "jsonb", "", "jsonb"},
{"uuid", "uuid", "", "uuid"},
{"numeric", "numeric", "", "numeric"},
{"real", "float4", "", "real"},
{"double precision", "float8", "", "double precision"},
{"date", "date", "", "date"},
{"time without time zone", "time", "", "time"},
{"bytea", "bytea", "", "bytea"},
{"unknown_type", "custom", "", "custom"}, // Should return UDT name
{"ARRAY", "_text", "", "text[]"},
{"USER-DEFINED", "vector", "vector(1536)", "vector(1536)"},
{"character varying", "varchar", "character varying(255)", "character varying(255)"},
}
for _, tt := range tests {
t.Run(tt.pgType, func(t *testing.T) {
result := reader.mapDataType(tt.pgType, tt.udtName, false)
result := reader.mapDataType(tt.pgType, tt.udtName, tt.formattedType, false)
if result != tt.expected {
t.Errorf("mapDataType(%s, %s) = %s, expected %s", tt.pgType, tt.udtName, result, tt.expected)
t.Errorf("mapDataType(%s, %s, %s) = %s, expected %s", tt.pgType, tt.udtName, tt.formattedType, result, tt.expected)
}
})
}
@@ -218,9 +222,9 @@ func TestMapDataType(t *testing.T) {
for _, tt := range serialTests {
t.Run(tt.pgType+"_with_nextval", func(t *testing.T) {
result := reader.mapDataType(tt.pgType, "", true)
result := reader.mapDataType(tt.pgType, "", "", true)
if result != tt.expected {
t.Errorf("mapDataType(%s, '', true) = %s, expected %s", tt.pgType, result, tt.expected)
t.Errorf("mapDataType(%s, '', '', true) = %s, expected %s", tt.pgType, result, tt.expected)
}
})
}
@@ -230,63 +234,63 @@ func TestParseIndexDefinition(t *testing.T) {
reader := &Reader{}
tests := []struct {
name string
indexName string
tableName string
schema string
indexDef string
wantType string
wantUnique bool
name string
indexName string
tableName string
schema string
indexDef string
wantType string
wantUnique bool
wantColumns int
}{
{
name: "simple btree index",
indexName: "idx_users_email",
tableName: "users",
schema: "public",
indexDef: "CREATE INDEX idx_users_email ON public.users USING btree (email)",
wantType: "btree",
wantUnique: false,
name: "simple btree index",
indexName: "idx_users_email",
tableName: "users",
schema: "public",
indexDef: "CREATE INDEX idx_users_email ON public.users USING btree (email)",
wantType: "btree",
wantUnique: false,
wantColumns: 1,
},
{
name: "unique index",
indexName: "idx_users_username",
tableName: "users",
schema: "public",
indexDef: "CREATE UNIQUE INDEX idx_users_username ON public.users USING btree (username)",
wantType: "btree",
wantUnique: true,
name: "unique index",
indexName: "idx_users_username",
tableName: "users",
schema: "public",
indexDef: "CREATE UNIQUE INDEX idx_users_username ON public.users USING btree (username)",
wantType: "btree",
wantUnique: true,
wantColumns: 1,
},
{
name: "composite index",
indexName: "idx_users_name",
tableName: "users",
schema: "public",
indexDef: "CREATE INDEX idx_users_name ON public.users USING btree (first_name, last_name)",
wantType: "btree",
wantUnique: false,
name: "composite index",
indexName: "idx_users_name",
tableName: "users",
schema: "public",
indexDef: "CREATE INDEX idx_users_name ON public.users USING btree (first_name, last_name)",
wantType: "btree",
wantUnique: false,
wantColumns: 2,
},
{
name: "gin index",
indexName: "idx_posts_tags",
tableName: "posts",
schema: "public",
indexDef: "CREATE INDEX idx_posts_tags ON public.posts USING gin (tags)",
wantType: "gin",
wantUnique: false,
name: "gin index",
indexName: "idx_posts_tags",
tableName: "posts",
schema: "public",
indexDef: "CREATE INDEX idx_posts_tags ON public.posts USING gin (tags)",
wantType: "gin",
wantUnique: false,
wantColumns: 1,
},
{
name: "partial index with where clause",
indexName: "idx_users_active",
tableName: "users",
schema: "public",
indexDef: "CREATE INDEX idx_users_active ON public.users USING btree (id) WHERE (active = true)",
wantType: "btree",
wantUnique: false,
name: "partial index with where clause",
indexName: "idx_users_active",
tableName: "users",
schema: "public",
indexDef: "CREATE INDEX idx_users_active ON public.users USING btree (id) WHERE (active = true)",
wantType: "btree",
wantUnique: false,
wantColumns: 1,
},
}