Skip to content

Commit 4c7ddf6

Browse files
authored
[GH-3027] Consolidate Box predicates into ST_Intersects / ST_Contains (#3028)
1 parent 4752c61 commit 4c7ddf6

25 files changed

Lines changed: 353 additions & 448 deletions

File tree

docs/api/sql/Optimizer.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -437,52 +437,52 @@ Spatial predicate push-down to GeoParquet is enabled by default. Users can manua
437437

438438
## Box2D filter pushdown
439439

440-
When a query filters on a `Box2D`-typed column (see [Box2D Functions](box2d/Box2D-Functions.md)) using `ST_BoxIntersects` or `ST_BoxContains` against a literal `Box2D`, Sedona translates the predicate into Parquet row-group inequalities on the column's underlying `xmin` / `ymin` / `xmax` / `ymax` leaves and pushes them down via `ParquetInputFormat.setFilterPredicate`. Parquet's row-group statistics machinery then skips row groups whose recorded min/max disprove the predicate — no file metadata scan is required.
440+
When a query filters on a `Box2D`-typed column (see [Box2D Functions](box2d/Box2D-Functions.md)) using [ST_Intersects](Predicates/ST_Intersects.md) or [ST_Contains](Predicates/ST_Contains.md) against a literal `Box2D`, Sedona translates the predicate into Parquet row-group inequalities on the column's underlying `xmin` / `ymin` / `xmax` / `ymax` leaves and pushes them down via `ParquetInputFormat.setFilterPredicate`. Parquet's row-group statistics machinery then skips row groups whose recorded min/max disprove the predicate — no file metadata scan is required.
441441

442-
The pushdown applies whenever the column's Spark `dataType` is `Box2DUDT`. The simplest way to get one is to materialise the column with `ST_Box2D(geom)` before writing the dataset, or to use the SQL cast `CAST(geom AS box2d)`. Sedona's auto-generated `<geom>_bbox` covering column is written as a plain `struct<xmin, ymin, xmax, ymax>` — it satisfies the GeoParquet 1.1 covering-bbox contract but is not a `Box2D`, so `ST_BoxIntersects` / `ST_BoxContains` do not target it directly. Use it through [Push spatial predicates to GeoParquet](#push-spatial-predicates-to-geoparquet) (which prunes via the file-level bbox metadata), or write the column explicitly as `ST_Box2D(geom)` if you want row-group-level pruning through Box2D predicates.
442+
The pushdown applies whenever both arguments resolve to Spark `Box2DUDT`. The simplest way to get a Box2D column is to materialise it with `ST_Box2D(geom)` before writing the dataset, or to use the SQL cast `CAST(geom AS box2d)`. Sedona's auto-generated `<geom>_bbox` covering column is written as a plain `struct<xmin, ymin, xmax, ymax>` — it satisfies the GeoParquet 1.1 covering-bbox contract but is not a `Box2D`, so the Box2D pushdown does not target it directly. Use it through [Push spatial predicates to GeoParquet](#push-spatial-predicates-to-geoparquet) (which prunes via the file-level bbox metadata), or write the column explicitly as `ST_Box2D(geom)` if you want row-group-level pruning.
443443

444444
SQL Example
445445

446446
```sql
447447
SELECT *
448448
FROM geoparquet_dataset
449-
WHERE ST_BoxIntersects(
449+
WHERE ST_Intersects(
450450
geom_bbox,
451451
ST_MakeBox2D(ST_Point(0.0, 0.0), ST_Point(10.0, 10.0)))
452452
```
453453

454454
Predicate types and the per-row inequality system they translate to:
455455

456-
| Predicate | Pushed-down conjunction (per row) |
457-
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
458-
| `ST_BoxIntersects(box_col, lit)` | `box.xmax >= lit.xmin AND box.xmin <= lit.xmax AND box.ymax >= lit.ymin AND box.ymin <= lit.ymax` (symmetric — reverse arg order is identical) |
459-
| `ST_BoxContains(box_col, lit)` | `box.xmin <= lit.xmin AND box.xmax >= lit.xmax AND box.ymin <= lit.ymin AND box.ymax >= lit.ymax` |
460-
| `ST_BoxContains(lit, box_col)` | `box.xmin >= lit.xmin AND box.xmax <= lit.xmax AND box.ymin >= lit.ymin AND box.ymax <= lit.ymax` |
456+
| Predicate (Box2D / Box2D) | Pushed-down conjunction (per row) |
457+
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
458+
| `ST_Intersects(box_col, lit)` | `box.xmax >= lit.xmin AND box.xmin <= lit.xmax AND box.ymax >= lit.ymin AND box.ymin <= lit.ymax` (symmetric — reverse arg order is identical) |
459+
| `ST_Contains(box_col, lit)` | `box.xmin <= lit.xmin AND box.xmax >= lit.xmax AND box.ymin <= lit.ymin AND box.ymax >= lit.ymax` |
460+
| `ST_Contains(lit, box_col)` | `box.xmin >= lit.xmin AND box.xmax <= lit.xmax AND box.ymin >= lit.ymin AND box.ymax <= lit.ymax` |
461461

462462
Pushdown is enabled by default and is gated by two flags. The optimizer rule that attaches the Box2D spatial filter is controlled by `spark.sedona.geoparquet.spatialFilterPushDown` (Sedona's master spatial-pushdown toggle, default `true`); the actual injection into the Parquet read path is then additionally gated by Spark's `spark.sql.parquet.filterPushdown` (default `true`). Disabling either disables Box2D pushdown.
463463

464464
Inverted-bound literals (`xmin > xmax` / `ymin > ymax`) are not pushed down — the predicate falls back to per-row evaluation so callers see the expected `IllegalArgumentException` from the scalar contract.
465465

466466
## Box2D spatial join
467467

468-
`ST_BoxIntersects` and `ST_BoxContains` between two `Box2D` columns route through the same physical operators as their `Geometry` counterparts (`ST_Intersects` / `ST_Covers`). At the executor boundary, each `Box2D` row is materialised into the implied rectangular polygon, after which the partitioner, R-tree index, and refine evaluator run unchanged. JTS short-circuits axis-aligned rectangle predicates via `RectangleIntersects` / `RectangleContains`, so the refine step pays only the four-double envelope comparison.
468+
`ST_Intersects` and `ST_Contains` between two `Box2D` columns route through the same physical operators as their `Geometry` counterparts (`ST_Intersects` / `ST_Covers`). The planner picks the bbox path when both children resolve to `Box2DUDT`. At the executor boundary each `Box2D` row is materialised into the implied rectangular polygon, after which the partitioner, R-tree index, and refine evaluator run unchanged. JTS short-circuits axis-aligned rectangle predicates via `RectangleIntersects` / `RectangleContains`, so the refine step pays only the four-double envelope comparison.
469469

470-
`ST_BoxContains` uses `SpatialPredicate.COVERS` semantics at the join layer — JTS `covers` matches `ST_BoxContains`'s closed-interval contract (JTS `contains`, strict-interior, would reject edge-sharing pairs).
470+
`ST_Contains` between two Box2D columns uses `SpatialPredicate.COVERS` semantics at the join layer — JTS `covers` matches the closed-interval contract (JTS `contains`, strict-interior, would reject edge-sharing pairs).
471471

472472
SQL Example — range join:
473473

474474
```sql
475475
SELECT *
476476
FROM left_boxes L, right_boxes R
477-
WHERE ST_BoxIntersects(L.box, R.box)
477+
WHERE ST_Intersects(L.box, R.box)
478478
```
479479

480480
SQL Example — broadcast index join:
481481

482482
```sql
483483
SELECT /*+ BROADCAST(R) */ *
484484
FROM left_boxes L, right_boxes R
485-
WHERE ST_BoxIntersects(L.box, R.box)
485+
WHERE ST_Intersects(L.box, R.box)
486486
```
487487

488488
Inverted-bound input on either side raises `IllegalArgumentException` from the join-side validation path, matching the scalar predicate contract.

docs/api/sql/Predicates/ST_Contains.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,22 @@
1919

2020
# ST_Contains
2121

22-
Introduction: Return true if A fully contains B
22+
Introduction: Return true if A fully contains B. Polymorphic over input type:
23+
24+
- `(Geometry, Geometry)` — topological containment via JTS.
25+
- `(Geography, Geography)` — topological containment via S2.
26+
- `(Box2D, Box2D)` — closed-interval bbox containment on both axes. Matches PostGIS `~` on `box2d`. Throws `IllegalArgumentException` on inverted bounds (`xmin > xmax` or `ymin > ymax`).
27+
- `(Box3D, Box3D)` — closed-interval bbox containment on all three axes. Equal boxes contain each other. Throws on inverted bounds on any axis.
2328

2429
![ST_Contains returning true](../../../image/ST_Contains/ST_Contains_true.svg "ST_Contains returning true")
2530
![ST_Contains returning false](../../../image/ST_Contains/ST_Contains_false.svg "ST_Contains returning false")
2631

27-
Format: `ST_Contains (A: Geometry, B: Geometry)`
32+
Format:
33+
34+
- `ST_Contains(A: Geometry, B: Geometry)`
35+
- `ST_Contains(A: Geography, B: Geography)`
36+
- `ST_Contains(A: Box2D, B: Box2D)` (Since `v1.9.1`)
37+
- `ST_Contains(A: Box3D, B: Box3D)` (Since `v1.9.1`)
2838

2939
Return type: `Boolean`
3040

@@ -41,3 +51,24 @@ Output:
4151
```
4252
false
4353
```
54+
55+
Box2D example:
56+
57+
```sql
58+
SELECT ST_Contains(
59+
ST_MakeBox2D(ST_Point(0.0, 0.0), ST_Point(10.0, 10.0)),
60+
ST_MakeBox2D(ST_Point(2.0, 2.0), ST_Point(5.0, 5.0)))
61+
```
62+
63+
Output:
64+
65+
```
66+
true
67+
```
68+
69+
## Box2D optimization
70+
71+
`ST_Contains(box_col, lit_box)` over a `Box2D` column and a literal `Box2D` (and the reversed form) is recognised by Sedona's spatial optimizer:
72+
73+
- **Filter pushdown.** When the column is a `Box2D` stored in GeoParquet, the predicate translates to Parquet row-group inequalities on the `xmin` / `ymin` / `xmax` / `ymax` leaves. See [Box2D filter pushdown](../Optimizer.md#box2d-filter-pushdown).
74+
- **Spatial join.** `ST_Contains(a, b)` between two `Box2D` columns is planned as a range or broadcast-index join with `SpatialPredicate.COVERS` semantics (closed-interval containment — JTS `contains`, strict-interior, would reject edge-sharing pairs). See [Box2D spatial join](../Optimizer.md#box2d-spatial-join).

docs/api/sql/Predicates/ST_Intersects.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,22 @@
1919

2020
# ST_Intersects
2121

22-
Introduction: Return true if A intersects B
22+
Introduction: Return true if A intersects B. Polymorphic over input type:
23+
24+
- `(Geometry, Geometry)` — topological intersection via JTS.
25+
- `(Geography, Geography)` — topological intersection via S2.
26+
- `(Box2D, Box2D)` — closed-interval bbox intersection on both axes. Matches PostGIS `&&` on `box2d`. Edge- and corner-touching boxes count as intersecting. Throws `IllegalArgumentException` on inverted bounds.
27+
- `(Box3D, Box3D)` — closed-interval bbox intersection on all three axes. Matches PostGIS `&&&` on `box3d`. Edge-, face-, and corner-touching boxes count as intersecting. Throws on inverted bounds on any axis.
2328

2429
![ST_Intersects returning true](../../../image/ST_Intersects/ST_Intersects_true.svg "ST_Intersects returning true")
2530
![ST_Intersects returning false](../../../image/ST_Intersects/ST_Intersects_false.svg "ST_Intersects returning false")
2631

27-
Format: `ST_Intersects (A: Geometry, B: Geometry)`
32+
Format:
33+
34+
- `ST_Intersects(A: Geometry, B: Geometry)`
35+
- `ST_Intersects(A: Geography, B: Geography)`
36+
- `ST_Intersects(A: Box2D, B: Box2D)` (Since `v1.9.1`)
37+
- `ST_Intersects(A: Box3D, B: Box3D)` (Since `v1.9.1`)
2838

2939
Return type: `Boolean`
3040

@@ -41,3 +51,24 @@ Output:
4151
```
4252
true
4353
```
54+
55+
Box2D example:
56+
57+
```sql
58+
SELECT ST_Intersects(
59+
ST_MakeBox2D(ST_Point(0.0, 0.0), ST_Point(10.0, 10.0)),
60+
ST_MakeBox2D(ST_Point(5.0, 5.0), ST_Point(15.0, 15.0)))
61+
```
62+
63+
Output:
64+
65+
```
66+
true
67+
```
68+
69+
## Box2D optimization
70+
71+
`ST_Intersects(box_col, lit_box)` over a `Box2D` column and a literal `Box2D` is recognised by Sedona's spatial optimizer:
72+
73+
- **Filter pushdown.** When the column is a `Box2D` stored in GeoParquet, the predicate translates to Parquet row-group inequalities on the `xmin` / `ymin` / `xmax` / `ymax` leaves. See [Box2D filter pushdown](../Optimizer.md#box2d-filter-pushdown).
74+
- **Spatial join.** `ST_Intersects(a, b)` between two `Box2D` columns is planned as a range or broadcast-index join. See [Box2D spatial join](../Optimizer.md#box2d-spatial-join).

docs/api/sql/box2d/Box2D-Functions.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ The `Box2D` type in Sedona represents a planar axis-aligned bounding box — a r
2525

2626
## Semantic notes
2727

28-
- `Box2D` values use closed-interval semantics: edge-touching boxes are considered intersecting and (per [ST_BoxContains](Box2D-Predicates/ST_BoxContains.md)) contained.
28+
- `Box2D` values use closed-interval semantics: edge-touching boxes are considered intersecting and (per [ST_Contains](../Predicates/ST_Contains.md)) contained.
2929
- Absence is represented by SQL `NULL` rather than an in-band sentinel.
3030
- Bounds are required to be ordered (`xmin <= xmax`, `ymin <= ymax`). Inverted-bound values are reserved for a future antimeridian-wraparound semantics on geography bboxes; predicates and join planning throw `IllegalArgumentException` on inverted input today.
3131
- Unlike [ST_Envelope](../Bounding-Box-Functions/ST_Envelope.md), which returns the envelope as a `Geometry` (typically a polygon, but JTS may return a `Point` or `LineString` for degenerate inputs), [ST_Box2D](Box2D-Constructors/ST_Box2D.md) always returns a typed `Box2D` value. Prefer the typed form when downstream code only needs the four bounds, and prefer the geometry form when downstream code expects a `Geometry`.
@@ -51,10 +51,12 @@ The same `ST_XMin` / `ST_YMin` / `ST_XMax` / `ST_YMax` functions also accept `Ge
5151

5252
## Box2D Predicates
5353

54+
`Box2D` inputs are accepted by the existing `ST_Intersects` / `ST_Contains` / `ST_DWithin` predicates as type-dispatched overloads — there are no separate `ST_Box*` functions.
55+
5456
| Function | Return type | Description | Since |
5557
| :--- | :--- | :--- | :--- |
56-
| [ST_BoxIntersects](Box2D-Predicates/ST_BoxIntersects.md) | Boolean | Closed-interval bbox intersection over two Box2D arguments. Matches PostGIS `&&` on `box2d`. | v1.9.1 |
57-
| [ST_BoxContains](Box2D-Predicates/ST_BoxContains.md) | Boolean | Closed-interval bbox containment over two Box2D arguments. Matches PostGIS `~` on `box2d`. | v1.9.1 |
58+
| [ST_Intersects](../Predicates/ST_Intersects.md) | Boolean | Closed-interval bbox intersection on both axes when both arguments are `Box2D`. Matches PostGIS `&&` on `box2d`. | v1.9.1 |
59+
| [ST_Contains](../Predicates/ST_Contains.md) | Boolean | Closed-interval bbox containment on both axes when both arguments are `Box2D`. Matches PostGIS `~` on `box2d`. | v1.9.1 |
5860
| [ST_DWithin](Box2D-Predicates/ST_DWithin.md) | Boolean | Closed-interval planar distance test between two Box2D rectangles. | v1.9.1 |
5961

6062
## Box2D Functions
@@ -85,5 +87,5 @@ The cast forms require the Sedona SQL parser extension (`spark.sql.extensions=or
8587

8688
Box2D-typed columns are first-class participants in Sedona's spatial optimizer:
8789

88-
- **Filter pushdown.** `ST_BoxIntersects` / `ST_BoxContains` predicates over a Box2D column and a literal Box2D push down to Parquet row-group statistics on the column's underlying `xmin` / `ymin` / `xmax` / `ymax` leaves. See [Query optimization → Box2D filter pushdown](../Optimizer.md#box2d-filter-pushdown).
89-
- **Spatial joins.** `ST_BoxIntersects` and `ST_BoxContains` route through the same physical operators (`RangeJoinExec`, `BroadcastIndexJoinExec`) used for `ST_Intersects` / `ST_Covers`. See [Query optimization → Range join](../Optimizer.md#range-join) and [Broadcast index join](../Optimizer.md#broadcast-index-join).
90+
- **Filter pushdown.** `ST_Intersects` / `ST_Contains` over a `Box2D` column and a literal `Box2D` push down to Parquet row-group statistics on the column's underlying `xmin` / `ymin` / `xmax` / `ymax` leaves. See [Query optimization → Box2D filter pushdown](../Optimizer.md#box2d-filter-pushdown).
91+
- **Spatial joins.** `ST_Intersects` and `ST_Contains` between two `Box2D` columns route through the same physical operators (`RangeJoinExec`, `BroadcastIndexJoinExec`) used for the Geometry-typed forms — the dataType-aware planner picks the bbox path when both children are `Box2DUDT`. See [Query optimization → Range join](../Optimizer.md#range-join) and [Broadcast index join](../Optimizer.md#broadcast-index-join).

docs/api/sql/box2d/Box2D-Predicates/ST_BoxContains.md

Lines changed: 0 additions & 57 deletions
This file was deleted.

0 commit comments

Comments
 (0)