diff --git a/server/functions/format_type.go b/server/functions/format_type.go index 933051707f..ec014075ae 100644 --- a/server/functions/format_type.go +++ b/server/functions/format_type.go @@ -15,11 +15,15 @@ package functions import ( + "bytes" + "fmt" + "github.com/dolthub/go-mysql-server/sql" "github.com/lib/pq/oid" + "github.com/dolthub/doltgresql/core" "github.com/dolthub/doltgresql/core/id" - + "github.com/dolthub/doltgresql/postgres/parser/lex" "github.com/dolthub/doltgresql/postgres/parser/types" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" @@ -47,6 +51,74 @@ var format_type = framework.Function2{ return t.SQLStandardNameWithTypmod(true, int(val2.(int32))), nil } } - return "???", nil + typ, err := getDoltgresTypeFromId(ctx, val1.(id.Id)) + if err != nil { + if pgtypes.ErrTypeDoesNotExist.Is(err) { + return "???", nil + } + return nil, err + } + return formatUserDefinedType(ctx, typ, val2) }, } + +// formatUserDefinedType renders a catalog type using PostgreSQL's generic type-name and typmod rules. +func formatUserDefinedType(ctx *sql.Context, typ *pgtypes.DoltgresType, typmodValue any) (string, error) { + isArray := typ.IsArrayType() + if isArray { + typ = typ.ArrayBaseType() + } + + name, err := visibleTypeName(ctx, typ.ID) + if err != nil { + return "", err + } + if typmodValue != nil && typmodValue.(int32) >= 0 { + typmod := typmodValue.(int32) + if typ.ModOutFunc == 0 { + name = fmt.Sprintf("%s(%d)", name, typmod) + } else { + modifier, err := typ.TypModOut(ctx, typmod) + if err != nil { + return "", err + } + name += modifier + } + } + if isArray { + name += "[]" + } + return name, nil +} + +// visibleTypeName returns a quoted type name, schema-qualifying it when an unqualified reference would resolve elsewhere. +func visibleTypeName(ctx *sql.Context, typID id.Type) (string, error) { + typCol, err := core.GetTypesCollectionFromContext(ctx, "") + if err != nil { + return "", err + } + searchPath, err := core.SearchPath(ctx) + if err != nil { + return "", err + } + for _, schema := range searchPath { + candidate, err := typCol.GetType(ctx, id.NewType(schema, typID.TypeName())) + if err != nil { + return "", err + } + if candidate != nil { + if candidate.ID == typID { + return quoteTypeIdentifier(typID.TypeName()), nil + } + break + } + } + return quoteTypeIdentifier(typID.SchemaName()) + "." + quoteTypeIdentifier(typID.TypeName()), nil +} + +// quoteTypeIdentifier quotes a type or schema name when PostgreSQL would not accept it as a bare identifier. +func quoteTypeIdentifier(name string) string { + var buf bytes.Buffer + lex.EncodeRestrictedSQLIdent(&buf, name, 0) + return buf.String() +} diff --git a/server/tables/pgcatalog/pg_attribute.go b/server/tables/pgcatalog/pg_attribute.go index f774fa3c72..94137658d0 100644 --- a/server/tables/pgcatalog/pg_attribute.go +++ b/server/tables/pgcatalog/pg_attribute.go @@ -97,8 +97,10 @@ func cachePgAttributes(ctx *sql.Context, pgCatalogCache *pgCatalogCache) error { Table: func(ctx *sql.Context, _ functions.ItemSchema, table functions.ItemTable) (cont bool, err error) { for i, col := range table.Item.Schema(ctx) { typeOid := id.Null + typeMod := int32(-1) if doltgresType, ok := col.Type.(*pgtypes.DoltgresType); ok { typeOid = doltgresType.ID.AsId() + typeMod = doltgresType.GetAttTypMod() } else { // TODO: Remove once all information_schema tables are converted to use DoltgresType dt := pgtypes.FromGmsType(col.Type) @@ -122,6 +124,7 @@ func cachePgAttributes(ctx *sql.Context, pgCatalogCache *pgCatalogCache) error { attrelidNative: id.Cache().ToOID(table.OID.AsId()), attname: col.Name, atttypid: typeOid, + atttypmod: typeMod, attnum: int16(i + 1), attndims: dimensions, attnotnull: !col.Nullable, @@ -164,8 +167,10 @@ func cachePgAttributes(ctx *sql.Context, pgCatalogCache *pgCatalogCache) error { for i, col := range analyzed.Schema(ctx) { typeOid := id.Null + typeMod := int32(-1) if doltgresType, ok := col.Type.(*pgtypes.DoltgresType); ok { typeOid = doltgresType.ID.AsId() + typeMod = doltgresType.GetAttTypMod() } else { dt := pgtypes.FromGmsType(col.Type) typeOid = dt.ID.AsId() @@ -176,6 +181,7 @@ func cachePgAttributes(ctx *sql.Context, pgCatalogCache *pgCatalogCache) error { attrelidNative: id.Cache().ToOID(view.OID.AsId()), attname: col.Name, atttypid: typeOid, + atttypmod: typeMod, attnum: int16(i + 1), } attrelidIdx.Add(attr) @@ -411,6 +417,7 @@ type pgAttribute struct { attrelidNative uint32 attname string atttypid id.Id + atttypmod int32 attnum int16 attndims int16 attnotnull bool @@ -467,7 +474,7 @@ func pgAttributeToRow(attr *pgAttribute) sql.Row { int16(0), // attlen attr.attnum, // attnum int32(-1), // attcacheoff - int32(-1), // atttypmod + attr.atttypmod, // atttypmod attr.attndims, // attndims false, // attbyval "i", // attalign diff --git a/testing/go/extensions/pgvector_catalog_test.go b/testing/go/extensions/pgvector_catalog_test.go index 76659b0865..740ffd6c7f 100644 --- a/testing/go/extensions/pgvector_catalog_test.go +++ b/testing/go/extensions/pgvector_catalog_test.go @@ -24,6 +24,60 @@ import ( func TestPgvectorCatalog(t *testing.T) { framework.RunScripts(t, []framework.ScriptTest{ + { + Name: "dimensioned types expose their modifiers through pg_attribute", + SetUpScript: []string{ + "CREATE EXTENSION vector;", + "CREATE TABLE vector_typmod_probe (v vector(1024), h halfvec(7), s sparsevec(99), unbounded vector, vectors vector(3)[]);", + "CREATE VIEW vector_typmod_view AS SELECT v FROM vector_typmod_probe;", + }, + Assertions: []framework.ScriptTestAssertion{ + { + Query: `SELECT attribute.attname, attribute.atttypmod, format_type(attribute.atttypid, attribute.atttypmod) +FROM pg_attribute AS attribute +JOIN pg_class AS relation ON relation.oid = attribute.attrelid +WHERE relation.relname = 'vector_typmod_probe' AND attribute.attnum > 0 AND NOT attribute.attisdropped +ORDER BY attribute.attnum;`, + Expected: []sql.Row{ + {"v", int32(1024), "vector(1024)"}, + {"h", int32(7), "halfvec(7)"}, + {"s", int32(99), "sparsevec(99)"}, + {"unbounded", int32(-1), "vector"}, + {"vectors", int32(3), "vector(3)[]"}, + }, + }, + { + Query: `SELECT attribute.atttypmod, format_type(attribute.atttypid, attribute.atttypmod) +FROM pg_attribute AS attribute +JOIN pg_class AS relation ON relation.oid = attribute.attrelid +WHERE relation.relname = 'vector_typmod_view' AND attribute.attname = 'v';`, + Expected: []sql.Row{{int32(1024), "vector(1024)"}}, + }, + }, + }, + { + Name: "user-defined type names are quoted and qualified according to visibility", + SetUpScript: []string{ + "CREATE SCHEMA first_schema;", + "CREATE SCHEMA second_schema;", + `CREATE TYPE first_schema."Mixed Type" AS ENUM ('first');`, + `CREATE TYPE second_schema."Mixed Type" AS ENUM ('second');`, + "SET search_path TO first_schema, second_schema, public;", + }, + Assertions: []framework.ScriptTestAssertion{ + { + Query: `SELECT namespace.nspname, format_type(type.oid, NULL) +FROM pg_type AS type +JOIN pg_namespace AS namespace ON namespace.oid = type.typnamespace +WHERE type.typname = 'Mixed Type' +ORDER BY namespace.nspname;`, + Expected: []sql.Row{ + {"first_schema", `"Mixed Type"`}, + {"second_schema", `second_schema."Mixed Type"`}, + }, + }, + }, + }, { Name: "pg_am lists the extension access methods once installed", Assertions: []framework.ScriptTestAssertion{ diff --git a/testing/go/information_schema_test.go b/testing/go/information_schema_test.go index e7b7054097..e8e9bb8b6b 100644 --- a/testing/go/information_schema_test.go +++ b/testing/go/information_schema_test.go @@ -157,7 +157,7 @@ func TestInfoSchemaColumns(t *testing.T) { ) WHERE ("table_schema" = 'public' AND "table_name" = 'test_table');`, Expected: []sql.Row{ {"id", nil, "integer", "integer"}, - {"col1", nil, "character varying", "character varying"}, + {"col1", nil, "character varying", "character varying(255)"}, }, }, {