diff --git a/src/include/postgres_binary_writer.hpp b/src/include/postgres_binary_writer.hpp index eff48d1b6..fef2fafe0 100644 --- a/src/include/postgres_binary_writer.hpp +++ b/src/include/postgres_binary_writer.hpp @@ -352,6 +352,11 @@ class PostgresBinaryWriter { WriteRawBlob(data); break; } + case LogicalTypeId::GEOMETRY: { + auto data = FlatVector::GetData(col)[r]; + WriteRawBlob(data); + break; + } case LogicalTypeId::ENUM: { idx_t pos; switch (type.InternalType()) { diff --git a/src/postgres_binary_reader.cpp b/src/postgres_binary_reader.cpp index 0b85922bb..c7fd72eec 100644 --- a/src/postgres_binary_reader.cpp +++ b/src/postgres_binary_reader.cpp @@ -276,6 +276,16 @@ void PostgresBinaryReader::ReadValue(const LogicalType &type, const PostgresType FlatVector::GetData(out_vec)[output_offset] = StringVector::AddStringOrBlob(out_vec, str, value_len); break; } + case LogicalTypeId::GEOMETRY: { + const auto str = ReadString(value_len); + + string_t res_val; + if (!Geometry::FromBinary(string_t(str, value_len), res_val, out_vec, true)) { + throw InvalidInputException("Failed to parse Postgres geometry data"); + } + FlatVector::GetData(out_vec)[output_offset] = res_val; + break; + } case LogicalTypeId::BOOLEAN: D_ASSERT(value_len == sizeof(bool)); FlatVector::GetData(out_vec)[output_offset] = ReadBoolean(); diff --git a/src/postgres_copy_to.cpp b/src/postgres_copy_to.cpp index ef8027c50..72580a358 100644 --- a/src/postgres_copy_to.cpp +++ b/src/postgres_copy_to.cpp @@ -264,6 +264,7 @@ void CastBlobToPostgres(ClientContext &context, Vector &input, Vector &result, i } void CastGeometryToPostgres(ClientContext &context, Vector &input, Vector &result, idx_t size) { + // Cast to WKT format VectorOperations::Cast(context, input, result, size); } @@ -276,11 +277,10 @@ void CastToPostgresVarchar(ClientContext &context, Vector &input, Vector &result case LogicalTypeId::STRUCT: CastStructToPostgres(context, input, result, size); break; + case LogicalTypeId::GEOMETRY: + CastGeometryToPostgres(context, input, result, size); + break; case LogicalTypeId::BLOB: - if (type.HasAlias() && StringUtil::CIEquals(type.GetAlias(), "wkb_blob")) { - CastGeometryToPostgres(context, input, result, size); - break; - } CastBlobToPostgres(context, input, result, size); break; default: diff --git a/src/postgres_text_reader.cpp b/src/postgres_text_reader.cpp index 94db174ea..9a1bbed8e 100644 --- a/src/postgres_text_reader.cpp +++ b/src/postgres_text_reader.cpp @@ -303,6 +303,52 @@ void PostgresTextReader::ConvertBlob(Vector &source, Vector &target, idx_t count } } +static void ConvertGeometry(Vector &source, Vector &target, idx_t count) { + // Geometry is encoded in HEXWKB format + + UnifiedVectorFormat vdata; + source.ToUnifiedFormat(count, vdata); + const auto strings = UnifiedVectorFormat::GetData(vdata); + const auto result = FlatVector::GetData(target); + + string result_blob; + + for (idx_t out_idx = 0; out_idx < count; out_idx++) { + const auto row_idx = vdata.sel->get_index(out_idx); + + if (!vdata.validity.RowIsValid(row_idx)) { + // NULL value - skip + FlatVector::SetNull(target, row_idx, true); + continue; + } + auto blob_str = strings[row_idx]; + const auto data = blob_str.GetData(); + const auto size = blob_str.GetSize(); + if (size % 2 != 0) { + throw InvalidInputException("Blob size must be modulo 2 (\\xAA)"); + } + + // Reset buffer + result_blob.clear(); + + // Decode the HEX string into binary data + for (idx_t i = 0; i < size; i += 2) { + int byte_a = Blob::HEX_MAP[static_cast(data[i])]; + int byte_b = Blob::HEX_MAP[static_cast(data[i + 1])]; + if (byte_a == -1 || byte_b == -1) { + throw InvalidInputException("Invalid character in HEX WKB string: '%c%c'", byte_a, byte_b); + } + + result_blob += UnsafeNumericCast((byte_a << 4) + byte_b); + } + + // Finally convert from WKB (which will handle big-endian format too) + if (!Geometry::FromBinary(result_blob, result[out_idx], target, true)) { + throw InvalidInputException("Failed to parse geometry from WKB - invalid format"); + } + } +} + void PostgresTextReader::ConvertVector(Vector &source, Vector &target, const PostgresType &postgres_type, idx_t count) { if (source.GetType().id() != LogicalTypeId::VARCHAR) { throw InternalException("Source needs to be VARCHAR"); @@ -321,6 +367,9 @@ void PostgresTextReader::ConvertVector(Vector &source, Vector &target, const Pos case LogicalTypeId::BLOB: ConvertBlob(source, target, count); break; + case LogicalTypeId::GEOMETRY: + ConvertGeometry(source, target, count); + break; default: VectorOperations::Cast(context, source, target, count); } diff --git a/src/postgres_utils.cpp b/src/postgres_utils.cpp index ae3d02172..646696b62 100644 --- a/src/postgres_utils.cpp +++ b/src/postgres_utils.cpp @@ -21,12 +21,11 @@ PGconn *PostgresUtils::PGConnect(const string &dsn, const string &attach_path) { string PostgresUtils::TypeToString(const LogicalType &input) { if (input.HasAlias()) { - if (StringUtil::CIEquals(input.GetAlias(), "wkb_blob")) { - return "GEOMETRY"; - } return input.GetAlias(); } switch (input.id()) { + case LogicalTypeId::GEOMETRY: + return "GEOMETRY"; case LogicalTypeId::FLOAT: return "REAL"; case LogicalTypeId::DOUBLE: @@ -50,12 +49,6 @@ string PostgresUtils::TypeToString(const LogicalType &input) { } } -LogicalType GetGeometryType() { - auto blob_type = LogicalType(LogicalTypeId::BLOB); - blob_type.SetAlias("WKB_BLOB"); - return blob_type; -} - LogicalType PostgresUtils::RemoveAlias(const LogicalType &type) { if (!type.HasAlias()) { return type; @@ -63,9 +56,6 @@ LogicalType PostgresUtils::RemoveAlias(const LogicalType &type) { if (StringUtil::CIEquals(type.GetAlias(), "json")) { return type; } - if (StringUtil::CIEquals(type.GetAlias(), "geometry")) { - return GetGeometryType(); - } switch (type.id()) { case LogicalTypeId::STRUCT: { auto child_types = StructType::GetChildTypes(type); @@ -157,7 +147,7 @@ LogicalType PostgresUtils::TypeToLogicalType(optional_ptr t postgres_type.info = PostgresTypeAnnotation::JSONB; return LogicalType::VARCHAR; } else if (pgtypename == "geometry") { - return GetGeometryType(); + return LogicalType::GEOMETRY(); } else if (pgtypename == "date") { return LogicalType::DATE; } else if (pgtypename == "bytea") { @@ -270,6 +260,8 @@ LogicalType PostgresUtils::ToPostgresType(const LogicalType &input) { return LogicalType::DECIMAL(20, 0); case LogicalTypeId::HUGEINT: return LogicalType::DOUBLE; + case LogicalTypeId::GEOMETRY: + return LogicalType::GEOMETRY(); default: return LogicalType::VARCHAR; } diff --git a/test/sql/storage/attach_postgis.test b/test/sql/storage/attach_postgis.test index 3756dacdb..5a3cde405 100644 --- a/test/sql/storage/attach_postgis.test +++ b/test/sql/storage/attach_postgis.test @@ -6,8 +6,8 @@ require postgres_scanner require-env POSTGRES_TEST_DATABASE_AVAILABLE -# e.g. export SPATIAL_EXTENSION='~/Programs/duckdb-spatial/build/debug/extension/spatial/spatial.duckdb_extension' -require-env SPATIAL_EXTENSION +# This is just to disable the test until we figure out how to get PostGIS in the CI +require-env HAS_POSTGIS statement ok PRAGMA enable_verification @@ -15,18 +15,16 @@ PRAGMA enable_verification statement ok ATTACH 'dbname=postgres' AS s (TYPE POSTGRES); -# make sure PostGIS is installed +# Cleanup statement ok -SELECT * FROM postgres_query(s, 'SELECT PostGIS_Version()') +DROP TABLE IF EXISTS s.my_points; -# spatial not loaded yet -statement error -CREATE OR REPLACE TABLE s.my_points(geom GEOMETRY); ----- -spatial +statement ok +DROP TABLE if EXISTS s.t1_copy; +# make sure PostGIS is installed statement ok -LOAD '${SPATIAL_EXTENSION}' +SELECT * FROM postgres_query(s, 'SELECT PostGIS_Version()') # create a table statement ok @@ -34,11 +32,11 @@ CREATE OR REPLACE TABLE s.my_points(geom GEOMETRY); # insert data statement ok -INSERT INTO s.my_points VALUES (ST_Point(1,1)); +INSERT INTO s.my_points VALUES ('POINT (1 1)'::GEOMETRY); # try binary copy query I -SELECT geom::VARCHAR FROM s.my_points; +SELECT geom FROM s.my_points; ---- POINT (1 1) @@ -47,7 +45,7 @@ statement ok SET pg_use_binary_copy=false; statement ok -INSERT INTO s.my_points VALUES (ST_Point(2,2)); +INSERT INTO s.my_points VALUES ('POINT (2 2)'::GEOMETRY); query I SELECT geom::VARCHAR FROM s.my_points; @@ -55,6 +53,24 @@ SELECT geom::VARCHAR FROM s.my_points; POINT (1 1) POINT (2 2) +query I +SELECT geom from s.my_points; +---- +POINT (1 1) +POINT (2 2) + +statement ok +set pg_use_text_protocol=true; + +query I +SELECT geom from s.my_points; +---- +POINT (1 1) +POINT (2 2) + +statement ok +set pg_use_text_protocol=false + # make sure Postgres itself can read the values query I SELECT * FROM postgres_query(s, 'SELECT ST_AsText(geom) FROM my_points') @@ -77,10 +93,42 @@ POINT (2 2) # update statement ok -UPDATE s.my_points SET geom=ST_Point(ST_X(geom::GEOMETRY) + 10, ST_Y(geom::GEOMETRY) + 10) +UPDATE s.my_points SET geom='LINESTRING (0 0, 1 1)'::GEOMETRY query I SELECT geom::VARCHAR FROM s.my_points; ---- -POINT (11 11) -POINT (12 12) +LINESTRING (0 0, 1 1) +LINESTRING (0 0, 1 1) + +# Also test COPY to +statement ok +CREATE table t1 (g GEOMETRY); + +statement ok +CREATE table s.t1_copy (g GEOMETRY); + +statement ok +insert into t1 select geom from s.my_points; + +query I +select * from t1; +---- +LINESTRING (0 0, 1 1) +LINESTRING (0 0, 1 1) + +statement ok +insert into s.t1_copy select g from t1; + +query I +SELECT * FROM postgres_query(s, 'SELECT ST_AsText(g) FROM t1_copy') +---- +LINESTRING(0 0,1 1) +LINESTRING(0 0,1 1) + +# Cleanup +statement ok +DROP TABLE IF EXISTS s.my_points; + +statement ok +DROP TABLE if EXISTS s.t1_copy; \ No newline at end of file