Skip to content

[GH-2926] Add ST_BoxIntersects and ST_BoxContains for Box2D#2932

Merged
jiayuasu merged 2 commits intoapache:masterfrom
jiayuasu:feature/box2d-predicates
May 8, 2026
Merged

[GH-2926] Add ST_BoxIntersects and ST_BoxContains for Box2D#2932
jiayuasu merged 2 commits intoapache:masterfrom
jiayuasu:feature/box2d-predicates

Conversation

@jiayuasu
Copy link
Copy Markdown
Member

@jiayuasu jiayuasu commented May 8, 2026

Did you read the Contributor Guide?

Is this PR related to a ticket?

What changes were proposed in this PR?

Adds two planar bbox predicates that operate on Box2D arguments:

  • ST_BoxIntersects(a: Box2D, b: Box2D) -> Boolean — true if the two bboxes share any point on either axis. Mirrors PostGIS && on box2d.
  • ST_BoxContains(a: Box2D, b: Box2D) -> Boolean — true if a fully contains b. Mirrors PostGIS ~ on box2d.

Both use closed intervals, matching PostGIS semantics — edge-touching and corner-touching count as intersection; equal boxes contain each other. NULL on null input.

Where the changes land

Layer Change
common/.../Predicates.java Two new helpers boxIntersects(Box2D, Box2D) and boxContains(Box2D, Box2D)
spark/common/.../expressions/Predicates.scala Two new ST_BoxIntersects / ST_BoxContains case classes
spark/common/.../UDF/Catalog.scala Register both in predicateExprs
spark/common/.../expressions/st_predicates.scala Scala DataFrame API wrappers (Column + String overloads)
python/sedona/spark/sql/st_predicates.py PySpark wrappers
flink/.../expressions/Predicates.java Two new ScalarFunction classes
flink/.../Catalog.java Register both

How was this patch tested?

  • common/.../PredicatesTest.javatestBoxIntersects (full overlap, partial overlap, edge-touching, corner-touching, disjoint X, disjoint Y) and testBoxContains (inside, equal, smaller-inside, outside, crosses-boundary).
  • spark/common/.../predicateTestScala.scala — "Passed ST_BoxIntersects and ST_BoxContains" — full SQL-level coverage of both predicates including NULL propagation.
  • flink/.../PredicateTest.javatestBoxIntersects and testBoxContains through Flink Table API.
  • python/tests/sql/test_predicate.pytest_st_box_intersects_and_contains through PySpark SQL.

What's not in scope

  • Mixed Box2D / Geometry predicates. Users can wrap geometry inputs with ST_Box2D(geom) first.
  • Spatial join planner integration that uses Box2D predicates for partitioning. That's a separate optimizer change.
  • 3D box predicates — wait for Box3D (Add Box3D type and 3D bbox SQL surface #2928).

Did this PR include necessary documentation updates?

  • No, this PR does not affect any public SQL API documentation surface in isolation. Documentation lands as part of the consolidated Phase 1+2 Box2D docs update once the remaining deferred follow-ups are scoped.

Two planar bbox predicates on Box2D arguments:

  ST_BoxIntersects(a: Box2D, b: Box2D) -> Boolean
  ST_BoxContains(a: Box2D, b: Box2D) -> Boolean

Both use closed intervals, matching PostGIS && and ~ operators on
box2d. NULL on null input.

JVM, Python, Flink wrappers all updated. New case classes in
spark-common Predicates.scala registered in Catalog. Scala DataFrame
API wrappers in st_predicates. Flink ScalarFunction subclasses
registered in the Flink Catalog.

Closes apache#2926.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds two Box2D-based planar bounding-box predicates (ST_BoxIntersects, ST_BoxContains) across Sedona’s common layer and exposes them consistently via Spark SQL/DataFrame APIs, PySpark wrappers, and Flink Table API, with corresponding cross-language tests.

Changes:

  • Introduces Predicates.boxIntersects(Box2D, Box2D) and Predicates.boxContains(Box2D, Box2D) in common.
  • Adds Spark SQL expressions + Catalog registration + Scala DataFrame API wrappers and PySpark wrappers.
  • Adds Flink ScalarFunctions + Catalog registration and test coverage across common/Spark/Flink/Python.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
common/src/main/java/org/apache/sedona/common/Predicates.java Adds core Box2D predicate implementations used by higher-level APIs.
common/src/test/java/org/apache/sedona/common/PredicatesTest.java Adds unit tests for the new common-layer Box2D predicates.
spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Predicates.scala Adds Spark Catalyst expressions for the new Box2D predicates.
spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala Registers the new predicates as Spark SQL functions.
spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_predicates.scala Adds Scala DataFrame API wrappers for the new predicates.
spark/common/src/test/scala/org/apache/sedona/sql/predicateTestScala.scala Adds Spark SQL-level tests (including NULL propagation) for the new predicates.
python/sedona/spark/sql/st_predicates.py Adds PySpark wrapper functions for ST_BoxIntersects / ST_BoxContains.
python/tests/sql/test_predicate.py Adds PySpark SQL test coverage for the new predicates.
flink/src/main/java/org/apache/sedona/flink/expressions/Predicates.java Adds Flink ScalarFunctions for the new Box2D predicates.
flink/src/main/java/org/apache/sedona/flink/Catalog.java Registers the new Flink predicate functions.
flink/src/test/java/org/apache/sedona/flink/PredicateTest.java Adds Flink Table API tests for the new predicates.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +36 to +40
public static boolean boxIntersects(Box2D a, Box2D b) {
return !(a.getXMax() < b.getXMin()
|| a.getXMin() > b.getXMax()
|| a.getYMax() < b.getYMin()
|| a.getYMin() > b.getYMax());
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 6726b1b — went with option (a). boxIntersects/boxContains now call requireOrderedPlanarBox and throw IllegalArgumentException with a clear message on inverted bounds. Defining wraparound semantics on a planar predicate without the geography work in #2929 would be premature; failing fast is the honest contract for now.

Comment on lines +48 to +52
public static boolean boxContains(Box2D a, Box2D b) {
return a.getXMin() <= b.getXMin()
&& a.getYMin() <= b.getYMin()
&& a.getXMax() >= b.getXMax()
&& a.getYMax() >= b.getYMax();
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same fix in 6726b1bboxContains now validates ordered bounds before evaluating containment.

Comment on lines +32 to +34
* Closed-interval bbox intersection: true if {@code a} and {@code b} share any point on either
* axis (matches PostGIS {@code &&} on box2d). Either argument being null returns null at the SQL
* layer; this Java entry point throws {@link NullPointerException} on null input.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reworded in 6726b1b: now says overlap on both the X and Y axes.

Comment on lines +36 to +52
@Test
public void testBoxIntersects() {
Box2D a = new Box2D(0.0, 0.0, 5.0, 5.0);

// Full overlap
assertTrue(Predicates.boxIntersects(a, new Box2D(1.0, 1.0, 2.0, 2.0)));
// Partial overlap
assertTrue(Predicates.boxIntersects(a, new Box2D(3.0, 3.0, 7.0, 7.0)));
// Edge-touching (closed intervals)
assertTrue(Predicates.boxIntersects(a, new Box2D(5.0, 0.0, 10.0, 5.0)));
// Corner-touching (closed intervals)
assertTrue(Predicates.boxIntersects(a, new Box2D(5.0, 5.0, 10.0, 10.0)));
// Disjoint on X
assertFalse(Predicates.boxIntersects(a, new Box2D(6.0, 0.0, 10.0, 5.0)));
// Disjoint on Y
assertFalse(Predicates.boxIntersects(a, new Box2D(0.0, 6.0, 5.0, 10.0)));
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added testBoxPredicatesRejectInvertedBounds in 6726b1b covering both inverted-X (the antimeridian-style case) and inverted-Y, asserting the documented IllegalArgumentException is thrown.

Comment on lines 93 to +101
/**
* Test if leftGeometry full intersects rightGeometry. Supports both Geometry (JTS) and Geography
* (S2) inputs via InferredExpression dual dispatch.
*
* @param inputExpressions
*/
private[apache] case class ST_BoxIntersects(inputExpressions: Seq[Expression])
extends InferredExpression(Predicates.boxIntersects _) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 6726b1b — replaced the bleed-through Scaladoc above ST_BoxIntersects with proper Box2D semantics, added a parallel doc on ST_BoxContains, and restored the original ST_Intersects Scaladoc below.

package org.apache.spark.sql.sedona_sql.expressions

import org.apache.sedona.common.Predicates
import org.apache.sedona.common.geometryObjects.Box2D
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Removed in 6726b1bBox2D was used implicitly via Predicates.boxIntersects _ eta-expansion, no explicit reference needed in this file.

- Validate ordered planar bounds in boxIntersects/boxContains and
  throw IllegalArgumentException on inverted (xmin>xmax / ymin>ymax)
  input. Box2D allows inverted bounds (reserved for future antimeridian
  wraparound semantics), but planar predicates have no defined meaning
  for inverted intervals; failing fast beats silently misleading output.
- Add testBoxPredicatesRejectInvertedBounds covering both inverted X
  and inverted Y, asserting the documented exception.
- Tighten Javadocs to say overlap on "both" axes (not "either").
- Replace the stale ST_Intersects-bleed Scaladoc above ST_BoxIntersects
  with proper Box2D semantics; add a parallel doc block on
  ST_BoxContains; restore the Scaladoc on ST_Intersects.
- Remove unused Box2D import from Predicates.scala.
@jiayuasu jiayuasu added this to the sedona-1.9.1 milestone May 8, 2026
@jiayuasu jiayuasu merged commit c863a46 into apache:master May 8, 2026
43 of 56 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.

Add Box2D predicates: ST_BoxIntersects, ST_BoxContains

2 participants