Skip to content

Commit d1a5801

Browse files
committed
add support for new geometry type instead of relying on spatial
1 parent 582100d commit d1a5801

File tree

6 files changed

+135
-37
lines changed

6 files changed

+135
-37
lines changed

src/include/postgres_binary_writer.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,11 @@ class PostgresBinaryWriter {
352352
WriteRawBlob(data);
353353
break;
354354
}
355+
case LogicalTypeId::GEOMETRY: {
356+
auto data = FlatVector::GetData<string_t>(col)[r];
357+
WriteRawBlob(data);
358+
break;
359+
}
355360
case LogicalTypeId::ENUM: {
356361
idx_t pos;
357362
switch (type.InternalType()) {

src/postgres_binary_reader.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,16 @@ void PostgresBinaryReader::ReadValue(const LogicalType &type, const PostgresType
276276
FlatVector::GetData<string_t>(out_vec)[output_offset] = StringVector::AddStringOrBlob(out_vec, str, value_len);
277277
break;
278278
}
279+
case LogicalTypeId::GEOMETRY: {
280+
const auto str = ReadString(value_len);
281+
282+
string_t res_val;
283+
if (!Geometry::FromBinary(string_t(str, value_len), res_val, out_vec, true)) {
284+
throw InvalidInputException("Failed to parse Postgres geometry data");
285+
}
286+
FlatVector::GetData<string_t>(out_vec)[output_offset] = res_val;
287+
break;
288+
}
279289
case LogicalTypeId::BOOLEAN:
280290
D_ASSERT(value_len == sizeof(bool));
281291
FlatVector::GetData<bool>(out_vec)[output_offset] = ReadBoolean();

src/postgres_copy_to.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ void CastBlobToPostgres(ClientContext &context, Vector &input, Vector &result, i
264264
}
265265

266266
void CastGeometryToPostgres(ClientContext &context, Vector &input, Vector &result, idx_t size) {
267+
// Cast to WKT format
267268
VectorOperations::Cast(context, input, result, size);
268269
}
269270

@@ -276,11 +277,10 @@ void CastToPostgresVarchar(ClientContext &context, Vector &input, Vector &result
276277
case LogicalTypeId::STRUCT:
277278
CastStructToPostgres(context, input, result, size);
278279
break;
280+
case LogicalTypeId::GEOMETRY:
281+
CastGeometryToPostgres(context, input, result, size);
282+
break;
279283
case LogicalTypeId::BLOB:
280-
if (type.HasAlias() && StringUtil::CIEquals(type.GetAlias(), "wkb_blob")) {
281-
CastGeometryToPostgres(context, input, result, size);
282-
break;
283-
}
284284
CastBlobToPostgres(context, input, result, size);
285285
break;
286286
default:

src/postgres_text_reader.cpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,52 @@ void PostgresTextReader::ConvertBlob(Vector &source, Vector &target, idx_t count
303303
}
304304
}
305305

306+
static void ConvertGeometry(Vector &source, Vector &target, idx_t count) {
307+
// Geometry is encoded in HEXWKB format
308+
309+
UnifiedVectorFormat vdata;
310+
source.ToUnifiedFormat(count, vdata);
311+
const auto strings = UnifiedVectorFormat::GetData<string_t>(vdata);
312+
const auto result = FlatVector::GetData<string_t>(target);
313+
314+
string result_blob;
315+
316+
for (idx_t out_idx = 0; out_idx < count; out_idx++) {
317+
const auto row_idx = vdata.sel->get_index(out_idx);
318+
319+
if (!vdata.validity.RowIsValid(row_idx)) {
320+
// NULL value - skip
321+
FlatVector::SetNull(target, row_idx, true);
322+
continue;
323+
}
324+
auto blob_str = strings[row_idx];
325+
const auto data = blob_str.GetData();
326+
const auto size = blob_str.GetSize();
327+
if (size % 2 != 0) {
328+
throw InvalidInputException("Blob size must be modulo 2 (\\xAA)");
329+
}
330+
331+
// Reset buffer
332+
result_blob.clear();
333+
334+
// Decode the HEX string into binary data
335+
for (idx_t i = 0; i < size; i += 2) {
336+
int byte_a = Blob::HEX_MAP[static_cast<uint8_t>(data[i])];
337+
int byte_b = Blob::HEX_MAP[static_cast<uint8_t>(data[i + 1])];
338+
if (byte_a == -1 || byte_b == -1) {
339+
throw InvalidInputException("Invalid character in HEX WKB string: '%c%c'", byte_a, byte_b);
340+
}
341+
342+
result_blob += UnsafeNumericCast<data_t>((byte_a << 4) + byte_b);
343+
}
344+
345+
// Finally convert from WKB (which will handle big-endian format too)
346+
if (!Geometry::FromBinary(result_blob, result[out_idx], target, true)) {
347+
throw InvalidInputException("Failed to parse geometry from WKB - invalid format");
348+
}
349+
}
350+
}
351+
306352
void PostgresTextReader::ConvertVector(Vector &source, Vector &target, const PostgresType &postgres_type, idx_t count) {
307353
if (source.GetType().id() != LogicalTypeId::VARCHAR) {
308354
throw InternalException("Source needs to be VARCHAR");
@@ -321,6 +367,9 @@ void PostgresTextReader::ConvertVector(Vector &source, Vector &target, const Pos
321367
case LogicalTypeId::BLOB:
322368
ConvertBlob(source, target, count);
323369
break;
370+
case LogicalTypeId::GEOMETRY:
371+
ConvertGeometry(source, target, count);
372+
break;
324373
default:
325374
VectorOperations::Cast(context, source, target, count);
326375
}

src/postgres_utils.cpp

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,9 @@ PGconn *PostgresUtils::PGConnect(const string &dsn, const string &attach_path) {
2020
}
2121

2222
string PostgresUtils::TypeToString(const LogicalType &input) {
23-
if (input.HasAlias()) {
24-
if (StringUtil::CIEquals(input.GetAlias(), "wkb_blob")) {
25-
return "GEOMETRY";
26-
}
27-
return input.GetAlias();
28-
}
2923
switch (input.id()) {
24+
case LogicalTypeId::GEOMETRY:
25+
return "GEOMETRY";
3026
case LogicalTypeId::FLOAT:
3127
return "REAL";
3228
case LogicalTypeId::DOUBLE:
@@ -50,22 +46,13 @@ string PostgresUtils::TypeToString(const LogicalType &input) {
5046
}
5147
}
5248

53-
LogicalType GetGeometryType() {
54-
auto blob_type = LogicalType(LogicalTypeId::BLOB);
55-
blob_type.SetAlias("WKB_BLOB");
56-
return blob_type;
57-
}
58-
5949
LogicalType PostgresUtils::RemoveAlias(const LogicalType &type) {
6050
if (!type.HasAlias()) {
6151
return type;
6252
}
6353
if (StringUtil::CIEquals(type.GetAlias(), "json")) {
6454
return type;
6555
}
66-
if (StringUtil::CIEquals(type.GetAlias(), "geometry")) {
67-
return GetGeometryType();
68-
}
6956
switch (type.id()) {
7057
case LogicalTypeId::STRUCT: {
7158
auto child_types = StructType::GetChildTypes(type);
@@ -157,7 +144,7 @@ LogicalType PostgresUtils::TypeToLogicalType(optional_ptr<PostgresTransaction> t
157144
postgres_type.info = PostgresTypeAnnotation::JSONB;
158145
return LogicalType::VARCHAR;
159146
} else if (pgtypename == "geometry") {
160-
return GetGeometryType();
147+
return LogicalType::GEOMETRY();
161148
} else if (pgtypename == "date") {
162149
return LogicalType::DATE;
163150
} else if (pgtypename == "bytea") {
@@ -270,6 +257,8 @@ LogicalType PostgresUtils::ToPostgresType(const LogicalType &input) {
270257
return LogicalType::DECIMAL(20, 0);
271258
case LogicalTypeId::HUGEINT:
272259
return LogicalType::DOUBLE;
260+
case LogicalTypeId::GEOMETRY:
261+
return LogicalType::GEOMETRY();
273262
default:
274263
return LogicalType::VARCHAR;
275264
}

test/sql/storage/attach_postgis.test

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,34 @@ require postgres_scanner
66

77
require-env POSTGRES_TEST_DATABASE_AVAILABLE
88

9-
# e.g. export SPATIAL_EXTENSION='~/Programs/duckdb-spatial/build/debug/extension/spatial/spatial.duckdb_extension'
10-
require-env SPATIAL_EXTENSION
11-
129
statement ok
1310
PRAGMA enable_verification
1411

1512
statement ok
1613
ATTACH 'dbname=postgres' AS s (TYPE POSTGRES);
1714

18-
# make sure PostGIS is installed
15+
# Cleanup
1916
statement ok
20-
SELECT * FROM postgres_query(s, 'SELECT PostGIS_Version()')
17+
DROP TABLE IF EXISTS s.my_points;
2118

22-
# spatial not loaded yet
23-
statement error
24-
CREATE OR REPLACE TABLE s.my_points(geom GEOMETRY);
25-
----
26-
spatial
19+
statement ok
20+
DROP TABLE if EXISTS s.t1_copy;
2721

22+
# make sure PostGIS is installed
2823
statement ok
29-
LOAD '${SPATIAL_EXTENSION}'
24+
SELECT * FROM postgres_query(s, 'SELECT PostGIS_Version()')
3025

3126
# create a table
3227
statement ok
3328
CREATE OR REPLACE TABLE s.my_points(geom GEOMETRY);
3429

3530
# insert data
3631
statement ok
37-
INSERT INTO s.my_points VALUES (ST_Point(1,1));
32+
INSERT INTO s.my_points VALUES ('POINT (1 1)'::GEOMETRY);
3833

3934
# try binary copy
4035
query I
41-
SELECT geom::VARCHAR FROM s.my_points;
36+
SELECT geom FROM s.my_points;
4237
----
4338
POINT (1 1)
4439

@@ -47,14 +42,32 @@ statement ok
4742
SET pg_use_binary_copy=false;
4843

4944
statement ok
50-
INSERT INTO s.my_points VALUES (ST_Point(2,2));
45+
INSERT INTO s.my_points VALUES ('POINT (2 2)'::GEOMETRY);
5146

5247
query I
5348
SELECT geom::VARCHAR FROM s.my_points;
5449
----
5550
POINT (1 1)
5651
POINT (2 2)
5752

53+
query I
54+
SELECT geom from s.my_points;
55+
----
56+
POINT (1 1)
57+
POINT (2 2)
58+
59+
statement ok
60+
set pg_use_text_protocol=true;
61+
62+
query I
63+
SELECT geom from s.my_points;
64+
----
65+
POINT (1 1)
66+
POINT (2 2)
67+
68+
statement ok
69+
set pg_use_text_protocol=false
70+
5871
# make sure Postgres itself can read the values
5972
query I
6073
SELECT * FROM postgres_query(s, 'SELECT ST_AsText(geom) FROM my_points')
@@ -77,10 +90,42 @@ POINT (2 2)
7790

7891
# update
7992
statement ok
80-
UPDATE s.my_points SET geom=ST_Point(ST_X(geom::GEOMETRY) + 10, ST_Y(geom::GEOMETRY) + 10)
93+
UPDATE s.my_points SET geom='LINESTRING (0 0, 1 1)'::GEOMETRY
8194

8295
query I
8396
SELECT geom::VARCHAR FROM s.my_points;
8497
----
85-
POINT (11 11)
86-
POINT (12 12)
98+
LINESTRING (0 0, 1 1)
99+
LINESTRING (0 0, 1 1)
100+
101+
# Also test COPY to
102+
statement ok
103+
CREATE table t1 (g GEOMETRY);
104+
105+
statement ok
106+
CREATE table s.t1_copy (g GEOMETRY);
107+
108+
statement ok
109+
insert into t1 select geom from s.my_points;
110+
111+
query I
112+
select * from t1;
113+
----
114+
LINESTRING (0 0, 1 1)
115+
LINESTRING (0 0, 1 1)
116+
117+
statement ok
118+
insert into s.t1_copy select g from t1;
119+
120+
query I
121+
SELECT * FROM postgres_query(s, 'SELECT ST_AsText(g) FROM t1_copy')
122+
----
123+
LINESTRING(0 0,1 1)
124+
LINESTRING(0 0,1 1)
125+
126+
# Cleanup
127+
statement ok
128+
DROP TABLE IF EXISTS s.my_points;
129+
130+
statement ok
131+
DROP TABLE if EXISTS s.t1_copy;

0 commit comments

Comments
 (0)