Skip to content
Merged
Changes from 1 commit
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
81 changes: 60 additions & 21 deletions rust/geodatafusion/src/udf/native/accessors/is_empty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ impl ScalarUDFImpl for IsEmpty {
Some(DOCUMENTATION.get_or_init(|| {
Documentation::builder(
DOC_SECTION_OTHER,
"Tests if a geometry is empty. ST_IsEmpty(NULL) is NULL.",
"Tests if a geometry is topologically empty. \
Multi-geometries and GeometryCollections where every leaf is empty \
(e.g. GEOMETRYCOLLECTION(POINT EMPTY, POLYGON EMPTY) are reported empty. \
ST_IsEmpty(NULL) is NULL.",
"ST_IsEmpty(geom)",
)
.with_argument("geom", "geometry")
Expand All @@ -70,6 +73,38 @@ impl ScalarUDFImpl for IsEmpty {
}
}

/// Recursive emptiness predicate.
///
/// This matches the behavior of PostGIS and JTS.
/// As a cross-reference, the javadoc for JTS `Geometry.isEmpty()` reads as follows:
/// "the emptiness test checks for topological emptiness,
/// not structural emptiness. A collection containing only empty elements is reported as empty."
pub(crate) fn is_geometry_topologically_empty(geom: &impl GeometryTrait<T = f64>) -> bool {
match geom.as_type() {
geo_traits::GeometryType::Point(p) => p.coord().is_none(),
geo_traits::GeometryType::LineString(ls) => ls.num_coords() == 0,
geo_traits::GeometryType::Polygon(p) => {
p.exterior().is_none_or(|ring| ring.num_coords() == 0)
}
geo_traits::GeometryType::MultiPoint(mp) => mp.points().all(|p| p.coord().is_none()),
geo_traits::GeometryType::MultiLineString(mls) => {
mls.line_strings().all(|ls| ls.num_coords() == 0)
}
geo_traits::GeometryType::MultiPolygon(mp) => mp
.polygons()
.all(|p| p.exterior().is_none_or(|ring| ring.num_coords() == 0)),
geo_traits::GeometryType::GeometryCollection(gc) => gc
.geometries()
.all(|child| is_geometry_topologically_empty(&child)),
// Rect/Triangle/Line always carry coordinates and so are never empty.
// NB: geoarrow has no curved-geometry support, so e.g. CIRCULARSTRING EMPTY
// from the PostGIS docs is not representable at the moment, but we can still model them.
geo_traits::GeometryType::Rect(_)
| geo_traits::GeometryType::Triangle(_)
| geo_traits::GeometryType::Line(_) => false,
}
}

fn is_empty_impl(args: ScalarFunctionArgs) -> GeoDataFusionResult<ColumnarValue> {
let array = ColumnarValue::values_to_arrays(&args.args)?
.into_iter()
Expand All @@ -92,28 +127,11 @@ fn impl_is_empty<'a>(
for item in array.iter() {
match item {
// A present geometry.
// Emptiness is the absence of coordinates,
// which is distinct from a SQL NULL (handled below).
// Emptiness is recursive so that it's topological rather than just the structure.
// so a collection where every leaf is empty counts as empty itself.
Some(geom) => {
let geom = geom?;
let is_empty = match geom.as_type() {
geo_traits::GeometryType::Point(p) => p.coord().is_none(),
geo_traits::GeometryType::LineString(ls) => ls.num_coords() == 0,
geo_traits::GeometryType::Polygon(p) => {
p.exterior().is_none_or(|ring| ring.num_coords() == 0)
}
geo_traits::GeometryType::MultiPoint(mp) => mp.num_points() == 0,
geo_traits::GeometryType::MultiLineString(mls) => mls.num_line_strings() == 0,
geo_traits::GeometryType::MultiPolygon(mp) => mp.num_polygons() == 0,
geo_traits::GeometryType::GeometryCollection(gc) => gc.num_geometries() == 0,
// Rect/Triangle/Line always carry coordinates and so are never empty.
// (geoarrow has no curved-geometry support, so e.g. CIRCULARSTRING EMPTY
// from the PostGIS docs is not representable here.)
geo_traits::GeometryType::Rect(_)
| geo_traits::GeometryType::Triangle(_)
| geo_traits::GeometryType::Line(_) => false,
};
builder.append_value(is_empty);
builder.append_value(is_geometry_topologically_empty(&geom));
}
// SQL NULL in, SQL NULL out.
// This matches the PostGIS behavior,
Expand Down Expand Up @@ -187,6 +205,27 @@ mod test {
false,
"non-empty geometry collection",
),
// Recursive emptiness (PostGIS / JTS topological semantics).
// A collection whose every leaf is empty is itself reported as empty,
// even though it has structurally non-zero children.
//
// NB: GeoArrow explicitly denies recursive collections,
// so we only need to test one level.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It sounds like this only applies to GeometryCollection? Can any other supported geometry types have an empty child?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question... so in OGC terms, no, it actually applies to everything. MultiPoints can have a mix of empty and non-empty points, etc... My quick reading of the GeoArrow spec also seems to allow this. However, when writing up some extra tests for those, I discovered that the WKT parser we depend on only accepts mixed empty/non-empty multi-linestrings (georust/wkt#111 seems to acknowledge this, but the symptom I see right now isn't a crash but a parse error). And multipolygons mixing empty and non-empty seem to crash, but those should also technically be allowed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could have a few WKB fixtures for this (this is what we do in SedonaDB). geoarrow.pyarrow's WKT parser should be able to generate these for you.

https://github.com/apache/sedona-db/blob/298911255f91e0d55146a8baf8e9b49aad404b35/rust/sedona-testing/src/fixtures.rs#L22-L29

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call! I generated them with PostGIS ST_AsBinary and interpolated the literal, but same idea.

The multipolygon case I came up with is still broken due to an assumption in geoarrow-array. I'm not sure if there is a geoarrow rule forbidding that structure, but it works in PostGIS 🤷

(
"GEOMETRYCOLLECTION(POINT EMPTY, POLYGON EMPTY)",
true,
"collection of all-empty atomic children is recursively empty",
),
(
"GEOMETRYCOLLECTION(MULTIPOINT EMPTY, MULTIPOLYGON EMPTY)",
true,
"collection of all-empty multi-children is recursively empty",
),
(
"GEOMETRYCOLLECTION(POINT(0 0), POLYGON EMPTY)",
false,
"collection with at least one non-empty child is non-empty",
),
];

for (wkt, expected, description) in cases {
Expand Down
Loading