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
5 changes: 5 additions & 0 deletions src/include/postgres_binary_writer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,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 32 in src/include/postgres_binary_writer.hpp

View workflow job for this annotation

GitHub Actions / Build extension binaries / windows_amd64

'>>': shift count negative or too big, undefined behavior

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

View workflow job for this annotation

GitHub Actions / Build extension binaries / windows_amd64

'>>': shift count negative or too big, undefined behavior
} else {
D_ASSERT(0);
return val;
Expand Down Expand Up @@ -352,6 +352,11 @@
WriteRawBlob(data);
break;
}
case LogicalTypeId::GEOMETRY: {
auto data = FlatVector::GetData<string_t>(col)[r];
WriteRawBlob(data);
break;
}
case LogicalTypeId::ENUM: {
idx_t pos;
switch (type.InternalType()) {
Expand Down
10 changes: 10 additions & 0 deletions src/postgres_binary_reader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,16 @@ void PostgresBinaryReader::ReadValue(const LogicalType &type, const PostgresType
FlatVector::GetData<string_t>(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<string_t>(out_vec)[output_offset] = res_val;
break;
}
case LogicalTypeId::BOOLEAN:
D_ASSERT(value_len == sizeof(bool));
FlatVector::GetData<bool>(out_vec)[output_offset] = ReadBoolean();
Expand Down
8 changes: 4 additions & 4 deletions src/postgres_copy_to.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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:
Expand Down
49 changes: 49 additions & 0 deletions src/postgres_text_reader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<string_t>(vdata);
const auto result = FlatVector::GetData<string_t>(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<uint8_t>(data[i])];
int byte_b = Blob::HEX_MAP[static_cast<uint8_t>(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<data_t>((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");
Expand All @@ -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);
}
Expand Down
18 changes: 5 additions & 13 deletions src/postgres_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -50,22 +49,13 @@ 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;
}
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);
Expand Down Expand Up @@ -157,7 +147,7 @@ LogicalType PostgresUtils::TypeToLogicalType(optional_ptr<PostgresTransaction> 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") {
Expand Down Expand Up @@ -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;
}
Expand Down
80 changes: 64 additions & 16 deletions test/sql/storage/attach_postgis.test
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,37 @@ 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

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
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)

Expand All @@ -47,14 +45,32 @@ 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;
----
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')
Expand All @@ -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;
Loading