diff --git a/src/include/postgres_binary_writer.hpp b/src/include/postgres_binary_writer.hpp index fef2fafe0..6f423930a 100644 --- a/src/include/postgres_binary_writer.hpp +++ b/src/include/postgres_binary_writer.hpp @@ -10,6 +10,7 @@ #include "duckdb.hpp" #include "duckdb/common/types/interval.hpp" +#include "duckdb/common/types/geometry_crs.hpp" #include "duckdb/common/serializer/memory_stream.hpp" #include "postgres_conversion.hpp" @@ -207,6 +208,44 @@ class PostgresBinaryWriter { stream.WriteData(const_data_ptr_cast(str_data), str_size); } + //! Write GEOMETRY to PostGIS. If the type carries CRS metadata, writes EWKB + //! (WKB with SRID) so PostGIS receives the correct SRID. + void WriteGeometry(string_t value, const LogicalType &type) { + if (!GeoType::HasCRS(type)) { + WriteRawBlob(value); + return; + } + auto &crs = GeoType::GetCRS(type); + + // Extract SRID from CRS identifier (e.g., "EPSG:4326" → 4326) + auto &id = crs.GetIdentifier(); + auto colon = id.find(':'); + if (colon == string::npos) { + WriteRawBlob(value); + return; + } + int32_t srid; + try { + srid = std::stoi(id.substr(colon + 1)); + } catch (...) { + WriteRawBlob(value); + return; + } + + // Write EWKB: WKB with SRID flag set on the type field + 4-byte SRID inserted. + // [byte_order:1] [type|0x20000000:4] [srid:4] [coordinates...] + auto wkb_size = value.GetSize(); + auto wkb_data = const_data_ptr_cast(value.GetData()); + WriteRawInteger(NumericCast(wkb_size + 4)); + stream.WriteData(wkb_data, 1); // byte order + uint32_t wkb_type; + memcpy(&wkb_type, wkb_data + 1, 4); + wkb_type |= 0x20000000; + stream.WriteData(const_data_ptr_cast(reinterpret_cast(&wkb_type)), 4); // type + SRID flag + stream.WriteData(const_data_ptr_cast(reinterpret_cast(&srid)), 4); // SRID (LE, matching WKB) + stream.WriteData(wkb_data + 5, wkb_size - 5); // rest of payload + } + void WriteVarchar(string_t value) { auto str_size = value.GetSize(); auto str_data = value.GetData(); @@ -354,7 +393,7 @@ class PostgresBinaryWriter { } case LogicalTypeId::GEOMETRY: { auto data = FlatVector::GetData(col)[r]; - WriteRawBlob(data); + WriteGeometry(data, type); break; } case LogicalTypeId::ENUM: { diff --git a/src/postgres_utils.cpp b/src/postgres_utils.cpp index 646696b62..671fca1e9 100644 --- a/src/postgres_utils.cpp +++ b/src/postgres_utils.cpp @@ -2,6 +2,7 @@ #include "storage/postgres_schema_entry.hpp" #include "storage/postgres_transaction.hpp" #include "postgres_type_oids.hpp" +#include "duckdb/common/types/geometry_crs.hpp" namespace duckdb { @@ -20,6 +21,26 @@ PGconn *PostgresUtils::PGConnect(const string &dsn, const string &attach_path) { } string PostgresUtils::TypeToString(const LogicalType &input) { + // Handle GEOMETRY('EPSG:N') first: the CRS-bearing GEOMETRY type can have + // an alias of "GEOMETRY", which would otherwise short-circuit the alias + // branch below and emit an untyped `geometry` column (typmod -1). When + // CRS is present, emit a typmod-bearing PostGIS column type so the SRID + // survives a CREATE TABLE round trip; otherwise fall through. + if (input.id() == LogicalTypeId::GEOMETRY && GeoType::HasCRS(input)) { + auto &crs = GeoType::GetCRS(input); + auto &id = crs.GetIdentifier(); + auto colon = id.find(':'); + if (colon != string::npos) { + try { + int32_t srid = std::stoi(id.substr(colon + 1)); + if (srid > 0) { + return "geometry(Geometry, " + std::to_string(srid) + ")"; + } + } catch (...) { + // fall through to plain GEOMETRY + } + } + } if (input.HasAlias()) { return input.GetAlias(); } @@ -147,6 +168,20 @@ LogicalType PostgresUtils::TypeToLogicalType(optional_ptr t postgres_type.info = PostgresTypeAnnotation::JSONB; return LogicalType::VARCHAR; } else if (pgtypename == "geometry") { + // PostGIS encodes the column-level SRID in the type modifier of + // `geometry(TYPE, SRID)` columns. The bit layout is: + // bits 0 : has-M + // bits 1 : has-Z + // bits 2-7 : geometry type code (POINT=1, LINESTRING=2, ...) + // bits 8-29 : SRID + // Untyped `geometry` columns carry typmod -1 and we fall back to + // plain GEOMETRY (no CRS), matching the previous behavior. + if (type_info.type_modifier > 0) { + int32_t srid = static_cast((static_cast(type_info.type_modifier) & 0x0FFFFF00u) >> 8); + if (srid > 0) { + return LogicalType::GEOMETRY("EPSG:" + std::to_string(srid)); + } + } return LogicalType::GEOMETRY(); } else if (pgtypename == "date") { return LogicalType::DATE; @@ -261,7 +296,11 @@ LogicalType PostgresUtils::ToPostgresType(const LogicalType &input) { case LogicalTypeId::HUGEINT: return LogicalType::DOUBLE; case LogicalTypeId::GEOMETRY: - return LogicalType::GEOMETRY(); + // Preserve CRS metadata so downstream TypeToString can emit a typmod- + // bearing PostGIS column type and the SRID survives a CREATE TABLE + // AS round trip. Returning a CRS-less GEOMETRY here strips the CRS + // before it ever reaches the schema-emission path. + return input; default: return LogicalType::VARCHAR; }