diff --git a/duckdb b/duckdb index ade3443f..a8ce02cc 160000 --- a/duckdb +++ b/duckdb @@ -1 +1 @@ -Subproject commit ade3443f4a8003fd726a74e5fbe2c176e85d7c9a +Subproject commit a8ce02cc2e740d8973d26ccdb77d0068c69c9124 diff --git a/spatial/include/spatial/core/geometry/cursor.hpp b/spatial/include/spatial/core/geometry/cursor.hpp new file mode 100644 index 00000000..151bc6a8 --- /dev/null +++ b/spatial/include/spatial/core/geometry/cursor.hpp @@ -0,0 +1,91 @@ +#pragma once +#include "spatial/common.hpp" + +namespace spatial { + +namespace core { + +class Cursor { +private: + data_ptr_t start; + data_ptr_t ptr; + data_ptr_t end; + +public: + enum class Offset { START, CURRENT, END }; + + explicit Cursor(data_ptr_t start, data_ptr_t end) : start(start), ptr(start), end(end) { + } + + explicit Cursor(string_t blob) : start((data_ptr_t)blob.GetDataUnsafe()), ptr(start), end(start + blob.GetSize()) { + } + + data_ptr_t GetPtr() { + return ptr; + } + + void SetPtr(data_ptr_t ptr_p) { + if (ptr_p < start || ptr_p > end) { + throw SerializationException("Trying to set ptr outside of buffer"); + } + ptr = ptr_p; + } + + template + T Read() { + static_assert(std::is_trivially_copyable::value, "T must be trivially copyable"); + if (ptr + sizeof(T) > end) { + throw SerializationException("Trying to read past end of buffer"); + } + auto result = Load(ptr); + ptr += sizeof(T); + return result; + } + + template + void Write(T value) { + static_assert(std::is_trivially_copyable::value, "T must be trivially copyable"); + if (ptr + sizeof(T) > end) { + throw SerializationException("Trying to write past end of buffer"); + } + Store(value, ptr); + ptr += sizeof(T); + } + + template + T Peek() { + static_assert(std::is_trivially_copyable::value, "T must be trivially copyable"); + if (ptr + sizeof(T) > end) { + throw SerializationException("Trying to read past end of buffer"); + } + return Load(ptr); + } + + void Skip(uint32_t bytes) { + if (ptr + bytes > end) { + throw SerializationException("Trying to read past end of buffer"); + } + ptr += bytes; + } + + void Seek(Offset offset, int32_t bytes) { + switch (offset) { + case Offset::START: + ptr = start + bytes; + break; + case Offset::CURRENT: + ptr += bytes; + break; + case Offset::END: + ptr = end + bytes; + break; + } + if (ptr < start || ptr > end) { + throw SerializationException("Trying to set ptr outside of buffer"); + } + } +}; + +} // namespace core + +} // namespace spatial \ No newline at end of file diff --git a/spatial/include/spatial/core/geometry/geometry.hpp b/spatial/include/spatial/core/geometry/geometry.hpp index bf3a495b..c5045424 100644 --- a/spatial/include/spatial/core/geometry/geometry.hpp +++ b/spatial/include/spatial/core/geometry/geometry.hpp @@ -1,7 +1,10 @@ #pragma once #include "spatial/common.hpp" +#include "spatial/core/geometry/geometry_properties.hpp" #include "spatial/core/geometry/vertex_vector.hpp" +#include "spatial/core/geometry/cursor.hpp" +#include "spatial/core/geometry/geometry_type.hpp" namespace spatial { @@ -51,16 +54,6 @@ struct Utils { struct Geometry; struct GeometryFactory; -enum class GeometryType : uint8_t { - POINT, - LINESTRING, - POLYGON, - MULTIPOINT, - MULTILINESTRING, - MULTIPOLYGON, - GEOMETRYCOLLECTION -}; - class Point { friend GeometryFactory; VertexVector vertices; @@ -263,6 +256,7 @@ struct Geometry { private: GeometryType type; + GeometryProperties properties; union { Point point; LineString linestring; @@ -296,6 +290,14 @@ struct Geometry { return type; } + inline GeometryProperties &Properties() { + return properties; + } + + inline const GeometryProperties &Properties() const { + return properties; + } + inline Point &GetPoint() { D_ASSERT(type == GeometryType::POINT); return point; @@ -386,29 +388,41 @@ RESULT_TYPE GeometryCollection::Aggregate(AGG agg, RESULT_TYPE zero) const { return result; } -struct GeometryPrefix { - uint8_t flags; +class GeometryHeader { +public: GeometryType type; + GeometryProperties properties; uint16_t hash; - - GeometryPrefix(uint8_t flags, GeometryType type, uint16_t hash) : flags(flags), type(type), hash(hash) { + explicit GeometryHeader() : type(GeometryType::POINT), properties(GeometryProperties()), hash(0) { } - uint32_t SerializedSize() const { - return sizeof(GeometryPrefix); + explicit GeometryHeader(GeometryType type, GeometryProperties properties, uint16_t hash) + : type(type), properties(properties), hash(hash) { } void Serialize(data_ptr_t &dst) const { - Store(flags, dst); - dst += sizeof(uint8_t); - Store((uint8_t)type, dst); + Store(type, dst); dst += sizeof(GeometryType); + Store(properties, dst); + dst += sizeof(GeometryProperties); Store(hash, dst); dst += sizeof(uint16_t); } + + // Deserialize a GeometryHeader from a string prefix + static GeometryHeader Get(const string_t &blob) { + auto prefix = const_data_ptr_cast(blob.GetPrefix()); + auto header = Load(prefix); + return header; + } + + // Deserialize a GeometryHeader from a Cursor + static GeometryHeader Deserialize(Cursor &cursor) { + return cursor.Read(); + } }; -static_assert(sizeof(GeometryPrefix) == 4, "GeometryPrefix should be 4 bytes"); -static_assert(sizeof(GeometryPrefix) == string_t::PREFIX_BYTES, "GeometryPrefix should fit in string_t prefix"); +static_assert(sizeof(GeometryHeader) == 4, "GeometryPrefix should be 4 bytes"); +static_assert(sizeof(GeometryHeader) == string_t::PREFIX_BYTES, "GeometryPrefix should fit in string_t prefix"); } // namespace core } // namespace spatial \ No newline at end of file diff --git a/spatial/include/spatial/core/geometry/geometry_factory.hpp b/spatial/include/spatial/core/geometry/geometry_factory.hpp index 53aae8a8..130be481 100644 --- a/spatial/include/spatial/core/geometry/geometry_factory.hpp +++ b/spatial/include/spatial/core/geometry/geometry_factory.hpp @@ -7,8 +7,7 @@ namespace spatial { namespace core { -class BinaryReader; -class BinaryWriter; +class Cursor; struct GeometryFactory { public: @@ -60,13 +59,13 @@ struct GeometryFactory { private: // Serialize - void SerializePoint(data_ptr_t &ptr, const Point &point); - void SerializeLineString(data_ptr_t &ptr, const LineString &linestring); - void SerializePolygon(data_ptr_t &ptr, const Polygon &polygon); - void SerializeMultiPoint(data_ptr_t &ptr, const MultiPoint &multipoint); - void SerializeMultiLineString(data_ptr_t &ptr, const MultiLineString &multilinestring); - void SerializeMultiPolygon(data_ptr_t &ptr, const MultiPolygon &multipolygon); - void SerializeGeometryCollection(data_ptr_t &ptr, const GeometryCollection &collection); + void SerializePoint(Cursor &cursor, const Point &point); + void SerializeLineString(Cursor &cursor, const LineString &linestring); + void SerializePolygon(Cursor &cursor, const Polygon &polygon); + void SerializeMultiPoint(Cursor &cursor, const MultiPoint &multipoint); + void SerializeMultiLineString(Cursor &cursor, const MultiLineString &multilinestring); + void SerializeMultiPolygon(Cursor &cursor, const MultiPolygon &multipolygon); + void SerializeGeometryCollection(Cursor &cursor, const GeometryCollection &collection); // Get Serialize Size uint32_t GetSerializedSize(const Point &point); @@ -79,13 +78,13 @@ struct GeometryFactory { uint32_t GetSerializedSize(const Geometry &geometry); // Deserialize - Point DeserializePoint(BinaryReader &reader); - LineString DeserializeLineString(BinaryReader &reader); - Polygon DeserializePolygon(BinaryReader &reader); - MultiPoint DeserializeMultiPoint(BinaryReader &reader); - MultiLineString DeserializeMultiLineString(BinaryReader &reader); - MultiPolygon DeserializeMultiPolygon(BinaryReader &reader); - GeometryCollection DeserializeGeometryCollection(BinaryReader &reader); + Point DeserializePoint(Cursor &reader); + LineString DeserializeLineString(Cursor &reader); + Polygon DeserializePolygon(Cursor &reader); + MultiPoint DeserializeMultiPoint(Cursor &reader); + MultiLineString DeserializeMultiLineString(Cursor &reader); + MultiPolygon DeserializeMultiPolygon(Cursor &reader); + GeometryCollection DeserializeGeometryCollection(Cursor &reader); }; } // namespace core diff --git a/spatial/include/spatial/core/geometry/geometry_properties.hpp b/spatial/include/spatial/core/geometry/geometry_properties.hpp new file mode 100644 index 00000000..7ad968f4 --- /dev/null +++ b/spatial/include/spatial/core/geometry/geometry_properties.hpp @@ -0,0 +1,57 @@ +#pragma once +#include "spatial/common.hpp" + +namespace spatial { + +namespace core { + +struct GeometryProperties { +private: + static constexpr const uint8_t Z = 0x01; + static constexpr const uint8_t M = 0x02; + static constexpr const uint8_t BBOX = 0x04; + static constexpr const uint8_t GEODETIC = 0x08; + static constexpr const uint8_t READONLY = 0x10; + static constexpr const uint8_t SOLID = 0x20; + uint8_t flags = 0; + +public: + explicit GeometryProperties(uint8_t flags = 0) : flags(flags) { + } + + inline bool HasZ() const { + return (flags & Z) != 0; + } + inline bool HasM() const { + return (flags & M) != 0; + } + inline bool HasBBox() const { + return (flags & BBOX) != 0; + } + inline bool IsGeodetic() const { + return (flags & GEODETIC) != 0; + } + inline bool IsReadOnly() const { + return (flags & READONLY) != 0; + } + + inline void SetZ(bool value) { + flags = value ? (flags | Z) : (flags & ~Z); + } + inline void SetM(bool value) { + flags = value ? (flags | M) : (flags & ~M); + } + inline void SetBBox(bool value) { + flags = value ? (flags | BBOX) : (flags & ~BBOX); + } + inline void SetGeodetic(bool value) { + flags = value ? (flags | GEODETIC) : (flags & ~GEODETIC); + } + inline void SetReadOnly(bool value) { + flags = value ? (flags | READONLY) : (flags & ~READONLY); + } +}; + +} // namespace core + +} // namespace spatial \ No newline at end of file diff --git a/spatial/include/spatial/core/geometry/geometry_type.hpp b/spatial/include/spatial/core/geometry/geometry_type.hpp new file mode 100644 index 00000000..52f9ee13 --- /dev/null +++ b/spatial/include/spatial/core/geometry/geometry_type.hpp @@ -0,0 +1,20 @@ +#pragma once +#include "spatial/common.hpp" + +namespace spatial { + +namespace core { + +enum class GeometryType : uint8_t { + POINT = 0, + LINESTRING, + POLYGON, + MULTIPOINT, + MULTILINESTRING, + MULTIPOLYGON, + GEOMETRYCOLLECTION +}; + +} // namespace core + +} // namespace spatial \ No newline at end of file diff --git a/spatial/include/spatial/core/geometry/serialization.hpp b/spatial/include/spatial/core/geometry/serialization.hpp deleted file mode 100644 index b83c8360..00000000 --- a/spatial/include/spatial/core/geometry/serialization.hpp +++ /dev/null @@ -1,103 +0,0 @@ -#pragma once -#include "spatial/common.hpp" - -namespace spatial { - -namespace core { - -//------------------------------------------------------------------------------ -// BinaryReader -//------------------------------------------------------------------------------ -class BinaryReader { -private: - data_ptr_t start; - uint32_t size; - uint32_t offset; - -public: - BinaryReader(const string_t &blob) { - start = (data_ptr_t)blob.GetDataUnsafe(); - size = blob.GetSize(); - offset = 0; - } - - void Reset() { - offset = 0; - } - - template - T Read() { - if (offset + sizeof(T) > size) { - throw SerializationException("Trying to read past end of buffer"); - } - auto ptr = start + offset; - offset += sizeof(T); - return Load(ptr); - } - - template - T Peek() { - if (offset + sizeof(T) > size) { - throw SerializationException("Trying to read past end of buffer"); - } - auto ptr = start + offset; - return Load(ptr); - } - - void Skip(uint32_t bytes) { - if (offset + bytes > size) { - throw SerializationException("Trying to read past end of buffer"); - } - offset += bytes; - } - - data_ptr_t GetPtr() { - return start + offset; - } - - void SetPtr(data_ptr_t ptr) { - if (ptr < start || ptr > start + size) { - throw SerializationException("Trying to set ptr outside of buffer"); - } - offset = ptr - start; - } -}; - -class BinaryWriter { -private: - data_ptr_t start; - uint32_t size; - uint32_t offset; - -public: - BinaryWriter(string_t &blob) { - start = (data_ptr_t)blob.GetDataUnsafe(); - size = blob.GetSize(); - offset = 0; - } - - void Reset() { - offset = 0; - } - - template - void Write(T value) { - if (offset + sizeof(T) > size) { - throw SerializationException("Trying to write past end of buffer, %d > %d", offset + sizeof(T), size); - } - auto ptr = start + offset; - offset += sizeof(T); - Store(value, ptr); - } - - void Skip(uint32_t bytes) { - if (offset + bytes > size) { - throw SerializationException("Trying to skip past end of buffer, %d > %d", offset + bytes, size); - } - offset += bytes; - } -}; - -} // namespace core - -} // namespace spatial \ No newline at end of file diff --git a/spatial/include/spatial/core/geometry/vertex_vector.hpp b/spatial/include/spatial/core/geometry/vertex_vector.hpp index 49982643..7af7130b 100644 --- a/spatial/include/spatial/core/geometry/vertex_vector.hpp +++ b/spatial/include/spatial/core/geometry/vertex_vector.hpp @@ -9,49 +9,7 @@ namespace spatial { namespace core { -struct Flags { -private: - static constexpr const uint8_t Z = 0x01; - static constexpr const uint8_t M = 0x02; - static constexpr const uint8_t BBOX = 0x04; - static constexpr const uint8_t GEODETIC = 0x08; - static constexpr const uint8_t READONLY = 0x10; - static constexpr const uint8_t SOLID = 0x20; - uint8_t flags; - -public: - inline bool HasZ() const { - return (flags & Z) != 0; - } - inline bool HasM() const { - return (flags & M) != 0; - } - inline bool HasBBox() const { - return (flags & BBOX) != 0; - } - inline bool IsGeodetic() const { - return (flags & GEODETIC) != 0; - } - inline bool IsReadOnly() const { - return (flags & READONLY) != 0; - } - - inline void SetZ(bool value) { - flags = value ? (flags | Z) : (flags & ~Z); - } - inline void SetM(bool value) { - flags = value ? (flags | M) : (flags & ~M); - } - inline void SetBBox(bool value) { - flags = value ? (flags | BBOX) : (flags & ~BBOX); - } - inline void SetGeodetic(bool value) { - flags = value ? (flags | GEODETIC) : (flags & ~GEODETIC); - } - inline void SetReadOnly(bool value) { - flags = value ? (flags | READONLY) : (flags & ~READONLY); - } -}; +class Cursor; struct Vertex { double x; @@ -159,10 +117,7 @@ class VertexVector { return sizeof(Vertex) * count; } - inline void Serialize(data_ptr_t &ptr) const { - memcpy((void *)ptr, (const char *)data, count * sizeof(Vertex)); - ptr += count * sizeof(Vertex); - } + void Serialize(Cursor &cursor) const; inline Vertex *Data() { return data; diff --git a/spatial/include/spatial/core/module.hpp b/spatial/include/spatial/core/module.hpp index bfcfa4b7..ff7e1fa7 100644 --- a/spatial/include/spatial/core/module.hpp +++ b/spatial/include/spatial/core/module.hpp @@ -1,6 +1,6 @@ #pragma once #include "spatial/common.hpp" - +#include "spatial/core/geometry/cursor.hpp" #include "spatial/core/geometry/geometry.hpp" namespace spatial { diff --git a/spatial/include/spatial/gdal/functions.hpp b/spatial/include/spatial/gdal/functions.hpp index d4ece179..383ecdb2 100644 --- a/spatial/include/spatial/gdal/functions.hpp +++ b/spatial/include/spatial/gdal/functions.hpp @@ -14,6 +14,7 @@ struct GdalTableFunction : ArrowTableFunction { private: static unique_ptr Bind(ClientContext &context, TableFunctionBindInput &input, vector &return_types, vector &names); + static void RenameColumns(vector &names); static unique_ptr InitGlobal(ClientContext &context, TableFunctionInitInput &input); @@ -21,6 +22,11 @@ struct GdalTableFunction : ArrowTableFunction { static idx_t MaxThreads(ClientContext &context, const FunctionData *bind_data_p); + static unique_ptr Cardinality(ClientContext &context, const FunctionData *data); + + static unique_ptr ReplacementScan(ClientContext &context, const string &table_name, + ReplacementScanData *data); + public: static void Register(ClientContext &context); }; diff --git a/spatial/src/spatial/core/functions/scalar/st_asgeojson.cpp b/spatial/src/spatial/core/functions/scalar/st_asgeojson.cpp index a3f131df..e8ab61bd 100644 --- a/spatial/src/spatial/core/functions/scalar/st_asgeojson.cpp +++ b/spatial/src/spatial/core/functions/scalar/st_asgeojson.cpp @@ -176,7 +176,8 @@ static void ToGeoJSON(const Geometry &geom, yyjson_mut_doc *doc, yyjson_mut_val ToGeoJSON(geom.GetGeometryCollection(), doc, obj); break; default: { - throw NotImplementedException("Geometry type not supported"); + throw NotImplementedException( + StringUtil::Format("Geometry type %d not supported", static_cast(geom.Type()))); } } } diff --git a/spatial/src/spatial/core/functions/scalar/st_npoints.cpp b/spatial/src/spatial/core/functions/scalar/st_npoints.cpp index de46ebf8..449fd43b 100644 --- a/spatial/src/spatial/core/functions/scalar/st_npoints.cpp +++ b/spatial/src/spatial/core/functions/scalar/st_npoints.cpp @@ -116,7 +116,8 @@ static uint32_t GetVertexCount(const Geometry &geometry) { return count; } default: - throw NotImplementedException("Geometry type not supported"); + throw NotImplementedException( + StringUtil::Format("Geometry type %d not supported", static_cast(geometry.Type()))); } } static void GeometryNumPointsFunction(DataChunk &args, ExpressionState &state, Vector &result) { diff --git a/spatial/src/spatial/core/geometry/geometry.cpp b/spatial/src/spatial/core/geometry/geometry.cpp index 938273fc..c3cc8045 100644 --- a/spatial/src/spatial/core/geometry/geometry.cpp +++ b/spatial/src/spatial/core/geometry/geometry.cpp @@ -323,12 +323,12 @@ string MultiPolygon::ToString() const { str += "("; bool first_ring = true; for (auto &ring : poly.Rings()) { - str += "("; if (first_ring) { first_ring = false; } else { str += ", "; } + str += "("; bool first_vert = true; for (auto &vert : ring) { if (first_vert) { diff --git a/spatial/src/spatial/core/geometry/geometry_factory.cpp b/spatial/src/spatial/core/geometry/geometry_factory.cpp index de42a869..889a18fa 100644 --- a/spatial/src/spatial/core/geometry/geometry_factory.cpp +++ b/spatial/src/spatial/core/geometry/geometry_factory.cpp @@ -1,9 +1,10 @@ +#include "spatial/core/geometry/geometry_factory.hpp" + #include "spatial/common.hpp" +#include "spatial/core/geometry/cursor.hpp" #include "spatial/core/geometry/geometry.hpp" -#include "spatial/core/geometry/geometry_factory.hpp" #include "spatial/core/geometry/wkb_reader.hpp" #include "spatial/core/geometry/wkb_writer.hpp" -#include "spatial/core/geometry/serialization.hpp" namespace spatial { @@ -108,12 +109,22 @@ GeometryCollection GeometryFactory::CreateEmptyGeometryCollection() { return GeometryCollection(nullptr, 0); } +enum class SerializedGeometryType : uint32_t { + POINT, + LINESTRING, + POLYGON, + MULTIPOINT, + MULTILINESTRING, + MULTIPOLYGON, + GEOMETRYCOLLECTION +}; + //---------------------------------------------------------------------- // Serialization //---------------------------------------------------------------------- // We always want the coordinates to be double aligned (8 bytes) // layout: -// GeometryPrefix (4 bytes) +// GeometryHeader (4 bytes) // Padding (4 bytes) (or SRID?) // Data (variable length) // -- Point @@ -140,190 +151,185 @@ string_t GeometryFactory::Serialize(Vector &result, const Geometry &geometry) { auto geom_size = GetSerializedSize(geometry); auto type = geometry.Type(); + auto properties = geometry.Properties(); + uint16_t hash = 0; // Hash geom_size uint32_t to uint16_t - uint16_t hash = 0; for (uint32_t i = 0; i < sizeof(uint32_t); i++) { hash ^= (geom_size >> (i * 8)) & 0xFF; } + GeometryHeader header(type, properties, hash); - GeometryPrefix prefix(0, type, hash); - auto header_size = prefix.SerializedSize() + 4; // + 4 for padding - - auto size = header_size + geom_size; + auto header_size = sizeof(GeometryHeader); + auto size = header_size + 4 + geom_size; // + 4 for padding auto str = StringVector::EmptyString(result, size); - auto ptr = (uint8_t *)str.GetDataUnsafe(); - prefix.Serialize(ptr); - ptr += 4; // skip padding + Cursor cursor(str); + + // Write the header + cursor.Write(header); + // Pad with 4 bytes (we might want to use this to store SRID in the future) + cursor.Write(0); switch (type) { case GeometryType::POINT: { auto &point = geometry.GetPoint(); - SerializePoint(ptr, point); + SerializePoint(cursor, point); break; } case GeometryType::LINESTRING: { auto &linestring = geometry.GetLineString(); - SerializeLineString(ptr, linestring); + SerializeLineString(cursor, linestring); break; } case GeometryType::POLYGON: { auto &polygon = geometry.GetPolygon(); - SerializePolygon(ptr, polygon); + SerializePolygon(cursor, polygon); break; } case GeometryType::MULTIPOINT: { auto &multipoint = geometry.GetMultiPoint(); - SerializeMultiPoint(ptr, multipoint); + SerializeMultiPoint(cursor, multipoint); break; } case GeometryType::MULTILINESTRING: { auto &multilinestring = geometry.GetMultiLineString(); - SerializeMultiLineString(ptr, multilinestring); + SerializeMultiLineString(cursor, multilinestring); break; } case GeometryType::MULTIPOLYGON: { auto &multipolygon = geometry.GetMultiPolygon(); - SerializeMultiPolygon(ptr, multipolygon); + SerializeMultiPolygon(cursor, multipolygon); break; } case GeometryType::GEOMETRYCOLLECTION: { auto &collection = geometry.GetGeometryCollection(); - SerializeGeometryCollection(ptr, collection); + SerializeGeometryCollection(cursor, collection); break; } default: - throw NotImplementedException("Unimplemented geometry type for serialization!"); + auto msg = StringUtil::Format("Unimplemented geometry type for serialization: %d", type); + throw SerializationException(msg); } str.Finalize(); return str; } -void GeometryFactory::SerializePoint(data_ptr_t &ptr, const Point &point) { - Store((uint32_t)GeometryType::POINT, ptr); - ptr += sizeof(uint32_t); +void GeometryFactory::SerializePoint(Cursor &cursor, const Point &point) { + // Write type (4 bytes) + cursor.Write(SerializedGeometryType::POINT); - // write point count - Store(point.vertices.Count(), ptr); - ptr += sizeof(uint32_t); + // Write point count (0 or 1) (4 bytes) + cursor.Write(point.vertices.Count()); // write data - point.vertices.Serialize(ptr); + point.vertices.Serialize(cursor); } -void GeometryFactory::SerializeLineString(data_ptr_t &ptr, const LineString &linestring) { - Store((uint32_t)GeometryType::LINESTRING, ptr); - ptr += sizeof(uint32_t); +void GeometryFactory::SerializeLineString(Cursor &cursor, const LineString &linestring) { + // Write type (4 bytes) + cursor.Write(SerializedGeometryType::LINESTRING); - // write point count - Store(linestring.vertices.Count(), ptr); - ptr += sizeof(uint32_t); + // Write point count (4 bytes) + cursor.Write(linestring.vertices.Count()); // write data - linestring.vertices.Serialize(ptr); + linestring.vertices.Serialize(cursor); } -void GeometryFactory::SerializePolygon(data_ptr_t &ptr, const Polygon &polygon) { - Store((uint32_t)GeometryType::POLYGON, ptr); - ptr += sizeof(uint32_t); +void GeometryFactory::SerializePolygon(Cursor &cursor, const Polygon &polygon) { + // Write type (4 bytes) + cursor.Write(SerializedGeometryType::POLYGON); - // write number of rings - Store(polygon.num_rings, ptr); - ptr += sizeof(uint32_t); + // Write number of rings (4 bytes) + cursor.Write(polygon.num_rings); - // write ring counts + // Write ring lengths for (uint32_t i = 0; i < polygon.num_rings; i++) { - Store(polygon.rings[i].Count(), ptr); - ptr += sizeof(uint32_t); + cursor.Write(polygon.rings[i].Count()); } if (polygon.num_rings % 2 == 1) { - // padding - Store(0, ptr); - ptr += sizeof(uint32_t); + // Write padding (4 bytes) + cursor.Write(0); } - // write ring data + // Write ring data for (uint32_t i = 0; i < polygon.num_rings; i++) { - polygon.rings[i].Serialize(ptr); + polygon.rings[i].Serialize(cursor); } } -void GeometryFactory::SerializeMultiPoint(data_ptr_t &ptr, const MultiPoint &multipoint) { - Store((uint32_t)GeometryType::MULTIPOINT, ptr); - ptr += sizeof(uint32_t); +void GeometryFactory::SerializeMultiPoint(Cursor &cursor, const MultiPoint &multipoint) { + // Write type (4 bytes) + cursor.Write(SerializedGeometryType::MULTIPOINT); - // write point count - Store(multipoint.num_points, ptr); - ptr += sizeof(uint32_t); + // Write number of points (4 bytes) + cursor.Write(multipoint.num_points); - // write data + // Write point data for (uint32_t i = 0; i < multipoint.num_points; i++) { - SerializePoint(ptr, multipoint.points[i]); + SerializePoint(cursor, multipoint.points[i]); } } -void GeometryFactory::SerializeMultiLineString(data_ptr_t &ptr, const MultiLineString &multilinestring) { - Store((uint32_t)GeometryType::MULTILINESTRING, ptr); - ptr += sizeof(uint32_t); +void GeometryFactory::SerializeMultiLineString(Cursor &cursor, const MultiLineString &multilinestring) { + // Write type (4 bytes) + cursor.Write(SerializedGeometryType::MULTILINESTRING); - // write number of linestrings - Store(multilinestring.count, ptr); - ptr += sizeof(uint32_t); + // Write number of linestrings (4 bytes) + cursor.Write(multilinestring.count); - // write linestring data + // Write linestring data for (uint32_t i = 0; i < multilinestring.count; i++) { - SerializeLineString(ptr, multilinestring.lines[i]); + SerializeLineString(cursor, multilinestring.lines[i]); } } -void GeometryFactory::SerializeMultiPolygon(data_ptr_t &ptr, const MultiPolygon &multipolygon) { - Store((uint32_t)GeometryType::MULTIPOLYGON, ptr); - ptr += sizeof(uint32_t); +void GeometryFactory::SerializeMultiPolygon(Cursor &cursor, const MultiPolygon &multipolygon) { + // Write type (4 bytes) + cursor.Write(SerializedGeometryType::MULTIPOLYGON); - // write number of polygons - Store(multipolygon.count, ptr); - ptr += sizeof(uint32_t); + // Write number of polygons (4 bytes) + cursor.Write(multipolygon.count); - // write polygon data + // Write polygon data for (uint32_t i = 0; i < multipolygon.count; i++) { - SerializePolygon(ptr, multipolygon.polygons[i]); + SerializePolygon(cursor, multipolygon.polygons[i]); } } -void GeometryFactory::SerializeGeometryCollection(data_ptr_t &ptr, const GeometryCollection &collection) { - Store((uint32_t)GeometryType::GEOMETRYCOLLECTION, ptr); - ptr += sizeof(uint32_t); +void GeometryFactory::SerializeGeometryCollection(Cursor &cursor, const GeometryCollection &collection) { + // Write type (4 bytes) + cursor.Write(SerializedGeometryType::GEOMETRYCOLLECTION); - // write number of geometries - Store(collection.count, ptr); - ptr += sizeof(uint32_t); + // Write number of geometries (4 bytes) + cursor.Write(collection.count); // write geometry data for (uint32_t i = 0; i < collection.count; i++) { auto &geom = collection.geometries[i]; switch (geom.Type()) { case GeometryType::POINT: - SerializePoint(ptr, geom.GetPoint()); + SerializePoint(cursor, geom.GetPoint()); break; case GeometryType::LINESTRING: - SerializeLineString(ptr, geom.GetLineString()); + SerializeLineString(cursor, geom.GetLineString()); break; case GeometryType::POLYGON: - SerializePolygon(ptr, geom.GetPolygon()); + SerializePolygon(cursor, geom.GetPolygon()); break; case GeometryType::MULTIPOINT: - SerializeMultiPoint(ptr, geom.GetMultiPoint()); + SerializeMultiPoint(cursor, geom.GetMultiPoint()); break; case GeometryType::MULTILINESTRING: - SerializeMultiLineString(ptr, geom.GetMultiLineString()); + SerializeMultiLineString(cursor, geom.GetMultiLineString()); break; case GeometryType::MULTIPOLYGON: - SerializeMultiPolygon(ptr, geom.GetMultiPolygon()); + SerializeMultiPolygon(cursor, geom.GetMultiPolygon()); break; case GeometryType::GEOMETRYCOLLECTION: - SerializeGeometryCollection(ptr, geom.GetGeometryCollection()); + SerializeGeometryCollection(cursor, geom.GetGeometryCollection()); break; default: throw NotImplementedException("Unimplemented geometry type!"); @@ -431,35 +437,36 @@ uint32_t GeometryFactory::GetSerializedSize(const Geometry &geometry) { // Deserialization //---------------------------------------------------------------------- Geometry GeometryFactory::Deserialize(const string_t &data) { - BinaryReader reader(data); - reader.Skip(4); // Skip prefix - reader.Skip(4); // Skip padding + Cursor cursor(data); + cursor.Skip(4); // Skip prefix + cursor.Skip(4); // Skip padding // peek the type - auto ty = (GeometryType)reader.Peek(); - switch (ty) { - case GeometryType::POINT: - return Geometry(DeserializePoint(reader)); - case GeometryType::LINESTRING: - return Geometry(DeserializeLineString(reader)); - case GeometryType::POLYGON: - return Geometry(DeserializePolygon(reader)); - case GeometryType::MULTIPOINT: - return Geometry(DeserializeMultiPoint(reader)); - case GeometryType::MULTILINESTRING: - return Geometry(DeserializeMultiLineString(reader)); - case GeometryType::MULTIPOLYGON: - return Geometry(DeserializeMultiPolygon(reader)); - case GeometryType::GEOMETRYCOLLECTION: - return Geometry(DeserializeGeometryCollection(reader)); + auto type = cursor.Peek(); + switch (type) { + case SerializedGeometryType::POINT: + return Geometry(DeserializePoint(cursor)); + case SerializedGeometryType::LINESTRING: + return Geometry(DeserializeLineString(cursor)); + case SerializedGeometryType::POLYGON: + return Geometry(DeserializePolygon(cursor)); + case SerializedGeometryType::MULTIPOINT: + return Geometry(DeserializeMultiPoint(cursor)); + case SerializedGeometryType::MULTILINESTRING: + return Geometry(DeserializeMultiLineString(cursor)); + case SerializedGeometryType::MULTIPOLYGON: + return Geometry(DeserializeMultiPolygon(cursor)); + case SerializedGeometryType::GEOMETRYCOLLECTION: + return Geometry(DeserializeGeometryCollection(cursor)); default: - throw NotImplementedException("Deserialize::Geometry type not implemented yet!"); + throw NotImplementedException( + StringUtil::Format("Deserialize: Geometry type %d not supported", static_cast(type))); } } -Point GeometryFactory::DeserializePoint(BinaryReader &reader) { - auto type = reader.Read(); - D_ASSERT(type == (uint32_t)GeometryType::POINT); +Point GeometryFactory::DeserializePoint(Cursor &reader) { + auto type = reader.Read(); + D_ASSERT(type == SerializedGeometryType::POINT); (void)type; // Points can be empty too, in which case the count is 0 @@ -476,9 +483,9 @@ Point GeometryFactory::DeserializePoint(BinaryReader &reader) { } } -LineString GeometryFactory::DeserializeLineString(BinaryReader &reader) { - auto type = reader.Read(); - D_ASSERT(type == (uint32_t)GeometryType::LINESTRING); +LineString GeometryFactory::DeserializeLineString(Cursor &reader) { + auto type = reader.Read(); + D_ASSERT(type == SerializedGeometryType::LINESTRING); (void)type; // 0 if the linestring is empty auto count = reader.Read(); @@ -490,9 +497,9 @@ LineString GeometryFactory::DeserializeLineString(BinaryReader &reader) { return LineString(vertex_data); } -Polygon GeometryFactory::DeserializePolygon(BinaryReader &reader) { - auto type = reader.Read(); - D_ASSERT(type == (uint32_t)GeometryType::POLYGON); +Polygon GeometryFactory::DeserializePolygon(Cursor &reader) { + auto type = reader.Read(); + D_ASSERT(type == SerializedGeometryType::POLYGON); (void)type; // read num rings auto num_rings = reader.Read(); @@ -510,9 +517,9 @@ Polygon GeometryFactory::DeserializePolygon(BinaryReader &reader) { return Polygon(rings, num_rings); } -MultiPoint GeometryFactory::DeserializeMultiPoint(BinaryReader &reader) { - auto type = reader.Read(); - D_ASSERT(type == (uint32_t)GeometryType::MULTIPOINT); +MultiPoint GeometryFactory::DeserializeMultiPoint(Cursor &reader) { + auto type = reader.Read(); + D_ASSERT(type == SerializedGeometryType::MULTIPOINT); (void)type; // read num points auto num_points = reader.Read(); @@ -524,9 +531,9 @@ MultiPoint GeometryFactory::DeserializeMultiPoint(BinaryReader &reader) { return MultiPoint(points, num_points); } -MultiLineString GeometryFactory::DeserializeMultiLineString(BinaryReader &reader) { - auto type = reader.Read(); - D_ASSERT(type == (uint32_t)GeometryType::MULTILINESTRING); +MultiLineString GeometryFactory::DeserializeMultiLineString(Cursor &reader) { + auto type = reader.Read(); + D_ASSERT(type == SerializedGeometryType::MULTILINESTRING); (void)type; // read num linestrings auto num_linestrings = reader.Read(); @@ -538,9 +545,9 @@ MultiLineString GeometryFactory::DeserializeMultiLineString(BinaryReader &reader return MultiLineString(linestrings, num_linestrings); } -MultiPolygon GeometryFactory::DeserializeMultiPolygon(BinaryReader &reader) { - auto type = reader.Read(); - D_ASSERT(type == (uint32_t)GeometryType::MULTIPOLYGON); +MultiPolygon GeometryFactory::DeserializeMultiPolygon(Cursor &reader) { + auto type = reader.Read(); + D_ASSERT(type == SerializedGeometryType::MULTIPOLYGON); (void)type; // read num polygons auto num_polygons = reader.Read(); @@ -552,38 +559,41 @@ MultiPolygon GeometryFactory::DeserializeMultiPolygon(BinaryReader &reader) { return MultiPolygon(polygons, num_polygons); } -GeometryCollection GeometryFactory::DeserializeGeometryCollection(BinaryReader &reader) { - auto type = reader.Read(); - D_ASSERT(type == (uint32_t)GeometryType::GEOMETRYCOLLECTION); +GeometryCollection GeometryFactory::DeserializeGeometryCollection(Cursor &reader) { + auto type = reader.Read(); + D_ASSERT(type == SerializedGeometryType::GEOMETRYCOLLECTION); (void)type; // read num geometries auto num_geometries = reader.Read(); auto geometries = reinterpret_cast(allocator.AllocateAligned(sizeof(Geometry) * num_geometries)); for (uint32_t i = 0; i < num_geometries; i++) { // peek at the type - auto geometry_type = (GeometryType)reader.Peek(); + auto geometry_type = reader.Peek(); switch (geometry_type) { - case GeometryType::POINT: + case SerializedGeometryType::POINT: geometries[i] = Geometry(DeserializePoint(reader)); break; - case GeometryType::LINESTRING: + case SerializedGeometryType::LINESTRING: geometries[i] = Geometry(DeserializeLineString(reader)); break; - case GeometryType::POLYGON: + case SerializedGeometryType::POLYGON: geometries[i] = Geometry(DeserializePolygon(reader)); break; - case GeometryType::MULTIPOINT: + case SerializedGeometryType::MULTIPOINT: geometries[i] = Geometry(DeserializeMultiPoint(reader)); break; - case GeometryType::MULTILINESTRING: + case SerializedGeometryType::MULTILINESTRING: geometries[i] = Geometry(DeserializeMultiLineString(reader)); break; - case GeometryType::MULTIPOLYGON: + case SerializedGeometryType::MULTIPOLYGON: geometries[i] = Geometry(DeserializeMultiPolygon(reader)); break; - case GeometryType::GEOMETRYCOLLECTION: + case SerializedGeometryType::GEOMETRYCOLLECTION: geometries[i] = Geometry(DeserializeGeometryCollection(reader)); break; + default: + auto msg = StringUtil::Format("Unimplemented geometry type for deserialization: %d", geometry_type); + throw SerializationException(msg); } } return GeometryCollection(geometries, num_geometries); diff --git a/spatial/src/spatial/core/geometry/vertex_vector.cpp b/spatial/src/spatial/core/geometry/vertex_vector.cpp index 7ccf3e58..4452a22e 100644 --- a/spatial/src/spatial/core/geometry/vertex_vector.cpp +++ b/spatial/src/spatial/core/geometry/vertex_vector.cpp @@ -1,4 +1,5 @@ #include "spatial/core/geometry/vertex_vector.hpp" +#include "spatial/core/geometry/cursor.hpp" #include namespace spatial { @@ -22,6 +23,13 @@ double Vertex::DistanceSquared(const Vertex &p1, const Vertex &p2) const { return DistanceSquared(p); } +void VertexVector::Serialize(Cursor &cursor) const { + auto ptr = cursor.GetPtr(); + memcpy((void *)ptr, (const char *)data, count * sizeof(Vertex)); + ptr += count * sizeof(Vertex); + cursor.SetPtr(ptr); +} + double VertexVector::Length() const { double length = 0; if (count < 2) { diff --git a/spatial/src/spatial/core/geometry/wkb_writer.cpp b/spatial/src/spatial/core/geometry/wkb_writer.cpp index 3480321b..3ccdd430 100644 --- a/spatial/src/spatial/core/geometry/wkb_writer.cpp +++ b/spatial/src/spatial/core/geometry/wkb_writer.cpp @@ -34,7 +34,8 @@ uint32_t WKBWriter::GetRequiredSize(const Geometry &geom) { case GeometryType::GEOMETRYCOLLECTION: return GetRequiredSize(geom.GetGeometryCollection()); default: - throw NotImplementedException("Geometry type not supported"); + throw NotImplementedException( + StringUtil::Format("Geometry type %d not supported", static_cast(geom.Type()))); } } @@ -129,7 +130,8 @@ void WKBWriter::Write(const Geometry &geom, data_ptr_t &ptr) { Write(geom.GetGeometryCollection(), ptr); break; default: - throw NotImplementedException("Geometry type not supported"); + throw NotImplementedException( + StringUtil::Format("Geometry type %d not supported", static_cast(geom.Type()))); } } diff --git a/spatial/src/spatial/gdal/functions/st_read.cpp b/spatial/src/spatial/gdal/functions/st_read.cpp index 76a5c753..37f656bb 100644 --- a/spatial/src/spatial/gdal/functions/st_read.cpp +++ b/spatial/src/spatial/gdal/functions/st_read.cpp @@ -1,8 +1,13 @@ #include "duckdb/parser/parsed_data/create_table_function_info.hpp" +#include "duckdb/parser/expression/constant_expression.hpp" +#include "duckdb/parser/expression/function_expression.hpp" +#include "duckdb/parser/tableref/table_function_ref.hpp" #include "duckdb/planner/filter/conjunction_filter.hpp" #include "duckdb/planner/filter/constant_filter.hpp" #include "duckdb/planner/table_filter.hpp" #include "duckdb/function/function.hpp" +#include "duckdb/function/replacement_scan.hpp" + #include "spatial/common.hpp" #include "spatial/core/types.hpp" #include "spatial/gdal/functions.hpp" @@ -110,17 +115,16 @@ struct GdalScanLocalState : ArrowScanLocalState { struct GdalScanGlobalState : ArrowScanGlobalState {}; - struct ScopedOption { string option; string original_value; - ScopedOption(string option_p, const char* default_value) : option(option_p) { + ScopedOption(string option_p, const char *default_value) : option(option_p) { // Save current value original_value = CPLGetConfigOption(option.c_str(), default_value); } - void Set(const char* new_value) { + void Set(const char *new_value) { CPLSetThreadLocalConfigOption(option.c_str(), new_value); } @@ -130,7 +134,6 @@ struct ScopedOption { } }; - unique_ptr GdalTableFunction::Bind(ClientContext &context, TableFunctionBindInput &input, vector &return_types, vector &names) { @@ -180,24 +183,19 @@ unique_ptr GdalTableFunction::Bind(ClientContext &context, TableFu for (auto &option : gdal_open_options) { if (option == nullptr) { break; - } - else if (strcmp(option, "HEADERS=FORCE") == 0) { + } else if (strcmp(option, "HEADERS=FORCE") == 0) { xlsx_headers.Set("FORCE"); - } - else if (strcmp(option, "HEADERS=DISABLE") == 0) { + } else if (strcmp(option, "HEADERS=DISABLE") == 0) { xlsx_headers.Set("DISABLE"); - } - else if (strcmp(option, "HEADERS=AUTO") == 0) { + } else if (strcmp(option, "HEADERS=AUTO") == 0) { xlsx_headers.Set("AUTO"); - } - else if (strcmp(option, "FIELD_TYPES=STRING") == 0) { + } else if (strcmp(option, "FIELD_TYPES=STRING") == 0) { xlsx_field_types.Set("STRING"); - } - else if (strcmp(option, "FIELD_TYPES=AUTO") == 0) { + } else if (strcmp(option, "FIELD_TYPES=AUTO") == 0) { xlsx_field_types.Set("AUTO"); } } - + // Now we can open the dataset auto file_name = input.inputs[0].GetValue(); auto dataset = @@ -206,7 +204,6 @@ unique_ptr GdalTableFunction::Bind(ClientContext &context, TableFu gdal_open_options.empty() ? nullptr : gdal_open_options.data(), gdal_sibling_files.empty() ? nullptr : gdal_sibling_files.data())); - if (dataset == nullptr) { auto error = string(CPLGetLastErrorMsg()); throw IOException("Could not open file: " + file_name + " (" + error + ")"); @@ -341,8 +338,8 @@ unique_ptr GdalTableFunction::Bind(ClientContext &context, TableFu auto attribute_count = schema.n_children; auto attributes = schema.children; - result->all_names.reserve(attribute_count); - names.reserve(attribute_count); + result->all_names.reserve(attribute_count + 1); + names.reserve(attribute_count + 1); for (idx_t col_idx = 0; col_idx < (idx_t)attribute_count; col_idx++) { auto &attribute = *attributes[col_idx]; @@ -382,7 +379,7 @@ unique_ptr GdalTableFunction::Bind(ClientContext &context, TableFu schema.release(&schema); stream.release(&stream); - RenameArrowColumns(names); + GdalTableFunction::RenameColumns(names); result->dataset = std::move(dataset); result->all_types = return_types; @@ -390,17 +387,36 @@ unique_ptr GdalTableFunction::Bind(ClientContext &context, TableFu return std::move(result); }; +void GdalTableFunction::RenameColumns(vector &names) { + unordered_map name_map; + for (auto &column_name : names) { + // put it all lower_case + auto low_column_name = StringUtil::Lower(column_name); + if (name_map.find(low_column_name) == name_map.end()) { + // Name does not exist yet + name_map[low_column_name]++; + } else { + // Name already exists, we add _x where x is the repetition number + string new_column_name = column_name + "_" + std::to_string(name_map[low_column_name]); + auto new_column_name_low = StringUtil::Lower(new_column_name); + while (name_map.find(new_column_name_low) != name_map.end()) { + // This name is already here due to a previous definition + name_map[low_column_name]++; + new_column_name = column_name + "_" + std::to_string(name_map[low_column_name]); + new_column_name_low = StringUtil::Lower(new_column_name); + } + column_name = new_column_name; + name_map[new_column_name_low]++; + } + } +} + idx_t GdalTableFunction::MaxThreads(ClientContext &context, const FunctionData *bind_data_p) { auto data = (const GdalScanFunctionData *)bind_data_p; return data->max_threads; } -// init global -unique_ptr GdalTableFunction::InitGlobal(ClientContext &context, - TableFunctionInitInput &input) { - auto &data = (GdalScanFunctionData &)*input.bind_data; - - auto global_state = make_uniq(); +OGRLayer *open_layer(const GdalScanFunctionData &data) { // Get selected layer OGRLayer *layer = nullptr; @@ -434,6 +450,16 @@ unique_ptr GdalTableFunction::InitGlobal(ClientContext } } + return layer; +} + +// init global +unique_ptr GdalTableFunction::InitGlobal(ClientContext &context, + TableFunctionInitInput &input) { + auto &data = input.bind_data->Cast(); + auto global_state = make_uniq(); + + auto layer = open_layer(data); // TODO: Apply projection pushdown // Apply predicate pushdown @@ -507,13 +533,54 @@ void GdalTableFunction::Scan(ClientContext &context, TableFunctionInput &input, state.chunk_offset += output.size(); } +unique_ptr GdalTableFunction::Cardinality(ClientContext &context, const FunctionData *data) { + auto &gdal_data = data->Cast(); + auto result = make_uniq(); + + if (gdal_data.sequential_layer_scan) { + // It would be too expensive to calculate the cardinality for a sequential layer scan + // as we would have to scan through all the layers twice. + return result; + } + + // Some drivers wont return a feature count if it would be to expensive to calculate + // (unless we pass 1 "= force" to the function calculate) + auto layer = open_layer(gdal_data); + + auto count = layer->GetFeatureCount(0); + if (count > -1) { + result->has_estimated_cardinality = true; + result->estimated_cardinality = count; + } + + return result; +} + +unique_ptr GdalTableFunction::ReplacementScan(ClientContext &, const string &table_name, + ReplacementScanData *) { + + auto lower_name = StringUtil::Lower(table_name); + // Check if the table name ends with some common geospatial file extensions + if (StringUtil::EndsWith(lower_name, ".shp") || StringUtil::EndsWith(lower_name, ".gpkg") || + StringUtil::EndsWith(lower_name, ".fgb")) { + + auto table_function = make_uniq(); + vector> children; + children.push_back(make_uniq(Value(table_name))); + table_function->function = make_uniq("st_read", std::move(children)); + return std::move(table_function); + } + // else not something we can replace + return nullptr; +} + void GdalTableFunction::Register(ClientContext &context) { TableFunctionSet set("st_read"); TableFunction scan({LogicalType::VARCHAR}, GdalTableFunction::Scan, GdalTableFunction::Bind, GdalTableFunction::InitGlobal, ArrowTableFunction::ArrowScanInitLocal); - scan.cardinality = ArrowTableFunction::ArrowScanCardinality; + scan.cardinality = GdalTableFunction::Cardinality; scan.get_batch_index = ArrowTableFunction::ArrowGetBatchIndex; scan.projection_pushdown = true; @@ -532,6 +599,10 @@ void GdalTableFunction::Register(ClientContext &context) { auto &catalog = Catalog::GetSystemCatalog(context); CreateTableFunctionInfo info(set); catalog.CreateTableFunction(context, &info); + + // Replacement scan + auto &config = DBConfig::GetConfig(*context.db); + config.replacement_scans.emplace_back(GdalTableFunction::ReplacementScan); } } // namespace gdal diff --git a/spatial/src/spatial/geos/geos_wrappers.cpp b/spatial/src/spatial/geos/geos_wrappers.cpp index 69ad8bad..cc5375bf 100644 --- a/spatial/src/spatial/geos/geos_wrappers.cpp +++ b/spatial/src/spatial/geos/geos_wrappers.cpp @@ -1,7 +1,7 @@ #include "spatial/common.hpp" #include "spatial/geos/geos_wrappers.hpp" #include "spatial/core/geometry/geometry.hpp" -#include "spatial/core/geometry/serialization.hpp" +#include "spatial/core/geometry/cursor.hpp" namespace spatial { @@ -15,8 +15,8 @@ using namespace core; // Note: We dont use GEOSCoordSeq_CopyFromBuffer here because we cant actually guarantee that the // double* is aligned to 8 bytes when duckdb loads the blob from storage, and GEOS only performs a // memcpy for 3d geometry. In the future this may change on our end though. -static GEOSGeometry *DeserializeGeometry(BinaryReader &reader, GEOSContextHandle_t ctx); -static GEOSGeometry *DeserializePoint(BinaryReader &reader, GEOSContextHandle_t ctx) { +static GEOSGeometry *DeserializeGeometry(Cursor &reader, GEOSContextHandle_t ctx); +static GEOSGeometry *DeserializePoint(Cursor &reader, GEOSContextHandle_t ctx) { reader.Skip(4); // skip type auto count = reader.Read(); if (count == 0) { @@ -33,7 +33,7 @@ static GEOSGeometry *DeserializePoint(BinaryReader &reader, GEOSContextHandle_t } } -static GEOSGeometry *DeserializeLineString(BinaryReader &reader, GEOSContextHandle_t ctx) { +static GEOSGeometry *DeserializeLineString(Cursor &reader, GEOSContextHandle_t ctx) { reader.Skip(4); // skip type auto count = reader.Read(); if (count == 0) { @@ -50,7 +50,7 @@ static GEOSGeometry *DeserializeLineString(BinaryReader &reader, GEOSContextHand } } -static GEOSGeometry *DeserializePolygon(BinaryReader &reader, GEOSContextHandle_t ctx) { +static GEOSGeometry *DeserializePolygon(Cursor &reader, GEOSContextHandle_t ctx) { reader.Skip(4); // skip type auto num_rings = reader.Read(); if (num_rings == 0) { @@ -79,7 +79,7 @@ static GEOSGeometry *DeserializePolygon(BinaryReader &reader, GEOSContextHandle_ } } -static GEOSGeometry *DeserializeMultiPoint(BinaryReader &reader, GEOSContextHandle_t ctx) { +static GEOSGeometry *DeserializeMultiPoint(Cursor &reader, GEOSContextHandle_t ctx) { reader.Skip(4); // skip type auto num_points = reader.Read(); if (num_points == 0) { @@ -95,7 +95,7 @@ static GEOSGeometry *DeserializeMultiPoint(BinaryReader &reader, GEOSContextHand } } -static GEOSGeometry *DeserializeMultiLineString(BinaryReader &reader, GEOSContextHandle_t ctx) { +static GEOSGeometry *DeserializeMultiLineString(Cursor &reader, GEOSContextHandle_t ctx) { reader.Skip(4); // skip type auto num_lines = reader.Read(); if (num_lines == 0) { @@ -111,7 +111,7 @@ static GEOSGeometry *DeserializeMultiLineString(BinaryReader &reader, GEOSContex } } -static GEOSGeometry *DeserializeMultiPolygon(BinaryReader &reader, GEOSContextHandle_t ctx) { +static GEOSGeometry *DeserializeMultiPolygon(Cursor &reader, GEOSContextHandle_t ctx) { reader.Skip(4); // skip type auto num_polygons = reader.Read(); if (num_polygons == 0) { @@ -127,7 +127,7 @@ static GEOSGeometry *DeserializeMultiPolygon(BinaryReader &reader, GEOSContextHa } } -static GEOSGeometry *DeserializeGeometryCollection(BinaryReader &reader, GEOSContextHandle_t ctx) { +static GEOSGeometry *DeserializeGeometryCollection(Cursor &reader, GEOSContextHandle_t ctx) { reader.Skip(4); // skip type auto num_geoms = reader.Read(); if (num_geoms == 0) { @@ -143,7 +143,7 @@ static GEOSGeometry *DeserializeGeometryCollection(BinaryReader &reader, GEOSCon } } -static GEOSGeometry *DeserializeGeometry(BinaryReader &reader, GEOSContextHandle_t ctx) { +static GEOSGeometry *DeserializeGeometry(Cursor &reader, GEOSContextHandle_t ctx) { auto type = reader.Peek(); switch (type) { case GeometryType::POINT: { @@ -168,13 +168,14 @@ static GEOSGeometry *DeserializeGeometry(BinaryReader &reader, GEOSContextHandle return DeserializeGeometryCollection(reader, ctx); } default: { - throw NotImplementedException("Geometry type not implemented for deserialization"); + throw NotImplementedException( + StringUtil::Format("GEOS Deserialize: Geometry type %d not supported", static_cast(type))); } } } GeometryPtr GeosContextWrapper::Deserialize(const string_t &blob) { - BinaryReader reader(blob); + Cursor reader(blob); reader.Skip(4); // Skip type, flags and hash reader.Skip(4); // Skip padding return GeometryPtr(DeserializeGeometry(reader, ctx)); @@ -184,7 +185,8 @@ GeometryPtr GeosContextWrapper::Deserialize(const string_t &blob) { // Serialize //------------------------------------------------------------------- static uint32_t GetSerializedSize(const GEOSGeometry *geom, const GEOSContextHandle_t ctx) { - switch (GEOSGeomTypeId_r(ctx, geom)) { + auto type = GEOSGeomTypeId_r(ctx, geom); + switch (type) { case GEOS_POINT: { // 4 bytes for type, // 4 bytes for num points, @@ -280,13 +282,13 @@ static uint32_t GetSerializedSize(const GEOSGeometry *geom, const GEOSContextHan return size; } default: { - throw NotImplementedException("Geometry type not implemented for serialization"); + throw NotImplementedException(StringUtil::Format("GEOS SerializedSize: Geometry type %d not supported", type)); } } } -static void SerializeGeometry(BinaryWriter &writer, const GEOSGeometry *geom, const GEOSContextHandle_t ctx); +static void SerializeGeometry(Cursor &writer, const GEOSGeometry *geom, const GEOSContextHandle_t ctx); -static void SerializePoint(BinaryWriter &writer, const GEOSGeometry *geom, const GEOSContextHandle_t ctx) { +static void SerializePoint(Cursor &writer, const GEOSGeometry *geom, const GEOSContextHandle_t ctx) { writer.Write((uint32_t)GeometryType::POINT); if (GEOSisEmpty_r(ctx, geom)) { @@ -302,7 +304,7 @@ static void SerializePoint(BinaryWriter &writer, const GEOSGeometry *geom, const writer.Write(y); } -static void SerializeLineString(BinaryWriter &writer, const GEOSGeometry *geom, const GEOSContextHandle_t ctx) { +static void SerializeLineString(Cursor &writer, const GEOSGeometry *geom, const GEOSContextHandle_t ctx) { writer.Write((uint32_t)GeometryType::LINESTRING); auto seq = GEOSGeom_getCoordSeq_r(ctx, geom); uint32_t count; @@ -317,7 +319,7 @@ static void SerializeLineString(BinaryWriter &writer, const GEOSGeometry *geom, } } -static void SerializePolygon(BinaryWriter &writer, const GEOSGeometry *geom, const GEOSContextHandle_t ctx) { +static void SerializePolygon(Cursor &writer, const GEOSGeometry *geom, const GEOSContextHandle_t ctx) { // TODO: check this writer.Write((uint32_t)GeometryType::POLYGON); @@ -380,7 +382,7 @@ static void SerializePolygon(BinaryWriter &writer, const GEOSGeometry *geom, con } } -static void SerializeMultiPoint(BinaryWriter &writer, const GEOSGeometry *geom, const GEOSContextHandle_t ctx) { +static void SerializeMultiPoint(Cursor &writer, const GEOSGeometry *geom, const GEOSContextHandle_t ctx) { writer.Write((uint32_t)GeometryType::MULTIPOINT); auto num_points = GEOSGetNumGeometries_r(ctx, geom); writer.Write(num_points); @@ -390,7 +392,7 @@ static void SerializeMultiPoint(BinaryWriter &writer, const GEOSGeometry *geom, } } -static void SerializeMultiLineString(BinaryWriter &writer, const GEOSGeometry *geom, const GEOSContextHandle_t ctx) { +static void SerializeMultiLineString(Cursor &writer, const GEOSGeometry *geom, const GEOSContextHandle_t ctx) { writer.Write((uint32_t)GeometryType::MULTILINESTRING); auto num_linestrings = GEOSGetNumGeometries_r(ctx, geom); writer.Write(num_linestrings); @@ -400,7 +402,7 @@ static void SerializeMultiLineString(BinaryWriter &writer, const GEOSGeometry *g } } -static void SerializeMultiPolygon(BinaryWriter &writer, const GEOSGeometry *geom, const GEOSContextHandle_t ctx) { +static void SerializeMultiPolygon(Cursor &writer, const GEOSGeometry *geom, const GEOSContextHandle_t ctx) { writer.Write((uint32_t)GeometryType::MULTIPOLYGON); auto num_polygons = GEOSGetNumGeometries_r(ctx, geom); writer.Write(num_polygons); @@ -410,7 +412,7 @@ static void SerializeMultiPolygon(BinaryWriter &writer, const GEOSGeometry *geom } } -static void SerializeGeometryCollection(BinaryWriter &writer, const GEOSGeometry *geom, const GEOSContextHandle_t ctx) { +static void SerializeGeometryCollection(Cursor &writer, const GEOSGeometry *geom, const GEOSContextHandle_t ctx) { writer.Write((uint32_t)GeometryType::GEOMETRYCOLLECTION); auto num_geometries = GEOSGetNumGeometries_r(ctx, geom); writer.Write(num_geometries); @@ -420,8 +422,9 @@ static void SerializeGeometryCollection(BinaryWriter &writer, const GEOSGeometry } } -static void SerializeGeometry(BinaryWriter &writer, const GEOSGeometry *geom, const GEOSContextHandle_t ctx) { - switch (GEOSGeomTypeId_r(ctx, geom)) { +static void SerializeGeometry(Cursor &writer, const GEOSGeometry *geom, const GEOSContextHandle_t ctx) { + auto type = GEOSGeomTypeId_r(ctx, geom); + switch (type) { case GEOS_POINT: SerializePoint(writer, geom, ctx); break; @@ -444,19 +447,17 @@ static void SerializeGeometry(BinaryWriter &writer, const GEOSGeometry *geom, co SerializeGeometryCollection(writer, geom, ctx); break; default: - throw NotImplementedException("Geometry type not implemented for serialization"); + throw NotImplementedException(StringUtil::Format("GEOS Serialize: Geometry type %d not supported", type)); } } string_t GeosContextWrapper::Serialize(Vector &result, const GeometryPtr &geom) { auto size = GetSerializedSize(geom.get(), ctx); - size += 1; // 1 bytes for flags - size += 1; // 1 bytes for type - size += 2; // 2 bytes for hash - size += 4; // 4 bytes padding + size += sizeof(GeometryHeader); // Header + size += sizeof(uint32_t); // Padding auto blob = StringVector::EmptyString(result, size); - BinaryWriter writer(blob); + Cursor writer(blob); uint16_t hash = 0; for (uint32_t i = 0; i < sizeof(uint32_t); i++) { @@ -464,7 +465,8 @@ string_t GeosContextWrapper::Serialize(Vector &result, const GeometryPtr &geom) } GeometryType type; - switch (GEOSGeomTypeId_r(ctx, geom.get())) { + auto geos_type = GEOSGeomTypeId_r(ctx, geom.get()); + switch (geos_type) { case GEOS_POINT: type = GeometryType::POINT; break; @@ -487,13 +489,17 @@ string_t GeosContextWrapper::Serialize(Vector &result, const GeometryPtr &geom) type = GeometryType::GEOMETRYCOLLECTION; break; default: - throw NotImplementedException("Geometry type not implemented for serialization"); + throw NotImplementedException( + StringUtil::Format("GEOS Wrapper Serialize: Geometry type %d not supported", geos_type)); } - writer.Write(0); // Flags - writer.Write(type); // Type - writer.Write(hash); // Hash - writer.Write(0); // Padding + GeometryHeader header; + header.type = type; + header.hash = hash; + header.properties = GeometryProperties(); + + writer.Write(header); // Header + writer.Write(0); // Padding SerializeGeometry(writer, geom.get(), ctx); diff --git a/spatial/src/spatial/proj/functions.cpp b/spatial/src/spatial/proj/functions.cpp index d47c0062..1618c5d0 100644 --- a/spatial/src/spatial/proj/functions.cpp +++ b/spatial/src/spatial/proj/functions.cpp @@ -2,6 +2,8 @@ #include "duckdb/parser/parsed_data/create_scalar_function_info.hpp" #include "duckdb/parser/parsed_data/create_table_function_info.hpp" #include "duckdb/parser/parsed_data/create_view_info.hpp" +#include "duckdb/execution/expression_executor.hpp" +#include "duckdb/planner/expression/bound_function_expression.hpp" #include "spatial/common.hpp" #include "spatial/core/types.hpp" @@ -40,6 +42,40 @@ struct ProjFunctionLocalState : public FunctionLocalState { } }; +struct TransformFunctionData : FunctionData { + + // Whether or not to always return XY coordinates, even when the CRS has a different axis order. + bool conventional_gis_order = false; + + unique_ptr Copy() const override { + auto result = make_uniq(); + result->conventional_gis_order = conventional_gis_order; + return result; + } + bool Equals(const FunctionData &other) const override { + auto &data = other.Cast(); + return conventional_gis_order == data.conventional_gis_order; + } +}; + +static unique_ptr TransformBind(ClientContext &context, ScalarFunction &bound_function, + vector> &arguments) { + + auto result = make_uniq(); + if (arguments.size() == 4) { + // Ensure the "always_xy" parameter is a constant + auto &arg = arguments[3]; + if (arg->HasParameter()) { + throw InvalidInputException("The 'always_xy' parameter must be a constant"); + } + if (!arg->IsFoldable()) { + throw InvalidInputException("The 'always_xy' parameter must be a constant"); + } + result->conventional_gis_order = BooleanValue::Get(ExpressionExecutor::EvaluateScalar(context, *arg)); + } + return result; +} + static void Box2DTransformFunction(DataChunk &args, ExpressionState &state, Vector &result) { using BOX_TYPE = StructTypeQuaternary; using PROJ_TYPE = PrimitiveType; @@ -51,27 +87,70 @@ static void Box2DTransformFunction(DataChunk &args, ExpressionState &state, Vect auto &local_state = ProjFunctionLocalState::ResetAndGet(state); auto &proj_ctx = local_state.proj_ctx; + auto &func_expr = state.expr.Cast(); + auto &info = func_expr.bind_info->Cast(); - GenericExecutor::ExecuteTernary( - box, proj_from, proj_to, result, count, [&](BOX_TYPE box_in, PROJ_TYPE proj_from, PROJ_TYPE proj_to) { - auto from_str = proj_from.val.GetString(); - auto to_str = proj_to.val.GetString(); + if (proj_from.GetVectorType() == VectorType::CONSTANT_VECTOR && + proj_to.GetVectorType() == VectorType::CONSTANT_VECTOR && !ConstantVector::IsNull(proj_from) && + !ConstantVector::IsNull(proj_to)) { + // Special case: both projections are constant, so we can create the projection once and reuse it + auto from_str = ConstantVector::GetData(proj_from)[0].val.GetString(); + auto to_str = ConstantVector::GetData(proj_to)[0].val.GetString(); - auto crs = proj_create_crs_to_crs(nullptr, from_str.c_str(), to_str.c_str(), nullptr); - if (!crs) { - throw InvalidInputException("Could not create projection: " + from_str + " -> " + to_str); - } + auto crs = proj_create_crs_to_crs(proj_ctx, from_str.c_str(), to_str.c_str(), nullptr); + if (!crs) { + throw InvalidInputException("Could not create projection: " + from_str + " -> " + to_str); + } - // TODO: this may be interesting to use, but at that point we can only return a BOX_TYPE - int densify_pts = 0; - BOX_TYPE box_out; - proj_trans_bounds(proj_ctx, crs, PJ_FWD, box_in.a_val, box_in.b_val, box_in.c_val, box_in.d_val, - &box_out.a_val, &box_out.b_val, &box_out.c_val, &box_out.d_val, densify_pts); + if (info.conventional_gis_order) { + auto normalized_crs = proj_normalize_for_visualization(proj_ctx, crs); + if (normalized_crs) { + proj_destroy(crs); + crs = normalized_crs; + } + // otherwise fall back to the original CRS + } - proj_destroy(crs); + GenericExecutor::ExecuteUnary(box, result, count, [&](BOX_TYPE box_in) { + BOX_TYPE box_out; + int densify_pts = 0; + proj_trans_bounds(proj_ctx, crs, PJ_FWD, box_in.a_val, box_in.b_val, box_in.c_val, box_in.d_val, + &box_out.a_val, &box_out.b_val, &box_out.c_val, &box_out.d_val, densify_pts); + return box_out; + }); - return box_out; - }); + proj_destroy(crs); + } else { + GenericExecutor::ExecuteTernary( + box, proj_from, proj_to, result, count, [&](BOX_TYPE box_in, PROJ_TYPE proj_from, PROJ_TYPE proj_to) { + auto from_str = proj_from.val.GetString(); + auto to_str = proj_to.val.GetString(); + + auto crs = proj_create_crs_to_crs(nullptr, from_str.c_str(), to_str.c_str(), nullptr); + if (!crs) { + throw InvalidInputException("Could not create projection: " + from_str + " -> " + to_str); + } + + if (info.conventional_gis_order) { + auto normalized_crs = proj_normalize_for_visualization(proj_ctx, crs); + if (normalized_crs) { + proj_destroy(crs); + crs = normalized_crs; + } + // otherwise fall back to the original CRS + } + + // TODO: this may be interesting to use, but at that point we can only return a BOX_TYPE + int densify_pts = 0; + BOX_TYPE box_out; + proj_trans_bounds(proj_ctx, crs, PJ_FWD, box_in.a_val, box_in.b_val, box_in.c_val, box_in.d_val, + &box_out.a_val, &box_out.b_val, &box_out.c_val, &box_out.d_val, densify_pts); + + proj_destroy(crs); + + return box_out; + }); + } } static void Point2DTransformFunction(DataChunk &args, ExpressionState &state, Vector &result) { @@ -85,6 +164,8 @@ static void Point2DTransformFunction(DataChunk &args, ExpressionState &state, Ve auto &local_state = ProjFunctionLocalState::ResetAndGet(state); auto &proj_ctx = local_state.proj_ctx; + auto &func_expr = state.expr.Cast(); + auto &info = func_expr.bind_info->Cast(); if (proj_from.GetVectorType() == VectorType::CONSTANT_VECTOR && proj_to.GetVectorType() == VectorType::CONSTANT_VECTOR && !ConstantVector::IsNull(proj_from) && @@ -98,6 +179,15 @@ static void Point2DTransformFunction(DataChunk &args, ExpressionState &state, Ve throw InvalidInputException("Could not create projection: " + from_str + " -> " + to_str); } + if (info.conventional_gis_order) { + auto normalized_crs = proj_normalize_for_visualization(proj_ctx, crs); + if (normalized_crs) { + proj_destroy(crs); + crs = normalized_crs; + } + // otherwise fall back to the original CRS + } + GenericExecutor::ExecuteUnary(point, result, count, [&](POINT_TYPE point_in) { POINT_TYPE point_out; auto transformed = proj_trans(crs, PJ_FWD, proj_coord(point_in.a_val, point_in.b_val, 0, 0)).xy; @@ -118,6 +208,15 @@ static void Point2DTransformFunction(DataChunk &args, ExpressionState &state, Ve throw InvalidInputException("Could not create projection: " + from_str + " -> " + to_str); } + if (info.conventional_gis_order) { + auto normalized_crs = proj_normalize_for_visualization(proj_ctx, crs); + if (normalized_crs) { + proj_destroy(crs); + crs = normalized_crs; + } + // otherwise fall back to the original CRS + } + POINT_TYPE point_out; auto transformed = proj_trans(crs, PJ_FWD, proj_coord(point_in.a_val, point_in.b_val, 0, 0)).xy; point_out.a_val = transformed.x; @@ -217,8 +316,8 @@ struct ProjCRSDelete { proj_destroy(crs); } }; -// TODO: duckdbs safe unique_ptr does not support custom deleters yet -using ProjCRS = std::unique_ptr; + +using ProjCRS = unique_ptr; static void GeometryTransformFunction(DataChunk &args, ExpressionState &state, Vector &result) { auto count = args.size(); @@ -227,6 +326,10 @@ static void GeometryTransformFunction(DataChunk &args, ExpressionState &state, V auto &proj_to_vec = args.data[2]; auto &local_state = ProjFunctionLocalState::ResetAndGet(state); + + auto &func_expr = state.expr.Cast(); + auto &info = func_expr.bind_info->Cast(); + auto &proj_ctx = local_state.proj_ctx; auto &factory = local_state.factory; @@ -245,6 +348,14 @@ static void GeometryTransformFunction(DataChunk &args, ExpressionState &state, V throw InvalidInputException("Could not create projection: " + from_str + " -> " + to_str); } + if (info.conventional_gis_order) { + auto normalized_crs = proj_normalize_for_visualization(proj_ctx, crs.get()); + if (normalized_crs) { + crs = ProjCRS(normalized_crs); + } + // otherwise fall back to the original CRS + } + UnaryExecutor::Execute(geom_vec, result, count, [&](string_t input_geom) { auto geom = factory.Deserialize(input_geom); auto copy = factory.CopyGeometry(geom); @@ -265,6 +376,14 @@ static void GeometryTransformFunction(DataChunk &args, ExpressionState &state, V throw InvalidInputException("Could not create projection: " + from_str + " -> " + to_str); } + if (info.conventional_gis_order) { + auto normalized_crs = proj_normalize_for_visualization(proj_ctx, crs.get()); + if (normalized_crs) { + crs = ProjCRS(normalized_crs); + } + // otherwise fall back to the original CRS + } + auto geom = factory.Deserialize(input_geom); auto copy = factory.CopyGeometry(geom); TransformGeometry(crs.get(), copy); @@ -379,16 +498,28 @@ void ProjFunctions::Register(ClientContext &context) { ScalarFunctionSet set("st_transform"); - set.AddFunction(ScalarFunction({spatial::core::GeoTypes::BOX_2D(), LogicalType::VARCHAR, LogicalType::VARCHAR}, - spatial::core::GeoTypes::BOX_2D(), Box2DTransformFunction, nullptr, nullptr, nullptr, + using namespace spatial::core; + + set.AddFunction(ScalarFunction({GeoTypes::BOX_2D(), LogicalType::VARCHAR, LogicalType::VARCHAR}, GeoTypes::BOX_2D(), + Box2DTransformFunction, TransformBind, nullptr, nullptr, ProjFunctionLocalState::Init)); - set.AddFunction(ScalarFunction({spatial::core::GeoTypes::POINT_2D(), LogicalType::VARCHAR, LogicalType::VARCHAR}, - spatial::core::GeoTypes::POINT_2D(), Point2DTransformFunction, nullptr, nullptr, - nullptr, ProjFunctionLocalState::Init)); + set.AddFunction(ScalarFunction( + {GeoTypes::BOX_2D(), LogicalType::VARCHAR, LogicalType::VARCHAR, LogicalType::BOOLEAN}, GeoTypes::BOX_2D(), + Box2DTransformFunction, TransformBind, nullptr, nullptr, ProjFunctionLocalState::Init)); - set.AddFunction(ScalarFunction({spatial::core::GeoTypes::GEOMETRY(), LogicalType::VARCHAR, LogicalType::VARCHAR}, - spatial::core::GeoTypes::GEOMETRY(), GeometryTransformFunction, nullptr, nullptr, - nullptr, ProjFunctionLocalState::Init)); + set.AddFunction(ScalarFunction({GeoTypes::POINT_2D(), LogicalType::VARCHAR, LogicalType::VARCHAR}, + GeoTypes::POINT_2D(), Point2DTransformFunction, TransformBind, nullptr, nullptr, + ProjFunctionLocalState::Init)); + set.AddFunction(ScalarFunction( + {GeoTypes::POINT_2D(), LogicalType::VARCHAR, LogicalType::VARCHAR, LogicalType::BOOLEAN}, GeoTypes::POINT_2D(), + Point2DTransformFunction, TransformBind, nullptr, nullptr, ProjFunctionLocalState::Init)); + + set.AddFunction(ScalarFunction({GeoTypes::GEOMETRY(), LogicalType::VARCHAR, LogicalType::VARCHAR}, + GeoTypes::GEOMETRY(), GeometryTransformFunction, TransformBind, nullptr, nullptr, + ProjFunctionLocalState::Init)); + set.AddFunction(ScalarFunction( + {GeoTypes::GEOMETRY(), LogicalType::VARCHAR, LogicalType::VARCHAR, LogicalType::BOOLEAN}, GeoTypes::GEOMETRY(), + GeometryTransformFunction, TransformBind, nullptr, nullptr, ProjFunctionLocalState::Init)); CreateScalarFunctionInfo info(std::move(set)); info.on_conflict = OnCreateConflict::ALTER_ON_CONFLICT; diff --git a/test/sql/geometry/st_astext.test b/test/sql/geometry/st_astext.test new file mode 100644 index 00000000..e2197724 --- /dev/null +++ b/test/sql/geometry/st_astext.test @@ -0,0 +1,7 @@ +require spatial + +# Issue #118 +query I +SELECT ST_GeomFromText('MULTIPOLYGON (((0 0, 1 1, 2 2, 0 0), (0 0, 1 1, 2 2, 0 0)))'); +---- +MULTIPOLYGON (((0 0, 1 1, 2 2, 0 0), (0 0, 1 1, 2 2, 0 0))) \ No newline at end of file diff --git a/test/sql/proj.test b/test/sql/proj.test index 8d1e06b9..d4e09096 100644 --- a/test/sql/proj.test +++ b/test/sql/proj.test @@ -13,4 +13,6 @@ require spatial query I SELECT st_transform({'x': 52.3676, 'y': 4.9041}::POINT_2D, 'EPSG:4326', 'EPSG:3857') ---- -POINT (545921.9147992929 6866867.121983132) \ No newline at end of file +POINT (545921.9147992929 6866867.121983132) + +