github-actions[bot] commented on code in PR #69032:
URL: https://github.com/apache/doris/pull/69032#discussion_r4374330645
##########
be/src/exprs/function/geo/functions_geo.cpp:
##########
@@ -18,32 +18,634 @@
#include "exprs/function/geo/functions_geo.h"
#include <glog/logging.h>
+#include <s2/mutable_s2shape_index.h>
+#include <s2/s2boolean_operation.h>
+#include <s2/s2builderutil_s2polyline_vector_layer.h>
+#include <s2/s2closest_edge_query.h>
+#include <s2/s2closest_point_query.h>
+#include <s2/s2earth.h>
+#include <s2/s2lax_polygon_shape.h>
+#include <s2/s2lax_polyline_shape.h>
+#include <s2/s2loop.h>
#include <s2/s2point.h>
+#include <s2/s2point_index.h>
+#include <s2/s2point_vector_shape.h>
+#include <s2/s2polygon.h>
+#include <s2/s2polyline.h>
+#include <s2/s2predicates.h>
#include <algorithm>
+#include <array>
#include <boost/iterator/iterator_facade.hpp>
+#include <cmath>
+#include <cstdint>
+#include <limits>
+#include <map>
+#include <optional>
#include <utility>
+#include "common/cast_set.h"
#include "common/compiler_util.h"
#include "core/assert_cast.h"
#include "core/block/block.h"
#include "core/block/column_with_type_and_name.h"
#include "core/column/column.h"
#include "core/column/column_array.h"
#include "core/column/column_execute_util.h"
+#include "core/column/column_map.h"
#include "core/column/column_nullable.h"
+#include "core/column/column_struct.h"
+#include "core/column/column_varbinary.h"
#include "core/data_type/data_type_array.h"
+#include "core/data_type/data_type_map.h"
#include "core/data_type/data_type_nullable.h"
#include "core/data_type/data_type_number.h"
+#include "core/data_type/data_type_spatial.h"
#include "core/data_type/data_type_string.h"
+#include "core/data_type/data_type_struct.h"
+#include "core/data_type/data_type_varbinary.h"
#include "core/data_type/define_primitive_type.h"
#include "core/string_ref.h"
#include "exprs/function/geo/geo_common.h"
#include "exprs/function/geo/geo_types.h"
+#include "exprs/function/geo/wkb_parse.h"
#include "exprs/function/simple_function_factory.h"
+#include "exprs/function/string_hex_util.h"
namespace doris {
+static bool is_spatial_type(const DataTypePtr& type) {
+ const auto primitive_type = remove_nullable(type)->get_primitive_type();
+ return primitive_type == TYPE_GEOMETRY || primitive_type == TYPE_GEOGRAPHY;
+}
+
+static Status validate_geography_semantics(const DataTypePtr& type, const
char* function_name) {
+ const auto& nested_type = remove_nullable(type);
+ if (!is_spatial_type(nested_type)) {
+ return Status::OK();
+ }
+
+ const auto* spatial_type = dynamic_cast<const
DataTypeSpatial*>(nested_type.get());
+ DCHECK(spatial_type != nullptr);
+ if (spatial_type->get_primitive_type() == TYPE_GEOGRAPHY &&
+ spatial_type->crs() == "OGC:CRS84" && spatial_type->algorithm() ==
"spherical") {
+ return Status::OK();
+ }
+ return Status::NotSupported(
+ "Function {} requires GEOGRAPHY(OGC:CRS84, spherical) for spatial
inputs",
+ function_name);
+}
+
+static std::unique_ptr<GeoShape> decode_geo_shape(StringRef value, const
DataTypePtr& type) {
+ if (!is_spatial_type(type)) {
+ return GeoShape::from_encoded(value.data, value.size);
+ }
+
+ GeoParseStatus status;
+ auto shape = GeoShape::from_wkb_bytes(value.data, value.size, status);
+ return status == GeoParseStatus::GEO_PARSE_OK ? std::move(shape) : nullptr;
+}
+
+static bool any_geo_argument_is_null(const Block& block, const ColumnNumbers&
arguments,
+ size_t row) {
+ return std::ranges::any_of(arguments, [&](auto argument) {
+ return block.get_by_position(argument).column->is_null_at(row);
+ });
+}
+
+static Status decode_calculation_shape(StringRef value, const DataTypePtr&
type,
+ std::unique_ptr<GeoShape>* shape,
+ const char* function_name) {
+ *shape = decode_geo_shape(value, type);
+ // POLYGON EMPTY decodes successfully, but distance helpers retain
infinity/max sentinels.
+ // Reject it for typed calculations just like empty points/lines, leaving
raw WKB untouched.
+ if (is_spatial_type(type) &&
+ (*shape == nullptr ||
+ ((*shape)->type() == GeoShapeType::GEO_SHAPE_POLYGON &&
+ assert_cast<const GeoPolygon&>(**shape).polygon()->num_loops() ==
0))) {
+ return Status::NotSupported("Function {} does not support this
Geography WKB shape",
+ function_name);
+ }
+ return Status::OK();
+}
+
+// Calculations use canonical positions; raw spatial accessors still return
the original WKB.
+static S2Point canonical_geography_point(const S2Point& point) {
+ const auto coordinate = S2LatLng(point);
+ const double latitude = coordinate.lat().radians();
+ // All longitudes at a pole and +/-180 at the date line denote the same
position.
+ if (std::abs(latitude) == M_PI_2) {
+ return {0, 0, latitude > 0 ? 1.0 : -1.0};
+ }
+ if (coordinate.lng().radians() == M_PI) {
+ // Flip only sin(longitude): rebuilding latitude can change its last
bit.
+ return {point.x(), -point.y(), point.z()};
+ }
+ return point;
+}
+
+// S2 predicates operate on exact rounded vectors. Splitting a meridian at a
new latitude
+// can otherwise turn one common edge into two almost-coplanar curves. Node
only edges whose
+// shared great circle is proved by original WKB ordinates; never snap nearby
parallel curves.
+class GeographyEdgeNodes {
+public:
+ GeographyEdgeNodes(StringRef left, StringRef right) {
+ GeoCoordinateList coordinates;
+ const auto left_status = WkbParse::xy_coordinates(left.data,
left.size, &coordinates);
+ const auto right_status = WkbParse::xy_coordinates(right.data,
right.size, &coordinates);
+ DCHECK_EQ(left_status, GeoParseStatus::GEO_PARSE_OK);
+ DCHECK_EQ(right_status, GeoParseStatus::GEO_PARSE_OK);
+ for (auto coordinate : coordinates.list) {
+ if (coordinate.x == 180) {
+ coordinate.x = -180;
+ }
+ const auto point = canonical_geography_point(
+ S2LatLng::FromDegrees(coordinate.y,
coordinate.x).ToPoint());
+ _coordinates.emplace(point, coordinate);
+ _vertex_index.Add(point, 0);
+ if (std::abs(coordinate.y) != 90) {
+ _meridians[meridian(coordinate.x)][coordinate.x < 0].push_back(
+ {coordinate.y, point});
+ }
+ }
+ for (auto& entry : _meridians) {
+ for (auto& side : entry.second) {
+ std::ranges::sort(side, {}, &Vertex::latitude);
+ const auto duplicate = std::ranges::unique(side, {},
&Vertex::point);
+ side.erase(duplicate.begin(), duplicate.end());
+ }
+ }
+ }
+
+ void append_edge(const S2Point& raw_start, const S2Point& raw_end,
+ std::vector<S2Point>* output) const {
+ const auto start = canonical_geography_point(raw_start);
+ const auto end = canonical_geography_point(raw_end);
+ output->push_back(start);
+ // Different raw longitudes at one pole denote the same calculation
point.
+ if (start == end) {
+ return;
+ }
+ const auto& a = _coordinates.at(start);
+ const auto& b = _coordinates.at(end);
+ const bool a_pole = std::abs(a.y) == 90;
+ const bool b_pole = std::abs(b.y) == 90;
+ const auto key = meridian(a_pole ? b.x : a.x);
+ const bool common_meridian = a_pole || b_pole || key == meridian(b.x);
+ // Exact antipodes have no unique edge; rounded S2 vectors can conceal
this ambiguity.
+ if (a.y == -b.y && common_meridian && ((a_pole && b_pole) || (a.x < 0)
!= (b.x < 0))) {
+ throw Exception(ErrorCode::NOT_IMPLEMENTED_ERROR,
+ "Geography calculations do not support antipodal
edges");
+ }
+ if (common_meridian) {
+ const bool a_side = (a_pole ? b.x : a.x) < 0;
+ const bool b_side = (b_pole ? a.x : b.x) < 0;
+ const auto& group = _meridians.at(key);
+ if (a_side == b_side) {
+ append_range(group[a_side], a.y, b.y, output);
+ } else {
+ // (10,80) -> (-170,80) passes through the north pole. Keep
latitude
+ // as-is on each side: computing 180-latitude would erase tiny
ordinates.
+ const double latitude = a.y > -b.y ? 90 : -90;
+ append_range(group[a_side], a.y, latitude, output);
+ output->emplace_back(0, 0, latitude > 0 ? 1 : -1);
+ append_range(group[b_side], latitude, b.y, output);
+ }
+ return;
+ }
+ // For other great circles, including the equator, node only exact
coplanar
+ // points on the edge. The query radius selects candidates, never
equality.
+ S2ClosestPointQuery<int> query(&_vertex_index);
+ query.mutable_options()->set_conservative_max_distance(
+ S1Angle::Radians(8 * std::numeric_limits<double>::epsilon()));
+ S2ClosestPointQuery<int>::EdgeTarget target(start, end);
+ std::vector<S2Point> on_edge;
+ for (const auto& candidate : query.FindClosestPoints(&target)) {
+ const auto& point = candidate.point();
+ if (point != start && point != end &&
+ s2pred::ExpensiveSign(start, end, point, false) == 0 &&
Review Comment:
[P2] Preserve non-meridian geodesic subdivisions in topology predicates
For default GEOGRAPHY values A=`LINESTRING(45 -45,135 45)` and
B=`LINESTRING(45 -45,90 0,135 45)` imported as WKB, `(90,0)` is exactly the
midpoint of the same 120-degree great-circle arc. However, `ToPoint()` rounds
the three vectors independently: their determinant is about `-3.52e-17`, so
this exact-coplanarity condition rejects the midpoint and A is never noded to
match B. `ST_Equals(A,B)`, mutual `ST_Contains`, and `ST_Within` consequently
return false. These are valid nonempty XY lines with no antipodal edge. S2's
[Boolean-output
path](https://github.com/google/s2geometry/blob/v0.10.0/src/s2/s2boolean_operation.cc#L2570-L2577)
computes the exact predicate before constructing its builder, so builder
snapping cannot repair this. The conversion-distance fallback below only
repairs Intersects/Disjoint; the prior point-membership and intersection
threads do not cover these new Equals/nonpoint-containment paths. Preserve
shared non-meridian arcs under coordinate conversion without
merging nearby distinct curves, and add rotated subdivision tests for
Equals/Contains/Within plus nearby controls. This result follows from static
code and exact arithmetic, not a Doris runtime execution.
##########
be/src/exprs/function/geo/functions_geo.cpp:
##########
@@ -18,32 +18,634 @@
#include "exprs/function/geo/functions_geo.h"
#include <glog/logging.h>
+#include <s2/mutable_s2shape_index.h>
+#include <s2/s2boolean_operation.h>
+#include <s2/s2builderutil_s2polyline_vector_layer.h>
+#include <s2/s2closest_edge_query.h>
+#include <s2/s2closest_point_query.h>
+#include <s2/s2earth.h>
+#include <s2/s2lax_polygon_shape.h>
+#include <s2/s2lax_polyline_shape.h>
+#include <s2/s2loop.h>
#include <s2/s2point.h>
+#include <s2/s2point_index.h>
+#include <s2/s2point_vector_shape.h>
+#include <s2/s2polygon.h>
+#include <s2/s2polyline.h>
+#include <s2/s2predicates.h>
#include <algorithm>
+#include <array>
#include <boost/iterator/iterator_facade.hpp>
+#include <cmath>
+#include <cstdint>
+#include <limits>
+#include <map>
+#include <optional>
#include <utility>
+#include "common/cast_set.h"
#include "common/compiler_util.h"
#include "core/assert_cast.h"
#include "core/block/block.h"
#include "core/block/column_with_type_and_name.h"
#include "core/column/column.h"
#include "core/column/column_array.h"
#include "core/column/column_execute_util.h"
+#include "core/column/column_map.h"
#include "core/column/column_nullable.h"
+#include "core/column/column_struct.h"
+#include "core/column/column_varbinary.h"
#include "core/data_type/data_type_array.h"
+#include "core/data_type/data_type_map.h"
#include "core/data_type/data_type_nullable.h"
#include "core/data_type/data_type_number.h"
+#include "core/data_type/data_type_spatial.h"
#include "core/data_type/data_type_string.h"
+#include "core/data_type/data_type_struct.h"
+#include "core/data_type/data_type_varbinary.h"
#include "core/data_type/define_primitive_type.h"
#include "core/string_ref.h"
#include "exprs/function/geo/geo_common.h"
#include "exprs/function/geo/geo_types.h"
+#include "exprs/function/geo/wkb_parse.h"
#include "exprs/function/simple_function_factory.h"
+#include "exprs/function/string_hex_util.h"
namespace doris {
+static bool is_spatial_type(const DataTypePtr& type) {
+ const auto primitive_type = remove_nullable(type)->get_primitive_type();
+ return primitive_type == TYPE_GEOMETRY || primitive_type == TYPE_GEOGRAPHY;
+}
+
+static Status validate_geography_semantics(const DataTypePtr& type, const
char* function_name) {
+ const auto& nested_type = remove_nullable(type);
+ if (!is_spatial_type(nested_type)) {
+ return Status::OK();
+ }
+
+ const auto* spatial_type = dynamic_cast<const
DataTypeSpatial*>(nested_type.get());
+ DCHECK(spatial_type != nullptr);
+ if (spatial_type->get_primitive_type() == TYPE_GEOGRAPHY &&
+ spatial_type->crs() == "OGC:CRS84" && spatial_type->algorithm() ==
"spherical") {
+ return Status::OK();
+ }
+ return Status::NotSupported(
+ "Function {} requires GEOGRAPHY(OGC:CRS84, spherical) for spatial
inputs",
+ function_name);
+}
+
+static std::unique_ptr<GeoShape> decode_geo_shape(StringRef value, const
DataTypePtr& type) {
+ if (!is_spatial_type(type)) {
+ return GeoShape::from_encoded(value.data, value.size);
+ }
+
+ GeoParseStatus status;
+ auto shape = GeoShape::from_wkb_bytes(value.data, value.size, status);
+ return status == GeoParseStatus::GEO_PARSE_OK ? std::move(shape) : nullptr;
+}
+
+static bool any_geo_argument_is_null(const Block& block, const ColumnNumbers&
arguments,
+ size_t row) {
+ return std::ranges::any_of(arguments, [&](auto argument) {
+ return block.get_by_position(argument).column->is_null_at(row);
+ });
+}
+
+static Status decode_calculation_shape(StringRef value, const DataTypePtr&
type,
+ std::unique_ptr<GeoShape>* shape,
+ const char* function_name) {
+ *shape = decode_geo_shape(value, type);
+ // POLYGON EMPTY decodes successfully, but distance helpers retain
infinity/max sentinels.
+ // Reject it for typed calculations just like empty points/lines, leaving
raw WKB untouched.
+ if (is_spatial_type(type) &&
+ (*shape == nullptr ||
+ ((*shape)->type() == GeoShapeType::GEO_SHAPE_POLYGON &&
+ assert_cast<const GeoPolygon&>(**shape).polygon()->num_loops() ==
0))) {
+ return Status::NotSupported("Function {} does not support this
Geography WKB shape",
+ function_name);
+ }
+ return Status::OK();
+}
+
+// Calculations use canonical positions; raw spatial accessors still return
the original WKB.
+static S2Point canonical_geography_point(const S2Point& point) {
+ const auto coordinate = S2LatLng(point);
+ const double latitude = coordinate.lat().radians();
+ // All longitudes at a pole and +/-180 at the date line denote the same
position.
+ if (std::abs(latitude) == M_PI_2) {
+ return {0, 0, latitude > 0 ? 1.0 : -1.0};
+ }
+ if (coordinate.lng().radians() == M_PI) {
+ // Flip only sin(longitude): rebuilding latitude can change its last
bit.
+ return {point.x(), -point.y(), point.z()};
+ }
+ return point;
+}
+
+// S2 predicates operate on exact rounded vectors. Splitting a meridian at a
new latitude
+// can otherwise turn one common edge into two almost-coplanar curves. Node
only edges whose
+// shared great circle is proved by original WKB ordinates; never snap nearby
parallel curves.
+class GeographyEdgeNodes {
+public:
+ GeographyEdgeNodes(StringRef left, StringRef right) {
+ GeoCoordinateList coordinates;
+ const auto left_status = WkbParse::xy_coordinates(left.data,
left.size, &coordinates);
+ const auto right_status = WkbParse::xy_coordinates(right.data,
right.size, &coordinates);
+ DCHECK_EQ(left_status, GeoParseStatus::GEO_PARSE_OK);
+ DCHECK_EQ(right_status, GeoParseStatus::GEO_PARSE_OK);
+ for (auto coordinate : coordinates.list) {
+ if (coordinate.x == 180) {
+ coordinate.x = -180;
+ }
+ const auto point = canonical_geography_point(
+ S2LatLng::FromDegrees(coordinate.y,
coordinate.x).ToPoint());
+ _coordinates.emplace(point, coordinate);
+ _vertex_index.Add(point, 0);
+ if (std::abs(coordinate.y) != 90) {
+ _meridians[meridian(coordinate.x)][coordinate.x < 0].push_back(
+ {coordinate.y, point});
+ }
+ }
+ for (auto& entry : _meridians) {
+ for (auto& side : entry.second) {
+ std::ranges::sort(side, {}, &Vertex::latitude);
+ const auto duplicate = std::ranges::unique(side, {},
&Vertex::point);
+ side.erase(duplicate.begin(), duplicate.end());
+ }
+ }
+ }
+
+ void append_edge(const S2Point& raw_start, const S2Point& raw_end,
+ std::vector<S2Point>* output) const {
+ const auto start = canonical_geography_point(raw_start);
+ const auto end = canonical_geography_point(raw_end);
+ output->push_back(start);
+ // Different raw longitudes at one pole denote the same calculation
point.
+ if (start == end) {
+ return;
+ }
+ const auto& a = _coordinates.at(start);
+ const auto& b = _coordinates.at(end);
+ const bool a_pole = std::abs(a.y) == 90;
+ const bool b_pole = std::abs(b.y) == 90;
+ const auto key = meridian(a_pole ? b.x : a.x);
+ const bool common_meridian = a_pole || b_pole || key == meridian(b.x);
+ // Exact antipodes have no unique edge; rounded S2 vectors can conceal
this ambiguity.
+ if (a.y == -b.y && common_meridian && ((a_pole && b_pole) || (a.x < 0)
!= (b.x < 0))) {
+ throw Exception(ErrorCode::NOT_IMPLEMENTED_ERROR,
+ "Geography calculations do not support antipodal
edges");
+ }
+ if (common_meridian) {
+ const bool a_side = (a_pole ? b.x : a.x) < 0;
+ const bool b_side = (b_pole ? a.x : b.x) < 0;
+ const auto& group = _meridians.at(key);
+ if (a_side == b_side) {
+ append_range(group[a_side], a.y, b.y, output);
+ } else {
+ // (10,80) -> (-170,80) passes through the north pole. Keep
latitude
+ // as-is on each side: computing 180-latitude would erase tiny
ordinates.
+ const double latitude = a.y > -b.y ? 90 : -90;
+ append_range(group[a_side], a.y, latitude, output);
+ output->emplace_back(0, 0, latitude > 0 ? 1 : -1);
+ append_range(group[b_side], latitude, b.y, output);
+ }
+ return;
+ }
+ // For other great circles, including the equator, node only exact
coplanar
+ // points on the edge. The query radius selects candidates, never
equality.
+ S2ClosestPointQuery<int> query(&_vertex_index);
+ query.mutable_options()->set_conservative_max_distance(
+ S1Angle::Radians(8 * std::numeric_limits<double>::epsilon()));
+ S2ClosestPointQuery<int>::EdgeTarget target(start, end);
+ std::vector<S2Point> on_edge;
+ for (const auto& candidate : query.FindClosestPoints(&target)) {
+ const auto& point = candidate.point();
+ if (point != start && point != end &&
+ s2pred::ExpensiveSign(start, end, point, false) == 0 &&
+ s2pred::CompareEdgeDistance(point, start, end,
S1ChordAngle::Zero()) <= 0) {
+ on_edge.push_back(point);
+ }
+ }
+ // Rounded angles can tie near the middle of a long edge, reversing
adjacent nodes.
+ std::ranges::sort(on_edge, [&](const S2Point& a, const S2Point& b) {
+ return s2pred::CompareDistances(start, a, b) < 0;
+ });
+ for (const auto& point : on_edge) {
+ if (output->back() != point) {
+ output->push_back(point);
+ }
+ }
+ }
+
+private:
+ struct Vertex {
+ double latitude;
+ S2Point point;
+ };
+ static std::pair<double, double> meridian(double longitude) {
+ if (longitude >= 0) {
+ return {longitude, 0};
+ }
+ // Error-free TwoSum: -1e-15+180 and -2e-15+180 both round to 180.
+ // Retain the residual so distinct original meridians never become one.
+ const double high = longitude + 180;
+ const double offset = high - 180;
+ const double low = (180 - (high - offset)) + (longitude - offset);
+ return {high, low};
+ }
+ static void append_range(const std::vector<Vertex>& vertices, double from,
double to,
+ std::vector<S2Point>* output) {
+ if (from == to) {
+ return;
+ }
+ auto first = std::ranges::upper_bound(vertices, std::min(from, to),
{}, &Vertex::latitude);
+ auto last = std::ranges::lower_bound(vertices, std::max(from, to), {},
&Vertex::latitude);
+ if (from < to) {
+ for (auto it = first; it != last; ++it) {
+ output->push_back(it->point);
Review Comment:
[P2] Avoid quadratic scratch expansion when noding meridian edges
Every common-meridian edge copies all vertices inside its latitude range.
Let A have N edges alternating `(10,0)` and `(10,1)`, and B have M distinct
vertices `(10,i/(M+1))`, `i=1..M`. These are admitted default-Geography lines:
only adjacent duplicates are removed, and [S2
validation](https://github.com/google/s2geometry/blob/v0.10.0/src/s2/s2polyline.cc#L160-L182)
permits retracing. Each A edge appends all M B vertices, so
`geography_shape_index` materializes `N*(M+1)+1` points before even
`ST_Intersects(A,B)` can return true. At N=M=10,000, the two WKB values total
320,034 bytes but the first vector alone contains 100,010,001 S2Points (at
least 2.4 GB of payload); larger valid lines quickly exhaust memory.
Deduplicating the candidate list does not reduce these repeated per-edge
copies. Avoid the full cross-product materialization and bound/account scratch
before allocating it; add retraced-line versus dense-collinear-line complexity
coverage. Counts are derived statically; no O
OM experiment or benchmark was run.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]