Skip to content
Open
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
41 changes: 40 additions & 1 deletion src/include/postgres_binary_writer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -29,7 +30,7 @@
} else if (sizeof(T) == sizeof(uint32_t)) {
return htonl(val);
} else if (sizeof(T) == sizeof(uint64_t)) {
return htonll(val);

Check warning on line 33 in src/include/postgres_binary_writer.hpp

View workflow job for this annotation

GitHub Actions / Windows Tests

'>>': shift count negative or too big, undefined behavior
} else {
D_ASSERT(0);
return val;
Expand Down Expand Up @@ -207,6 +208,44 @@
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<int32_t>(NumericCast<int32_t>(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<const uint8_t *>(&wkb_type)), 4); // type + SRID flag
stream.WriteData(const_data_ptr_cast(reinterpret_cast<const uint8_t *>(&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();
Expand Down Expand Up @@ -354,7 +393,7 @@
}
case LogicalTypeId::GEOMETRY: {
auto data = FlatVector::GetData<string_t>(col)[r];
WriteRawBlob(data);
WriteGeometry(data, type);
break;
}
case LogicalTypeId::ENUM: {
Expand Down
41 changes: 40 additions & 1 deletion src/postgres_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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();
}
Expand Down Expand Up @@ -147,6 +168,20 @@ LogicalType PostgresUtils::TypeToLogicalType(optional_ptr<PostgresTransaction> 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<int32_t>((static_cast<uint32_t>(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;
Expand Down Expand Up @@ -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;
}
Expand Down
Loading