From ef7c1b67161d662554e8882f01b70a2f19c3fde7 Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 4 Jun 2026 15:40:20 -0700 Subject: [PATCH] fix: match native column types in scoped pg_tables/pg_views/pg_sequences views The session-scoped pg_tables/pg_views/pg_sequences compat views (added in the catalog-separation work) replace DuckDB's native pg_catalog views in client queries. Their synthesized columns must match the native column types exactly so the only behavior change is the catalog row-filter, not the result shape. DuckDB's native pg_tables.tablespace and pg_sequences.data_type/cache_size are INTEGER (not text/bigint). A bare `NULL AS tablespace` types as INT32 which happened to match, but `'bigint' AS data_type` (VARCHAR) and other nulls diverged, so a client filtering e.g. `WHERE tablespace = 'x'` could hit a type error it would not have hit against the native view. Pin the synthesized columns to native types (tablespace/data_type/ cache_size -> INTEGER, value columns -> BIGINT) and add TestPgCatalogConvenienceViewsMatchNativeShape, which DESCRIBEs each compat view and asserts its (column_name, column_type) list is identical to the native pg_catalog view. Co-Authored-By: Claude Opus 4.8 (1M context) --- server/session_database_metadata_test.go | 48 ++++++++++++++++++++++++ server/sessionmeta/sessionmeta.go | 24 ++++++------ 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/server/session_database_metadata_test.go b/server/session_database_metadata_test.go index 24ed2054..f09a4962 100644 --- a/server/session_database_metadata_test.go +++ b/server/session_database_metadata_test.go @@ -712,6 +712,54 @@ func TestPgTablesViewsSequencesOnlyExposeSelectedCatalog(t *testing.T) { } } +func TestPgCatalogConvenienceViewsMatchNativeShape(t *testing.T) { + // The scoped pg_tables/pg_views/pg_sequences compat views replace DuckDB's + // native pg_catalog views in client queries. To make the only behavior change + // the catalog row-filter (not the result shape), each compat view's column + // names AND types must match the native view exactly. + db, err := sql.Open("duckdb", ":memory:") + if err != nil { + t.Fatalf("open duckdb: %v", err) + } + db.SetMaxOpenConns(1) + defer func() { _ = db.Close() }() + + if _, err := db.Exec(`ATTACH ':memory:' AS ducklake`); err != nil { + t.Fatalf("attach ducklake: %v", err) + } + executor := NewLocalExecutor(db) + if err := sessionmeta.InitSessionDatabaseMetadata(context.Background(), executor, "ducklake"); err != nil { + t.Fatalf("init session database metadata: %v", err) + } + + shapeOf := func(relation string) string { + rows, err := db.Query("SELECT column_name || ' ' || column_type FROM (DESCRIBE SELECT * FROM " + relation + ") ORDER BY column_name") + if err != nil { + t.Fatalf("describe %s: %v", relation, err) + } + defer func() { _ = rows.Close() }() + var cols []string + for rows.Next() { + var c string + if err := rows.Scan(&c); err != nil { + t.Fatalf("scan %s: %v", relation, err) + } + cols = append(cols, c) + } + return strings.Join(cols, ", ") + } + + for _, view := range []string{"pg_tables", "pg_views", "pg_sequences"} { + t.Run(view, func(t *testing.T) { + native := shapeOf("pg_catalog." + view) + compat := shapeOf("memory.main." + view) + if native != compat { + t.Fatalf("%s shape mismatch:\n native = %s\n compat = %s", view, native, compat) + } + }) + } +} + func TestInformationSchemaColumnsCompatScopesLoadedMetadataToSelectedCatalog(t *testing.T) { db, err := sql.Open("duckdb", ":memory:") if err != nil { diff --git a/server/sessionmeta/sessionmeta.go b/server/sessionmeta/sessionmeta.go index 5a8759ad..7d87ef8c 100644 --- a/server/sessionmeta/sessionmeta.go +++ b/server/sessionmeta/sessionmeta.go @@ -636,7 +636,7 @@ func buildSessionPgTablesViewSQL() string { CASE WHEN t.schema_name = 'main' THEN 'public' ELSE t.schema_name END AS schemaname, t.table_name AS tablename, 'duckdb' AS tableowner, - NULL AS tablespace, + NULL::INTEGER AS tablespace, (t.index_count > 0) AS hasindexes, false AS hasrules, false AS hastriggers @@ -675,9 +675,11 @@ func buildSessionPgViewsViewSQL() string { // buildSessionPgSequencesViewSQL builds the catalog-scoped pg_catalog.pg_sequences // compat view (same cross-catalog-leak rationale as buildSessionPgTablesViewSQL). -// Sources duckdb_sequences() filtered to current_database(). DuckDB's -// duckdb_sequences() does not expose data_type/cache_size, so those are -// synthesized to match PostgreSQL's pg_sequences shape. +// Sources duckdb_sequences() filtered to current_database(). Column names and +// types mirror DuckDB's native pg_sequences (the view these queries resolved to +// before scoping) so the only behavior change is the catalog filter: data_type +// and cache_size are INTEGER (DuckDB does not expose real values, so NULL), and +// the value columns are BIGINT. func buildSessionPgSequencesViewSQL() string { return ` CREATE OR REPLACE VIEW main.pg_sequences AS @@ -688,14 +690,14 @@ func buildSessionPgSequencesViewSQL() string { CASE WHEN s.schema_name = 'main' THEN 'public' ELSE s.schema_name END AS schemaname, s.sequence_name AS sequencename, 'duckdb' AS sequenceowner, - 'bigint' AS data_type, - s.start_value AS start_value, - s.min_value AS min_value, - s.max_value AS max_value, - s.increment_by AS increment_by, + NULL::INTEGER AS data_type, + s.start_value::BIGINT AS start_value, + s.min_value::BIGINT AS min_value, + s.max_value::BIGINT AS max_value, + s.increment_by::BIGINT AS increment_by, s.cycle AS cycle, - NULL AS cache_size, - s.last_value AS last_value + NULL::INTEGER AS cache_size, + s.last_value::BIGINT AS last_value FROM duckdb_sequences() s CROSS JOIN active_catalog ac WHERE s.database_name = ac.catalog