Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions server/session_database_metadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
24 changes: 13 additions & 11 deletions server/sessionmeta/sessionmeta.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading