From 75bf8214b36fac3c64757fce7aa0e1ca1aed7742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Cholewi=C5=84ski?= Date: Tue, 26 May 2026 08:42:42 +0000 Subject: [PATCH 1/3] Resolve cross-schema row types to STRUCT (not VARCHAR) Follow-up to #474 / #475. After PR #475, row types of relations are discovered and registered under their own schema, so the in-schema case works. But when a relation in schema B has a column whose type is the row type of a relation in schema A, PostgresUtils::TypeToLogicalType still looked up the type entry against the relation's schema (B). It finds nothing there and falls back to VARCHAR. Plumb the type's own namespace through PostgresTypeData. The two discovery queries now join pg_namespace on pg_type.typnamespace and expose nspname as the type's schema, and TypeToLogicalType uses Catalog::GetSchema to resolve the lookup against that schema when it differs from the relation's own. Extend test/sql/storage/attach_types_table_row.test with a cross- schema view and table; the existing test put both ends in the same schema, which is why this slipped through. --- src/include/postgres_utils.hpp | 1 + src/postgres_utils.cpp | 13 +++- src/storage/postgres_table_set.cpp | 9 ++- src/storage/postgres_type_set.cpp | 7 ++- test/sql/storage/attach_types_table_row.test | 62 ++++++++++++++++++++ 5 files changed, 87 insertions(+), 5 deletions(-) diff --git a/src/include/postgres_utils.hpp b/src/include/postgres_utils.hpp index 9941d988a..e28797bf7 100644 --- a/src/include/postgres_utils.hpp +++ b/src/include/postgres_utils.hpp @@ -20,6 +20,7 @@ class PostgresTransaction; struct PostgresTypeData { int64_t type_modifier = 0; string type_name; + string type_schema; idx_t array_dimensions = 0; }; diff --git a/src/postgres_utils.cpp b/src/postgres_utils.cpp index e3f275349..faa987cb1 100644 --- a/src/postgres_utils.cpp +++ b/src/postgres_utils.cpp @@ -143,6 +143,7 @@ LogicalType PostgresUtils::TypeToLogicalType(optional_ptr t PostgresTypeData child_type_info; child_type_info.type_name = pgtypename.substr(1); child_type_info.type_modifier = type_info.type_modifier; + child_type_info.type_schema = type_info.type_schema; PostgresType child_pg_type; auto child_type = PostgresUtils::TypeToLogicalType(transaction, schema, child_type_info, child_pg_type); // populate the child OID from the actual Postgres type name @@ -244,8 +245,16 @@ LogicalType PostgresUtils::TypeToLogicalType(optional_ptr t if (!context) { throw InternalException("Context is destroyed!?"); } - auto entry = schema->GetEntry(CatalogTransaction(schema->ParentCatalog(), *context), CatalogType::TYPE_ENTRY, - pgtypename); + optional_ptr lookup_schema = schema.get(); + if (!type_info.type_schema.empty() && type_info.type_schema != schema->name) { + auto other_schema = + schema->ParentCatalog().GetSchema(*context, type_info.type_schema, OnEntryNotFound::RETURN_NULL); + if (other_schema) { + lookup_schema = other_schema; + } + } + auto entry = lookup_schema->GetEntry(CatalogTransaction(lookup_schema->ParentCatalog(), *context), + CatalogType::TYPE_ENTRY, pgtypename); if (!entry) { // unsupported so fallback to varchar postgres_type.info = PostgresTypeAnnotation::CAST_TO_VARCHAR; diff --git a/src/storage/postgres_table_set.cpp b/src/storage/postgres_table_set.cpp index 8df2c7b82..ccbc19c17 100644 --- a/src/storage/postgres_table_set.cpp +++ b/src/storage/postgres_table_set.cpp @@ -24,17 +24,18 @@ string PostgresTableSet::GetInitializeQuery(const string &schema, const string & SELECT pg_namespace.oid AS namespace_id, relname, relpages, attname, pg_type.typname type_name, atttypmod type_modifier, pg_attribute.attndims ndim, attnum, pg_attribute.attnotnull AS notnull, NULL constraint_id, - NULL constraint_type, NULL constraint_key + NULL constraint_type, NULL constraint_key, type_ns.nspname AS type_schema FROM pg_class JOIN pg_namespace ON relnamespace = pg_namespace.oid JOIN pg_attribute ON pg_class.oid=pg_attribute.attrelid JOIN pg_type ON atttypid=pg_type.oid +JOIN pg_namespace type_ns ON pg_type.typnamespace = type_ns.oid WHERE attnum > 0 AND relkind IN ('r', 'v', 'm', 'f', 'p') ${CONDITION} UNION ALL SELECT pg_namespace.oid AS namespace_id, relname, NULL relpages, NULL attname, NULL type_name, NULL type_modifier, NULL ndim, NULL attnum, NULL AS notnull, pg_constraint.oid AS constraint_id, contype AS constraint_type, - conkey AS constraint_key + conkey AS constraint_key, NULL AS type_schema FROM pg_class JOIN pg_namespace ON relnamespace = pg_namespace.oid JOIN pg_constraint ON (pg_class.oid=pg_constraint.conrelid) @@ -61,6 +62,10 @@ void PostgresTableSet::AddColumn(optional_ptr transaction, type_info.type_modifier = result.GetInt64(row, column_index + 2); type_info.array_dimensions = result.GetInt64(row, column_index + 3); bool is_not_null = result.GetBool(row, column_index + 5); + idx_t type_schema_index = column_index + 9; + if (!result.IsNull(row, type_schema_index)) { + type_info.type_schema = result.GetString(row, type_schema_index); + } string default_value; PostgresType postgres_type; diff --git a/src/storage/postgres_type_set.cpp b/src/storage/postgres_type_set.cpp index c4001f411..0e2f1d4b0 100644 --- a/src/storage/postgres_type_set.cpp +++ b/src/storage/postgres_type_set.cpp @@ -84,12 +84,14 @@ void PostgresTypeSet::InitializeEnums(PostgresTransaction &transaction, Postgres string PostgresTypeSet::GetInitializeCompositesQuery(const string &schema) { string base_query = R"( -SELECT n.oid, t.typrelid AS id, t.typname as type, pg_attribute.attname, sub_type.typname +SELECT n.oid, t.typrelid AS id, t.typname as type, pg_attribute.attname, sub_type.typname, + sub_type_ns.nspname AS sub_type_schema FROM pg_type t JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace JOIN pg_class ON pg_class.oid = t.typrelid JOIN pg_attribute ON attrelid=t.typrelid JOIN pg_type sub_type ON (pg_attribute.atttypid=sub_type.oid) +JOIN pg_catalog.pg_namespace sub_type_ns ON sub_type_ns.oid = sub_type.typnamespace WHERE pg_class.relkind IN ('c', 'r', 'v', 'm', 'f', 'p') AND t.typtype='c' AND pg_attribute.attnum > 0 @@ -116,6 +118,9 @@ void PostgresTypeSet::CreateCompositeType(PostgresTransaction &transaction, Post auto type_name = result.GetString(row, 3); PostgresTypeData type_data; type_data.type_name = result.GetString(row, 4); + if (!result.IsNull(row, 5)) { + type_data.type_schema = result.GetString(row, 5); + } PostgresType child_type; child_types.push_back( make_pair(type_name, PostgresUtils::TypeToLogicalType(&transaction, &schema, type_data, child_type))); diff --git a/test/sql/storage/attach_types_table_row.test b/test/sql/storage/attach_types_table_row.test index 52b1c751a..cbb26820d 100644 --- a/test/sql/storage/attach_types_table_row.test +++ b/test/sql/storage/attach_types_table_row.test @@ -71,3 +71,65 @@ SET pg_use_text_protocol=false; statement ok DROP SCHEMA IF EXISTS row_types CASCADE; + +statement ok +DROP SCHEMA IF EXISTS row_types_a CASCADE; + +statement ok +DROP SCHEMA IF EXISTS row_types_b CASCADE; + +statement ok +CREATE SCHEMA row_types_a; + +statement ok +CREATE SCHEMA row_types_b; + +statement ok +CREATE TABLE row_types_a.widget(id INT, label VARCHAR); + +statement ok +INSERT INTO row_types_a.widget VALUES (1, 'hello'); + +# view in schema B whose column is row type of a relation in schema A +statement ok +CALL postgres_execute('s', 'CREATE VIEW row_types_b.widget_view AS SELECT w FROM row_types_a.widget AS w'); + +# table in schema B whose column is declared with schema A's row type +statement ok +CALL postgres_execute('s', 'CREATE TABLE row_types_b.wrapper(id INT, payload row_types_a.widget)'); + +statement ok +CALL postgres_execute('s', 'INSERT INTO row_types_b.wrapper VALUES (1, ROW(2, ''nested''))'); + +statement ok +CALL pg_clear_cache(); + +query II +SELECT w.id, w.label FROM row_types_b.widget_view +---- +1 hello + +query III +SELECT id, payload.id, payload.label FROM row_types_b.wrapper +---- +1 2 nested + +statement ok +SET pg_use_text_protocol=true; + +statement ok +CALL pg_clear_cache(); + +query II +SELECT w.id, w.label FROM row_types_b.widget_view +---- +1 hello + +statement ok +SET pg_use_text_protocol=false; + +statement ok +DROP SCHEMA IF EXISTS row_types_a CASCADE; + +statement ok +DROP SCHEMA IF EXISTS row_types_b CASCADE; From b7fff9cfb2d6cb253654d6c3b489f455f0699734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Cholewi=C5=84ski?= Date: Tue, 26 May 2026 14:27:08 +0200 Subject: [PATCH 2/3] Drop unreachable null/empty guards around type_schema The two SQL discovery queries pull type_schema from an INNER JOIN on pg_namespace, so the column is never NULL. --- src/postgres_utils.cpp | 12 ++++-------- src/storage/postgres_table_set.cpp | 4 +--- src/storage/postgres_type_set.cpp | 4 +--- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/postgres_utils.cpp b/src/postgres_utils.cpp index faa987cb1..41bb09306 100644 --- a/src/postgres_utils.cpp +++ b/src/postgres_utils.cpp @@ -245,14 +245,10 @@ LogicalType PostgresUtils::TypeToLogicalType(optional_ptr t if (!context) { throw InternalException("Context is destroyed!?"); } - optional_ptr lookup_schema = schema.get(); - if (!type_info.type_schema.empty() && type_info.type_schema != schema->name) { - auto other_schema = - schema->ParentCatalog().GetSchema(*context, type_info.type_schema, OnEntryNotFound::RETURN_NULL); - if (other_schema) { - lookup_schema = other_schema; - } - } + optional_ptr lookup_schema = + type_info.type_schema != schema->name + ? schema->ParentCatalog().GetSchema(*context, type_info.type_schema, OnEntryNotFound::THROW_EXCEPTION) + : schema.get(); auto entry = lookup_schema->GetEntry(CatalogTransaction(lookup_schema->ParentCatalog(), *context), CatalogType::TYPE_ENTRY, pgtypename); if (!entry) { diff --git a/src/storage/postgres_table_set.cpp b/src/storage/postgres_table_set.cpp index ccbc19c17..da298567e 100644 --- a/src/storage/postgres_table_set.cpp +++ b/src/storage/postgres_table_set.cpp @@ -63,9 +63,7 @@ void PostgresTableSet::AddColumn(optional_ptr transaction, type_info.array_dimensions = result.GetInt64(row, column_index + 3); bool is_not_null = result.GetBool(row, column_index + 5); idx_t type_schema_index = column_index + 9; - if (!result.IsNull(row, type_schema_index)) { - type_info.type_schema = result.GetString(row, type_schema_index); - } + type_info.type_schema = result.GetString(row, type_schema_index); string default_value; PostgresType postgres_type; diff --git a/src/storage/postgres_type_set.cpp b/src/storage/postgres_type_set.cpp index 0e2f1d4b0..69f0b35fd 100644 --- a/src/storage/postgres_type_set.cpp +++ b/src/storage/postgres_type_set.cpp @@ -118,9 +118,7 @@ void PostgresTypeSet::CreateCompositeType(PostgresTransaction &transaction, Post auto type_name = result.GetString(row, 3); PostgresTypeData type_data; type_data.type_name = result.GetString(row, 4); - if (!result.IsNull(row, 5)) { - type_data.type_schema = result.GetString(row, 5); - } + type_data.type_schema = result.GetString(row, 5); PostgresType child_type; child_types.push_back( make_pair(type_name, PostgresUtils::TypeToLogicalType(&transaction, &schema, type_data, child_type))); From e3ca5d45bf27f42a8d97979eda89c9834712e375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Cholewi=C5=84ski?= Date: Tue, 26 May 2026 15:08:46 +0200 Subject: [PATCH 3/3] Revert null-check condition in one SQL discovery result --- src/storage/postgres_table_set.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/storage/postgres_table_set.cpp b/src/storage/postgres_table_set.cpp index da298567e..ccbc19c17 100644 --- a/src/storage/postgres_table_set.cpp +++ b/src/storage/postgres_table_set.cpp @@ -63,7 +63,9 @@ void PostgresTableSet::AddColumn(optional_ptr transaction, type_info.array_dimensions = result.GetInt64(row, column_index + 3); bool is_not_null = result.GetBool(row, column_index + 5); idx_t type_schema_index = column_index + 9; - type_info.type_schema = result.GetString(row, type_schema_index); + if (!result.IsNull(row, type_schema_index)) { + type_info.type_schema = result.GetString(row, type_schema_index); + } string default_value; PostgresType postgres_type;