From 034927220b0fa4a0c29078c6440fee873359e793 Mon Sep 17 00:00:00 2001 From: eric Date: Fri, 19 Dec 2025 10:34:41 -0800 Subject: [PATCH 1/2] Rewrite information_schema.columns to return text for VARCHAR types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add transpiler transform to rewrite information_schema.columns queries to use information_schema_columns_compat view (which maps VARCHAR -> text) - Fix: initInformationSchema was never called, now called during connection setup - Add test to verify the rewrite works correctly PostgreSQL clients expect data_type to return 'text' or 'character varying' for string columns, but DuckDB returns 'VARCHAR'. The compat view handles this conversion. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- server/catalog.go | 5 ++++- server/server.go | 5 +++++ transpiler/transform/pgcatalog.go | 35 ++++++++++++++++++++++--------- transpiler/transpiler_test.go | 24 +++++++++++++++++++++ 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/server/catalog.go b/server/catalog.go index 3cbe1e9..859b092 100644 --- a/server/catalog.go +++ b/server/catalog.go @@ -2,6 +2,7 @@ package server import ( "database/sql" + "log" ) // initPgCatalog creates PostgreSQL compatibility functions and views in DuckDB @@ -415,7 +416,9 @@ func initInformationSchema(db *sql.DB) error { 'YES' AS is_updatable FROM information_schema.columns ` - db.Exec(columnsViewSimpleSQL) + if _, err := db.Exec(columnsViewSimpleSQL); err != nil { + log.Printf("Warning: failed to create information_schema_columns_compat view: %v", err) + } } // Create information_schema.tables wrapper view with additional PostgreSQL columns diff --git a/server/server.go b/server/server.go index 93835d3..333e6c9 100644 --- a/server/server.go +++ b/server/server.go @@ -290,6 +290,11 @@ func (s *Server) createDBConnection(username string) (*sql.DB, error) { // Continue anyway - basic queries will still work } + // Initialize information_schema wrapper views + if err := initInformationSchema(db); err != nil { + log.Printf("Warning: failed to initialize information_schema views for user %q: %v", username, err) + } + return db, nil } diff --git a/transpiler/transform/pgcatalog.go b/transpiler/transform/pgcatalog.go index 811e64f..e9884ed 100644 --- a/transpiler/transform/pgcatalog.go +++ b/transpiler/transform/pgcatalog.go @@ -12,6 +12,9 @@ type PgCatalogTransform struct { // ViewMappings maps pg_catalog table names to our compatibility views ViewMappings map[string]string + // InformationSchemaMappings maps information_schema table names to our compatibility views + InformationSchemaMappings map[string]string + // Functions that need pg_catalog prefix stripped Functions map[string]bool } @@ -20,17 +23,20 @@ type PgCatalogTransform struct { func NewPgCatalogTransform() *PgCatalogTransform { return &PgCatalogTransform{ ViewMappings: map[string]string{ - "pg_class": "pg_class_full", - "pg_database": "pg_database", - "pg_collation": "pg_collation", - "pg_policy": "pg_policy", - "pg_roles": "pg_roles", - "pg_statistic_ext": "pg_statistic_ext", + "pg_class": "pg_class_full", + "pg_database": "pg_database", + "pg_collation": "pg_collation", + "pg_policy": "pg_policy", + "pg_roles": "pg_roles", + "pg_statistic_ext": "pg_statistic_ext", "pg_publication_tables": "pg_publication_tables", - "pg_rules": "pg_rules", - "pg_publication": "pg_publication", - "pg_publication_rel": "pg_publication_rel", - "pg_inherits": "pg_inherits", + "pg_rules": "pg_rules", + "pg_publication": "pg_publication", + "pg_publication_rel": "pg_publication_rel", + "pg_inherits": "pg_inherits", + }, + InformationSchemaMappings: map[string]string{ + "columns": "information_schema_columns_compat", }, Functions: map[string]bool{ "pg_get_userbyid": true, @@ -90,6 +96,15 @@ func (t *PgCatalogTransform) walkAndTransform(node *pg_query.Node, changed *bool n.RangeVar.Schemaname = "" *changed = true } + // Table references: information_schema.columns -> information_schema_columns_compat + if n.RangeVar != nil && strings.EqualFold(n.RangeVar.Schemaname, "information_schema") { + relname := strings.ToLower(n.RangeVar.Relname) + if newName, ok := t.InformationSchemaMappings[relname]; ok { + n.RangeVar.Relname = newName + n.RangeVar.Schemaname = "" + *changed = true + } + } case *pg_query.Node_FuncCall: // Function calls: pg_catalog.format_type() -> format_type() diff --git a/transpiler/transpiler_test.go b/transpiler/transpiler_test.go index 5a61383..3d2d7db 100644 --- a/transpiler/transpiler_test.go +++ b/transpiler/transpiler_test.go @@ -375,6 +375,10 @@ func TestTranspile_ETLPatterns(t *testing.T) { name: "information_schema query", input: "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'", }, + { + name: "information_schema.columns rewrite", + input: "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'users'", + }, { name: "pg_namespace query", input: "SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname NOT LIKE 'pg_%'", @@ -439,6 +443,26 @@ func TestTranspile_DDL_Complex(t *testing.T) { } } +func TestTranspile_InformationSchemaColumnsRewrite(t *testing.T) { + tr := New(DefaultConfig()) + + input := "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'users'" + result, err := tr.Transpile(input) + if err != nil { + t.Fatalf("Transpile error: %v", err) + } + + // Should rewrite information_schema.columns to information_schema_columns_compat + if !strings.Contains(result.SQL, "information_schema_columns_compat") { + t.Errorf("Expected information_schema.columns to be rewritten to information_schema_columns_compat, got: %s", result.SQL) + } + + // Should NOT contain original schema-qualified name + if strings.Contains(result.SQL, "information_schema.columns") { + t.Errorf("Should NOT contain information_schema.columns after rewrite, got: %s", result.SQL) + } +} + func TestTranspile_EmptyQuery(t *testing.T) { tr := New(DefaultConfig()) From f197f8d80ee80927337f8b463e237d13814c96b1 Mon Sep 17 00:00:00 2001 From: eric Date: Fri, 19 Dec 2025 10:40:40 -0800 Subject: [PATCH 2/2] Create views in memory database to avoid DuckLake conflicts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When DuckLake is the default database, view creation causes transaction conflicts between concurrent connections. Fix by switching to memory database before creating views, then switching back. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- server/catalog.go | 10 ++++++++++ server/conn.go | 2 ++ 2 files changed, 12 insertions(+) diff --git a/server/catalog.go b/server/catalog.go index 859b092..cffa57c 100644 --- a/server/catalog.go +++ b/server/catalog.go @@ -8,6 +8,11 @@ import ( // initPgCatalog creates PostgreSQL compatibility functions and views in DuckDB // DuckDB already has a pg_catalog schema with basic views, so we just add missing functions func initPgCatalog(db *sql.DB) error { + // Switch to memory database to avoid creating views in DuckLake + // (DuckLake may be the default, but we want views in the local memory db) + db.Exec("USE memory") + defer db.Exec("USE ducklake") // Switch back if DuckLake is attached + // Create our own pg_database view that has all the columns psql expects // We put it in main schema and rewrite queries to use it pgDatabaseSQL := ` @@ -287,6 +292,11 @@ func initPgCatalog(db *sql.DB) error { // initInformationSchema creates the column metadata table and information_schema wrapper views. // This enables accurate type information (VARCHAR lengths, NUMERIC precision) in information_schema. func initInformationSchema(db *sql.DB) error { + // Switch to memory database to avoid creating views in DuckLake + // (DuckLake may be the default, but we want views in the local memory db) + db.Exec("USE memory") + defer db.Exec("USE ducklake") // Switch back if DuckLake is attached + // Create metadata table to store column type information that DuckDB doesn't preserve metadataTableSQL := ` CREATE TABLE IF NOT EXISTS __duckgres_column_metadata ( diff --git a/server/conn.go b/server/conn.go index 742d803..2fbd690 100644 --- a/server/conn.go +++ b/server/conn.go @@ -88,6 +88,8 @@ func (c *clientConn) serve() error { if c.db != nil { // Detach DuckLake to release the RDS metadata connection if c.server.cfg.DuckLake.MetadataStore != "" { + // Switch to memory database first (can't detach the default database) + c.db.Exec("USE memory") if _, err := c.db.Exec("DETACH ducklake"); err != nil { log.Printf("Warning: failed to detach DuckLake for user %q: %v", c.username, err) }