2
0
Эх сурвалжийг харах

broaden pg_catalog and information_schema introspection

Add read-only virtual catalog relations so an unmodified PostgreSQL ORM or
migration tool can introspect the durable schema:

- pg_class, pg_namespace, pg_attribute, pg_type, pg_index, pg_constraint,
  pg_attrdef, pg_database, pg_tablespace, pg_proc, pg_am, pg_description,
  pg_tables, and pg_indexes
- information_schema tables, columns, table_constraints, key_column_usage,
  referential_constraints, constraint_column_usage, schemata, sequences, and
  views
- deterministic FNV-1a OIDs shared by pg_class/pg_attribute/pg_type/pg_index/
  pg_constraint, with public=2200, pg_catalog=11, information_schema=13207
- regclass/regtype casts, format_type, pg_table_is_visible,
  pg_get_indexdef, pg_get_constraintdef, pg_get_expr, current_schema,
  current_database, current_setting, pg_get_serial_sequence, description
  functions, has_table_privilege, and pg_get_userbyid
- SET/SHOW compatibility for driver session setup, retaining search_path
- SELECT over catalog relations supports WHERE, ORDER BY, LIMIT/OFFSET,
  projection, aggregates, table.* expansion, and joins
- writes to catalog relations fail with 0A000

Remove the pgserver single-relation catalog shortcut: catalog queries now run
through the executor, so joins, filters, transactional DDL visibility, result
typing, and binary formats follow the normal query path. The SQLite catalog
and PRAGMA emulation is unchanged.

Register the new surface in the capabilities document and cover it with
executor, wire-protocol, and pgx tests.
Danilo Fragoso 1 өдөр өмнө
parent
commit
588ac667ff

+ 11 - 3
pkg/analyzer/analyzer.go

@@ -326,15 +326,23 @@ func (a *Analyzer) defineTableRef(ref parser.TableRef) error {
 		return nil
 	}
 
-	table, ok := a.catalog.GetTable(ref.Name)
+	// A non-public schema qualifier selects a schema-qualified catalog entry.
+	// Public tables are registered by bare name, so `public.t` resolves to `t`.
+	catalogName := ref.Name
+	if ref.Schema != "" && !strings.EqualFold(ref.Schema, "public") {
+		// Qualified catalog relations are registered under their lowercase
+		// schema-qualified name; schema names are case-insensitive here.
+		catalogName = strings.ToLower(ref.Schema) + "." + strings.ToLower(ref.Name)
+	}
+	table, ok := a.catalog.GetTable(catalogName)
 	if !ok {
 		return &AnalysisError{
 			Type:    ErrTableNotFound,
-			Message: fmt.Sprintf("table not found: %s", ref.Name),
+			Message: fmt.Sprintf("table not found: %s", catalogName),
 		}
 	}
 	a.scope.DefineTable(&TableInfo{
-		Name:    table.Name,
+		Name:    ref.Name,
 		Columns: table.Columns,
 		Alias:   ref.Alias,
 		IsView:  table.IsView,

+ 25 - 6
pkg/analyzer/types.go

@@ -270,12 +270,31 @@ var builtinFunctions = map[string]FunctionSignature{
 	"TIMEDIFF":  {Name: "TIMEDIFF", MinArgs: 2, MaxArgs: 2, ArgTypes: []Type{TypeAny, TypeAny}, ReturnType: TypeText, IsAggregate: false},
 
 	// SQLite specific
-	"SQLITE_VERSION":    {Name: "SQLITE_VERSION", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeText, IsAggregate: false},
-	"PIZZASQL_VERSION":  {Name: "PIZZASQL_VERSION", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeText, IsAggregate: false},
-	"PG_BACKEND_PID":    {Name: "PG_BACKEND_PID", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeInteger, IsAggregate: false},
-	"LAST_INSERT_ROWID": {Name: "LAST_INSERT_ROWID", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeInteger, IsAggregate: false},
-	"CHANGES":           {Name: "CHANGES", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeInteger, IsAggregate: false},
-	"TOTAL_CHANGES":     {Name: "TOTAL_CHANGES", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeInteger, IsAggregate: false},
+	"SQLITE_VERSION":   {Name: "SQLITE_VERSION", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeText, IsAggregate: false},
+	"PIZZASQL_VERSION": {Name: "PIZZASQL_VERSION", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeText, IsAggregate: false},
+	"PG_BACKEND_PID":   {Name: "PG_BACKEND_PID", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeInteger, IsAggregate: false},
+	"VERSION":          {Name: "VERSION", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeText, IsAggregate: false},
+
+	// PostgreSQL catalog-introspection functions. The engine answers these from
+	// its virtual pg_catalog snapshot; obj_description, col_description, and
+	// pg_get_serial_sequence return NULL because there are no comments or
+	// sequences to describe.
+	"CURRENT_SCHEMA":         {Name: "CURRENT_SCHEMA", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeText, IsAggregate: false},
+	"CURRENT_DATABASE":       {Name: "CURRENT_DATABASE", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeText, IsAggregate: false},
+	"CURRENT_SETTING":        {Name: "CURRENT_SETTING", MinArgs: 1, MaxArgs: 2, ArgTypes: []Type{TypeText, TypeBoolean}, ReturnType: TypeText, IsAggregate: false},
+	"FORMAT_TYPE":            {Name: "FORMAT_TYPE", MinArgs: 1, MaxArgs: 2, ArgTypes: []Type{TypeAny, TypeInteger}, ReturnType: TypeText, IsAggregate: false},
+	"PG_TABLE_IS_VISIBLE":    {Name: "PG_TABLE_IS_VISIBLE", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeAny}, ReturnType: TypeBoolean, IsAggregate: false},
+	"PG_GET_INDEXDEF":        {Name: "PG_GET_INDEXDEF", MinArgs: 1, MaxArgs: 3, ArgTypes: []Type{TypeAny, TypeAny, TypeBoolean}, ReturnType: TypeText, IsAggregate: false},
+	"PG_GET_CONSTRAINTDEF":   {Name: "PG_GET_CONSTRAINTDEF", MinArgs: 1, MaxArgs: 2, ArgTypes: []Type{TypeAny, TypeBoolean}, ReturnType: TypeText, IsAggregate: false},
+	"PG_GET_EXPR":            {Name: "PG_GET_EXPR", MinArgs: 1, MaxArgs: 3, ArgTypes: []Type{TypeAny, TypeAny, TypeBoolean}, ReturnType: TypeText, IsAggregate: false},
+	"PG_GET_SERIAL_SEQUENCE": {Name: "PG_GET_SERIAL_SEQUENCE", MinArgs: 2, MaxArgs: 2, ArgTypes: []Type{TypeText, TypeText}, ReturnType: TypeText, IsAggregate: false},
+	"OBJ_DESCRIPTION":        {Name: "OBJ_DESCRIPTION", MinArgs: 1, MaxArgs: 2, ArgTypes: []Type{TypeAny, TypeText}, ReturnType: TypeText, IsAggregate: false},
+	"COL_DESCRIPTION":        {Name: "COL_DESCRIPTION", MinArgs: 2, MaxArgs: 2, ArgTypes: []Type{TypeAny, TypeInteger}, ReturnType: TypeText, IsAggregate: false},
+	"HAS_TABLE_PRIVILEGE":    {Name: "HAS_TABLE_PRIVILEGE", MinArgs: 2, MaxArgs: 3, ArgTypes: []Type{TypeAny, TypeText, TypeText}, ReturnType: TypeBoolean, IsAggregate: false},
+	"PG_GET_USERBYID":        {Name: "PG_GET_USERBYID", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: false},
+	"LAST_INSERT_ROWID":      {Name: "LAST_INSERT_ROWID", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeInteger, IsAggregate: false},
+	"CHANGES":                {Name: "CHANGES", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeInteger, IsAggregate: false},
+	"TOTAL_CHANGES":          {Name: "TOTAL_CHANGES", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeInteger, IsAggregate: false},
 
 	// Other
 	"HEX":      {Name: "HEX", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeBlob}, ReturnType: TypeText, IsAggregate: false},

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 13 - 0
pkg/capabilities/capabilities.go


+ 74 - 0
pkg/capabilities/capabilities_test.go

@@ -347,3 +347,77 @@ func TestBoundedExecuteIsAdvertised(t *testing.T) {
 		}
 	}
 }
+
+// The catalog document advertises the read-only introspection surface an ORM
+// uses: the relation and function matrix, the deterministic OID scheme, and the
+// SET/SHOW compatibility surface. pkg/executor covers each relation.
+func TestCatalogIntrospectionIsAdvertised(t *testing.T) {
+	doc := Current()
+	catalog := doc.Catalog
+	if !catalog.PgCatalog.Supported || !catalog.InformationSchema.Supported {
+		t.Fatalf("catalog = %+v, want both schemas supported", catalog)
+	}
+	if catalog.PgCatalog.SQLState != pgerror.CodeFeatureNotSupported {
+		t.Fatalf("pg_catalog SQLSTATE = %q, want 0A000 for writes", catalog.PgCatalog.SQLState)
+	}
+	if catalog.Writable {
+		t.Fatal("catalog relations are read-only")
+	}
+	for _, want := range []string{
+		"pg_catalog.pg_class", "pg_catalog.pg_namespace", "pg_catalog.pg_attribute",
+		"pg_catalog.pg_type", "pg_catalog.pg_index", "pg_catalog.pg_constraint",
+		"pg_catalog.pg_attrdef", "pg_catalog.pg_database", "pg_catalog.pg_tablespace",
+		"pg_catalog.pg_proc", "pg_catalog.pg_am", "pg_catalog.pg_description",
+		"information_schema.tables", "information_schema.columns",
+		"information_schema.table_constraints", "information_schema.key_column_usage",
+		"information_schema.referential_constraints",
+		"information_schema.constraint_column_usage", "information_schema.schemata",
+		"information_schema.sequences", "information_schema.views",
+	} {
+		found := false
+		for _, relation := range catalog.Relations {
+			if relation == want {
+				found = true
+				break
+			}
+		}
+		if !found {
+			t.Errorf("catalog.relations does not list %q", want)
+		}
+	}
+	for _, want := range []string{
+		"current_schema", "current_database", "format_type", "pg_table_is_visible",
+		"pg_get_indexdef", "pg_get_constraintdef", "pg_get_expr", "pg_get_serial_sequence",
+		"obj_description", "col_description", "has_table_privilege",
+	} {
+		found := false
+		for _, function := range catalog.Functions {
+			if function == want {
+				found = true
+				break
+			}
+		}
+		if !found {
+			t.Errorf("catalog.functions does not list %q", want)
+		}
+	}
+	if !strings.Contains(catalog.OIDStrategy, "FNV-1a") || !strings.Contains(catalog.OIDStrategy, "deterministic") {
+		t.Fatalf("oid_strategy = %q", catalog.OIDStrategy)
+	}
+	if !catalog.SetStatements.Supported || catalog.SetStatements.Status != "accepted_and_ignored" {
+		t.Fatalf("set_statements = %+v", catalog.SetStatements)
+	}
+	for _, want := range []string{"search_path", "SHOW", "current_schema"} {
+		if !strings.Contains(catalog.SetStatements.Notes, want) {
+			t.Errorf("set_statements notes do not mention %q", want)
+		}
+	}
+
+	body, err := json.Marshal(doc)
+	if err != nil {
+		t.Fatalf("marshal: %v", err)
+	}
+	if !strings.Contains(string(body), `"writable":false`) {
+		t.Fatalf("document does not advertise the catalog as read-only: %s", body)
+	}
+}

+ 116 - 1
pkg/executor/executor.go

@@ -40,6 +40,16 @@ type Executor struct {
 	// Last SchemaManager version reflected in catalog.
 	catalogVersion uint64
 
+	// pgCatalog caches the virtual PostgreSQL catalog snapshot for the current
+	// statement. It is discarded by Execute so each statement observes DDL
+	// committed (or staged) since the previous one.
+	pgCatalog *catalogSnapshot
+
+	// searchPath is the session search path used by current_schema() and SHOW
+	// search_path. Only public, pg_catalog, and information_schema are
+	// materialized; drivers may name other schemas.
+	searchPath []string
+
 	// Multi-database support
 	attachedDatabases map[string]*DatabaseConnection // alias -> connection
 	currentDatabase   string                         // current database alias (default is "main")
@@ -148,6 +158,7 @@ func New(schema *storage.SchemaManager, table *storage.TableManager) *Executor {
 		currentDatabase:   "main",
 		views:             make(map[string]*parser.SelectStmt),
 		listens:           make(map[string]bool),
+		searchPath:        []string{publicSchemaName},
 	}
 
 	// Expression indexes and CHECK predicates persist only SQL text, so the
@@ -222,10 +233,37 @@ func (e *Executor) SyncCatalog() error {
 		}
 	}
 
+	e.registerCatalogRelations()
 	e.catalogVersion = e.session.Version()
 	return nil
 }
 
+// registerCatalogRelations adds the read-only pg_catalog and information_schema
+// relations to the analyzer catalog. Qualified names are always registered;
+// a bare pg_catalog name is registered only when no durable table shadows it,
+// matching PostgreSQL's implicit pg_catalog search path.
+func (e *Executor) registerCatalogRelations() {
+	register := func(def catalogRelationDef, name string) {
+		if e.catalog.TableExists(name) {
+			return
+		}
+		entry := pgCatalogRelationIndex[def.schema+"."+strings.ToLower(def.name)]
+		info := entry.schema.ToAnalyzerTableInfo()
+		info.Name = name
+		info.IsView = true
+		_ = e.catalog.CreateTable(info)
+	}
+	for _, def := range pgCatalogRelationDefs {
+		register(def, def.schema+"."+def.name)
+	}
+	for _, def := range informationSchemaRelationDefs {
+		register(def, def.schema+"."+def.name)
+	}
+	for _, def := range pgCatalogRelationDefs {
+		register(def, def.name)
+	}
+}
+
 // Execute executes a SQL statement.
 func (e *Executor) Execute(stmt parser.Statement) (*Result, error) {
 	if _, beginning := stmt.(*parser.BeginStmt); !beginning && !e.inTransaction {
@@ -237,12 +275,17 @@ func (e *Executor) Execute(stmt parser.Statement) (*Result, error) {
 	e.arrayCache = make(map[parser.Expr]*arrayParseEntry)
 	e.arrayValueCache = make(map[parser.Expr]interface{})
 	e.arrayCanonicalCache = make(map[parser.Expr]*arrayCanonicalEntry)
+	// The virtual catalog snapshot is statement-scoped: DDL executed by this
+	// statement (or committed since the last one) must be visible to the next
+	// catalog read.
+	e.pgCatalog = nil
 	defer func() {
 		e.subqueryCache = nil
 		e.correlatedAggCache = nil
 		e.arrayCache = nil
 		e.arrayValueCache = nil
 		e.arrayCanonicalCache = nil
+		e.pgCatalog = nil
 	}()
 
 	// PRAGMA doesn't need analysis. SQLite catalog-introspection pragmas
@@ -260,6 +303,22 @@ func (e *Executor) Execute(stmt parser.Statement) (*Result, error) {
 		return e.executeExplain(explain)
 	}
 
+	// Session settings and SHOW are acknowledged without analysis. SET is
+	// accepted for driver/ORM compatibility; only search_path is retained.
+	switch s := stmt.(type) {
+	case *parser.SetStmt:
+		return e.executeSet(s)
+	case *parser.ShowStmt:
+		return e.executeShow(s)
+	}
+
+	// Catalog relations are read-only virtual relations. Reject writes with a
+	// clear feature-not-supported error instead of letting them fall through to
+	// a confusing table-not-found from storage.
+	if err := e.rejectCatalogWriteTarget(stmt); err != nil {
+		return nil, err
+	}
+
 	// Transaction statements don't need analysis
 	switch s := stmt.(type) {
 	case *parser.BeginStmt:
@@ -461,6 +520,13 @@ func (e *Executor) executeSelect(stmt *parser.SelectStmt) (*Result, error) {
 		return e.executeSelectFromTableFunction(stmt)
 	}
 
+	// PostgreSQL catalog relations are materialized virtual relations. Answer
+	// any SELECT whose FROM reads one of them before the storage path, which
+	// would otherwise reject the relation as unknown.
+	if e.fromReferencesCatalogRelation(stmt.From) {
+		return e.executeSelectOnCatalogRelations(stmt)
+	}
+
 	// Non-recursive CTEs are desugared to derived tables by the parser. Preserve
 	// every entry in a comma-separated list instead of dispatching solely on the
 	// first derived table.
@@ -2331,6 +2397,22 @@ func (e *Executor) executeJoinsWithMode(tableRef parser.TableRef, leftRows []sto
 		rightSchema = schemaFromColumns(cte.columns)
 		e.recordJoinColumns(rightAlias, cte.columns, cte.types)
 	default:
+		if entry, ok := e.catalogRelationFor(*rightTableRef); ok {
+			rightSchema = entry.schema
+			snapshot, snapErr := e.catalogSnapshotForQuery()
+			if snapErr != nil {
+				return nil, snapErr
+			}
+			rightRows = cloneRows(snapshot.relationRows(entry.def.schema + "." + entry.def.name))
+			columns := make([]string, len(rightSchema.Columns))
+			types := make([]string, len(rightSchema.Columns))
+			for i, column := range rightSchema.Columns {
+				columns[i] = column.Name
+				types[i] = column.Type
+			}
+			e.recordJoinColumns(rightAlias, columns, types)
+			break
+		}
 		rightSchema, err = e.session.GetSchema(rightTable)
 		if err != nil {
 			return nil, err
@@ -2473,6 +2555,21 @@ func (e *Executor) executeJoin(tableRef parser.TableRef, leftRows []storage.Row)
 		rightRows = cloneRows(cte.rows)
 		e.recordJoinColumns(rightQualifier, cte.columns, cte.types)
 	default:
+		if entry, ok := e.catalogRelationFor(*join.Table); ok {
+			snapshot, snapErr := e.catalogSnapshotForQuery()
+			if snapErr != nil {
+				return nil, snapErr
+			}
+			rightRows = cloneRows(snapshot.relationRows(entry.def.schema + "." + entry.def.name))
+			columns := make([]string, len(entry.schema.Columns))
+			types := make([]string, len(entry.schema.Columns))
+			for i, column := range entry.schema.Columns {
+				columns[i] = column.Name
+				types[i] = column.Type
+			}
+			e.recordJoinColumns(rightQualifier, columns, types)
+			break
+		}
 		rightRows, err = e.session.Select(rightTable, nil)
 	}
 	if err != nil {
@@ -2659,6 +2756,10 @@ func makeNullRow(rightRows []storage.Row, rightTable string, e *Executor) storag
 			for _, col := range rightSchema.Columns {
 				nullRight[col.Name] = nil
 			}
+		} else if schema, ok := e.catalogSchemaByName(rightTable); ok {
+			for _, col := range schema.Columns {
+				nullRight[col.Name] = nil
+			}
 		}
 	}
 	return nullRight
@@ -5968,6 +6069,9 @@ func (e *Executor) evalFunctionCall(fn *parser.FunctionCall, row storage.Row) (i
 	if val, handled, err := evalJSONFunction(name, args); handled {
 		return val, err
 	}
+	if val, handled, err := e.evalCatalogFunction(name, fn, args); handled {
+		return val, err
+	}
 
 	switch name {
 	case "UPPER":
@@ -6245,6 +6349,8 @@ func (e *Executor) evalFunctionCall(fn *parser.FunctionCall, row storage.Row) (i
 		return e.evalArrayLength(fn.Args[0], args)
 	case "UNNEST":
 		return nil, pgerror.Unsupported("unnest() is only supported as a table function in FROM, as in SELECT * FROM unnest(array)")
+	case "VERSION":
+		return "PostgreSQL " + version.PostgreSQLCompatVersion + " (PizzaSQL)", nil
 	case "PIZZASQL_VERSION":
 		return version.String(), nil
 	case "SQLITE_VERSION":
@@ -6567,6 +6673,9 @@ func (e *Executor) evalCastExpr(expr *parser.CastExpr, row storage.Row) (interfa
 	if expr.Type.Array {
 		return evalArrayCast(expr.Type, val)
 	}
+	if cast, handled, err := e.catalogCast(expr, val); handled {
+		return cast, err
+	}
 
 	typeName := strings.ToUpper(expr.Type.Name)
 	if typeName == "JSONB" {
@@ -7905,7 +8014,7 @@ func (e *Executor) evalIntExpr(expr parser.Expr) int {
 
 func isIntVal(v interface{}) bool {
 	switch v.(type) {
-	case int64, int, bool:
+	case int64, int, bool, RegOID:
 		return true
 	default:
 		return false
@@ -7920,6 +8029,8 @@ func toInt64(v interface{}) int64 {
 		return int64(val)
 	case float64:
 		return int64(val)
+	case RegOID:
+		return val.oid
 	case bool:
 		if val {
 			return 1
@@ -7997,6 +8108,8 @@ func toString(v interface{}) string {
 		return string(t)
 	case jsonText:
 		return string(t)
+	case RegOID:
+		return t.name
 	case bool:
 		if t {
 			return "1"
@@ -8057,6 +8170,8 @@ func toNumeric(v interface{}) (float64, bool) {
 		return float64(val), true
 	case float64:
 		return val, true
+	case RegOID:
+		return float64(val.oid), true
 	case string:
 		f, err := strconv.ParseFloat(val, 64)
 		return f, err == nil

+ 2500 - 0
pkg/executor/pg_catalog.go

@@ -0,0 +1,2500 @@
+package executor
+
+import (
+	"fmt"
+	"sort"
+	"strconv"
+	"strings"
+
+	"github.com/danfragoso/pizzasql-next/pkg/parser"
+	"github.com/danfragoso/pizzasql-next/pkg/pgerror"
+	"github.com/danfragoso/pizzasql-next/pkg/storage"
+)
+
+// PostgreSQL catalog emulation.
+//
+// pg_catalog and information_schema are synthesized from the durable schema the
+// connection's session observes (including DDL staged by an open transaction).
+// Nothing is persisted and no virtual relation accepts writes. OIDs are
+// deterministic: a 32-bit FNV-1a hash of the namespace-qualified name, lifted
+// above PostgreSQL's FirstNormalObjectId and collision-probed in sorted order.
+// The same allocator feeds pg_class, pg_attribute, pg_type, pg_index, and
+// pg_constraint, so catalog joins agree.
+//
+// The emulated schemas are pg_catalog (OID 11), public (OID 2200), and
+// information_schema (OID 13207). User relations always live in public.
+
+const (
+	pgCatalogSchemaName    = "pg_catalog"
+	informationSchemaName  = "information_schema"
+	publicSchemaName       = "public"
+	pgCatalogNamespaceOID  = int64(11)
+	publicNamespaceOID     = int64(2200)
+	infoSchemaNamespaceOID = int64(13207)
+
+	firstNormalObjectID = int64(16384)
+
+	// Built-in OIDs the emulation refers to.
+	pgDefaultTablespaceOID = int64(1663)
+	pgGlobalTablespaceOID  = int64(1664)
+	pgBtreeAMOID           = int64(403)
+	pgHeapAMOID            = int64(2)
+	catalogOwnerOID        = int64(10)
+)
+
+// catalogRelationDef declares a virtual relation's column list.
+type catalogRelationDef struct {
+	schema  string
+	name    string
+	columns []catalogColumnDef
+}
+
+type catalogColumnDef struct {
+	name string
+	typ  string
+}
+
+// pgCatalogRelationDefs lists the read-only pg_catalog relations.
+var pgCatalogRelationDefs = []catalogRelationDef{
+	{schema: pgCatalogSchemaName, name: "pg_namespace", columns: []catalogColumnDef{
+		{"oid", "OID"}, {"nspname", "NAME"}, {"nspowner", "OID"}, {"nspacl", "TEXT"},
+	}},
+	{schema: pgCatalogSchemaName, name: "pg_class", columns: []catalogColumnDef{
+		{"oid", "OID"}, {"relname", "NAME"}, {"relnamespace", "OID"}, {"reltype", "OID"},
+		{"reloftype", "OID"}, {"relowner", "OID"}, {"relam", "OID"}, {"relfilenode", "OID"},
+		{"reltablespace", "OID"}, {"relpages", "INT4"}, {"reltuples", "FLOAT4"},
+		{"relallvisible", "INT4"}, {"reltoastrelid", "OID"}, {"relhasindex", "BOOL"},
+		{"relisshared", "BOOL"}, {"relpersistence", "CHAR"}, {"relkind", "CHAR"},
+		{"relnatts", "INT2"}, {"relchecks", "INT2"}, {"relhasrules", "BOOL"},
+		{"relhastriggers", "BOOL"}, {"relhassubclass", "BOOL"}, {"relrowsecurity", "BOOL"},
+		{"relforcerowsecurity", "BOOL"}, {"relispopulated", "BOOL"}, {"relreplident", "CHAR"},
+		{"relispartition", "BOOL"}, {"relrewrite", "OID"}, {"relfrozenxid", "OID"},
+		{"relminmxid", "OID"}, {"relacl", "TEXT"}, {"reloptions", "TEXT"},
+		{"relpartbound", "TEXT"},
+	}},
+	{schema: pgCatalogSchemaName, name: "pg_attribute", columns: []catalogColumnDef{
+		{"attrelid", "OID"}, {"attname", "NAME"}, {"atttypid", "OID"},
+		{"attstattarget", "INT4"}, {"attlen", "INT2"}, {"attnum", "INT2"},
+		{"attndims", "INT4"}, {"attcacheoff", "INT4"}, {"atttypmod", "INT4"},
+		{"attbyval", "BOOL"}, {"attalign", "CHAR"}, {"attstorage", "CHAR"},
+		{"attcompression", "CHAR"}, {"attnotnull", "BOOL"}, {"atthasdef", "BOOL"},
+		{"atthasmissing", "BOOL"}, {"attidentity", "CHAR"}, {"attgenerated", "CHAR"},
+		{"attisdropped", "BOOL"}, {"attislocal", "BOOL"}, {"attinhcount", "INT2"},
+		{"attcollation", "OID"}, {"attacl", "TEXT"}, {"attoptions", "TEXT"},
+		{"attfdwoptions", "TEXT"}, {"attmissingval", "TEXT"},
+	}},
+	{schema: pgCatalogSchemaName, name: "pg_type", columns: []catalogColumnDef{
+		{"oid", "OID"}, {"typname", "NAME"}, {"typnamespace", "OID"},
+		{"typowner", "OID"}, {"typlen", "INT2"}, {"typbyval", "BOOL"},
+		{"typtype", "CHAR"}, {"typcategory", "CHAR"}, {"typispreferred", "BOOL"},
+		{"typisdefined", "BOOL"}, {"typdelim", "CHAR"}, {"typrelid", "OID"},
+		{"typsubscript", "TEXT"}, {"typelem", "OID"}, {"typarray", "OID"},
+		{"typinput", "TEXT"}, {"typoutput", "TEXT"}, {"typreceive", "TEXT"},
+		{"typsend", "TEXT"}, {"typmodin", "TEXT"}, {"typmodout", "TEXT"},
+		{"typanalyze", "TEXT"}, {"typalign", "CHAR"}, {"typstorage", "CHAR"},
+		{"typnotnull", "BOOL"}, {"typbasetype", "OID"}, {"typtypmod", "INT4"},
+		{"typndims", "INT4"}, {"typcollation", "OID"}, {"typdefaultbin", "TEXT"},
+		{"typdefault", "TEXT"}, {"typacl", "TEXT"},
+	}},
+	{schema: pgCatalogSchemaName, name: "pg_index", columns: []catalogColumnDef{
+		{"indexrelid", "OID"}, {"indrelid", "OID"}, {"indnatts", "INT2"},
+		{"indnkeyatts", "INT2"}, {"indisunique", "BOOL"}, {"indisprimary", "BOOL"},
+		{"indisexclusion", "BOOL"}, {"indimmediate", "BOOL"}, {"indisclustered", "BOOL"},
+		{"indisvalid", "BOOL"}, {"indcheckxmin", "BOOL"}, {"indisready", "BOOL"},
+		{"indislive", "BOOL"}, {"indisreplident", "BOOL"}, {"indkey", "INT2[]"},
+		{"indcollation", "TEXT"}, {"indclass", "TEXT"}, {"indoption", "INT2[]"},
+		{"indexprs", "TEXT"}, {"indpred", "TEXT"},
+	}},
+	{schema: pgCatalogSchemaName, name: "pg_constraint", columns: []catalogColumnDef{
+		{"oid", "OID"}, {"conname", "NAME"}, {"connamespace", "OID"},
+		{"contype", "CHAR"}, {"condeferrable", "BOOL"}, {"condeferred", "BOOL"},
+		{"convalidated", "BOOL"}, {"conrelid", "OID"}, {"contypid", "OID"},
+		{"conindid", "OID"}, {"conparentid", "OID"}, {"confrelid", "OID"},
+		{"confupdtype", "CHAR"}, {"confdeltype", "CHAR"}, {"confmatchtype", "CHAR"},
+		{"conislocal", "BOOL"}, {"coninhcount", "INT2"}, {"connoinherit", "BOOL"},
+		{"conkey", "INT2[]"}, {"confkey", "INT2[]"}, {"conpfeqop", "TEXT"},
+		{"conppeqop", "TEXT"}, {"conffeqop", "TEXT"}, {"conexclop", "TEXT"},
+		{"conbin", "TEXT"}, {"consrc", "TEXT"},
+	}},
+	{schema: pgCatalogSchemaName, name: "pg_attrdef", columns: []catalogColumnDef{
+		{"oid", "OID"}, {"adrelid", "OID"}, {"adnum", "INT2"}, {"adbin", "TEXT"},
+	}},
+	{schema: pgCatalogSchemaName, name: "pg_database", columns: []catalogColumnDef{
+		{"oid", "OID"}, {"datname", "NAME"}, {"datdba", "OID"}, {"encoding", "INT4"},
+		{"datlocprovider", "CHAR"}, {"datistemplate", "BOOL"}, {"datallowconn", "BOOL"},
+		{"datconnlimit", "INT4"}, {"datlastsysoid", "OID"}, {"datfrozenxid", "OID"},
+		{"datminmxid", "OID"}, {"dattablespace", "OID"}, {"datcollate", "TEXT"},
+		{"datctype", "TEXT"}, {"datlocale", "TEXT"}, {"daticurules", "TEXT"},
+		{"datcollversion", "TEXT"}, {"datacl", "TEXT"},
+	}},
+	{schema: pgCatalogSchemaName, name: "pg_tablespace", columns: []catalogColumnDef{
+		{"oid", "OID"}, {"spcname", "NAME"}, {"spcowner", "OID"}, {"spcacl", "TEXT"},
+		{"spcoptions", "TEXT"},
+	}},
+	{schema: pgCatalogSchemaName, name: "pg_proc", columns: []catalogColumnDef{
+		{"oid", "OID"}, {"proname", "NAME"}, {"pronamespace", "OID"},
+		{"proowner", "OID"}, {"prolang", "OID"}, {"procost", "FLOAT4"},
+		{"prorows", "FLOAT4"}, {"provariadic", "OID"}, {"prosupport", "TEXT"},
+		{"prokind", "CHAR"}, {"prosecdef", "BOOL"}, {"proleakproof", "BOOL"},
+		{"proisstrict", "BOOL"}, {"proretset", "BOOL"}, {"provolatile", "CHAR"},
+		{"proparallel", "CHAR"}, {"pronargs", "INT2"}, {"pronargdefaults", "INT2"},
+		{"prorettype", "OID"}, {"proargtypes", "TEXT"}, {"proallargtypes", "TEXT"},
+		{"proargmodes", "TEXT"}, {"proargnames", "TEXT"}, {"proargdefaults", "TEXT"},
+		{"protrftypes", "TEXT"}, {"prosrc", "TEXT"}, {"probin", "TEXT"},
+		{"prosqlbody", "TEXT"}, {"proconfig", "TEXT"}, {"proacl", "TEXT"},
+	}},
+	{schema: pgCatalogSchemaName, name: "pg_am", columns: []catalogColumnDef{
+		{"oid", "OID"}, {"amname", "NAME"}, {"amhandler", "OID"}, {"amtype", "CHAR"},
+	}},
+	{schema: pgCatalogSchemaName, name: "pg_description", columns: []catalogColumnDef{
+		{"objoid", "OID"}, {"classoid", "OID"}, {"objsubid", "INT4"}, {"description", "TEXT"},
+	}},
+	{schema: pgCatalogSchemaName, name: "pg_tables", columns: []catalogColumnDef{
+		{"schemaname", "NAME"}, {"tablename", "NAME"}, {"tableowner", "NAME"},
+		{"tablespace", "NAME"}, {"hasindexes", "BOOL"}, {"hasrules", "BOOL"},
+		{"hastriggers", "BOOL"}, {"rowsecurity", "BOOL"},
+	}},
+	{schema: pgCatalogSchemaName, name: "pg_indexes", columns: []catalogColumnDef{
+		{"schemaname", "NAME"}, {"tablename", "NAME"}, {"indexname", "NAME"},
+		{"tablespace", "NAME"}, {"indexdef", "TEXT"},
+	}},
+}
+
+// informationSchemaRelationDefs lists the read-only information_schema views.
+var informationSchemaRelationDefs = []catalogRelationDef{
+	{schema: informationSchemaName, name: "tables", columns: []catalogColumnDef{
+		{"table_catalog", "TEXT"}, {"table_schema", "TEXT"}, {"table_name", "TEXT"},
+		{"table_type", "TEXT"}, {"self_referencing_column_name", "TEXT"},
+		{"reference_generation", "TEXT"}, {"user_defined_type_catalog", "TEXT"},
+		{"user_defined_type_schema", "TEXT"}, {"user_defined_type_name", "TEXT"},
+		{"is_insertable_into", "TEXT"}, {"is_typed", "TEXT"}, {"commit_action", "TEXT"},
+	}},
+	{schema: informationSchemaName, name: "columns", columns: []catalogColumnDef{
+		{"table_catalog", "TEXT"}, {"table_schema", "TEXT"}, {"table_name", "TEXT"},
+		{"column_name", "TEXT"}, {"ordinal_position", "INT4"}, {"column_default", "TEXT"},
+		{"is_nullable", "TEXT"}, {"data_type", "TEXT"}, {"character_maximum_length", "INT4"},
+		{"character_octet_length", "INT4"}, {"numeric_precision", "INT4"},
+		{"numeric_precision_radix", "INT4"}, {"numeric_scale", "INT4"},
+		{"datetime_precision", "INT4"}, {"interval_type", "TEXT"},
+		{"interval_precision", "INT4"}, {"character_set_catalog", "TEXT"},
+		{"character_set_schema", "TEXT"}, {"character_set_name", "TEXT"},
+		{"collation_catalog", "TEXT"}, {"collation_schema", "TEXT"},
+		{"collation_name", "TEXT"}, {"domain_catalog", "TEXT"}, {"domain_schema", "TEXT"},
+		{"domain_name", "TEXT"}, {"udt_catalog", "TEXT"}, {"udt_schema", "TEXT"},
+		{"udt_name", "TEXT"}, {"scope_catalog", "TEXT"}, {"scope_schema", "TEXT"},
+		{"scope_name", "TEXT"}, {"maximum_cardinality", "INT4"}, {"dtd_identifier", "TEXT"},
+		{"is_self_referencing", "TEXT"}, {"is_identity", "TEXT"},
+		{"identity_generation", "TEXT"}, {"identity_start", "TEXT"},
+		{"identity_increment", "TEXT"}, {"identity_maximum", "TEXT"},
+		{"identity_minimum", "TEXT"}, {"identity_cycle", "TEXT"},
+		{"is_generated", "TEXT"}, {"generation_expression", "TEXT"},
+		{"is_updatable", "TEXT"},
+	}},
+	{schema: informationSchemaName, name: "table_constraints", columns: []catalogColumnDef{
+		{"constraint_catalog", "TEXT"}, {"constraint_schema", "TEXT"},
+		{"constraint_name", "TEXT"}, {"table_catalog", "TEXT"}, {"table_schema", "TEXT"},
+		{"table_name", "TEXT"}, {"constraint_type", "TEXT"}, {"is_deferrable", "TEXT"},
+		{"initially_deferred", "TEXT"}, {"enforced", "TEXT"}, {"nulls_distinct", "TEXT"},
+	}},
+	{schema: informationSchemaName, name: "key_column_usage", columns: []catalogColumnDef{
+		{"constraint_catalog", "TEXT"}, {"constraint_schema", "TEXT"},
+		{"constraint_name", "TEXT"}, {"table_catalog", "TEXT"}, {"table_schema", "TEXT"},
+		{"table_name", "TEXT"}, {"column_name", "TEXT"}, {"ordinal_position", "INT4"},
+		{"position_in_unique_constraint", "INT4"},
+	}},
+	{schema: informationSchemaName, name: "referential_constraints", columns: []catalogColumnDef{
+		{"constraint_catalog", "TEXT"}, {"constraint_schema", "TEXT"},
+		{"constraint_name", "TEXT"}, {"unique_constraint_catalog", "TEXT"},
+		{"unique_constraint_schema", "TEXT"}, {"unique_constraint_name", "TEXT"},
+		{"match_option", "TEXT"}, {"update_rule", "TEXT"}, {"delete_rule", "TEXT"},
+		{"is_deferrable", "TEXT"}, {"initially_deferred", "TEXT"}, {"enforced", "TEXT"},
+	}},
+	{schema: informationSchemaName, name: "constraint_column_usage", columns: []catalogColumnDef{
+		{"table_catalog", "TEXT"}, {"table_schema", "TEXT"}, {"table_name", "TEXT"},
+		{"constraint_catalog", "TEXT"}, {"constraint_schema", "TEXT"},
+		{"constraint_name", "TEXT"}, {"column_name", "TEXT"},
+	}},
+	{schema: informationSchemaName, name: "schemata", columns: []catalogColumnDef{
+		{"catalog_name", "TEXT"}, {"schema_name", "TEXT"}, {"schema_owner", "TEXT"},
+		{"default_character_set_catalog", "TEXT"},
+		{"default_character_set_schema", "TEXT"},
+		{"default_character_set_name", "TEXT"}, {"sql_path", "TEXT"},
+	}},
+	{schema: informationSchemaName, name: "sequences", columns: []catalogColumnDef{
+		{"sequence_catalog", "TEXT"}, {"sequence_schema", "TEXT"},
+		{"sequence_name", "TEXT"}, {"data_type", "TEXT"},
+		{"numeric_precision", "INT4"}, {"numeric_precision_radix", "INT4"},
+		{"numeric_scale", "INT4"}, {"start_value", "TEXT"}, {"minimum_value", "TEXT"},
+		{"maximum_value", "TEXT"}, {"increment", "TEXT"}, {"cycle_option", "TEXT"},
+	}},
+	{schema: informationSchemaName, name: "views", columns: []catalogColumnDef{
+		{"table_catalog", "TEXT"}, {"table_schema", "TEXT"}, {"table_name", "TEXT"},
+		{"view_definition", "TEXT"}, {"check_option", "TEXT"}, {"is_updatable", "TEXT"},
+		{"is_insertable_into", "TEXT"}, {"is_trigger_updatable", "TEXT"},
+		{"is_trigger_deletable", "TEXT"}, {"is_trigger_insertable_into", "TEXT"},
+	}},
+}
+
+// catalogRelationSchema builds the storage schema that the analyzer and the
+// materialized SELECT path use for a virtual relation.
+func catalogRelationSchema(def catalogRelationDef) *storage.Schema {
+	schema := &storage.Schema{Name: def.name, Columns: make([]storage.Column, len(def.columns))}
+	for i, column := range def.columns {
+		schema.Columns[i] = storage.Column{Name: column.name, Type: column.typ, Nullable: true}
+	}
+	return schema
+}
+
+var pgCatalogRelationIndex = buildCatalogRelationIndex()
+
+type catalogRelationEntry struct {
+	def    catalogRelationDef
+	schema *storage.Schema
+}
+
+func buildCatalogRelationIndex() map[string]catalogRelationEntry {
+	index := make(map[string]catalogRelationEntry, len(pgCatalogRelationDefs)+len(informationSchemaRelationDefs))
+	for _, def := range append(append([]catalogRelationDef(nil), pgCatalogRelationDefs...), informationSchemaRelationDefs...) {
+		index[def.schema+"."+strings.ToLower(def.name)] = catalogRelationEntry{def: def, schema: catalogRelationSchema(def)}
+	}
+	return index
+}
+
+// catalogRelationFor resolves a possibly schema-qualified table reference to a
+// virtual catalog relation. Unqualified names resolve to pg_catalog only when
+// no durable table shadows them, matching PostgreSQL's implicit search_path.
+func (e *Executor) catalogRelationFor(ref parser.TableRef) (catalogRelationEntry, bool) {
+	if ref.Subquery != nil || ref.Function != nil {
+		return catalogRelationEntry{}, false
+	}
+	name := strings.ToLower(ref.Name)
+	switch strings.ToLower(ref.Schema) {
+	case pgCatalogSchemaName:
+		entry, ok := pgCatalogRelationIndex[pgCatalogSchemaName+"."+name]
+		return entry, ok
+	case informationSchemaName:
+		entry, ok := pgCatalogRelationIndex[informationSchemaName+"."+name]
+		return entry, ok
+	case "", publicSchemaName:
+		if e.session.TableExists(ref.Name) {
+			return catalogRelationEntry{}, false
+		}
+		entry, ok := pgCatalogRelationIndex[pgCatalogSchemaName+"."+name]
+		return entry, ok
+	default:
+		return catalogRelationEntry{}, false
+	}
+}
+
+// isCatalogRelationRef reports whether a FROM entry names a virtual relation.
+func (e *Executor) isCatalogRelationRef(ref parser.TableRef) bool {
+	_, ok := e.catalogRelationFor(ref)
+	return ok
+}
+
+// fromReferencesCatalogRelation walks a FROM clause, including JOIN chains.
+func (e *Executor) fromReferencesCatalogRelation(from []parser.TableRef) bool {
+	for _, ref := range from {
+		cur := ref
+		for {
+			if e.isCatalogRelationRef(cur) {
+				return true
+			}
+			if cur.Join == nil || cur.Join.Table == nil {
+				break
+			}
+			cur = *cur.Join.Table
+		}
+	}
+	return false
+}
+
+// ---------------------------------------------------------------------------
+// Deterministic OIDs
+
+func fnv1a32(text string) uint32 {
+	hash := uint32(2166136261)
+	for i := 0; i < len(text); i++ {
+		hash ^= uint32(text[i])
+		hash *= 16777619
+	}
+	return hash
+}
+
+// allocateOID maps a kind and fully qualified name to a stable, positive OID.
+// The first probe is the FNV-1a hash lifted into [firstNormalObjectID,
+// MaxInt32]; collisions are resolved by linear probing in the caller's sorted
+// order, so the assignment is deterministic for a given schema.
+func (s *catalogSnapshot) allocateOID(kind, key string) int64 {
+	owner := kind + ":" + key
+	oid := int64(fnv1a32("pizzasql:" + owner))
+	oid &= 0x7fffffff
+	if oid < firstNormalObjectID {
+		oid += firstNormalObjectID
+	}
+	for {
+		if existing, used := s.oidOwners[oid]; !used || existing == owner {
+			s.oidOwners[oid] = owner
+			return oid
+		}
+		oid++
+		if oid > 0x7fffffff {
+			oid = firstNormalObjectID
+		}
+	}
+}
+
+// ---------------------------------------------------------------------------
+// Built-in type table
+
+type pgBuiltinType struct {
+	oid      int64
+	name     string // pg_type.typname
+	sqlName  string // format_type / information_schema spelling
+	length   int64
+	byval    bool
+	category string
+	delim    string
+	elem     int64
+	array    int64
+	align    string
+	storage  string
+}
+
+func (t pgBuiltinType) isArray() bool { return t.category == "A" }
+
+var pgBuiltinTypes = []pgBuiltinType{
+	{oid: 16, name: "bool", sqlName: "boolean", length: 1, byval: true, category: "B", delim: ",", array: 1000, align: "c", storage: "p"},
+	{oid: 17, name: "bytea", sqlName: "bytea", length: -1, category: "U", delim: ",", array: 1001, align: "i", storage: "x"},
+	{oid: 18, name: "char", sqlName: "\"char\"", length: 1, byval: true, category: "S", delim: ",", array: 1002, align: "c", storage: "p"},
+	{oid: 19, name: "name", sqlName: "name", length: 64, category: "S", delim: ",", array: 1003, align: "c", storage: "p"},
+	{oid: 20, name: "int8", sqlName: "bigint", length: 8, byval: true, category: "N", delim: ",", array: 1016, align: "d", storage: "p"},
+	{oid: 21, name: "int2", sqlName: "smallint", length: 2, byval: true, category: "N", delim: ",", array: 1005, align: "s", storage: "p"},
+	{oid: 23, name: "int4", sqlName: "integer", length: 4, byval: true, category: "N", delim: ",", array: 1007, align: "i", storage: "p"},
+	{oid: 25, name: "text", sqlName: "text", length: -1, category: "S", delim: ",", array: 1009, align: "i", storage: "x"},
+	{oid: 26, name: "oid", sqlName: "oid", length: 4, byval: true, category: "N", delim: ",", array: 1028, align: "i", storage: "p"},
+	{oid: 114, name: "json", sqlName: "json", length: -1, category: "U", delim: ",", array: 199, align: "i", storage: "x"},
+	{oid: 700, name: "float4", sqlName: "real", length: 4, byval: true, category: "N", delim: ",", array: 1021, align: "i", storage: "p"},
+	{oid: 701, name: "float8", sqlName: "double precision", length: 8, byval: true, category: "N", delim: ",", array: 1022, align: "d", storage: "p"},
+	{oid: 1042, name: "bpchar", sqlName: "character", length: -1, category: "S", delim: ",", array: 1014, align: "i", storage: "x"},
+	{oid: 1043, name: "varchar", sqlName: "character varying", length: -1, category: "S", delim: ",", array: 1015, align: "i", storage: "x"},
+	{oid: 1082, name: "date", sqlName: "date", length: 4, byval: true, category: "D", delim: ",", array: 1182, align: "i", storage: "p"},
+	{oid: 1083, name: "time", sqlName: "time without time zone", length: 8, byval: true, category: "D", delim: ",", array: 1183, align: "d", storage: "p"},
+	{oid: 1184, name: "timestamptz", sqlName: "timestamp with time zone", length: 8, byval: true, category: "D", delim: ",", array: 1185, align: "d", storage: "p"},
+	{oid: 1700, name: "numeric", sqlName: "numeric", length: -1, category: "N", delim: ",", array: 1231, align: "i", storage: "x"},
+	{oid: 2205, name: "regclass", sqlName: "regclass", length: 4, byval: true, category: "N", delim: ",", array: 2210, align: "i", storage: "p"},
+	{oid: 2206, name: "regtype", sqlName: "regtype", length: 4, byval: true, category: "N", delim: ",", array: 2211, align: "i", storage: "p"},
+	{oid: 2950, name: "uuid", sqlName: "uuid", length: 16, category: "U", delim: ",", array: 2951, align: "c", storage: "p"},
+	{oid: 3802, name: "jsonb", sqlName: "jsonb", length: -1, category: "U", delim: ",", array: 3807, align: "i", storage: "x"},
+
+	// Array types for the supported element types.
+	{oid: 199, name: "_json", sqlName: "json[]", length: -1, category: "A", delim: ",", elem: 114, align: "i", storage: "x"},
+	{oid: 1000, name: "_bool", sqlName: "boolean[]", length: -1, category: "A", delim: ",", elem: 16, align: "i", storage: "x"},
+	{oid: 1001, name: "_bytea", sqlName: "bytea[]", length: -1, category: "A", delim: ",", elem: 17, align: "i", storage: "x"},
+	{oid: 1002, name: "_char", sqlName: "\"char\"[]", length: -1, category: "A", delim: ",", elem: 18, align: "i", storage: "x"},
+	{oid: 1003, name: "_name", sqlName: "name[]", length: -1, category: "A", delim: ",", elem: 19, align: "i", storage: "x"},
+	{oid: 1005, name: "_int2", sqlName: "smallint[]", length: -1, category: "A", delim: ",", elem: 21, align: "i", storage: "x"},
+	{oid: 1007, name: "_int4", sqlName: "integer[]", length: -1, category: "A", delim: ",", elem: 23, align: "i", storage: "x"},
+	{oid: 1009, name: "_text", sqlName: "text[]", length: -1, category: "A", delim: ",", elem: 25, align: "i", storage: "x"},
+	{oid: 1014, name: "_bpchar", sqlName: "character[]", length: -1, category: "A", delim: ",", elem: 1042, align: "i", storage: "x"},
+	{oid: 1015, name: "_varchar", sqlName: "character varying[]", length: -1, category: "A", delim: ",", elem: 1043, align: "i", storage: "x"},
+	{oid: 1016, name: "_int8", sqlName: "bigint[]", length: -1, category: "A", delim: ",", elem: 20, align: "i", storage: "x"},
+	{oid: 1021, name: "_float4", sqlName: "real[]", length: -1, category: "A", delim: ",", elem: 700, align: "i", storage: "x"},
+	{oid: 1022, name: "_float8", sqlName: "double precision[]", length: -1, category: "A", delim: ",", elem: 701, align: "i", storage: "x"},
+	{oid: 1028, name: "_oid", sqlName: "oid[]", length: -1, category: "A", delim: ",", elem: 26, align: "i", storage: "x"},
+	{oid: 1182, name: "_date", sqlName: "date[]", length: -1, category: "A", delim: ",", elem: 1082, align: "i", storage: "x"},
+	{oid: 1183, name: "_time", sqlName: "time without time zone[]", length: -1, category: "A", delim: ",", elem: 1083, align: "i", storage: "x"},
+	{oid: 1185, name: "_timestamptz", sqlName: "timestamp with time zone[]", length: -1, category: "A", delim: ",", elem: 1184, align: "i", storage: "x"},
+	{oid: 1231, name: "_numeric", sqlName: "numeric[]", length: -1, category: "A", delim: ",", elem: 1700, align: "i", storage: "x"},
+	{oid: 2210, name: "_regclass", sqlName: "regclass[]", length: -1, category: "A", delim: ",", elem: 2205, align: "i", storage: "x"},
+	{oid: 2211, name: "_regtype", sqlName: "regtype[]", length: -1, category: "A", delim: ",", elem: 2206, align: "i", storage: "x"},
+	{oid: 2951, name: "_uuid", sqlName: "uuid[]", length: -1, category: "A", delim: ",", elem: 2950, align: "i", storage: "x"},
+	{oid: 3807, name: "_jsonb", sqlName: "jsonb[]", length: -1, category: "A", delim: ",", elem: 3802, align: "i", storage: "x"},
+}
+
+var pgBuiltinTypeByOID = func() map[int64]pgBuiltinType {
+	index := make(map[int64]pgBuiltinType, len(pgBuiltinTypes))
+	for _, t := range pgBuiltinTypes {
+		index[t.oid] = t
+	}
+	return index
+}()
+
+var pgBuiltinTypeByName = func() map[string]pgBuiltinType {
+	index := make(map[string]pgBuiltinType, len(pgBuiltinTypes))
+	for _, t := range pgBuiltinTypes {
+		index[t.name] = t
+	}
+	return index
+}()
+
+// pgTypeForDeclaredName maps an engine column type ("INTEGER", "TEXT[]", ...)
+// to its PostgreSQL type row. Unknown names report TEXT rather than claiming a
+// type the engine does not have.
+func pgTypeForDeclaredName(name string) pgBuiltinType {
+	upper := strings.ToUpper(strings.TrimSpace(name))
+	if idx := strings.IndexByte(upper, '('); idx >= 0 {
+		upper = strings.TrimSpace(upper[:idx])
+	}
+	if strings.HasSuffix(upper, "[]") {
+		element := strings.TrimSpace(strings.TrimSuffix(upper, "[]"))
+		base := pgTypeForDeclaredName(element)
+		if array, ok := pgBuiltinTypeByOID[base.array]; ok && base.array != 0 {
+			return array
+		}
+		if fallback, ok := pgBuiltinTypeByOID[1009]; ok {
+			return fallback
+		}
+	}
+	switch upper {
+	case "INTEGER", "INT", "INT4":
+		return pgBuiltinTypeByOID[23]
+	case "BIGINT", "INT8":
+		return pgBuiltinTypeByOID[20]
+	case "SMALLINT", "INT2", "TINYINT", "MEDIUMINT":
+		return pgBuiltinTypeByOID[21]
+	case "TEXT", "CLOB", "NCHAR", "NVARCHAR":
+		return pgBuiltinTypeByOID[25]
+	case "VARCHAR", "CHARACTER VARYING":
+		return pgBuiltinTypeByOID[1043]
+	case "CHAR", "CHARACTER":
+		return pgBuiltinTypeByOID[1042]
+	case "BOOLEAN", "BOOL":
+		return pgBuiltinTypeByOID[16]
+	case "REAL", "FLOAT", "FLOAT4":
+		return pgBuiltinTypeByOID[700]
+	case "DOUBLE", "DOUBLE PRECISION", "FLOAT8":
+		return pgBuiltinTypeByOID[701]
+	case "NUMERIC", "DECIMAL":
+		return pgBuiltinTypeByOID[1700]
+	case "JSONB":
+		return pgBuiltinTypeByOID[3802]
+	case "JSON":
+		return pgBuiltinTypeByOID[114]
+	case "BLOB", "BYTEA":
+		return pgBuiltinTypeByOID[17]
+	case "DATETIME", "TIMESTAMP", "TIMESTAMPTZ", "TIMESTAMP WITH TIME ZONE", "TIMESTAMP WITHOUT TIME ZONE":
+		return pgBuiltinTypeByOID[1184]
+	case "DATE":
+		return pgBuiltinTypeByOID[1082]
+	case "TIME", "TIME WITHOUT TIME ZONE", "TIME WITH TIME ZONE":
+		return pgBuiltinTypeByOID[1083]
+	case "UUID":
+		return pgBuiltinTypeByOID[2950]
+	case "OID":
+		return pgBuiltinTypeByOID[26]
+	case "NAME":
+		return pgBuiltinTypeByOID[19]
+	case "REGCLASS":
+		return pgBuiltinTypeByOID[2205]
+	case "REGTYPE":
+		return pgBuiltinTypeByOID[2206]
+	default:
+		return pgBuiltinTypeByOID[25]
+	}
+}
+
+// ---------------------------------------------------------------------------
+// Snapshot
+
+type catalogRelation struct {
+	schemaName string
+	name       string
+	oid        int64
+	kind       byte // 'r' table, 'i' index, 'v' view / virtual relation
+	columns    []storage.Column
+	table      *storage.Schema
+	index      *storage.Index
+	// primaryIndex marks the synthetic index that backs a PRIMARY KEY. The
+	// engine does not persist an index object for the key, but PostgreSQL tools
+	// expect pg_index/pg_constraint to point at one.
+	primaryIndex bool
+}
+
+type catalogConstraint struct {
+	oid      int64
+	name     string
+	table    *catalogRelation
+	contype  byte
+	indexOID int64
+	columns  []string // child columns (PK/UNIQUE/FK) or empty for CHECK
+	fk       *storage.ForeignKeyConstraint
+	check    *storage.CheckConstraint
+}
+
+type catalogSnapshot struct {
+	database string
+
+	relations     []*catalogRelation
+	relationByKey map[string]*catalogRelation
+	relationByOID map[int64]*catalogRelation
+	tableByName   map[string]*catalogRelation
+	tableIndexes  map[string][]*catalogRelation
+
+	constraints     []*catalogConstraint
+	constraintByOID map[int64]*catalogConstraint
+
+	rows map[string][]storage.Row
+
+	views []string
+
+	// Preallocated OIDs keep every catalog relation deterministic regardless of
+	// the order in which row sets are materialized.
+	attrdefOIDs map[string]int64
+	databaseOID int64
+	procOIDs    map[string]int64
+
+	oidOwners map[int64]string
+}
+
+// catalogSnapshotForQuery builds (once per statement) and returns the catalog
+// snapshot the statement observes.
+func (e *Executor) catalogSnapshotForQuery() (*catalogSnapshot, error) {
+	if e.schema == nil || e.session == nil {
+		return nil, pgerror.Unsupported("catalog introspection is not available in a stored expression")
+	}
+	if e.pgCatalog != nil {
+		return e.pgCatalog, nil
+	}
+	snapshot, err := buildCatalogSnapshot(e)
+	if err != nil {
+		return nil, err
+	}
+	e.pgCatalog = snapshot
+	return snapshot, nil
+}
+
+// visibleCatalogColumns drops the engine's hidden synthetic rowid column, which
+// is not a user-visible PostgreSQL attribute.
+func visibleCatalogColumns(schema *storage.Schema) []storage.Column {
+	columns := make([]storage.Column, 0, len(schema.Columns))
+	for _, column := range schema.Columns {
+		if schema.UsesSyntheticRowID() && column.Name == "_rowid_" {
+			continue
+		}
+		columns = append(columns, column)
+	}
+	return columns
+}
+
+func indexCatalogColumns(index *storage.Index) []storage.Column {
+	columns := make([]storage.Column, len(index.Columns))
+	for i, column := range index.Columns {
+		name := column.Name
+		if name == "" {
+			name = fmt.Sprintf("expr%d", i+1)
+		}
+		columns[i] = storage.Column{Name: name, Type: "TEXT", Nullable: true}
+	}
+	return columns
+}
+
+func relationNamespaceOID(schemaName string) int64 {
+	switch schemaName {
+	case pgCatalogSchemaName:
+		return pgCatalogNamespaceOID
+	case informationSchemaName:
+		return infoSchemaNamespaceOID
+	default:
+		return publicNamespaceOID
+	}
+}
+
+// uniqueCatalogName returns base, or base with a numeric suffix, so a synthetic
+// relation name never collides with a durable index name.
+func uniqueCatalogName(base string, used map[string]bool) string {
+	name := base
+	for suffix := 2; used[strings.ToLower(name)]; suffix++ {
+		name = fmt.Sprintf("%s%d", base, suffix)
+	}
+	used[strings.ToLower(name)] = true
+	return name
+}
+
+func buildCatalogSnapshot(e *Executor) (*catalogSnapshot, error) {
+	snapshot := &catalogSnapshot{
+		database:        e.schema.GetDatabaseName(),
+		relationByKey:   make(map[string]*catalogRelation),
+		relationByOID:   make(map[int64]*catalogRelation),
+		tableByName:     make(map[string]*catalogRelation),
+		tableIndexes:    make(map[string][]*catalogRelation),
+		constraintByOID: make(map[int64]*catalogConstraint),
+		rows:            make(map[string][]storage.Row),
+		views:           sortedViewNames(e.views),
+		attrdefOIDs:     make(map[string]int64),
+		procOIDs:        make(map[string]int64),
+		oidOwners:       make(map[int64]string),
+	}
+	if snapshot.database == "" {
+		snapshot.database = "pizzasql"
+	}
+
+	tables, err := e.session.ListTables()
+	if err != nil {
+		return nil, err
+	}
+	sort.Strings(tables)
+
+	indexNames, err := e.session.ListIndexes()
+	if err != nil {
+		return nil, err
+	}
+	sort.Strings(indexNames)
+
+	var descriptors []*catalogRelation
+	for _, tableName := range tables {
+		schema, err := e.session.GetSchema(tableName)
+		if err != nil {
+			return nil, fmt.Errorf("pg_catalog: resolve table %q: %w", tableName, err)
+		}
+		descriptors = append(descriptors, &catalogRelation{
+			schemaName: publicSchemaName,
+			name:       schema.Name,
+			kind:       'r',
+			columns:    visibleCatalogColumns(schema),
+			table:      schema,
+		})
+	}
+
+	usedNames := make(map[string]bool, len(indexNames))
+	for _, indexName := range indexNames {
+		index, err := e.session.GetIndex(indexName)
+		if err != nil {
+			return nil, fmt.Errorf("pg_catalog: resolve index %q: %w", indexName, err)
+		}
+		usedNames[strings.ToLower(index.Name)] = true
+		descriptors = append(descriptors, &catalogRelation{
+			schemaName: publicSchemaName,
+			name:       index.Name,
+			kind:       'i',
+			columns:    indexCatalogColumns(index),
+			index:      index,
+		})
+	}
+
+	// One synthetic index per primary key so pg_index/pg_constraint have a
+	// stable conindid. Composite keys keep their declared order.
+	for _, tableName := range tables {
+		schema, err := e.session.GetSchema(tableName)
+		if err != nil {
+			return nil, err
+		}
+		// A table without a declared PRIMARY KEY is addressed by the engine's
+		// synthetic _rowid_ column, which is not a PostgreSQL attribute and has
+		// no index.
+		if schema.UsesSyntheticRowID() {
+			continue
+		}
+		keyColumns := schema.PrimaryKeyList()
+		if len(keyColumns) == 0 {
+			continue
+		}
+		index := &storage.Index{Name: uniqueCatalogName(schema.Name+"_pkey", usedNames), Table: schema.Name, Unique: true}
+		for _, column := range keyColumns {
+			index.Columns = append(index.Columns, storage.IndexColumn{Name: column})
+		}
+		descriptors = append(descriptors, &catalogRelation{
+			schemaName:   publicSchemaName,
+			name:         index.Name,
+			kind:         'i',
+			columns:      indexCatalogColumns(index),
+			index:        index,
+			primaryIndex: true,
+		})
+	}
+
+	for _, def := range pgCatalogRelationDefs {
+		descriptors = append(descriptors, &catalogRelation{
+			schemaName: def.schema,
+			name:       def.name,
+			kind:       'v',
+			columns:    pgCatalogRelationIndex[def.schema+"."+def.name].schema.Columns,
+		})
+	}
+	for _, def := range informationSchemaRelationDefs {
+		descriptors = append(descriptors, &catalogRelation{
+			schemaName: def.schema,
+			name:       def.name,
+			kind:       'v',
+			columns:    pgCatalogRelationIndex[def.schema+"."+def.name].schema.Columns,
+		})
+	}
+
+	sort.SliceStable(descriptors, func(i, j int) bool {
+		if descriptors[i].schemaName != descriptors[j].schemaName {
+			return descriptors[i].schemaName < descriptors[j].schemaName
+		}
+		if descriptors[i].name != descriptors[j].name {
+			return descriptors[i].name < descriptors[j].name
+		}
+		return descriptors[i].kind < descriptors[j].kind
+	})
+
+	for _, relation := range descriptors {
+		relation.oid = snapshot.allocateOID("relation", relation.schemaName+"."+relation.name)
+		snapshot.relations = append(snapshot.relations, relation)
+		snapshot.relationByKey[strings.ToLower(relation.schemaName+"."+relation.name)] = relation
+		snapshot.relationByOID[relation.oid] = relation
+		if relation.kind == 'r' {
+			snapshot.tableByName[strings.ToLower(relation.name)] = relation
+		}
+		if relation.kind == 'i' {
+			tableKey := strings.ToLower(relation.index.Table)
+			snapshot.tableIndexes[tableKey] = append(snapshot.tableIndexes[tableKey], relation)
+		}
+	}
+
+	// Constraints are allocated after relations so conindid can point at the
+	// durable or synthetic index OID.
+	for _, relation := range snapshot.relations {
+		if relation.kind != 'r' {
+			continue
+		}
+		usedConstraintNames := make(map[string]bool)
+		addConstraint := func(constraint *catalogConstraint) {
+			constraint.table = relation
+			constraint.oid = snapshot.allocateOID("constraint", relation.name+"."+constraint.name)
+			snapshot.constraints = append(snapshot.constraints, constraint)
+			snapshot.constraintByOID[constraint.oid] = constraint
+		}
+
+		for _, indexRelation := range snapshot.tableIndexes[strings.ToLower(relation.name)] {
+			if !indexRelation.primaryIndex {
+				continue
+			}
+			usedConstraintNames[strings.ToLower(indexRelation.name)] = true
+			addConstraint(&catalogConstraint{
+				name:     indexRelation.name,
+				contype:  'p',
+				indexOID: indexRelation.oid,
+				columns:  indexColumnNames(indexRelation.index),
+			})
+		}
+		for _, indexRelation := range snapshot.tableIndexes[strings.ToLower(relation.name)] {
+			if indexRelation.primaryIndex || !indexRelation.index.Unique {
+				continue
+			}
+			if usedConstraintNames[strings.ToLower(indexRelation.name)] {
+				continue
+			}
+			usedConstraintNames[strings.ToLower(indexRelation.name)] = true
+			addConstraint(&catalogConstraint{
+				name:     indexRelation.name,
+				contype:  'u',
+				indexOID: indexRelation.oid,
+				columns:  indexColumnNames(indexRelation.index),
+			})
+		}
+		for i := range relation.table.ForeignKeys {
+			fk := &relation.table.ForeignKeys[i]
+			name := fk.Name
+			if name == "" {
+				name = relation.name + "_" + strings.Join(fk.Columns, "_") + "_fkey"
+			}
+			name = uniqueCatalogName(name, usedConstraintNames)
+			addConstraint(&catalogConstraint{name: name, contype: 'f', columns: fk.Columns, fk: fk})
+		}
+		for i := range relation.table.Checks {
+			check := &relation.table.Checks[i]
+			name := check.Name
+			if name == "" {
+				name = relation.name + "_check"
+			}
+			name = uniqueCatalogName(name, usedConstraintNames)
+			addConstraint(&catalogConstraint{name: name, contype: 'c', check: check})
+		}
+	}
+
+	// Default expressions, the database row, and function rows are allocated in
+	// sorted/declared order so their OIDs do not depend on which relation a
+	// query reads first.
+	for _, relation := range snapshot.relations {
+		if relation.kind != 'r' {
+			continue
+		}
+		for _, column := range relation.columns {
+			if column.DefaultLiteral() == nil {
+				continue
+			}
+			snapshot.attrdefOIDs[attrdefKey(relation.name, column.Name)] = snapshot.allocateOID("attrdef", relation.name+"."+column.Name)
+		}
+	}
+	snapshot.databaseOID = snapshot.allocateOID("database", snapshot.database)
+	for _, function := range pgCatalogFunctions {
+		snapshot.procOIDs[function.name] = snapshot.allocateOID("proc", function.name)
+	}
+
+	return snapshot, nil
+}
+
+// attrdefKey identifies a column default in the snapshot's OID table.
+func attrdefKey(table, column string) string {
+	return strings.ToLower(table) + "." + strings.ToLower(column)
+}
+
+// sortedViewNames returns the session's view names in deterministic order.
+func sortedViewNames(views map[string]*parser.SelectStmt) []string {
+	names := make([]string, 0, len(views))
+	for name := range views {
+		names = append(names, name)
+	}
+	sort.Strings(names)
+	return names
+}
+
+func indexColumnNames(index *storage.Index) []string {
+	names := make([]string, len(index.Columns))
+	for i, column := range index.Columns {
+		names[i] = column.Name
+	}
+	return names
+}
+
+// regclassOID resolves a relation name the way a regclass literal does:
+// an explicit schema or the public search path, with pg_catalog implicit.
+func (s *catalogSnapshot) regclassOID(name string) (int64, bool) {
+	name = strings.TrimSpace(name)
+	name = strings.Trim(name, `"`)
+	if name == "" {
+		return 0, false
+	}
+	if dot := strings.IndexByte(name, '.'); dot >= 0 {
+		schemaName := strings.Trim(strings.TrimSpace(name[:dot]), `"`)
+		tableName := strings.Trim(strings.TrimSpace(name[dot+1:]), `"`)
+		relation, ok := s.relationByKey[strings.ToLower(schemaName+"."+tableName)]
+		if ok {
+			return relation.oid, true
+		}
+		return 0, false
+	}
+	if relation, ok := s.relationByKey[strings.ToLower(publicSchemaName+"."+name)]; ok {
+		return relation.oid, true
+	}
+	if relation, ok := s.relationByKey[strings.ToLower(pgCatalogSchemaName+"."+name)]; ok {
+		return relation.oid, true
+	}
+	return 0, false
+}
+
+// regclassName renders a relation OID the way regclass output does.
+func (s *catalogSnapshot) regclassName(oid int64) (string, bool) {
+	relation, ok := s.relationByOID[oid]
+	if !ok {
+		return "", false
+	}
+	return relation.name, true
+}
+
+// tableRelationFor resolves a table name (unqualified or schema-qualified) to
+// its public relation row.
+func (s *catalogSnapshot) tableRelationFor(name string) *catalogRelation {
+	name = strings.TrimSpace(name)
+	if dot := strings.IndexByte(name, '.'); dot >= 0 {
+		name = name[dot+1:]
+	}
+	return s.tableByName[strings.ToLower(name)]
+}
+
+func catalogAttributeNumbers(schema *storage.Schema) map[string]int64 {
+	numbers := make(map[string]int64, len(schema.Columns))
+	position := int64(0)
+	for _, column := range visibleCatalogColumns(schema) {
+		position++
+		numbers[strings.ToLower(column.Name)] = position
+	}
+	return numbers
+}
+
+func oidListText(values []int64) string {
+	parts := make([]string, len(values))
+	for i, value := range values {
+		parts[i] = strconv.FormatInt(value, 10)
+	}
+	return "{" + strings.Join(parts, ",") + "}"
+}
+
+func defaultExpressionText(column storage.Column) interface{} {
+	value := column.DefaultLiteral()
+	if value == nil {
+		return nil
+	}
+	return sqlLiteral(value)
+}
+
+// ---------------------------------------------------------------------------
+// Row builders
+
+func (s *catalogSnapshot) relationRows(key string) []storage.Row {
+	key = strings.ToLower(key)
+	if rows, ok := s.rows[key]; ok {
+		return rows
+	}
+	var rows []storage.Row
+	switch key {
+	case "pg_catalog.pg_namespace":
+		rows = s.pgNamespaceRows()
+	case "pg_catalog.pg_class":
+		rows = s.pgClassRows()
+	case "pg_catalog.pg_attribute":
+		rows = s.pgAttributeRows()
+	case "pg_catalog.pg_type":
+		rows = s.pgTypeRows()
+	case "pg_catalog.pg_index":
+		rows = s.pgIndexRows()
+	case "pg_catalog.pg_constraint":
+		rows = s.pgConstraintRows()
+	case "pg_catalog.pg_attrdef":
+		rows = s.pgAttrdefRows()
+	case "pg_catalog.pg_database":
+		rows = s.pgDatabaseRows()
+	case "pg_catalog.pg_tablespace":
+		rows = s.pgTablespaceRows()
+	case "pg_catalog.pg_proc":
+		rows = s.pgProcRows()
+	case "pg_catalog.pg_am":
+		rows = s.pgAmRows()
+	case "pg_catalog.pg_description":
+		rows = []storage.Row{}
+	case "pg_catalog.pg_tables":
+		rows = s.pgTablesRows()
+	case "pg_catalog.pg_indexes":
+		rows = s.pgIndexesRows()
+	case "information_schema.tables":
+		rows = s.informationSchemaTablesRows()
+	case "information_schema.columns":
+		rows = s.informationSchemaColumnsRows()
+	case "information_schema.table_constraints":
+		rows = s.informationSchemaTableConstraintsRows()
+	case "information_schema.key_column_usage":
+		rows = s.informationSchemaKeyColumnUsageRows()
+	case "information_schema.referential_constraints":
+		rows = s.informationSchemaReferentialConstraintsRows()
+	case "information_schema.constraint_column_usage":
+		rows = s.informationSchemaConstraintColumnUsageRows()
+	case "information_schema.schemata":
+		rows = s.informationSchemaSchemataRows()
+	case "information_schema.sequences":
+		rows = []storage.Row{}
+	case "information_schema.views":
+		rows = s.informationSchemaViewsRows()
+	default:
+		rows = []storage.Row{}
+	}
+	s.rows[key] = rows
+	return rows
+}
+
+func (s *catalogSnapshot) pgNamespaceRows() []storage.Row {
+	return []storage.Row{
+		{"oid": pgCatalogNamespaceOID, "nspname": pgCatalogSchemaName, "nspowner": catalogOwnerOID, "nspacl": nil},
+		{"oid": publicNamespaceOID, "nspname": publicSchemaName, "nspowner": catalogOwnerOID, "nspacl": nil},
+		{"oid": infoSchemaNamespaceOID, "nspname": informationSchemaName, "nspowner": catalogOwnerOID, "nspacl": nil},
+	}
+}
+
+func (s *catalogSnapshot) pgClassRows() []storage.Row {
+	rows := make([]storage.Row, 0, len(s.relations))
+	for _, relation := range s.relations {
+		hasIndex := false
+		checks := int64(0)
+		relfilenode := int64(0)
+		relam := int64(0)
+		switch relation.kind {
+		case 'r':
+			hasIndex = len(s.tableIndexes[strings.ToLower(relation.name)]) > 0
+			checks = int64(len(relation.table.Checks))
+			relfilenode = relation.oid
+		case 'i':
+			relfilenode = relation.oid
+			relam = pgBtreeAMOID
+		}
+		rows = append(rows, storage.Row{
+			"oid":                 relation.oid,
+			"relname":             relation.name,
+			"relnamespace":        relationNamespaceOID(relation.schemaName),
+			"reltype":             int64(0),
+			"reloftype":           int64(0),
+			"relowner":            catalogOwnerOID,
+			"relam":               relam,
+			"relfilenode":         relfilenode,
+			"reltablespace":       int64(0),
+			"relpages":            int64(0),
+			"reltuples":           float64(0),
+			"relallvisible":       int64(0),
+			"reltoastrelid":       int64(0),
+			"relhasindex":         hasIndex,
+			"relisshared":         false,
+			"relpersistence":      "p",
+			"relkind":             string(relation.kind),
+			"relnatts":            int64(len(relation.columns)),
+			"relchecks":           checks,
+			"relhasrules":         false,
+			"relhastriggers":      false,
+			"relhassubclass":      false,
+			"relrowsecurity":      false,
+			"relforcerowsecurity": false,
+			"relispopulated":      true,
+			"relreplident":        "d",
+			"relispartition":      false,
+			"relrewrite":          int64(0),
+			"relfrozenxid":        int64(0),
+			"relminmxid":          int64(0),
+			"relacl":              nil,
+			"reloptions":          nil,
+			"relpartbound":        nil,
+		})
+	}
+	return rows
+}
+
+func (s *catalogSnapshot) relationAttributeType(relation *catalogRelation, column storage.Column) pgBuiltinType {
+	if relation.kind == 'i' && relation.index != nil {
+		if table := s.tableRelationFor(relation.index.Table); table != nil {
+			for _, tableColumn := range table.columns {
+				if strings.EqualFold(tableColumn.Name, column.Name) {
+					return pgTypeForDeclaredName(tableColumn.Type)
+				}
+			}
+		}
+	}
+	return pgTypeForDeclaredName(column.Type)
+}
+
+func (s *catalogSnapshot) pgAttributeRows() []storage.Row {
+	var rows []storage.Row
+	for _, relation := range s.relations {
+		for i, column := range relation.columns {
+			builtin := s.relationAttributeType(relation, column)
+			notNull := false
+			hasDefault := false
+			generated := ""
+			if relation.kind == 'r' {
+				notNull = !column.Nullable
+				hasDefault = column.DefaultLiteral() != nil
+				if column.GeneratedExpr != "" {
+					generated = "s"
+				}
+			}
+			arrayDimensions := int64(0)
+			if builtin.isArray() {
+				arrayDimensions = 1
+			}
+			rows = append(rows, storage.Row{
+				"attrelid":       relation.oid,
+				"attname":        column.Name,
+				"atttypid":       builtin.oid,
+				"attstattarget":  int64(-1),
+				"attlen":         builtin.length,
+				"attnum":         int64(i + 1),
+				"attndims":       arrayDimensions,
+				"attcacheoff":    int64(-1),
+				"atttypmod":      int64(-1),
+				"attbyval":       builtin.byval,
+				"attalign":       builtin.align,
+				"attstorage":     builtin.storage,
+				"attcompression": "",
+				"attnotnull":     notNull,
+				"atthasdef":      hasDefault,
+				"atthasmissing":  false,
+				"attidentity":    "",
+				"attgenerated":   generated,
+				"attisdropped":   false,
+				"attislocal":     true,
+				"attinhcount":    int64(0),
+				"attcollation":   int64(0),
+				"attacl":         nil,
+				"attoptions":     nil,
+				"attfdwoptions":  nil,
+				"attmissingval":  nil,
+			})
+		}
+	}
+	return rows
+}
+
+func (s *catalogSnapshot) pgTypeRows() []storage.Row {
+	rows := make([]storage.Row, 0, len(pgBuiltinTypes))
+	for _, t := range pgBuiltinTypes {
+		rows = append(rows, storage.Row{
+			"oid":            t.oid,
+			"typname":        t.name,
+			"typnamespace":   pgCatalogNamespaceOID,
+			"typowner":       catalogOwnerOID,
+			"typlen":         t.length,
+			"typbyval":       t.byval,
+			"typtype":        "b",
+			"typcategory":    t.category,
+			"typispreferred": false,
+			"typisdefined":   true,
+			"typdelim":       t.delim,
+			"typrelid":       int64(0),
+			"typsubscript":   nil,
+			"typelem":        t.elem,
+			"typarray":       t.array,
+			"typinput":       "-",
+			"typoutput":      "-",
+			"typreceive":     "-",
+			"typsend":        "-",
+			"typmodin":       "-",
+			"typmodout":      "-",
+			"typanalyze":     "-",
+			"typalign":       t.align,
+			"typstorage":     t.storage,
+			"typnotnull":     false,
+			"typbasetype":    int64(0),
+			"typtypmod":      int64(-1),
+			"typndims":       int64(0),
+			"typcollation":   int64(0),
+			"typdefaultbin":  nil,
+			"typdefault":     nil,
+			"typacl":         nil,
+		})
+	}
+	return rows
+}
+
+func (s *catalogSnapshot) indexAttributeNumbers(indexRelation *catalogRelation) []int64 {
+	numbers := make([]int64, len(indexRelation.index.Columns))
+	table := s.tableRelationFor(indexRelation.index.Table)
+	if table == nil {
+		return numbers
+	}
+	attnums := catalogAttributeNumbers(table.table)
+	for i, column := range indexRelation.index.Columns {
+		if column.Name == "" {
+			continue
+		}
+		numbers[i] = attnums[strings.ToLower(column.Name)]
+	}
+	return numbers
+}
+
+func (s *catalogSnapshot) pgIndexRows() []storage.Row {
+	var rows []storage.Row
+	for _, relation := range s.relations {
+		if relation.kind != 'i' || relation.index == nil {
+			continue
+		}
+		table := s.tableRelationFor(relation.index.Table)
+		tableOID := int64(0)
+		if table != nil {
+			tableOID = table.oid
+		}
+		attnums := s.indexAttributeNumbers(relation)
+		options := make([]int64, len(attnums))
+		var expressions []string
+		hasExpression := false
+		for _, column := range relation.index.Columns {
+			if column.Expression != "" {
+				expressions = append(expressions, column.Expression)
+				hasExpression = true
+			}
+		}
+		var indexprs interface{}
+		if hasExpression {
+			indexprs = strings.Join(expressions, ", ")
+		}
+		rows = append(rows, storage.Row{
+			"indexrelid":     relation.oid,
+			"indrelid":       tableOID,
+			"indnatts":       int64(len(attnums)),
+			"indnkeyatts":    int64(len(attnums)),
+			"indisunique":    relation.index.Unique,
+			"indisprimary":   relation.primaryIndex,
+			"indisexclusion": false,
+			"indimmediate":   true,
+			"indisclustered": false,
+			"indisvalid":     true,
+			"indcheckxmin":   false,
+			"indisready":     true,
+			"indislive":      true,
+			"indisreplident": false,
+			"indkey":         oidListText(attnums),
+			"indcollation":   oidListText(make([]int64, len(attnums))),
+			"indclass":       oidListText(make([]int64, len(attnums))),
+			"indoption":      oidListText(options),
+			"indexprs":       indexprs,
+			"indpred":        nil,
+		})
+	}
+	return rows
+}
+
+func constraintActionCode(action string) string {
+	switch strings.ToUpper(strings.TrimSpace(action)) {
+	case "CASCADE":
+		return "c"
+	case "RESTRICT":
+		return "r"
+	case "SET NULL":
+		return "n"
+	case "SET DEFAULT":
+		return "d"
+	default:
+		return "a"
+	}
+}
+
+func (s *catalogSnapshot) pgConstraintRows() []storage.Row {
+	rows := make([]storage.Row, 0, len(s.constraints))
+	for _, constraint := range s.constraints {
+		var conkey interface{}
+		if len(constraint.columns) > 0 {
+			attnums := catalogAttributeNumbers(constraint.table.table)
+			key := make([]int64, 0, len(constraint.columns))
+			for _, name := range constraint.columns {
+				key = append(key, attnums[strings.ToLower(name)])
+			}
+			conkey = oidListText(key)
+		}
+
+		var confrelid, confupdtype, confdeltype, confmatchtype, confkey interface{}
+		if constraint.fk != nil {
+			if parent := s.tableRelationFor(constraint.fk.RefTable); parent != nil {
+				confrelid = parent.oid
+				parentAttnums := catalogAttributeNumbers(parent.table)
+				key := make([]int64, 0, len(constraint.fk.RefColumns))
+				for _, name := range constraint.fk.RefColumns {
+					key = append(key, parentAttnums[strings.ToLower(name)])
+				}
+				confkey = oidListText(key)
+			} else {
+				confrelid = int64(0)
+			}
+			confupdtype = constraintActionCode(constraint.fk.OnUpdate)
+			confdeltype = constraintActionCode(constraint.fk.OnDelete)
+			confmatchtype = "s"
+		}
+
+		var conbin interface{}
+		if constraint.check != nil {
+			conbin = constraint.check.Expression
+		}
+
+		rows = append(rows, storage.Row{
+			"oid":           constraint.oid,
+			"conname":       constraint.name,
+			"connamespace":  publicNamespaceOID,
+			"contype":       string(constraint.contype),
+			"condeferrable": false,
+			"condeferred":   false,
+			"convalidated":  true,
+			"conrelid":      constraint.table.oid,
+			"contypid":      int64(0),
+			"conindid":      constraint.indexOID,
+			"conparentid":   int64(0),
+			"confrelid":     confrelid,
+			"confupdtype":   confupdtype,
+			"confdeltype":   confdeltype,
+			"confmatchtype": confmatchtype,
+			"conislocal":    true,
+			"coninhcount":   int64(0),
+			"connoinherit":  false,
+			"conkey":        conkey,
+			"confkey":       confkey,
+			"conpfeqop":     nil,
+			"conppeqop":     nil,
+			"conffeqop":     nil,
+			"conexclop":     nil,
+			"conbin":        conbin,
+			"consrc":        conbin,
+		})
+	}
+	return rows
+}
+
+func (s *catalogSnapshot) pgAttrdefRows() []storage.Row {
+	var rows []storage.Row
+	for _, relation := range s.relations {
+		if relation.kind != 'r' {
+			continue
+		}
+		attnums := catalogAttributeNumbers(relation.table)
+		for _, column := range relation.columns {
+			text := defaultExpressionText(column)
+			if text == nil {
+				continue
+			}
+			adnum := attnums[strings.ToLower(column.Name)]
+			rows = append(rows, storage.Row{
+				"oid":     s.attrdefOIDs[attrdefKey(relation.name, column.Name)],
+				"adrelid": relation.oid,
+				"adnum":   adnum,
+				"adbin":   text,
+			})
+		}
+	}
+	return rows
+}
+
+func (s *catalogSnapshot) pgDatabaseRows() []storage.Row {
+	return []storage.Row{{
+		"oid":            s.databaseOID,
+		"datname":        s.database,
+		"datdba":         catalogOwnerOID,
+		"encoding":       int64(6), // UTF8
+		"datlocprovider": "c",
+		"datistemplate":  false,
+		"datallowconn":   true,
+		"datconnlimit":   int64(-1),
+		"datlastsysoid":  int64(0),
+		"datfrozenxid":   int64(0),
+		"datminmxid":     int64(0),
+		"dattablespace":  pgDefaultTablespaceOID,
+		"datcollate":     "C",
+		"datctype":       "C",
+		"datlocale":      nil,
+		"daticurules":    nil,
+		"datcollversion": nil,
+		"datacl":         nil,
+	}}
+}
+
+func (s *catalogSnapshot) pgTablespaceRows() []storage.Row {
+	return []storage.Row{
+		{"oid": pgDefaultTablespaceOID, "spcname": "pg_default", "spcowner": catalogOwnerOID, "spcacl": nil, "spcoptions": nil},
+		{"oid": pgGlobalTablespaceOID, "spcname": "pg_global", "spcowner": catalogOwnerOID, "spcacl": nil, "spcoptions": nil},
+	}
+}
+
+// pgCatalogFunctions is the pg_proc surface the executor answers. It mirrors
+// evalCatalogFunction; names not listed here never appear in pg_proc.
+var pgCatalogFunctions = []struct {
+	name    string
+	nargs   int64
+	rettype int64
+}{
+	{"current_database", 0, 19},
+	{"current_schema", 0, 19},
+	{"format_type", 2, 25},
+	{"has_table_privilege", 3, 16},
+	{"obj_description", 2, 25},
+	{"col_description", 2, 25},
+	{"pg_get_constraintdef", 2, 25},
+	{"pg_get_expr", 3, 25},
+	{"pg_get_indexdef", 3, 25},
+	{"pg_get_serial_sequence", 2, 25},
+	{"pg_get_userbyid", 1, 19},
+	{"pg_table_is_visible", 1, 16},
+}
+
+func (s *catalogSnapshot) pgProcRows() []storage.Row {
+	rows := make([]storage.Row, 0, len(pgCatalogFunctions))
+	for _, function := range pgCatalogFunctions {
+		rows = append(rows, storage.Row{
+			"oid":             s.procOIDs[function.name],
+			"proname":         function.name,
+			"pronamespace":    pgCatalogNamespaceOID,
+			"proowner":        catalogOwnerOID,
+			"prolang":         int64(12), // internal
+			"procost":         float64(1),
+			"prorows":         float64(0),
+			"provariadic":     int64(0),
+			"prosupport":      nil,
+			"prokind":         "f",
+			"prosecdef":       false,
+			"proleakproof":    false,
+			"proisstrict":     true,
+			"proretset":       false,
+			"provolatile":     "s",
+			"proparallel":     "s",
+			"pronargs":        function.nargs,
+			"pronargdefaults": int64(0),
+			"prorettype":      function.rettype,
+			"proargtypes":     "",
+			"proallargtypes":  nil,
+			"proargmodes":     nil,
+			"proargnames":     nil,
+			"proargdefaults":  nil,
+			"protrftypes":     nil,
+			"prosrc":          function.name,
+			"probin":          nil,
+			"prosqlbody":      nil,
+			"proconfig":       nil,
+			"proacl":          nil,
+		})
+	}
+	return rows
+}
+
+func (s *catalogSnapshot) pgAmRows() []storage.Row {
+	return []storage.Row{
+		{"oid": pgHeapAMOID, "amname": "heap", "amhandler": int64(0), "amtype": "t"},
+		{"oid": pgBtreeAMOID, "amname": "btree", "amhandler": int64(0), "amtype": "i"},
+	}
+}
+
+func (s *catalogSnapshot) pgTablesRows() []storage.Row {
+	var rows []storage.Row
+	for _, relation := range s.relations {
+		if relation.kind != 'r' {
+			continue
+		}
+		hasIndex := len(s.tableIndexes[strings.ToLower(relation.name)]) > 0
+		rows = append(rows, storage.Row{
+			"schemaname":  relation.schemaName,
+			"tablename":   relation.name,
+			"tableowner":  "pizzasql",
+			"tablespace":  nil,
+			"hasindexes":  hasIndex,
+			"hasrules":    false,
+			"hastriggers": false,
+			"rowsecurity": false,
+		})
+	}
+	return rows
+}
+
+func (s *catalogSnapshot) pgIndexesRows() []storage.Row {
+	var rows []storage.Row
+	for _, relation := range s.relations {
+		if relation.kind != 'i' || relation.index == nil {
+			continue
+		}
+		rows = append(rows, storage.Row{
+			"schemaname": relation.schemaName,
+			"tablename":  relation.index.Table,
+			"indexname":  relation.name,
+			"tablespace": nil,
+			"indexdef":   s.indexDefinition(relation.index),
+		})
+	}
+	return rows
+}
+
+// ---------------------------------------------------------------------------
+// information_schema row builders
+
+func (s *catalogSnapshot) informationSchemaTablesRows() []storage.Row {
+	var rows []storage.Row
+	for _, relation := range s.relations {
+		if relation.kind == 'i' {
+			continue
+		}
+		tableType := "BASE TABLE"
+		if relation.kind != 'r' {
+			tableType = "VIEW"
+		}
+		insertable := "NO"
+		if relation.kind == 'r' {
+			insertable = "YES"
+		}
+		rows = append(rows, storage.Row{
+			"table_catalog":                s.database,
+			"table_schema":                 relation.schemaName,
+			"table_name":                   relation.name,
+			"table_type":                   tableType,
+			"self_referencing_column_name": nil,
+			"reference_generation":         nil,
+			"user_defined_type_catalog":    nil,
+			"user_defined_type_schema":     nil,
+			"user_defined_type_name":       nil,
+			"is_insertable_into":           insertable,
+			"is_typed":                     "NO",
+			"commit_action":                nil,
+		})
+	}
+	for _, view := range s.views {
+		rows = append(rows, storage.Row{
+			"table_catalog":                s.database,
+			"table_schema":                 publicSchemaName,
+			"table_name":                   view,
+			"table_type":                   "VIEW",
+			"self_referencing_column_name": nil,
+			"reference_generation":         nil,
+			"user_defined_type_catalog":    nil,
+			"user_defined_type_schema":     nil,
+			"user_defined_type_name":       nil,
+			"is_insertable_into":           "NO",
+			"is_typed":                     "NO",
+			"commit_action":                nil,
+		})
+	}
+	return rows
+}
+
+func informationSchemaColumn(relation *catalogRelation, column storage.Column, position int) storage.Row {
+	builtin := pgTypeForDeclaredName(column.Type)
+	dataType := builtin.sqlName
+	if builtin.isArray() {
+		dataType = "ARRAY"
+	}
+	generated := "NEVER"
+	generationExpression := interface{}(nil)
+	if column.GeneratedExpr != "" {
+		generated = "ALWAYS"
+		generationExpression = column.GeneratedExpr
+	}
+	return storage.Row{
+		"table_catalog":            "", // filled by the caller
+		"table_schema":             relation.schemaName,
+		"table_name":               relation.name,
+		"column_name":              column.Name,
+		"ordinal_position":         int64(position),
+		"column_default":           defaultExpressionText(column),
+		"is_nullable":              map[bool]string{true: "YES", false: "NO"}[column.Nullable],
+		"data_type":                dataType,
+		"character_maximum_length": nil,
+		"character_octet_length":   nil,
+		"numeric_precision":        nil,
+		"numeric_precision_radix":  nil,
+		"numeric_scale":            nil,
+		"datetime_precision":       nil,
+		"interval_type":            nil,
+		"interval_precision":       nil,
+		"character_set_catalog":    nil,
+		"character_set_schema":     nil,
+		"character_set_name":       nil,
+		"collation_catalog":        nil,
+		"collation_schema":         nil,
+		"collation_name":           nil,
+		"domain_catalog":           nil,
+		"domain_schema":            nil,
+		"domain_name":              nil,
+		"udt_catalog":              "", // filled by the caller
+		"udt_schema":               pgCatalogSchemaName,
+		"udt_name":                 builtin.name,
+		"scope_catalog":            nil,
+		"scope_schema":             nil,
+		"scope_name":               nil,
+		"maximum_cardinality":      nil,
+		"dtd_identifier":           strconv.Itoa(position),
+		"is_self_referencing":      "NO",
+		"is_identity":              "NO",
+		"identity_generation":      nil,
+		"identity_start":           nil,
+		"identity_increment":       nil,
+		"identity_maximum":         nil,
+		"identity_minimum":         nil,
+		"identity_cycle":           "NO",
+		"is_generated":             generated,
+		"generation_expression":    generationExpression,
+		"is_updatable":             "YES",
+	}
+}
+
+func (s *catalogSnapshot) informationSchemaColumnsRows() []storage.Row {
+	var rows []storage.Row
+	for _, relation := range s.relations {
+		if relation.kind != 'r' {
+			continue
+		}
+		for i, column := range relation.columns {
+			row := informationSchemaColumn(relation, column, i+1)
+			row["table_catalog"] = s.database
+			row["udt_catalog"] = s.database
+			rows = append(rows, row)
+		}
+	}
+	return rows
+}
+
+func constraintTypeName(contype byte) string {
+	switch contype {
+	case 'p':
+		return "PRIMARY KEY"
+	case 'u':
+		return "UNIQUE"
+	case 'f':
+		return "FOREIGN KEY"
+	case 'c':
+		return "CHECK"
+	default:
+		return "UNKNOWN"
+	}
+}
+
+func (s *catalogSnapshot) informationSchemaTableConstraintsRows() []storage.Row {
+	rows := make([]storage.Row, 0, len(s.constraints))
+	for _, constraint := range s.constraints {
+		rows = append(rows, storage.Row{
+			"constraint_catalog": s.database,
+			"constraint_schema":  publicSchemaName,
+			"constraint_name":    constraint.name,
+			"table_catalog":      s.database,
+			"table_schema":       publicSchemaName,
+			"table_name":         constraint.table.name,
+			"constraint_type":    constraintTypeName(constraint.contype),
+			"is_deferrable":      "NO",
+			"initially_deferred": "NO",
+			"enforced":           "YES",
+			"nulls_distinct":     nil,
+		})
+	}
+	return rows
+}
+
+func (s *catalogSnapshot) informationSchemaKeyColumnUsageRows() []storage.Row {
+	var rows []storage.Row
+	for _, constraint := range s.constraints {
+		var columns []string
+		switch constraint.contype {
+		case 'p', 'u':
+			columns = constraint.columns
+		case 'f':
+			columns = constraint.fk.Columns
+		default:
+			continue
+		}
+		for i, column := range columns {
+			var position interface{}
+			if constraint.contype == 'f' {
+				position = int64(i + 1)
+			}
+			rows = append(rows, storage.Row{
+				"constraint_catalog":            s.database,
+				"constraint_schema":             publicSchemaName,
+				"constraint_name":               constraint.name,
+				"table_catalog":                 s.database,
+				"table_schema":                  publicSchemaName,
+				"table_name":                    constraint.table.name,
+				"column_name":                   column,
+				"ordinal_position":              int64(i + 1),
+				"position_in_unique_constraint": position,
+			})
+		}
+	}
+	return rows
+}
+
+func (s *catalogSnapshot) uniqueConstraintFor(parent *catalogRelation, columns []string) string {
+	for _, constraint := range s.constraints {
+		if constraint.table != parent || (constraint.contype != 'p' && constraint.contype != 'u') {
+			continue
+		}
+		keyColumns := constraint.columns
+		if constraint.contype == 'u' && constraint.indexOID != 0 {
+			for _, indexRelation := range s.relations {
+				if indexRelation.oid == constraint.indexOID && indexRelation.index != nil {
+					keyColumns = indexColumnNames(indexRelation.index)
+					break
+				}
+			}
+		}
+		if len(keyColumns) != len(columns) {
+			continue
+		}
+		matches := true
+		for i := range columns {
+			if !strings.EqualFold(keyColumns[i], columns[i]) {
+				matches = false
+				break
+			}
+		}
+		if matches {
+			return constraint.name
+		}
+	}
+	return ""
+}
+
+func referentialRule(action string) string {
+	if strings.TrimSpace(action) == "" {
+		return "NO ACTION"
+	}
+	return strings.ToUpper(strings.TrimSpace(action))
+}
+
+func (s *catalogSnapshot) informationSchemaReferentialConstraintsRows() []storage.Row {
+	var rows []storage.Row
+	for _, constraint := range s.constraints {
+		if constraint.contype != 'f' || constraint.fk == nil {
+			continue
+		}
+		parent := s.tableRelationFor(constraint.fk.RefTable)
+		var uniqueName interface{}
+		if parent != nil {
+			if name := s.uniqueConstraintFor(parent, constraint.fk.RefColumns); name != "" {
+				uniqueName = name
+			}
+		}
+		rows = append(rows, storage.Row{
+			"constraint_catalog":        s.database,
+			"constraint_schema":         publicSchemaName,
+			"constraint_name":           constraint.name,
+			"unique_constraint_catalog": s.database,
+			"unique_constraint_schema":  publicSchemaName,
+			"unique_constraint_name":    uniqueName,
+			"match_option":              "NONE",
+			"update_rule":               referentialRule(constraint.fk.OnUpdate),
+			"delete_rule":               referentialRule(constraint.fk.OnDelete),
+			"is_deferrable":             "NO",
+			"initially_deferred":        "NO",
+			"enforced":                  "YES",
+		})
+	}
+	return rows
+}
+
+func (s *catalogSnapshot) informationSchemaConstraintColumnUsageRows() []storage.Row {
+	var rows []storage.Row
+	for _, constraint := range s.constraints {
+		switch constraint.contype {
+		case 'f':
+			if constraint.fk == nil {
+				continue
+			}
+			for _, column := range constraint.fk.RefColumns {
+				rows = append(rows, storage.Row{
+					"table_catalog":      s.database,
+					"table_schema":       publicSchemaName,
+					"table_name":         constraint.fk.RefTable,
+					"constraint_catalog": s.database,
+					"constraint_schema":  publicSchemaName,
+					"constraint_name":    constraint.name,
+					"column_name":        column,
+				})
+			}
+		case 'c':
+			if constraint.check == nil {
+				continue
+			}
+			expr, err := parser.ParseExpr(constraint.check.Expression)
+			if err != nil {
+				continue
+			}
+			for _, column := range collectColumnRefs(expr) {
+				rows = append(rows, storage.Row{
+					"table_catalog":      s.database,
+					"table_schema":       publicSchemaName,
+					"table_name":         constraint.table.name,
+					"constraint_catalog": s.database,
+					"constraint_schema":  publicSchemaName,
+					"constraint_name":    constraint.name,
+					"column_name":        column,
+				})
+			}
+		}
+	}
+	return rows
+}
+
+func (s *catalogSnapshot) informationSchemaSchemataRows() []storage.Row {
+	return []storage.Row{
+		{"catalog_name": s.database, "schema_name": pgCatalogSchemaName, "schema_owner": "pizzasql", "default_character_set_catalog": nil, "default_character_set_schema": nil, "default_character_set_name": nil, "sql_path": nil},
+		{"catalog_name": s.database, "schema_name": publicSchemaName, "schema_owner": "pizzasql", "default_character_set_catalog": nil, "default_character_set_schema": nil, "default_character_set_name": nil, "sql_path": nil},
+		{"catalog_name": s.database, "schema_name": informationSchemaName, "schema_owner": "pizzasql", "default_character_set_catalog": nil, "default_character_set_schema": nil, "default_character_set_name": nil, "sql_path": nil},
+	}
+}
+
+// catalogSchemaByName resolves an unqualified pg_catalog relation name that is
+// not shadowed by a durable table.
+func (e *Executor) catalogSchemaByName(name string) (*storage.Schema, bool) {
+	entry, ok := pgCatalogRelationIndex[pgCatalogSchemaName+"."+strings.ToLower(name)]
+	if !ok || e.session.TableExists(name) {
+		return nil, false
+	}
+	return entry.schema, true
+}
+
+func (s *catalogSnapshot) informationSchemaViewsRows() []storage.Row {
+	// Views are session-local and the engine does not retain their SQL text, so
+	// information_schema.views lists them with an unknown definition.
+	var rows []storage.Row
+	for _, view := range s.views {
+		rows = append(rows, storage.Row{
+			"table_catalog":              s.database,
+			"table_schema":               publicSchemaName,
+			"table_name":                 view,
+			"view_definition":            nil,
+			"check_option":               "NONE",
+			"is_updatable":               "NO",
+			"is_insertable_into":         "NO",
+			"is_trigger_updatable":       "NO",
+			"is_trigger_deletable":       "NO",
+			"is_trigger_insertable_into": "NO",
+		})
+	}
+	return rows
+}
+
+// ---------------------------------------------------------------------------
+// Function/relation helpers used by the executor's expression evaluator
+
+// indexDefinition renders CREATE INDEX statement text, used by pg_indexes and
+// pg_get_indexdef.
+func (s *catalogSnapshot) indexDefinition(index *storage.Index) string {
+	var builder strings.Builder
+	builder.WriteString("CREATE ")
+	if index.Unique {
+		builder.WriteString("UNIQUE ")
+	}
+	builder.WriteString("INDEX ")
+	builder.WriteString(index.Name)
+	builder.WriteString(" ON ")
+	builder.WriteString(publicSchemaName)
+	builder.WriteByte('.')
+	builder.WriteString(index.Table)
+	builder.WriteString(" USING btree (")
+	for i, column := range index.Columns {
+		if i > 0 {
+			builder.WriteString(", ")
+		}
+		if column.Expression != "" {
+			builder.WriteString("(")
+			builder.WriteString(column.Expression)
+			builder.WriteString(")")
+			continue
+		}
+		builder.WriteString(column.Name)
+		if column.Desc {
+			builder.WriteString(" DESC")
+		}
+	}
+	builder.WriteString(")")
+	return builder.String()
+}
+
+func (s *catalogSnapshot) indexDefinitionForOID(oid int64) (string, bool) {
+	relation, ok := s.relationByOID[oid]
+	if !ok || relation.kind != 'i' || relation.index == nil {
+		return "", false
+	}
+	return s.indexDefinition(relation.index), true
+}
+
+func (s *catalogSnapshot) constraintDefinition(constraint *catalogConstraint) string {
+	switch constraint.contype {
+	case 'p':
+		return "PRIMARY KEY (" + strings.Join(constraint.columns, ", ") + ")"
+	case 'u':
+		return "UNIQUE (" + strings.Join(constraint.columns, ", ") + ")"
+	case 'f':
+		if constraint.fk == nil {
+			return ""
+		}
+		clause := "FOREIGN KEY (" + strings.Join(constraint.fk.Columns, ", ") + ") REFERENCES " + constraint.fk.RefTable + "(" + strings.Join(constraint.fk.RefColumns, ", ") + ")"
+		if constraint.fk.OnDelete != "" {
+			clause += " ON DELETE " + strings.ToUpper(constraint.fk.OnDelete)
+		}
+		if constraint.fk.OnUpdate != "" {
+			clause += " ON UPDATE " + strings.ToUpper(constraint.fk.OnUpdate)
+		}
+		return clause
+	case 'c':
+		if constraint.check == nil {
+			return ""
+		}
+		return "CHECK ((" + constraint.check.Expression + "))"
+	default:
+		return ""
+	}
+}
+
+func (s *catalogSnapshot) constraintDefinitionForOID(oid int64) (string, bool) {
+	constraint, ok := s.constraintByOID[oid]
+	if !ok {
+		return "", false
+	}
+	return s.constraintDefinition(constraint), true
+}
+
+// formatType renders a type OID the way format_type() does. Precision modifiers
+// are not persisted by the engine, so callers pass -1.
+func (s *catalogSnapshot) formatType(oid int64, typmod int64) (string, bool) {
+	builtin, ok := pgBuiltinTypeByOID[oid]
+	if !ok {
+		return "???", false
+	}
+	name := builtin.sqlName
+	if typmod < 0 {
+		return name, true
+	}
+	switch oid {
+	case 1043, 1042: // varchar, bpchar
+		return fmt.Sprintf("%s(%d)", name, typmod-4), true
+	case 1700: // numeric
+		modifier := typmod - 4
+		precision := (modifier >> 16) & 0xffff
+		scale := modifier & 0xffff
+		return fmt.Sprintf("numeric(%d,%d)", precision, scale), true
+	default:
+		return name, true
+	}
+}
+
+// typeOIDForName resolves a regtype literal.
+func (s *catalogSnapshot) typeOIDForName(name string) (int64, bool) {
+	name = strings.TrimSpace(name)
+	name = strings.Trim(name, `"`)
+	upper := strings.ToUpper(name)
+	if strings.HasSuffix(upper, "[]") || strings.HasSuffix(upper, " ARRAY") {
+		elementName := strings.TrimSuffix(strings.TrimSuffix(upper, " ARRAY"), "[]")
+		element := pgTypeForDeclaredName(elementName)
+		if array, ok := pgBuiltinTypeByOID[element.array]; ok && element.array != 0 {
+			return array.oid, true
+		}
+		return 0, false
+	}
+	if !knownDeclaredTypeName(upper) {
+		if builtin, ok := pgBuiltinTypeByName[strings.ToLower(name)]; ok {
+			return builtin.oid, true
+		}
+		return 0, false
+	}
+	return pgTypeForDeclaredName(upper).oid, true
+}
+
+// knownDeclaredTypeName reports whether a type spelling maps to a built-in.
+func knownDeclaredTypeName(upper string) bool {
+	switch upper {
+	case "INTEGER", "INT", "INT4", "BIGINT", "INT8", "SMALLINT", "INT2", "TINYINT", "MEDIUMINT",
+		"TEXT", "CLOB", "NCHAR", "NVARCHAR", "VARCHAR", "CHARACTER VARYING", "CHAR", "CHARACTER",
+		"BOOLEAN", "BOOL", "REAL", "FLOAT", "FLOAT4", "DOUBLE", "DOUBLE PRECISION", "FLOAT8",
+		"NUMERIC", "DECIMAL", "JSONB", "JSON", "BLOB", "BYTEA", "DATETIME", "TIMESTAMP",
+		"TIMESTAMPTZ", "TIMESTAMP WITH TIME ZONE", "TIMESTAMP WITHOUT TIME ZONE", "DATE",
+		"TIME", "TIME WITHOUT TIME ZONE", "TIME WITH TIME ZONE", "UUID", "OID", "NAME",
+		"REGCLASS", "REGTYPE":
+		return true
+	default:
+		return false
+	}
+}
+
+func (s *catalogSnapshot) typeNameForOID(oid int64) (string, bool) {
+	builtin, ok := pgBuiltinTypeByOID[oid]
+	if !ok {
+		return "", false
+	}
+	return builtin.sqlName, true
+}
+
+func (s *catalogSnapshot) relationVisible(oid int64) bool {
+	relation, ok := s.relationByOID[oid]
+	if !ok {
+		return false
+	}
+	switch relation.schemaName {
+	case publicSchemaName, pgCatalogSchemaName, informationSchemaName:
+		return true
+	default:
+		return false
+	}
+}
+
+// numericArg converts a function argument that denotes an OID.
+func numericArg(value interface{}) (int64, bool) {
+	switch v := value.(type) {
+	case RegOID:
+		return v.oid, true
+	case int64:
+		return v, true
+	case int:
+		return int64(v), true
+	case float64:
+		return int64(v), true
+	case string:
+		parsed, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64)
+		if err != nil {
+			return 0, false
+		}
+		return parsed, true
+	default:
+		return 0, false
+	}
+}
+
+// RegOID is the value of a regclass or regtype cast. Numeric contexts use the
+// OID; text output renders the resolved name, matching PostgreSQL's symbolic
+// type rendering.
+type RegOID struct {
+	oid  int64
+	name string
+}
+
+// OID returns the numeric OID the value carries.
+func (r RegOID) OID() int64 { return r.oid }
+
+// String renders the symbolic name.
+func (r RegOID) String() string { return r.name }
+
+// evalCatalogFunction answers the PostgreSQL catalog-introspection builtins.
+// handled is false for every other function so evalFunctionCall keeps them.
+func (e *Executor) evalCatalogFunction(name string, fn *parser.FunctionCall, args []interface{}) (interface{}, bool, error) {
+	switch name {
+	case "CURRENT_SCHEMA":
+		return e.currentSchemaName(), true, nil
+	case "CURRENT_DATABASE":
+		if e.schema == nil {
+			return "pizzasql", true, nil
+		}
+		database := e.schema.GetDatabaseName()
+		if database == "" {
+			database = "pizzasql"
+		}
+		return database, true, nil
+	case "PG_GET_SERIAL_SEQUENCE", "OBJ_DESCRIPTION", "COL_DESCRIPTION":
+		// The engine has no sequences and stores no catalog comments.
+		return nil, true, nil
+	case "PG_GET_USERBYID":
+		return "pizzasql", true, nil
+	case "CURRENT_SETTING":
+		if len(args) == 0 || args[0] == nil {
+			return nil, true, nil
+		}
+		value, err := e.settingValue(toString(args[0]))
+		if err != nil {
+			return nil, true, err
+		}
+		return value, true, nil
+	case "PG_GET_EXPR":
+		if len(args) == 0 || args[0] == nil {
+			return nil, true, nil
+		}
+		if text, ok := args[0].(string); ok {
+			return text, true, nil
+		}
+		return toString(args[0]), true, nil
+	}
+
+	switch name {
+	case "FORMAT_TYPE", "PG_TABLE_IS_VISIBLE", "PG_GET_INDEXDEF", "PG_GET_CONSTRAINTDEF", "HAS_TABLE_PRIVILEGE":
+	default:
+		return nil, false, nil
+	}
+
+	snapshot, err := e.catalogSnapshotForQuery()
+	if err != nil {
+		return nil, true, err
+	}
+
+	switch name {
+	case "FORMAT_TYPE":
+		if len(args) == 0 {
+			return nil, true, pgerror.Unsupported("format_type requires a type OID")
+		}
+		oid, ok := numericArg(args[0])
+		if !ok {
+			return nil, true, pgerror.InvalidText("format_type requires a numeric type OID")
+		}
+		typmod := int64(-1)
+		if len(args) > 1 && args[1] != nil {
+			if value, ok := numericArg(args[1]); ok {
+				typmod = value
+			}
+		}
+		rendered, _ := snapshot.formatType(oid, typmod)
+		return rendered, true, nil
+	case "PG_TABLE_IS_VISIBLE":
+		if len(args) == 0 || args[0] == nil {
+			return nil, true, nil
+		}
+		oid, ok := numericArg(args[0])
+		if !ok {
+			return false, true, nil
+		}
+		return snapshot.relationVisible(oid), true, nil
+	case "PG_GET_INDEXDEF":
+		if len(args) == 0 || args[0] == nil {
+			return nil, true, nil
+		}
+		oid, ok := numericArg(args[0])
+		if !ok {
+			return nil, true, pgerror.InvalidText("pg_get_indexdef requires an index OID")
+		}
+		definition, found := snapshot.indexDefinitionForOID(oid)
+		if !found {
+			return nil, true, nil
+		}
+		return definition, true, nil
+	case "PG_GET_CONSTRAINTDEF":
+		if len(args) == 0 || args[0] == nil {
+			return nil, true, nil
+		}
+		oid, ok := numericArg(args[0])
+		if !ok {
+			return nil, true, pgerror.InvalidText("pg_get_constraintdef requires a constraint OID")
+		}
+		definition, found := snapshot.constraintDefinitionForOID(oid)
+		if !found {
+			return nil, true, nil
+		}
+		return definition, true, nil
+	case "HAS_TABLE_PRIVILEGE":
+		tableArg := interface{}(nil)
+		switch len(args) {
+		case 2:
+			tableArg = args[0]
+		case 3:
+			tableArg = args[1]
+		default:
+			return false, true, nil
+		}
+		if tableArg == nil {
+			return nil, true, nil
+		}
+		if oid, ok := numericArg(tableArg); ok {
+			return snapshot.relationVisible(oid), true, nil
+		}
+		_, ok := snapshot.regclassOID(toString(tableArg))
+		return ok, true, nil
+	default:
+		return nil, false, nil
+	}
+}
+
+// currentSchemaName reports the first schema in the session search path. The
+// engine only materializes public, pg_catalog, and information_schema, so a
+// search path naming anything else falls back to public.
+func (e *Executor) currentSchemaName() string {
+	if len(e.searchPath) > 0 && e.searchPath[0] != "" {
+		return e.searchPath[0]
+	}
+	return publicSchemaName
+}
+
+// catalogCast handles regclass/regtype casts and text casts of their values.
+func (e *Executor) catalogCast(expr *parser.CastExpr, value interface{}) (interface{}, bool, error) {
+	typeName := strings.ToUpper(strings.TrimSpace(expr.Type.Name))
+	switch typeName {
+	case "REGCLASS", "REGTYPE":
+		snapshot, err := e.catalogSnapshotForQuery()
+		if err != nil {
+			return nil, true, err
+		}
+		if reg, ok := value.(RegOID); ok {
+			return reg, true, nil
+		}
+		if text, ok := value.(string); ok {
+			if typeName == "REGTYPE" {
+				oid, found := snapshot.typeOIDForName(text)
+				if !found {
+					return nil, true, fmt.Errorf("type %q does not exist", text)
+				}
+				name, _ := snapshot.typeNameForOID(oid)
+				return RegOID{oid: oid, name: name}, true, nil
+			}
+			oid, found := snapshot.regclassOID(text)
+			if !found {
+				return nil, true, fmt.Errorf("relation %q does not exist", text)
+			}
+			name, _ := snapshot.regclassName(oid)
+			return RegOID{oid: oid, name: name}, true, nil
+		}
+		oid, ok := numericArg(value)
+		if !ok {
+			return nil, true, pgerror.InvalidText("cannot cast %T to %s", value, typeName)
+		}
+		var name string
+		if typeName == "REGTYPE" {
+			name, _ = snapshot.typeNameForOID(oid)
+		} else {
+			name, _ = snapshot.regclassName(oid)
+		}
+		if name == "" {
+			name = strconv.FormatInt(oid, 10)
+		}
+		return RegOID{oid: oid, name: name}, true, nil
+	case "TEXT", "VARCHAR", "CHAR", "CHARACTER", "CLOB", "NCHAR", "NVARCHAR":
+		if reg, ok := value.(RegOID); ok {
+			return reg.name, true, nil
+		}
+	case "OID":
+		if reg, ok := value.(RegOID); ok {
+			return reg.oid, true, nil
+		}
+	}
+	return nil, false, nil
+}
+
+// isCatalogWriteTarget reports a DML/DDL statement aimed at a virtual relation.
+func (e *Executor) rejectCatalogWriteTarget(stmt parser.Statement) error {
+	var refs []parser.TableRef
+	switch s := stmt.(type) {
+	case *parser.InsertStmt:
+		if s.Table != nil {
+			refs = append(refs, *s.Table)
+		}
+	case *parser.UpdateStmt:
+		if s.Table != nil {
+			refs = append(refs, *s.Table)
+		}
+	case *parser.DeleteStmt:
+		if s.Table != nil {
+			refs = append(refs, *s.Table)
+		}
+	case *parser.CreateTableStmt:
+		if s.Table != nil {
+			refs = append(refs, *s.Table)
+		}
+	case *parser.CreateIndexStmt:
+		refs = append(refs, parser.TableRef{Name: s.Table})
+	case *parser.AlterTableStmt:
+		refs = append(refs, parser.TableRef{Name: s.Table})
+	case *parser.CreateViewStmt:
+		if s.View != nil {
+			refs = append(refs, *s.View)
+		}
+	case *parser.DropTableStmt:
+		for _, table := range s.Tables {
+			if table != nil {
+				refs = append(refs, *table)
+			}
+		}
+	}
+	for _, ref := range refs {
+		if _, ok := e.catalogRelationFor(ref); !ok {
+			continue
+		}
+		schemaName := strings.ToLower(ref.Schema)
+		if schemaName == "" || schemaName == publicSchemaName {
+			schemaName = pgCatalogSchemaName
+		}
+		return pgerror.Unsupported("%s.%s is a read-only virtual relation; catalog relations are synthesized from the durable schema and cannot be written", schemaName, strings.ToLower(ref.Name))
+	}
+	return nil
+}
+
+// ---------------------------------------------------------------------------
+// SELECT over virtual catalog relations
+
+// catalogSelectRelation is the column metadata of one FROM entry.
+type catalogSelectRelation struct {
+	qualifier string
+	columns   []storage.Column
+}
+
+// catalogFlatRefs flattens a FROM clause, including JOIN chains, preserving the
+// schema qualifier of every entry.
+func catalogFlatRefs(from []parser.TableRef) []parser.TableRef {
+	var refs []parser.TableRef
+	for _, ref := range from {
+		cur := ref
+		for {
+			flat := cur
+			flat.Join = nil
+			flat.Subquery = cur.Subquery
+			flat.Function = cur.Function
+			refs = append(refs, flat)
+			if cur.Join == nil || cur.Join.Table == nil {
+				break
+			}
+			cur = *cur.Join.Table
+		}
+	}
+	return refs
+}
+
+func catalogRefQualifier(ref parser.TableRef) string {
+	if ref.Alias != "" {
+		return ref.Alias
+	}
+	return ref.Name
+}
+
+// catalogSelectMetadata resolves the column list of every FROM relation so
+// SELECT * and table.* expand, and so projection types stay accurate.
+func (e *Executor) catalogSelectMetadata(from []parser.TableRef) ([]catalogSelectRelation, error) {
+	var relations []catalogSelectRelation
+	for _, ref := range catalogFlatRefs(from) {
+		if ref.Subquery != nil || ref.Function != nil {
+			continue
+		}
+		var columns []storage.Column
+		if entry, ok := e.catalogRelationFor(ref); ok {
+			columns = entry.schema.Columns
+		} else if cte, ok := e.cteTableFor(ref.Name); ok {
+			columns = schemaFromColumns(cte.columns).Columns
+		} else {
+			schema, err := e.session.GetSchema(ref.Name)
+			if err != nil {
+				return nil, err
+			}
+			columns = visibleCatalogColumns(schema)
+		}
+		relations = append(relations, catalogSelectRelation{qualifier: catalogRefQualifier(ref), columns: columns})
+	}
+	return relations, nil
+}
+
+// materializeCatalogTableRef reads one FROM entry. Catalog relations come from
+// the statement's virtual snapshot; everything else goes through the ordinary
+// materializer so a catalog query can still join a real table or read a
+// derived table.
+func (e *Executor) materializeCatalogTableRef(ref parser.TableRef) ([]storage.Row, error) {
+	if entry, ok := e.catalogRelationFor(ref); ok {
+		snapshot, err := e.catalogSnapshotForQuery()
+		if err != nil {
+			return nil, err
+		}
+		rows := cloneRows(snapshot.relationRows(entry.def.schema + "." + entry.def.name))
+		alias := catalogRefQualifier(ref)
+		for i := range rows {
+			rows[i] = e.addTableAlias(rows[i], alias)
+		}
+		return rows, nil
+	}
+	if cte, ok := e.cteTableFor(ref.Name); ok {
+		rows := cloneRows(cte.rows)
+		alias := catalogRefQualifier(ref)
+		for i := range rows {
+			rows[i] = e.addTableAlias(rows[i], alias)
+		}
+		return rows, nil
+	}
+	return e.materializeTableRef(ref)
+}
+
+// materializeCatalogFrom materializes the whole FROM list, applying each
+// entry's JOIN chain and cross-joining comma-separated entries.
+func (e *Executor) materializeCatalogFrom(from []parser.TableRef) ([]storage.Row, error) {
+	rows, err := e.materializeCatalogTableRef(from[0])
+	if err != nil {
+		return nil, err
+	}
+	if from[0].Join != nil {
+		rows, err = e.executeJoins(from[0], rows)
+		if err != nil {
+			return nil, err
+		}
+	}
+	for _, ref := range from[1:] {
+		right, err := e.materializeCatalogTableRef(ref)
+		if err != nil {
+			return nil, err
+		}
+		if ref.Join != nil {
+			right, err = e.executeJoins(ref, right)
+			if err != nil {
+				return nil, err
+			}
+		}
+		joined := make([]storage.Row, 0, len(rows)*len(right))
+		for _, leftRow := range rows {
+			for _, rightRow := range right {
+				combined := make(storage.Row, len(leftRow)+len(rightRow))
+				for key, value := range leftRow {
+					combined[key] = value
+				}
+				for key, value := range rightRow {
+					combined[key] = value
+				}
+				joined = append(joined, combined)
+			}
+		}
+		rows = joined
+	}
+	return rows, nil
+}
+
+// expandCatalogTableStars rewrites table.* projections into explicit column
+// references so the shared materialized projection can read qualified keys.
+func expandCatalogTableStars(stmt *parser.SelectStmt, relations []catalogSelectRelation) (*parser.SelectStmt, error) {
+	expanded := *stmt
+	if len(stmt.Columns) == 0 {
+		return &expanded, nil
+	}
+	columns := make([]parser.SelectColumn, 0, len(stmt.Columns))
+	for _, projection := range stmt.Columns {
+		if projection.TableStar == "" {
+			columns = append(columns, projection)
+			continue
+		}
+		var matched *catalogSelectRelation
+		for i := range relations {
+			if strings.EqualFold(relations[i].qualifier, projection.TableStar) {
+				matched = &relations[i]
+				break
+			}
+		}
+		if matched == nil {
+			return nil, fmt.Errorf("cannot expand %s.*: relation columns are not available", projection.TableStar)
+		}
+		for _, column := range matched.columns {
+			columns = append(columns, parser.SelectColumn{
+				Expr:  &parser.ColumnRef{Table: matched.qualifier, Column: column.Name},
+				Alias: column.Name,
+			})
+		}
+	}
+	expanded.Columns = columns
+	return &expanded, nil
+}
+
+func catalogJoinKeyParts(key string) (string, string) {
+	if dot := strings.LastIndexByte(key, '.'); dot >= 0 {
+		return key[:dot], key[dot+1:]
+	}
+	return "", key
+}
+
+// catalogProjectionTypes computes result types parallel to the projection. The
+// resolver follows starKeys, so both qualified and unqualified references keep
+// the declared catalog type (OID, NAME, BOOL, ...) instead of falling back to
+// TEXT over the wire.
+func catalogProjectionTypes(stmt *parser.SelectStmt, starKeys, types []string) []string {
+	resolve := func(table, column string) (string, bool) {
+		for i, key := range starKeys {
+			qualifier, name := catalogJoinKeyParts(key)
+			if !strings.EqualFold(name, column) {
+				continue
+			}
+			if table != "" && !strings.EqualFold(table, qualifier) {
+				continue
+			}
+			if i < len(types) && types[i] != "" {
+				return types[i], true
+			}
+			return "", true
+		}
+		return "", false
+	}
+	result := make([]string, 0, len(stmt.Columns))
+	for _, projection := range stmt.Columns {
+		if projection.Star {
+			result = append(result, types...)
+			continue
+		}
+		result = append(result, inferExprType(projection.Expr, resolve))
+	}
+	return result
+}
+
+// executeSelectOnCatalogRelations answers a SELECT whose FROM clause reads at
+// least one virtual relation. Rows are materialized from the session's schema
+// view, so staged transactional DDL is visible to the issuing connection only.
+func (e *Executor) executeSelectOnCatalogRelations(stmt *parser.SelectStmt) (*Result, error) {
+	relations, err := e.catalogSelectMetadata(stmt.From)
+	if err != nil {
+		return nil, err
+	}
+	expanded, err := expandCatalogTableStars(stmt, relations)
+	if err != nil {
+		return nil, err
+	}
+
+	rows, err := e.materializeCatalogFrom(stmt.From)
+	if err != nil {
+		return nil, err
+	}
+
+	var columns, starKeys, types, qualifiers []string
+	for _, relation := range relations {
+		for _, column := range relation.columns {
+			columns = append(columns, column.Name)
+			starKeys = append(starKeys, relation.qualifier+"."+column.Name)
+			types = append(types, column.Type)
+			qualifiers = append(qualifiers, relation.qualifier)
+		}
+	}
+
+	// The JOIN chain is already applied by materializeCatalogFrom; hand the
+	// shared materialized path rows without a pending join.
+	tail := *expanded
+	tailFrom := make([]parser.TableRef, len(expanded.From))
+	copy(tailFrom, expanded.From)
+	tailFrom[0].Join = nil
+	tail.From = tailFrom
+
+	e.pushTypeScope(materializedTypeResolver(columns, types, qualifiers))
+	result, err := e.executeSelectOnMaterializedRows(&tail, columns, starKeys, rows)
+	e.popTypeScope()
+	if err != nil {
+		return nil, err
+	}
+	result.ColumnTypes = catalogProjectionTypes(&tail, starKeys, types)
+	if tail.Distinct {
+		result.Rows = e.applyDistinct(result.Rows)
+	}
+	return result, nil
+}

+ 457 - 0
pkg/executor/pg_catalog_test.go

@@ -0,0 +1,457 @@
+package executor
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+)
+
+// setupPGCatalogFixture creates a schema that exercises every catalog-backed
+// object shape: single and composite primary keys, UNIQUE, FOREIGN KEY, CHECK,
+// explicit indexes, arrays, JSONB, defaults, and a view.
+func setupPGCatalogFixture(t *testing.T) *Executor {
+	t.Helper()
+	_, schema, table := newTestDB(t)
+	e := newExec(schema, table)
+	for _, sql := range []string{
+		"CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT NOT NULL UNIQUE)",
+		"CREATE TABLE orders (id INTEGER PRIMARY KEY, account_id INTEGER NOT NULL, note TEXT DEFAULT 'new', tags TEXT[], meta JSONB, CHECK (id > 0), CONSTRAINT fk_orders_account FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE)",
+		"CREATE INDEX idx_orders_note ON orders (note)",
+		"CREATE TABLE pairs (a TEXT, b TEXT, v TEXT, PRIMARY KEY (a, b))",
+		"CREATE VIEW active_orders AS SELECT id, note FROM orders",
+	} {
+		execMust(t, e, sql)
+	}
+	return e
+}
+
+func catalogCell(t *testing.T, res *Result, row, col int) string {
+	t.Helper()
+	if row >= len(res.Rows) || col >= len(res.Rows[row]) {
+		t.Fatalf("row %d column %d out of range: %v", row, col, res.Rows)
+	}
+	value := res.Rows[row][col]
+	if value == nil {
+		return "<nil>"
+	}
+	return fmt.Sprintf("%v", value)
+}
+
+func catalogRows(t *testing.T, e *Executor, query string) [][]string {
+	t.Helper()
+	res, err := execSQL(e, query)
+	if err != nil {
+		t.Fatalf("%s: %v", query, err)
+	}
+	rows := make([][]string, len(res.Rows))
+	for i := range res.Rows {
+		rows[i] = make([]string, len(res.Rows[i]))
+		for j, value := range res.Rows[i] {
+			if value == nil {
+				rows[i][j] = "<nil>"
+				continue
+			}
+			rows[i][j] = fmt.Sprintf("%v", value)
+		}
+	}
+	return rows
+}
+
+// A PostgreSQL-style column listing joins pg_class, pg_namespace, and
+// pg_attribute; the synthetic OIDs must line up across all three.
+func TestPGCatalogJoinListsColumns(t *testing.T) {
+	e := setupPGCatalogFixture(t)
+	rows := catalogRows(t, e, `
+		SELECT n.nspname, c.relname, a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), a.attnotnull
+		FROM pg_catalog.pg_class c
+		JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
+		JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
+		WHERE c.relname = 'orders' AND n.nspname = 'public' AND a.attnum > 0 AND NOT a.attisdropped
+		ORDER BY a.attnum`)
+	want := [][]string{
+		{"public", "orders", "id", "integer", "true"},
+		{"public", "orders", "account_id", "integer", "true"},
+		{"public", "orders", "note", "text", "false"},
+		{"public", "orders", "tags", "text[]", "false"},
+		{"public", "orders", "meta", "jsonb", "false"},
+	}
+	if fmt.Sprint(rows) != fmt.Sprint(want) {
+		t.Fatalf("columns = %v, want %v", rows, want)
+	}
+}
+
+func TestPGCatalogInformationSchemaColumns(t *testing.T) {
+	e := setupPGCatalogFixture(t)
+	rows := catalogRows(t, e, `
+		SELECT table_name, column_name, data_type, is_nullable, udt_name, ordinal_position
+		FROM information_schema.columns
+		WHERE table_name = 'orders'
+		ORDER BY ordinal_position`)
+	want := [][]string{
+		{"orders", "id", "integer", "NO", "int4", "1"},
+		{"orders", "account_id", "integer", "NO", "int4", "2"},
+		{"orders", "note", "text", "YES", "text", "3"},
+		{"orders", "tags", "ARRAY", "YES", "_text", "4"},
+		{"orders", "meta", "jsonb", "YES", "jsonb", "5"},
+	}
+	if fmt.Sprint(rows) != fmt.Sprint(want) {
+		t.Fatalf("information_schema.columns = %v, want %v", rows, want)
+	}
+
+	// Schema names are case-insensitive, as in PostgreSQL.
+	res := execMust(t, e, `SELECT count(*) FROM INFORMATION_SCHEMA.TABLES WHERE table_name = 'orders'`)
+	if res.Rows[0][0] != int64(1) {
+		t.Fatalf("uppercase information schema lookup = %v, want 1", res.Rows[0][0])
+	}
+
+	res = execMust(t, e, `
+		SELECT count(*) FROM information_schema.tables
+		WHERE table_schema = 'public' AND table_type = 'BASE TABLE'`)
+	if res.Rows[0][0] != int64(3) {
+		t.Fatalf("base table count = %v, want 3", res.Rows[0][0])
+	}
+	res = execMust(t, e, `SELECT count(*) FROM information_schema.tables WHERE table_name = 'active_orders' AND table_type = 'VIEW'`)
+	if res.Rows[0][0] != int64(1) {
+		t.Fatalf("view count = %v, want 1", res.Rows[0][0])
+	}
+	res = execMust(t, e, `SELECT count(*) FROM information_schema.sequences`)
+	if res.Rows[0][0] != int64(0) {
+		t.Fatalf("sequence count = %v, want 0", res.Rows[0][0])
+	}
+}
+
+func TestPGCatalogIndexRelations(t *testing.T) {
+	e := setupPGCatalogFixture(t)
+	rows := catalogRows(t, e, `
+		SELECT c.relname, i.indisunique, i.indisprimary, i.indkey
+		FROM pg_catalog.pg_index i
+		JOIN pg_catalog.pg_class c ON c.oid = i.indexrelid
+		JOIN pg_catalog.pg_class t ON t.oid = i.indrelid
+		WHERE t.relname = 'orders'
+		ORDER BY c.relname`)
+	want := [][]string{
+		{"idx_orders_note", "false", "false", "{3}"},
+		{"orders_pkey", "true", "true", "{1}"},
+	}
+	if fmt.Sprint(rows) != fmt.Sprint(want) {
+		t.Fatalf("pg_index = %v, want %v", rows, want)
+	}
+
+	rows = catalogRows(t, e, `SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'orders' ORDER BY indexname`)
+	if len(rows) != 2 {
+		t.Fatalf("pg_indexes rows = %v, want 2", rows)
+	}
+	if !strings.Contains(rows[0][1], "CREATE INDEX idx_orders_note ON public.orders USING btree (note)") {
+		t.Fatalf("indexdef = %q", rows[0][1])
+	}
+	if !strings.Contains(rows[1][1], "CREATE UNIQUE INDEX orders_pkey ON public.orders USING btree (id)") {
+		t.Fatalf("indexdef = %q", rows[1][1])
+	}
+
+	// Composite keys keep declaration order in pg_index.indkey.
+	rows = catalogRows(t, e, `
+		SELECT i.indkey FROM pg_index i
+		JOIN pg_class c ON c.oid = i.indexrelid
+		WHERE c.relname = 'pairs_pkey'`)
+	if len(rows) != 1 || rows[0][0] != "{1,2}" {
+		t.Fatalf("composite indkey = %v, want {1,2}", rows)
+	}
+}
+
+func TestPGCatalogConstraints(t *testing.T) {
+	e := setupPGCatalogFixture(t)
+	rows := catalogRows(t, e, `
+		SELECT conname, contype, conkey, confkey, pg_get_constraintdef(oid)
+		FROM pg_constraint
+		WHERE conrelid = 'orders'::regclass
+		ORDER BY conname`)
+	want := [][]string{
+		{"fk_orders_account", "f", "{2}", "{1}", "FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE"},
+		{"orders_check", "c", "<nil>", "<nil>", "CHECK (((id > 0)))"},
+		{"orders_pkey", "p", "{1}", "<nil>", "PRIMARY KEY (id)"},
+	}
+	if fmt.Sprint(rows) != fmt.Sprint(want) {
+		t.Fatalf("pg_constraint = %v, want %v", rows, want)
+	}
+
+	rows = catalogRows(t, e, `SELECT conname, contype FROM pg_constraint WHERE conrelid = 'accounts'::regclass ORDER BY conname`)
+	want = [][]string{
+		{"accounts_pkey", "p"},
+		{"uniq_accounts_email", "u"},
+	}
+	if fmt.Sprint(rows) != fmt.Sprint(want) {
+		t.Fatalf("account constraints = %v, want %v", rows, want)
+	}
+
+	rows = catalogRows(t, e, `
+		SELECT constraint_name, constraint_type FROM information_schema.table_constraints
+		WHERE table_name = 'orders' ORDER BY constraint_name`)
+	want = [][]string{
+		{"fk_orders_account", "FOREIGN KEY"},
+		{"orders_check", "CHECK"},
+		{"orders_pkey", "PRIMARY KEY"},
+	}
+	if fmt.Sprint(rows) != fmt.Sprint(want) {
+		t.Fatalf("table_constraints = %v, want %v", rows, want)
+	}
+
+	rows = catalogRows(t, e, `
+		SELECT constraint_name, column_name, ordinal_position, position_in_unique_constraint
+		FROM information_schema.key_column_usage
+		WHERE table_name = 'orders' ORDER BY constraint_name, ordinal_position`)
+	want = [][]string{
+		{"fk_orders_account", "account_id", "1", "1"},
+		{"orders_pkey", "id", "1", "<nil>"},
+	}
+	if fmt.Sprint(rows) != fmt.Sprint(want) {
+		t.Fatalf("key_column_usage = %v, want %v", rows, want)
+	}
+
+	rows = catalogRows(t, e, `
+		SELECT constraint_name, unique_constraint_name, delete_rule, update_rule
+		FROM information_schema.referential_constraints ORDER BY constraint_name`)
+	want = [][]string{{"fk_orders_account", "accounts_pkey", "CASCADE", "NO ACTION"}}
+	if fmt.Sprint(rows) != fmt.Sprint(want) {
+		t.Fatalf("referential_constraints = %v, want %v", rows, want)
+	}
+}
+
+// regclass is symbolic in PostgreSQL: a name literal resolves to the same OID
+// pg_class reports, and rendering the OID yields the name again.
+func TestPGCatalogRegclassRoundTrip(t *testing.T) {
+	e := setupPGCatalogFixture(t)
+	rows := catalogRows(t, e, `
+		SELECT c.relname, c.oid, 'orders'::regclass, c.oid::regclass
+		FROM pg_class c WHERE c.relname = 'orders'`)
+	if len(rows) != 1 {
+		t.Fatalf("rows = %v", rows)
+	}
+	if rows[0][0] != "orders" || rows[0][2] != "orders" || rows[0][3] != "orders" {
+		t.Fatalf("regclass rendering = %v", rows)
+	}
+
+	// Text output of a regclass value is the relation name, as in PostgreSQL;
+	// ::oid exposes the numeric OID for comparisons.
+	rows = catalogRows(t, e, `
+		SELECT 'orders'::regclass::text,
+		       'orders'::regclass::oid = (SELECT oid FROM pg_class WHERE relname = 'orders'),
+		       (SELECT oid FROM pg_class WHERE relname = 'orders') = 'orders'::regclass`)
+	if rows[0][0] != "orders" || rows[0][1] != "true" || rows[0][2] != "true" {
+		t.Fatalf("regclass text/comparison = %v", rows)
+	}
+
+	// regtype resolves the built-in type table.
+	rows = catalogRows(t, e, `SELECT 'integer'::regtype, 'integer'::regtype = 'int4'::regtype`)
+	if rows[0][0] != "integer" || rows[0][1] != "true" {
+		t.Fatalf("regtype = %v", rows)
+	}
+
+	if _, err := execSQL(e, `SELECT 'missing_relation'::regclass`); err == nil {
+		t.Fatal("unknown relation cast must fail")
+	}
+}
+
+func TestPGCatalogIntrospectionFunctions(t *testing.T) {
+	e := setupPGCatalogFixture(t)
+	rows := catalogRows(t, e, `
+		SELECT current_schema(), current_database(),
+		       pg_table_is_visible('orders'::regclass),
+		       has_table_privilege('orders', 'SELECT'),
+		       has_table_privilege('missing_table', 'SELECT')`)
+	if rows[0][0] != "public" || rows[0][1] != "testdb" || rows[0][2] != "true" || rows[0][3] != "true" || rows[0][4] != "false" {
+		t.Fatalf("identity/privilege functions = %v", rows)
+	}
+
+	rows = catalogRows(t, e, `
+		SELECT pg_get_serial_sequence('orders', 'id'),
+		       obj_description('orders'::regclass, 'pg_class'),
+		       col_description('orders'::regclass, 1)`)
+	if len(rows) != 1 || rows[0][0] != "<nil>" || rows[0][1] != "<nil>" || rows[0][2] != "<nil>" {
+		t.Fatalf("description functions = %v, want NULLs", rows)
+	}
+
+	rows = catalogRows(t, e, `
+		SELECT pg_get_expr(d.adbin, d.adrelid)
+		FROM pg_attrdef d WHERE d.adrelid = 'orders'::regclass`)
+	if len(rows) != 1 || rows[0][0] != "'new'" {
+		t.Fatalf("pg_get_expr = %v, want the stored default text", rows)
+	}
+
+	rows = catalogRows(t, e, `SELECT pg_catalog.version(), current_setting('search_path')`)
+	if !strings.Contains(rows[0][0], "PostgreSQL") || rows[0][1] != "public" {
+		t.Fatalf("version/current_setting = %v", rows)
+	}
+
+	rows = catalogRows(t, e, `SELECT format_type(23, -1), format_type(1043, 14), format_type(999999, -1)`)
+	if rows[0][0] != "integer" || rows[0][1] != "character varying(10)" || rows[0][2] != "???" {
+		t.Fatalf("format_type = %v", rows)
+	}
+}
+
+// A psql-style column listing combines the catalog relations with LEFT JOINs;
+// it must return one row per live attribute and mark the primary key.
+func TestPGCatalogPsqlStyleDescribe(t *testing.T) {
+	e := setupPGCatalogFixture(t)
+	rows := catalogRows(t, e, `
+		SELECT a.attname,
+		       pg_catalog.format_type(a.atttypid, a.atttypmod) AS type,
+		       a.attnotnull AS notnull,
+		       pg_catalog.pg_get_expr(d.adbin, d.adrelid) AS dflt,
+		       i.indisprimary AS is_pk
+		FROM pg_catalog.pg_class c
+		JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
+		JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
+		LEFT JOIN pg_catalog.pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
+		LEFT JOIN pg_catalog.pg_index i ON i.indrelid = c.oid AND a.attnum = ANY(i.indkey)
+		WHERE c.relname = 'orders' AND c.relkind = 'r' AND n.nspname = 'public'
+		  AND a.attnum > 0 AND NOT a.attisdropped
+		ORDER BY a.attnum`)
+	want := [][]string{
+		{"id", "integer", "true", "<nil>", "true"},
+		{"account_id", "integer", "true", "<nil>", "<nil>"},
+		{"note", "text", "false", "'new'", "false"},
+		{"tags", "text[]", "false", "<nil>", "<nil>"},
+		{"meta", "jsonb", "false", "<nil>", "<nil>"},
+	}
+	if fmt.Sprint(rows) != fmt.Sprint(want) {
+		t.Fatalf("psql-style describe = %v, want %v", rows, want)
+	}
+}
+
+// Catalog rows reflect DDL immediately, and transactional DDL is visible only
+// to the connection that staged it.
+func TestPGCatalogTransactionalDDLVisibility(t *testing.T) {
+	_, schema, table := newTestDB(t)
+	writer := newExec(schema, table)
+	observer := newExec(schema, table)
+
+	execMust(t, writer, "CREATE TABLE base (id INTEGER PRIMARY KEY)")
+	execMust(t, writer, "BEGIN")
+	execMust(t, writer, "CREATE TABLE staged (id INTEGER PRIMARY KEY)")
+	execMust(t, writer, "ALTER TABLE base ADD COLUMN note TEXT")
+	execMust(t, writer, "CREATE INDEX idx_base_note ON base (note)")
+
+	res := execMust(t, writer, `SELECT table_name FROM information_schema.tables WHERE table_name = 'staged'`)
+	if len(res.Rows) != 1 {
+		t.Fatalf("writer does not see staged table: %v", res.Rows)
+	}
+	res = execMust(t, writer, `
+		SELECT column_name FROM information_schema.columns
+		WHERE table_name = 'base' ORDER BY ordinal_position`)
+	if len(res.Rows) != 2 || res.Rows[1][0] != "note" {
+		t.Fatalf("writer does not see staged column: %v", res.Rows)
+	}
+	res = execMust(t, writer, `SELECT count(*) FROM pg_indexes WHERE tablename = 'base' AND indexname = 'idx_base_note'`)
+	if res.Rows[0][0] != int64(1) {
+		t.Fatalf("writer does not see staged index: %v", res.Rows)
+	}
+
+	res = execMust(t, observer, `SELECT count(*) FROM information_schema.tables WHERE table_name = 'staged'`)
+	if res.Rows[0][0] != int64(0) {
+		t.Fatalf("observer sees staged table: %v", res.Rows)
+	}
+	res = execMust(t, observer, `SELECT count(*) FROM information_schema.columns WHERE table_name = 'base'`)
+	if res.Rows[0][0] != int64(1) {
+		t.Fatalf("observer sees staged column: %v", res.Rows)
+	}
+
+	execMust(t, writer, "ROLLBACK")
+	res = execMust(t, writer, `SELECT count(*) FROM information_schema.tables WHERE table_name = 'staged'`)
+	if res.Rows[0][0] != int64(0) {
+		t.Fatalf("rolled-back table still visible: %v", res.Rows)
+	}
+	res = execMust(t, writer, `SELECT count(*) FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid WHERE c.relname = 'base'`)
+	if res.Rows[0][0] != int64(1) {
+		t.Fatalf("rolled-back column still visible: %v", res.Rows)
+	}
+}
+
+// OIDs are deterministic per database, so two connections and repeated
+// statements agree.
+func TestPGCatalogOIDsAreDeterministic(t *testing.T) {
+	_, schema, table := newTestDB(t)
+	first := newExec(schema, table)
+	execMust(t, first, "CREATE TABLE deterministic (id INTEGER PRIMARY KEY, name TEXT)")
+	query := `SELECT oid, relname FROM pg_class WHERE relname = 'deterministic'`
+	one := catalogRows(t, first, query)
+	two := catalogRows(t, first, query)
+	if fmt.Sprint(one) != fmt.Sprint(two) {
+		t.Fatalf("OID changed between statements: %v vs %v", one, two)
+	}
+	second := newExec(schema, table)
+	three := catalogRows(t, second, query)
+	if fmt.Sprint(one) != fmt.Sprint(three) {
+		t.Fatalf("OID differs across connections: %v vs %v", one, three)
+	}
+	if len(one) != 1 || one[0][1] != "deterministic" {
+		t.Fatalf("unexpected relation row: %v", one)
+	}
+}
+
+func TestPGCatalogRejectsWrites(t *testing.T) {
+	e := setupPGCatalogFixture(t)
+	for _, sql := range []string{
+		`INSERT INTO pg_catalog.pg_class VALUES (1)`,
+		`INSERT INTO information_schema.tables VALUES ('x')`,
+		`UPDATE pg_catalog.pg_type SET typname = 'x'`,
+		`DELETE FROM pg_catalog.pg_attribute`,
+		`CREATE INDEX idx_pg_class_name ON pg_catalog.pg_class (relname)`,
+		`ALTER TABLE pg_catalog.pg_class ADD COLUMN nope TEXT`,
+		`DROP TABLE pg_catalog.pg_class`,
+		`CREATE VIEW pg_catalog.pg_class AS SELECT 1`,
+	} {
+		if _, err := execSQL(e, sql); err == nil {
+			t.Fatalf("%s: catalog write must be rejected", sql)
+		}
+	}
+}
+
+func TestPGSearchPathSettings(t *testing.T) {
+	e := setupPGCatalogFixture(t)
+	for _, sql := range []string{
+		"SET search_path TO public",
+		"SET search_path = public",
+		"SET client_encoding TO 'UTF8'",
+		"SET TIME ZONE 'UTC'",
+		"SET statement_timeout = 0",
+		"SET NAMES 'utf8'",
+		"SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED",
+	} {
+		if _, err := execSQL(e, sql); err != nil {
+			t.Fatalf("%s: %v", sql, err)
+		}
+	}
+	res := execMust(t, e, "SHOW search_path")
+	if len(res.Rows) != 1 || res.Rows[0][0] != "public" {
+		t.Fatalf("SHOW search_path = %v", res.Rows)
+	}
+	res = execMust(t, e, "SHOW server_version")
+	if len(res.Rows) != 1 || res.Rows[0][0] == "" {
+		t.Fatalf("SHOW server_version = %v", res.Rows)
+	}
+	if _, err := execSQL(e, "SHOW definitely_not_a_setting"); err == nil {
+		t.Fatal("unknown SHOW setting must fail")
+	}
+}
+
+// The SQLite introspection surface keeps working alongside the PostgreSQL
+// catalogs; sqlite_master must not list the virtual relations.
+func TestPGCatalogDoesNotChangeSQLiteIntrospection(t *testing.T) {
+	e := setupPGCatalogFixture(t)
+	res := execMust(t, e, `SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name`)
+	names := make([]string, len(res.Rows))
+	for i, row := range res.Rows {
+		names[i] = fmt.Sprintf("%v", row[0])
+	}
+	if strings.Join(names, ",") != "accounts,orders,pairs" {
+		t.Fatalf("sqlite_master tables = %v", names)
+	}
+	res = execMust(t, e, `SELECT name FROM sqlite_master WHERE type = 'index' ORDER BY name`)
+	if len(res.Rows) != 2 {
+		t.Fatalf("sqlite_master indexes = %v", res.Rows)
+	}
+	res = execMust(t, e, `PRAGMA table_info(orders)`)
+	if len(res.Rows) != 5 {
+		t.Fatalf("PRAGMA table_info rows = %v", res.Rows)
+	}
+}

+ 8 - 0
pkg/executor/result_types.go

@@ -387,6 +387,14 @@ func castResultType(name string) (string, bool) {
 		return "BLOB", true
 	case "JSONB":
 		return "JSONB", true
+	case "REGCLASS":
+		return "REGCLASS", true
+	case "REGTYPE":
+		return "REGTYPE", true
+	case "OID":
+		return "OID", true
+	case "NAME":
+		return "NAME", true
 	default:
 		return "", false
 	}

+ 110 - 0
pkg/executor/session_settings.go

@@ -0,0 +1,110 @@
+package executor
+
+import (
+	"strings"
+
+	"github.com/danfragoso/pizzasql-next/pkg/parser"
+	"github.com/danfragoso/pizzasql-next/pkg/pgerror"
+	"github.com/danfragoso/pizzasql-next/pkg/version"
+)
+
+// Session settings.
+//
+// Drivers and ORMs open a connection with SET statements for parameters that
+// have no PizzaSQL equivalent (client_encoding, extra_float_digits,
+// statement_timeout, ...). Rejecting them makes an unmodified client unusable,
+// so the engine acknowledges every SET and retains only search_path, which
+// current_schema() and SHOW report. The engine has no other configurable
+// settings; SHOW returns honest constants for the values it exposes.
+
+// normalizeSettingName lowercases a setting name and collapses whitespace so
+// SET TIME ZONE and SHOW time zone agree.
+func normalizeSettingName(name string) string {
+	return strings.ToLower(strings.Join(strings.Fields(name), " "))
+}
+
+// executeSet acknowledges a session setting.
+func (e *Executor) executeSet(stmt *parser.SetStmt) (*Result, error) {
+	name := normalizeSettingName(stmt.Name)
+	if name == "search_path" {
+		var entries []string
+		for _, value := range stmt.Values {
+			for _, part := range strings.Split(value, ",") {
+				part = strings.Trim(strings.TrimSpace(part), `"`)
+				if part != "" {
+					entries = append(entries, part)
+				}
+			}
+		}
+		// Only the emulated schemas exist; drop anything else so SHOW and
+		// current_schema() describe a schema the engine can actually resolve.
+		known := make([]string, 0, len(entries))
+		for _, entry := range entries {
+			switch strings.ToLower(entry) {
+			case publicSchemaName, pgCatalogSchemaName, informationSchemaName:
+				known = append(known, strings.ToLower(entry))
+			}
+		}
+		if len(known) == 0 {
+			known = []string{publicSchemaName}
+		}
+		e.searchPath = known
+		// A different search path changes which schema is visible; drop the
+		// statement-scoped catalog snapshot so the next read re-evaluates it.
+		e.pgCatalog = nil
+	}
+	return NewResult("SET"), nil
+}
+
+// executeShow returns the value of a setting. Values that the engine does not
+// model are reported as constants that describe the actual behavior, and
+// unknown names fail rather than inventing a value.
+func (e *Executor) executeShow(stmt *parser.ShowStmt) (*Result, error) {
+	name := normalizeSettingName(stmt.Name)
+	value, err := e.settingValue(name)
+	if err != nil {
+		return nil, err
+	}
+
+	result := NewResult("SHOW")
+	result.AddColumnWithType(name, "TEXT")
+	result.AddRow(value)
+	return result, nil
+}
+
+// settingValue reports the value of a setting exposed by SHOW and
+// current_setting(). Unknown names are rejected rather than invented.
+func (e *Executor) settingValue(name string) (string, error) {
+	name = normalizeSettingName(name)
+	var value string
+	switch name {
+	case "search_path":
+		if len(e.searchPath) == 0 {
+			value = publicSchemaName
+		} else {
+			value = strings.Join(e.searchPath, ", ")
+		}
+	case "server_version":
+		value = version.PostgreSQLCompatVersion
+	case "server_version_num":
+		// PostgreSQL encodes major*10000 + minor*100.
+		value = "140000"
+	case "server_encoding", "client_encoding":
+		value = "UTF8"
+	case "transaction_isolation", "default_transaction_isolation":
+		value = "read committed"
+	case "standard_conforming_strings", "integer_datetimes":
+		value = "on"
+	case "datestyle":
+		value = "ISO, MDY"
+	case "timezone", "time zone":
+		value = "UTC"
+	case "application_name":
+		value = ""
+	case "is_superuser":
+		value = "off"
+	default:
+		return "", pgerror.Unsupported("unrecognized configuration parameter %q", name)
+	}
+	return value, nil
+}

+ 22 - 0
pkg/parser/ast.go

@@ -532,6 +532,28 @@ type NotifyStmt struct {
 func (s *NotifyStmt) node()     {}
 func (s *NotifyStmt) stmtNode() {}
 
+// SetStmt represents a PostgreSQL SET statement. Drivers and ORMs issue these
+// for session configuration; PizzaSQL accepts them for compatibility and only
+// gives search_path observable behavior. Name is the normalized setting name
+// (for example "search_path" or "time zone") and Values holds the unquoted
+// value tokens in declaration order.
+type SetStmt struct {
+	Name   string
+	Values []string
+	Local  bool
+}
+
+func (s *SetStmt) node()     {}
+func (s *SetStmt) stmtNode() {}
+
+// ShowStmt represents a PostgreSQL SHOW statement for a single setting.
+type ShowStmt struct {
+	Name string
+}
+
+func (s *ShowStmt) node()     {}
+func (s *ShowStmt) stmtNode() {}
+
 // Expression types
 
 // BinaryExpr represents a binary expression.

+ 100 - 1
pkg/parser/parser.go

@@ -144,6 +144,8 @@ func (p *Parser) parseStatement() (Statement, error) {
 			return p.parseUnlisten()
 		case "NOTIFY":
 			return p.parseNotify()
+		case "SHOW":
+			return p.parseShow()
 		}
 	}
 	switch p.curToken.Type {
@@ -167,6 +169,8 @@ func (p *Parser) parseStatement() (Statement, error) {
 		return p.parseDetach()
 	case lexer.TokenPRAGMA:
 		return p.parsePragma()
+	case lexer.TokenSET:
+		return p.parseSet()
 	case lexer.TokenANALYZE:
 		return p.parseAnalyze()
 	case lexer.TokenEXPLAIN:
@@ -1348,7 +1352,11 @@ func (p *Parser) tryParseGeneratedColumn(col *ColumnDef) (bool, error) {
 // to their canonical data type name. These are stored with SQLite text
 // affinity and never gain native PostgreSQL semantics.
 var identTypeAliases = map[string]string{
-	"UUID": "UUID",
+	"UUID":     "UUID",
+	"OID":      "OID",
+	"REGCLASS": "REGCLASS",
+	"REGTYPE":  "REGTYPE",
+	"NAME":     "NAME",
 }
 
 func (p *Parser) parseDataType() (*DataType, error) {
@@ -2249,6 +2257,91 @@ func (p *Parser) parsePragma() (*PragmaStmt, error) {
 	return stmt, nil
 }
 
+// parseSet parses a PostgreSQL SET statement. Drivers and ORMs issue session
+// settings that have no effect on a PizzaSQL connection; the parser accepts the
+// setting name and values so the executor can acknowledge them. Only
+// search_path is retained, because current_schema() and pg_table_is_visible()
+// report it.
+func (p *Parser) parseSet() (*SetStmt, error) {
+	stmt := &SetStmt{}
+	p.nextToken() // consume SET
+
+	// SESSION CHARACTERISTICS AS TRANSACTION ... is a session-wide statement
+	// whose "SESSION" is not a scope modifier.
+	sessionCharacteristics := p.curTokenIs(lexer.TokenIdent) && strings.EqualFold(p.curToken.Literal, "SESSION") &&
+		p.peekTokenIs(lexer.TokenIdent) && strings.EqualFold(p.peekToken.Literal, "CHARACTERISTICS")
+	if !sessionCharacteristics && p.curTokenIs(lexer.TokenIdent) {
+		switch strings.ToUpper(p.curToken.Literal) {
+		case "LOCAL":
+			stmt.Local = true
+			p.nextToken()
+		case "SESSION":
+			p.nextToken()
+		}
+	}
+
+	var name strings.Builder
+	for p.isSettingNameToken() {
+		if name.Len() > 0 {
+			name.WriteByte(' ')
+		}
+		name.WriteString(p.curToken.Literal)
+		p.nextToken()
+	}
+	if name.Len() == 0 {
+		return nil, p.curError("expected setting name after SET")
+	}
+	stmt.Name = name.String()
+
+	if p.curTokenIs(lexer.TokenTO) || p.curTokenIs(lexer.TokenEq) {
+		p.nextToken()
+	}
+
+	// The remaining tokens are the setting value. They are retained as text for
+	// search_path and otherwise ignored, so forms such as SET TIME ZONE 'UTC',
+	// SET NAMES 'utf8', and SET SESSION CHARACTERISTICS AS TRANSACTION
+	// ISOLATION LEVEL READ COMMITTED parse without a value grammar.
+	for !p.curTokenIs(lexer.TokenSemicolon) && !p.curTokenIs(lexer.TokenEOF) {
+		switch p.curToken.Type {
+		case lexer.TokenIdent, lexer.TokenNumber, lexer.TokenString:
+			stmt.Values = append(stmt.Values, p.curToken.Literal)
+		}
+		p.nextToken()
+	}
+	return stmt, nil
+}
+
+// parseShow parses SHOW name. SHOW ALL is not supported because the engine has
+// no settings catalog.
+func (p *Parser) parseShow() (*ShowStmt, error) {
+	p.nextToken() // consume SHOW
+	if !p.isSettingNameToken() {
+		return nil, p.curError("expected setting name after SHOW")
+	}
+	var name strings.Builder
+	for p.isSettingNameToken() {
+		if name.Len() > 0 && p.curTokenIs(lexer.TokenIdent) {
+			name.WriteByte(' ')
+		}
+		name.WriteString(p.curToken.Literal)
+		p.nextToken()
+	}
+	return &ShowStmt{Name: name.String()}, nil
+}
+
+// isSettingNameToken reports whether the current token can be part of a SET or
+// SHOW setting name. PostgreSQL setting names may contain non-reserved words
+// that the lexer promotes to type keywords (TIME ZONE), so every token with
+// literal text qualifies except the TO/= punctuation that starts the value.
+func (p *Parser) isSettingNameToken() bool {
+	switch p.curToken.Type {
+	case lexer.TokenTO, lexer.TokenEq, lexer.TokenComma, lexer.TokenSemicolon,
+		lexer.TokenEOF, lexer.TokenLParen, lexer.TokenRParen:
+		return false
+	}
+	return p.curToken.Literal != ""
+}
+
 // parseAnalyze parses ANALYZE [schema.]table. PizzaSQL does not maintain
 // optimizer statistics, so the statement is accepted and ignored at execution.
 func (p *Parser) parseAnalyze() (*AnalyzeStmt, error) {
@@ -3085,6 +3178,12 @@ func (p *Parser) parseIdentOrFunction() (Expr, error) {
 			return nil, p.curError("expected column name after dot")
 		}
 		col := p.curToken.Literal
+		// A schema-qualified function call (pg_catalog.format_type(...)) keeps
+		// the bare function name: the engine has one function namespace.
+		if p.peekTokenIs(lexer.TokenLParen) {
+			p.nextToken()
+			return p.parseFunctionCall(col)
+		}
 		p.nextToken()
 		return &ColumnRef{Table: name, Column: col}, nil
 	}

+ 47 - 242
pkg/pgserver/connection.go

@@ -11,8 +11,6 @@ import (
 	"log"
 	"net"
 	"os"
-	"regexp"
-	"sort"
 	"strconv"
 	"strings"
 	"sync/atomic"
@@ -414,17 +412,6 @@ func (c *Connection) handleQuery(msg *Message) error {
 			return c.handleShowCommand(strings.ToUpper(kind))
 		}
 	}
-	if result, handled, err := c.catalogResult(sql); handled {
-		if err != nil {
-			c.sendError("ERROR", ErrCodeInternalError, err.Error())
-			return c.sendReadyForQuery()
-		}
-		if err := c.sendTabularResult(result, "SELECT", nil); err != nil {
-			return err
-		}
-		return c.sendReadyForQuery()
-	}
-
 	// Parse the complete batch before executing its first statement. This keeps
 	// unsupported trailing clauses from turning into committed writes.
 	l := lexer.New(sql)
@@ -754,37 +741,29 @@ type executeError struct {
 // means the portal is ready to stream.
 func (c *Connection) materializePortal(portal *portal) *executeError {
 	execution := &portalExecution{}
-	if result, handled, catalogErr := c.catalogResult(portal.query); handled {
-		if catalogErr != nil {
-			return &executeError{code: ErrCodeInternalError, err: catalogErr}
-		}
-		execution.result = result
-		execution.commandTag = fmt.Sprintf("SELECT %d", len(result.Rows))
-	} else {
-		stmt, err := parser.New(lexer.New(portal.query)).Parse()
-		if err != nil {
-			code := ErrCodeSyntaxError
-			if errors.Is(err, pgerror.ErrFeatureNotSupported) {
-				code = ErrCodeFeatureNotSupported
-				if c.txStatus == TxStatusInBlock {
-					c.txStatus = TxStatusFailed
-				}
+	stmt, err := parser.New(lexer.New(portal.query)).Parse()
+	if err != nil {
+		code := ErrCodeSyntaxError
+		if errors.Is(err, pgerror.ErrFeatureNotSupported) {
+			code = ErrCodeFeatureNotSupported
+			if c.txStatus == TxStatusInBlock {
+				c.txStatus = TxStatusFailed
 			}
-			return &executeError{code: code, err: fmt.Errorf("syntax error: %w", err)}
-		}
-		result, err := c.executor.Execute(stmt)
-		if err != nil && isIdleRollback(stmt, c.txStatus, err) {
-			result, err = executor.NewResult("ROLLBACK"), nil
-		}
-		if err != nil {
-			return c.classifyExecuteError(stmt, err)
 		}
-		execution.result = result
-		// getCommandTag applies the statement's transaction transition, so it
-		// belongs here: a repeated Execute must not replay BEGIN/COMMIT/ROLLBACK.
-		execution.commandTag = c.getCommandTag(stmt, result)
-		execution.clearsPortals = resetsPortals(stmt)
+		return &executeError{code: code, err: fmt.Errorf("syntax error: %w", err)}
 	}
+	result, err := c.executor.Execute(stmt)
+	if err != nil && isIdleRollback(stmt, c.txStatus, err) {
+		result, err = executor.NewResult("ROLLBACK"), nil
+	}
+	if err != nil {
+		return c.classifyExecuteError(stmt, err)
+	}
+	execution.result = result
+	// getCommandTag applies the statement's transaction transition, so it
+	// belongs here: a repeated Execute must not replay BEGIN/COMMIT/ROLLBACK.
+	execution.commandTag = c.getCommandTag(stmt, result)
+	execution.clearsPortals = resetsPortals(stmt)
 	columnTypes := wireColumnTypes(execution.result)
 	formats, formatErr := c.resolveResultFormats(portal.resultFormats, columnTypes, len(execution.result.Columns))
 	if formatErr != nil {
@@ -919,203 +898,6 @@ func (c *Connection) sendPortalRows(portal *portal, maxRows int32) error {
 	return nil
 }
 
-// catalogSession is the catalog view this connection serves. The executor's
-// session is authoritative because it resolves against the transaction-scoped
-// DDL overlay, so catalog emulation inside a transaction sees staged tables,
-// columns, and indexes. The fallback only covers a connection that has not
-// completed startup yet.
-func (c *Connection) catalogSession() *storage.Session {
-	if c.executor != nil {
-		return c.executor.Session()
-	}
-	if c.schema == nil || c.table == nil {
-		return nil
-	}
-	if c.catalogFallback == nil {
-		c.catalogFallback = storage.NewSession(c.schema, c.table)
-	}
-	return c.catalogFallback
-}
-
-var catalogFilterPattern = regexp.MustCompile(`(?i)\b(table_name|tablename|table_schema|schemaname|constraint_name|indexname)\s*=\s*'((?:''|[^'])*)'`)
-
-func (c *Connection) catalogResult(sql string) (*executor.Result, bool, error) {
-	if !isSingleStatementSQL(sql) {
-		return nil, false, nil
-	}
-	source := catalogSource(sql)
-	if source == "" {
-		return nil, false, nil
-	}
-	session := c.catalogSession()
-	if session == nil {
-		return nil, false, nil
-	}
-
-	tables, err := session.ListTables()
-	if err != nil {
-		return nil, true, err
-	}
-	rows := make([]map[string]interface{}, 0)
-	switch source {
-	case "INFORMATION_SCHEMA.TABLES":
-		for _, table := range tables {
-			rows = append(rows, map[string]interface{}{
-				"table_catalog": c.database, "table_schema": "public", "table_name": table, "table_type": "BASE TABLE",
-			})
-		}
-	case "PG_TABLES":
-		for _, table := range tables {
-			indexes, _ := session.ListTableIndexes(table)
-			rows = append(rows, map[string]interface{}{
-				"schemaname": "public", "tablename": table, "tableowner": c.params["user"], "tablespace": nil,
-				"hasindexes": len(indexes) > 0, "hasrules": false, "hastriggers": false, "rowsecurity": false,
-			})
-		}
-	case "INFORMATION_SCHEMA.COLUMNS":
-		for _, table := range tables {
-			schema, schemaErr := session.GetSchema(table)
-			if schemaErr != nil {
-				continue
-			}
-			for i, column := range schema.Columns {
-				rows = append(rows, map[string]interface{}{
-					"table_catalog": c.database, "table_schema": "public", "table_name": table,
-					"column_name": column.Name, "ordinal_position": int64(i + 1), "column_default": column.DefaultLiteral(),
-					"is_nullable": yesNo(column.Nullable), "data_type": strings.ToLower(column.Type),
-				})
-			}
-		}
-	case "INFORMATION_SCHEMA.TABLE_CONSTRAINTS", "INFORMATION_SCHEMA.KEY_COLUMN_USAGE":
-		for _, table := range tables {
-			schema, schemaErr := session.GetSchema(table)
-			if schemaErr != nil || schema.UsesSyntheticRowID() {
-				continue
-			}
-			columns := schema.PrimaryKeyList()
-			if source == "INFORMATION_SCHEMA.TABLE_CONSTRAINTS" {
-				rows = append(rows, map[string]interface{}{
-					"constraint_catalog": c.database, "constraint_schema": "public", "constraint_name": table + "_pkey",
-					"table_catalog": c.database, "table_schema": "public", "table_name": table,
-					"constraint_type": "PRIMARY KEY", "is_deferrable": "NO", "initially_deferred": "NO",
-				})
-				continue
-			}
-			// KEY_COLUMN_USAGE reports one row per key column, in key order.
-			for i, column := range columns {
-				rows = append(rows, map[string]interface{}{
-					"constraint_catalog": c.database, "constraint_schema": "public", "constraint_name": table + "_pkey",
-					"table_catalog": c.database, "table_schema": "public", "table_name": table,
-					"column_name": column, "ordinal_position": int64(i + 1),
-				})
-			}
-		}
-	case "PG_INDEXES":
-		for _, table := range tables {
-			indexes, _ := session.ListTableIndexes(table)
-			for _, index := range indexes {
-				columns := make([]string, len(index.Columns))
-				for i, column := range index.Columns {
-					columns[i] = column.Name
-				}
-				unique := ""
-				if index.Unique {
-					unique = "UNIQUE "
-				}
-				rows = append(rows, map[string]interface{}{
-					"schemaname": "public", "tablename": table, "indexname": index.Name, "tablespace": nil,
-					"indexdef": fmt.Sprintf("CREATE %sINDEX %s ON %s (%s)", unique, index.Name, table, strings.Join(columns, ", ")),
-				})
-			}
-		}
-	}
-
-	for _, match := range catalogFilterPattern.FindAllStringSubmatch(sql, -1) {
-		column, expected := strings.ToLower(match[1]), strings.ReplaceAll(match[2], "''", "'")
-		filtered := rows[:0]
-		for _, row := range rows {
-			if value, ok := row[column]; ok && strings.EqualFold(fmt.Sprintf("%v", value), expected) {
-				filtered = append(filtered, row)
-			}
-		}
-		rows = filtered
-	}
-
-	columns := catalogProjection(sql, rows)
-	result := executor.NewResult("SELECT")
-	if len(columns) == 1 && columns[0] == "count(*)" {
-		result.AddColumnWithType("count", "INTEGER")
-		result.AddRow(int64(len(rows)))
-		return result, true, nil
-	}
-	for _, column := range columns {
-		columnType := "TEXT"
-		if column == "ordinal_position" {
-			columnType = "INTEGER"
-		} else if strings.HasPrefix(column, "has") || column == "rowsecurity" {
-			columnType = "BOOLEAN"
-		}
-		result.AddColumnWithType(column, columnType)
-	}
-	for _, row := range rows {
-		values := make([]interface{}, len(columns))
-		for i, column := range columns {
-			values[i] = row[column]
-		}
-		result.AddRow(values...)
-	}
-	return result, true, nil
-}
-
-// isRollbackStatement reports whether sql parses as a single ROLLBACK
-// statement, including ROLLBACK TO SAVEPOINT.
-func isRollbackStatement(sql string) bool {
-	stmt, err := parser.New(lexer.New(sql)).Parse()
-	if err != nil {
-		return false
-	}
-	_, ok := stmt.(*parser.RollbackStmt)
-	return ok
-}
-
-func catalogProjection(sql string, rows []map[string]interface{}) []string {
-	upper := strings.ToUpper(sql)
-	selectPos, fromPos := strings.Index(upper, "SELECT"), strings.Index(upper, " FROM ")
-	if selectPos < 0 || fromPos < 0 || fromPos <= selectPos+6 {
-		return nil
-	}
-	projection := strings.TrimSpace(sql[selectPos+6 : fromPos])
-	projection = strings.TrimSpace(strings.TrimPrefix(strings.ToUpper(projection), "DISTINCT "))
-	if projection == "*" && len(rows) > 0 {
-		columns := make([]string, 0, len(rows[0]))
-		for column := range rows[0] {
-			columns = append(columns, column)
-		}
-		sort.Strings(columns)
-		return columns
-	}
-	parts := strings.Split(projection, ",")
-	columns := make([]string, 0, len(parts))
-	for _, part := range parts {
-		column := strings.TrimSpace(part)
-		if index := strings.Index(strings.ToUpper(column), " AS "); index >= 0 {
-			column = strings.TrimSpace(column[:index])
-		}
-		if index := strings.LastIndex(column, "."); index >= 0 {
-			column = column[index+1:]
-		}
-		columns = append(columns, strings.ToLower(strings.Trim(column, `"`)))
-	}
-	return columns
-}
-
-func yesNo(value bool) string {
-	if value {
-		return "YES"
-	}
-	return "NO"
-}
-
 func (c *Connection) sendTabularResult(result *executor.Result, tag string, bindFormats []int16) error {
 	formats, err := c.resolveResultFormats(bindFormats, result.ColumnTypes, len(result.Columns))
 	if err != nil {
@@ -1814,13 +1596,21 @@ func (c *Connection) getOIDForType(typeName string) int32 {
 		return 701 // FLOAT8OID
 	case "JSONB":
 		return 3802 // JSONBOID
+	case "OID":
+		return 26 // OIDOID
+	case "REGCLASS":
+		return 2205 // REGCLASSOID
+	case "REGTYPE":
+		return 2206 // REGTYPEOID
+	case "NAME":
+		return 19 // NAMEOID
 	case "TEXT", "VARCHAR", "CHAR", "UUID":
 		// UUID is stored as text (SQLite-style), so it reports TEXTOID rather
 		// than the native PG UUID OID.
 		return 25 // TEXTOID
-	case "REAL", "FLOAT":
+	case "REAL", "FLOAT", "FLOAT4":
 		return 700 // FLOAT4OID
-	case "DOUBLE":
+	case "DOUBLE", "FLOAT8":
 		return 701 // FLOAT8OID
 	case "BOOLEAN", "BOOL":
 		return 16 // BOOLOID
@@ -1851,9 +1641,9 @@ func (c *Connection) getTypeSizeForType(typeName string) int16 {
 		return 8
 	case "NUMERIC", "DECIMAL":
 		return 8 // Reported as float8; see getOIDForType.
-	case "REAL", "FLOAT":
+	case "REAL", "FLOAT", "FLOAT4":
 		return 4
-	case "DOUBLE":
+	case "DOUBLE", "FLOAT8":
 		return 8
 	case "BOOLEAN", "BOOL":
 		return 1
@@ -1861,6 +1651,10 @@ func (c *Connection) getTypeSizeForType(typeName string) int16 {
 		return 8
 	case "DATE":
 		return 4
+	case "OID", "REGCLASS", "REGTYPE":
+		return 4
+	case "NAME":
+		return 64
 	default:
 		return -1 // Variable length
 	}
@@ -1971,3 +1765,14 @@ func (c *Connection) handleShowCommand(sqlUpper string) error {
 
 	return c.sendReadyForQuery()
 }
+
+// isRollbackStatement reports whether sql parses as a single ROLLBACK
+// statement, including ROLLBACK TO SAVEPOINT.
+func isRollbackStatement(sql string) bool {
+	stmt, err := parser.New(lexer.New(sql)).Parse()
+	if err != nil {
+		return false
+	}
+	_, ok := stmt.(*parser.RollbackStmt)
+	return ok
+}

+ 95 - 0
pkg/pgserver/pg_catalog_test.go

@@ -0,0 +1,95 @@
+package pgserver
+
+import (
+	"strconv"
+	"strings"
+	"testing"
+)
+
+func simpleQuery(sql string) *Message {
+	return &Message{Type: MsgQuery, Data: append([]byte(sql), 0)}
+}
+
+// Catalog columns carry PostgreSQL's types over the wire (oid 26, name 19), so
+// a driver decodes them without falling back to TEXT.
+func TestPgCatalogWireTypes(t *testing.T) {
+	c, client := newDataConnection(t)
+	for _, sql := range []string{
+		"CREATE TABLE wire (id INTEGER PRIMARY KEY, payload JSONB)",
+		"CREATE INDEX idx_wire_payload ON wire (payload)",
+	} {
+		if msgs := runQuery(t, c, client, simpleQuery(sql)); errorCode(msgs[0]) != "" {
+			t.Fatalf("%s: %s", sql, errorMessage(msgs[0]))
+		}
+	}
+
+	msgs := runQuery(t, c, client, simpleQuery(`
+		SELECT c.oid, c.relname, c.relkind
+		FROM pg_catalog.pg_class c
+		WHERE c.relname = 'wire'`))
+	if msg := findError(msgs); msg != nil {
+		t.Fatalf("catalog query failed: %s", errorMessage(msg))
+	}
+	rd := findRowDescription(t, msgs)
+	if got := rowDescriptionOIDs(t, rd.Data); len(got) != 3 || got[0] != 26 || got[1] != 19 || got[2] != 25 {
+		t.Fatalf("catalog OIDs = %v, want [26 19 25]", got)
+	}
+	rows := catalogRows(t, msgs)
+	if len(rows) != 1 || rows[0][1] != "wire" || rows[0][2] != "r" {
+		t.Fatalf("pg_class row = %v", rows)
+	}
+	if _, err := strconv.ParseUint(rows[0][0], 10, 32); err != nil {
+		t.Fatalf("oid %q is not numeric: %v", rows[0][0], err)
+	}
+}
+
+// A join across pg_class/pg_namespace/pg_attribute works over the simple-query
+// protocol, which is how migration tools list columns.
+func TestPgCatalogJoinOverWire(t *testing.T) {
+	c, client := newDataConnection(t)
+	for _, sql := range []string{
+		"CREATE TABLE wire_join (id INTEGER PRIMARY KEY, label TEXT NOT NULL)",
+	} {
+		if msgs := runQuery(t, c, client, simpleQuery(sql)); errorCode(msgs[0]) != "" {
+			t.Fatalf("%s: %s", sql, errorMessage(msgs[0]))
+		}
+	}
+	msgs := runQuery(t, c, client, simpleQuery(`
+		SELECT n.nspname, a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod)
+		FROM pg_catalog.pg_class c
+		JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
+		JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
+		WHERE c.relname = 'wire_join' AND a.attnum > 0
+		ORDER BY a.attnum`))
+	if msg := findError(msgs); msg != nil {
+		t.Fatalf("catalog join failed: %s", errorMessage(msg))
+	}
+	rows := catalogRows(t, msgs)
+	if len(rows) != 2 || rows[0][1] != "id" || rows[0][2] != "integer" || rows[1][1] != "label" {
+		t.Fatalf("catalog join rows = %v", rows)
+	}
+}
+
+// ORM connections issue SET during startup; the engine acknowledges it and
+// SHOW reports the retained search path.
+func TestSetAndShowSearchPathOverWire(t *testing.T) {
+	c, client := newDataConnection(t)
+	if msgs := runQuery(t, c, client, simpleQuery("SET search_path TO public")); errorCode(msgs[0]) != "" {
+		t.Fatalf("SET search_path failed: %s", errorMessage(msgs[0]))
+	}
+	if msgs := runQuery(t, c, client, simpleQuery("SET TIME ZONE 'UTC'")); errorCode(msgs[0]) != "" {
+		t.Fatalf("SET TIME ZONE failed: %s", errorMessage(msgs[0]))
+	}
+	msgs := runQuery(t, c, client, simpleQuery("SHOW search_path"))
+	if msg := findError(msgs); msg != nil {
+		t.Fatalf("SHOW search_path failed: %s", errorMessage(msg))
+	}
+	rows := catalogRows(t, msgs)
+	if len(rows) != 1 || rows[0][0] != "public" {
+		t.Fatalf("SHOW search_path = %v", rows)
+	}
+	msgs = runQuery(t, c, client, simpleQuery("SHOW client_encoding"))
+	if rows := catalogRows(t, msgs); len(rows) != 1 || !strings.EqualFold(rows[0][0], "UTF8") {
+		t.Fatalf("SHOW client_encoding = %v", rows)
+	}
+}

+ 113 - 0
pkg/pgserver/pgx_catalog_test.go

@@ -0,0 +1,113 @@
+package pgserver
+
+import (
+	"context"
+	"testing"
+)
+
+// A stock pgx connection introspects the catalog with its normal
+// Parse/Bind/Execute path. This covers the real driver reading oid/name/regclass
+// columns and issuing SET during a session.
+func TestPGXCatalogIntrospection(t *testing.T) {
+	conn := connectPGX(t)
+	ctx := context.Background()
+	for _, sql := range []string{
+		`CREATE TABLE catalog_parent (id INTEGER PRIMARY KEY, label TEXT NOT NULL UNIQUE)`,
+		`CREATE TABLE catalog_child (id INTEGER PRIMARY KEY, parent_id INTEGER NOT NULL, note TEXT DEFAULT 'n', CONSTRAINT fk_catalog_child FOREIGN KEY (parent_id) REFERENCES catalog_parent(id))`,
+		`CREATE INDEX idx_catalog_child_note ON catalog_child (note)`,
+	} {
+		if _, err := conn.Exec(ctx, sql); err != nil {
+			t.Fatalf("%s: %v", sql, err)
+		}
+	}
+
+	var (
+		oid     uint32
+		relname string
+		relkind string
+	)
+	err := conn.QueryRow(ctx, `
+		SELECT c.oid, c.relname, c.relkind
+		FROM pg_catalog.pg_class c
+		WHERE c.relname = 'catalog_child'`).Scan(&oid, &relname, &relkind)
+	if err != nil {
+		t.Fatalf("pg_class scan: %v", err)
+	}
+	if oid == 0 || relname != "catalog_child" || relkind != "r" {
+		t.Fatalf("pg_class = %d %q %q", oid, relname, relkind)
+	}
+
+	rows, err := conn.Query(ctx, `
+		SELECT n.nspname, a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod)
+		FROM pg_catalog.pg_class c
+		JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
+		JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
+		WHERE c.relname = 'catalog_child' AND a.attnum > 0
+		ORDER BY a.attnum`)
+	if err != nil {
+		t.Fatalf("catalog join: %v", err)
+	}
+	defer rows.Close()
+	var columns []string
+	for rows.Next() {
+		var schema, column, typ string
+		if err := rows.Scan(&schema, &column, &typ); err != nil {
+			t.Fatalf("scan: %v", err)
+		}
+		if schema != "public" {
+			t.Fatalf("schema = %q", schema)
+		}
+		columns = append(columns, column+":"+typ)
+	}
+	if err := rows.Err(); err != nil {
+		t.Fatalf("rows: %v", err)
+	}
+	want := []string{"id:integer", "parent_id:integer", "note:text"}
+	if len(columns) != len(want) {
+		t.Fatalf("columns = %v, want %v", columns, want)
+	}
+	for i := range want {
+		if columns[i] != want[i] {
+			t.Fatalf("columns = %v, want %v", columns, want)
+		}
+	}
+
+	var (
+		tableName  string
+		columnName string
+		dataType   string
+		isNullable string
+	)
+	err = conn.QueryRow(ctx, `
+		SELECT table_name, column_name, data_type, is_nullable
+		FROM information_schema.columns
+		WHERE table_name = 'catalog_child' AND column_name = 'note'`).
+		Scan(&tableName, &columnName, &dataType, &isNullable)
+	if err != nil {
+		t.Fatalf("information_schema scan: %v", err)
+	}
+	if tableName != "catalog_child" || columnName != "note" || dataType != "text" || isNullable != "YES" {
+		t.Fatalf("information_schema row = %q %q %q %q", tableName, columnName, dataType, isNullable)
+	}
+
+	if _, err := conn.Exec(ctx, "SET search_path TO public"); err != nil {
+		t.Fatalf("SET search_path: %v", err)
+	}
+	var searchPath string
+	if err := conn.QueryRow(ctx, "SHOW search_path").Scan(&searchPath); err != nil {
+		t.Fatalf("SHOW search_path: %v", err)
+	}
+	if searchPath != "public" {
+		t.Fatalf("search_path = %q", searchPath)
+	}
+
+	var regclassRows int
+	if err := conn.QueryRow(ctx, `
+		SELECT count(*) FROM pg_catalog.pg_constraint
+		WHERE conrelid = 'catalog_child'::regclass AND contype = 'f'`).Scan(&regclassRows); err != nil {
+		t.Fatalf("regclass constraint query: %v", err)
+	}
+	if regclassRows != 1 {
+		t.Fatalf("foreign key constraint count = %d, want 1", regclassRows)
+	}
+}

+ 24 - 0
pkg/pgserver/result_binary.go

@@ -34,10 +34,14 @@ func canEncodeBinaryResult(oid int32) bool {
 		21,   // int2
 		23,   // int4
 		25,   // text
+		19,   // name
+		26,   // oid
 		700,  // float4
 		701,  // float8
 		1043, // varchar
 		1700, // numeric
+		2205, // regclass
+		2206, // regtype
 		3802: // jsonb
 		return true
 	}
@@ -135,6 +139,17 @@ func appendBinaryResultValue(dst []byte, oid int32, value interface{}) ([]byte,
 		default:
 			return appendUint64(dst, uint64(n)), nil
 		}
+	case 19: // name
+		if reg, ok := value.(executor.RegOID); ok {
+			return append(dst, reg.String()...), nil
+		}
+		return append(dst, textValueString(value)...), nil
+	case 26, 2205, 2206: // oid, regclass, regtype
+		n, err := catalogOIDBinaryValue(value)
+		if err != nil {
+			return dst, err
+		}
+		return append(dst, byte(n>>24), byte(n>>16), byte(n>>8), byte(n)), nil
 	case 25, 1043: // text, varchar
 		return append(dst, textValueString(value)...), nil
 	case 700: // float4
@@ -227,6 +242,15 @@ func appendUint64(dst []byte, n uint64) []byte {
 // Text is accepted because SQLite affinity can leave a text value in an integer
 // column; a value that does not fit the reported OID is an encoding error
 // instead of a silently truncated one.
+// catalogOIDBinaryValue encodes an oid/regclass/regtype value. A RegOID carries
+// its numeric OID separately from its display name.
+func catalogOIDBinaryValue(value interface{}) (int64, error) {
+	if reg, ok := value.(executor.RegOID); ok {
+		return binaryInt64(reg.OID(), 23)
+	}
+	return binaryInt64(value, 23)
+}
+
 func binaryInt64(value interface{}, oid int32) (int64, error) {
 	var n int64
 	switch v := value.(type) {

+ 3 - 1
pkg/pgserver/result_binary_test.go

@@ -865,7 +865,9 @@ func TestBinaryResultsRejectAfterDMLExecution(t *testing.T) {
 // results: their text and boolean columns encode as binary.
 func TestBinaryResultsCatalogQuery(t *testing.T) {
 	c, conn := extendedResultConnection(t)
-	if _, err := c.executor.Execute(parseStmt(t, `CREATE TABLE t (id INTEGER PRIMARY KEY)`)); err != nil {
+	// A table without a primary key or explicit index has no indexes; the
+	// synthetic primary-key index is covered by the catalog tests.
+	if _, err := c.executor.Execute(parseStmt(t, `CREATE TABLE t (id INTEGER)`)); err != nil {
 		t.Fatal(err)
 	}
 	sendExtended(t, c,

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно