Skip to content

Conversation

@joonaspessi
Copy link
Contributor

@joonaspessi joonaspessi commented Oct 19, 2025

Hello!

Thought that implementing something relatively simple from your first issue proposal would be a good way to understand how sedona-db works. Thanks for creating this project.

This PR implements ST_IsRing with GEOS.

I did some research on how other spatial libraries handle this function when a non-LineString geometry is passed:

System Non-LineString Behavior
DuckDB spatial extensions Returns false
Apache Sedona Returns null
PostGIS Throws error
This PR Returns false

I chose to follow the original Apache Sedona rather than PostGIS because it matches the existing Sedona ecosystem.

Copy link
Collaborator

@petern48 petern48 left a comment

Choose a reason for hiding this comment

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

Welcome! I left a few comments, but it looks good so far. Just need to agree on which behavior to follow.

Comment on lines 71 to 90
// Check if geometry is a LineString
let geom_type = geos_geom.geometry_type();

// ST_IsRing only applies to LineStrings - return false for other types
// This matches DuckDB spatial extension and Apache Sedona behavior
if geom_type != GeometryTypes::LineString {
return Ok(false);
}

// Check if the LineString is closed
let is_closed = geos_geom.is_closed().map_err(|e| {
DataFusionError::Execution(format!("Failed to check if geometry is closed: {e}"))
})?;

// Check if the LineString is simple (no self-intersections)
let is_simple = geos_geom.is_simple().map_err(|e| {
DataFusionError::Execution(format!("Failed to check if geometry is simple: {e}"))
})?;

Ok(is_closed && is_simple)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// Check if geometry is a LineString
let geom_type = geos_geom.geometry_type();
// ST_IsRing only applies to LineStrings - return false for other types
// This matches DuckDB spatial extension and Apache Sedona behavior
if geom_type != GeometryTypes::LineString {
return Ok(false);
}
// Check if the LineString is closed
let is_closed = geos_geom.is_closed().map_err(|e| {
DataFusionError::Execution(format!("Failed to check if geometry is closed: {e}"))
})?;
// Check if the LineString is simple (no self-intersections)
let is_simple = geos_geom.is_simple().map_err(|e| {
DataFusionError::Execution(format!("Failed to check if geometry is simple: {e}"))
})?;
Ok(is_closed && is_simple)
Ok(geos_geom.is_ring().map_err(|e| {
DataFusionError::Execution(format!("Failed to check if geometry is a ring: {e}"))
})?)

We can actually simplify this code to simply call geos's .is_ring() method here. Seems to pass everything when I tried this locally.

("LINESTRING(2 0, 2 2, 3 3)", False),
("LINESTRING(0 0, 2 2)", False),
# Empty LineString
("LINESTRING EMPTY", False),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
("LINESTRING EMPTY", False),
("LINESTRING EMPTY", False),
# Collections of linestrings that would have been considered rings
("MULTILINESTRING((0 0, 0 1, 1 1, 1 0, 0 0))", False),
("GEOMETRYCOLLECTION(LINESTRING(0 0, 0 1, 1 1, 1 0, 0 0))", False),

Let's add these two cases. Returning False is the natural behavior for the geos's is_ring() method, which agrees with DuckDB

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense. Added these under test_st_isring_non_linestring as PostGis is throwing error on these.

@petern48
Copy link
Collaborator

I personally agree with the proposed behavior. PostGIS's implementation is odd imo. For non-linestrings, it returns ERROR: ST_IsRing() should only be called on a linear feature if it's non-empty, but returns False if it's empty. The thing I don't like about this is that if you have any non-empty non-linestring geometry in your column, your query will just fail.

CREATE TABLE geom (geom GEOMETRY);
INSERT INTO geom VALUES (ST_GeomFromText('LINESTRING EMPTY')), (ST_Point(3, 4));
SELECT st_isring(geom) FROM geom;
-- ERROR:  ST_IsRing() should only be called on a linear feature

DuckDB returns False instead of the error, which agrees with the natural behavior of the is_ring() function. Sedona actually returns NULL for any non-linestring geometry (empty or not).

WDYT @paleolimbot?

Copy link
Member

@paleolimbot paleolimbot left a comment

Choose a reason for hiding this comment

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

Thank you!

DuckDB returns False instead of the error, which agrees with the natural behavior of the is_ring() function. Sedona actually returns NULL for any non-linestring geometry (empty or not).

I think we should follow PostGIS here and file a bug in Sedona-spark to discuss if that decision was intentional. If a user is trying to check the ring status of something that isn't even the correct geometry type there is likely an error in the query or an error in the data.

In the next DataFusion release we'll have the ability to pipe options into scalar functions and so we can do things like enable a Sedona-spark compatibility mode. Here I think PostGIS compatibility is the right choice because it's the easiest to test and review (and in general has more intentional corner case behaviour.

@jiayuasu
Copy link
Member

Fine by me. PostGIS compatibility is more important

@joonaspessi
Copy link
Contributor Author

Thanks for the comments. I think the PostGis compatibility is a good choice to follow. Updated the PR to follow that approach. While doing it, also noticed that for empty geometries, PostGis will return false.

Copy link
Member

@paleolimbot paleolimbot left a comment

Choose a reason for hiding this comment

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

Just one nit on the exception check (which should also fix the pre-commit)...thank you!

Comment on lines 714 to 721
with pytest.raises(Exception) as exc_info:
eng.assert_query_result(f"SELECT ST_IsRing(ST_GeomFromText('{geom}'))", None)

# Verify error message contains the expected text
error_msg = str(exc_info.value).lower()
assert (
"linear" in error_msg or "linestring" in error_msg
), f"Expected error about linear feature, got: {exc_info.value}"
Copy link
Member

Choose a reason for hiding this comment

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

I think this will work:

Suggested change
with pytest.raises(Exception) as exc_info:
eng.assert_query_result(f"SELECT ST_IsRing(ST_GeomFromText('{geom}'))", None)
# Verify error message contains the expected text
error_msg = str(exc_info.value).lower()
assert (
"linear" in error_msg or "linestring" in error_msg
), f"Expected error about linear feature, got: {exc_info.value}"
with pytest.raises(Exception, match="linear|linestring") as exc_info:
eng.assert_query_result(f"SELECT ST_IsRing(ST_GeomFromText('{geom}'))", None)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea! Updated to cleanly utilize pytest match pattern. Pre-commit passes locally

Copy link
Member

@paleolimbot paleolimbot left a comment

Choose a reason for hiding this comment

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

Thank you!

@paleolimbot paleolimbot changed the title implement ST_IsRing feat(c/sedona-geos): Implement ST_IsRing Oct 21, 2025
@paleolimbot paleolimbot merged commit c17260b into apache:main Oct 21, 2025
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants