Skip to content
Merged
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
1 change: 1 addition & 0 deletions src/include/postgres_utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class PostgresUtils {
static string TypeToString(const LogicalType &input);
static string PostgresOidToName(uint32_t oid);
static uint32_t ToPostgresOid(const LogicalType &input);
static uint32_t TypeNameToPostgresOid(const string &type_name);
static bool SupportedPostgresOid(const LogicalType &input);
static LogicalType RemoveAlias(const LogicalType &type);
static PostgresType CreateEmptyPostgresType(const LogicalType &type);
Expand Down
53 changes: 51 additions & 2 deletions src/postgres_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,45 @@ LogicalType PostgresUtils::RemoveAlias(const LogicalType &type) {
}
}

uint32_t PostgresUtils::TypeNameToPostgresOid(const string &type_name) {
if (type_name == "bool") {
return BOOLOID;
} else if (type_name == "int2") {
return INT2OID;
} else if (type_name == "int4") {
return INT4OID;
} else if (type_name == "int8") {
return INT8OID;
} else if (type_name == "float4") {
return FLOAT4OID;
} else if (type_name == "float8") {
return FLOAT8OID;
} else if (type_name == "varchar") {
return VARCHAROID;
} else if (type_name == "text") {
return TEXTOID;
} else if (type_name == "bytea") {
return BYTEAOID;
} else if (type_name == "date") {
return DATEOID;
} else if (type_name == "time") {
return TIMEOID;
} else if (type_name == "timestamp") {
return TIMESTAMPOID;
} else if (type_name == "timestamptz") {
return TIMESTAMPTZOID;
} else if (type_name == "interval") {
return INTERVALOID;
} else if (type_name == "timetz") {
return TIMETZOID;
} else if (type_name == "bit") {
return BITOID;
} else if (type_name == "uuid") {
return UUIDOID;
}
return 0;
}

LogicalType PostgresUtils::TypeToLogicalType(optional_ptr<PostgresTransaction> transaction,
optional_ptr<PostgresSchemaEntry> schema,
const PostgresTypeData &type_info, PostgresType &postgres_type) {
Expand Down Expand Up @@ -103,6 +142,10 @@ LogicalType PostgresUtils::TypeToLogicalType(optional_ptr<PostgresTransaction> t
child_type_info.type_modifier = type_info.type_modifier;
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
if (child_pg_type.oid == 0) {
child_pg_type.oid = TypeNameToPostgresOid(child_type_info.type_name);
}
// construct the child type based on the number of dimensions
for (idx_t i = 1; i < dimensions; i++) {
PostgresType new_pg_type;
Expand Down Expand Up @@ -275,9 +318,15 @@ PostgresType PostgresUtils::CreateEmptyPostgresType(const LogicalType &type) {
result.children.push_back(CreateEmptyPostgresType(child_type.second));
}
break;
case LogicalTypeId::LIST:
result.children.push_back(CreateEmptyPostgresType(ListType::GetChildType(type)));
case LogicalTypeId::LIST: {
auto child_pg_type = CreateEmptyPostgresType(ListType::GetChildType(type));
auto &child_type = ListType::GetChildType(type);
if (child_type.id() != LogicalTypeId::LIST && SupportedPostgresOid(child_type)) {
child_pg_type.oid = ToPostgresOid(child_type);
}
result.children.push_back(std::move(child_pg_type));
break;
}
default:
break;
}
Expand Down
117 changes: 117 additions & 0 deletions test/sql/storage/attach_array_binary_copy.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# name: test/sql/storage/attach_array_binary_copy.test
# description: Test that binary copy works for tables with array columns (issue #431)
# group: [storage]

require postgres_scanner

require-env POSTGRES_TEST_DATABASE_AVAILABLE

statement ok
PRAGMA enable_verification

statement ok
ATTACH 'dbname=postgresscanner' AS s (TYPE POSTGRES)

statement ok
USE s;

# Scenario 1: DDL-created table with varchar[] (CreateEmptyPostgresType path)
# Before the fix, this silently fell back to TEXT copy because PostgresType.oid was never populated
statement ok
CREATE OR REPLACE TABLE array_binary_ddl(id INTEGER, name VARCHAR, tags VARCHAR[], scores INT[]);

statement ok
INSERT INTO array_binary_ddl VALUES (1, 'alice', ['rock', 'jazz'], [10, 20, 30]);

statement ok
INSERT INTO array_binary_ddl VALUES (2, 'bob', ['pop'], []);

statement ok
INSERT INTO array_binary_ddl VALUES (3, 'charlie', [], [42]);

statement ok
INSERT INTO array_binary_ddl VALUES (NULL, NULL, NULL, NULL);

query IIII
SELECT * FROM array_binary_ddl ORDER BY id
----
1 alice [rock, jazz] [10, 20, 30]
2 bob [pop] []
3 charlie [] [42]
NULL NULL NULL NULL

# Scenario 2: DDL-created table with other array types
statement ok
CREATE OR REPLACE TABLE array_binary_types(bools BOOLEAN[], dates DATE[], floats DOUBLE[], uuids UUID[]);

statement ok
INSERT INTO array_binary_types VALUES ([true, false, NULL], ['2024-01-01', '2024-12-31'], [1.5, 2.5], ['6d3d2541-710b-4bde-b3af-4711738636bf']);

statement ok
INSERT INTO array_binary_types VALUES ([], [], [], []);

statement ok
INSERT INTO array_binary_types VALUES (NULL, NULL, NULL, NULL);

query IIII
SELECT * FROM array_binary_types ORDER BY bools::VARCHAR
----
[] [] [] []
[true, false, NULL] [2024-01-01, 2024-12-31] [1.5, 2.5] [6d3d2541-710b-4bde-b3af-4711738636bf]
NULL NULL NULL NULL

# Scenario 3: Existing table with varchar[] created via postgres_execute (TypeToLogicalType path)
statement ok
DROP TABLE IF EXISTS array_binary_catalog_varchar;

statement ok
CALL postgres_execute('s', 'CREATE TABLE array_binary_catalog_varchar(id INTEGER, tags VARCHAR[])');

statement ok
INSERT INTO array_binary_catalog_varchar VALUES (1, ['a', 'b', 'c']);

statement ok
INSERT INTO array_binary_catalog_varchar VALUES (2, []);

statement ok
INSERT INTO array_binary_catalog_varchar VALUES (3, [NULL, 'x']);

query II
SELECT * FROM array_binary_catalog_varchar ORDER BY id
----
1 [a, b, c]
2 []
3 [NULL, x]

# Scenario 4: Existing table with text[] created via postgres_execute
# text[] should still work (falls back to TEXT copy because binary writer emits VARCHAROID but Postgres expects TEXTOID)
statement ok
DROP TABLE IF EXISTS array_binary_catalog_text;

statement ok
CALL postgres_execute('s', 'CREATE TABLE array_binary_catalog_text(id INTEGER, notes TEXT[])');

statement ok
INSERT INTO array_binary_catalog_text VALUES (1, ['hello', 'world']);

statement ok
INSERT INTO array_binary_catalog_text VALUES (2, []);

query II
SELECT * FROM array_binary_catalog_text ORDER BY id
----
1 [hello, world]
2 []

# Cleanup
statement ok
DROP TABLE IF EXISTS array_binary_ddl;

statement ok
DROP TABLE IF EXISTS array_binary_types;

statement ok
DROP TABLE IF EXISTS array_binary_catalog_varchar;

statement ok
DROP TABLE IF EXISTS array_binary_catalog_text;
Loading