@@ -17,7 +17,7 @@ Architecture
1717 │ applyFilter() → bbox + Z2 TupleDomain │
1818 │ │
1919 │ CQL clients ──► geomesa-trino-datastore ──► TrinoFilterToSQL │
20- │ (CQL filter ──► bbox-overlap + CASE WHEN shortcut SQL) │
20+ │ (CQL filter ──► pure row-level ST_* SQL, column-first ) │
2121 └──────────────┬──────────────────────────────────────┬──────────────────┘
2222 │ │
2323 ┌───────▼──────────┐ ┌────────▼─────────────────┐
@@ -91,59 +91,72 @@ set — files in non-overlapping Z2 cells have non-overlapping bbox stats, so
9191benchmark deltas between the two connectors read ~0% on rectangular queries:
9292both land at the same file count via different mechanisms.
9393
94- Layer 3: Row-Level CASE WHEN bbox-Contained Shortcut
95- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
96-
97- For surviving rows, the GeoMesa data store's ``TrinoFilterToSQL `` emits SQL that
98- short-circuits the expensive geometry test when the row's bbox is fully inside
99- the query envelope. The form differs by spatial filter type:
94+ Layer 3: Connector-Side Page-Source bbox Filter
95+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
96+
97+ The data store emits only row-level ``ST_* `` predicates (no ``CASE WHEN ``), so the
98+ row-level saving that shortcut used to provide is recovered *inside the connector *, where
99+ it applies uniformly to data store queries and hand-written Trino SQL alike. When
100+ ``geomesa.spatial.bbox-page-filter `` is enabled (the default), ``SpatialPageSourceProvider ``
101+ wraps the Iceberg page source with ``BboxFilteringPageSource ``. It reads only the cheap
102+ ``__<geom>_bbox__ `` sub-field columns (the REAL domains injected in Layer 2, carried to the
103+ worker in the table handle's unenforced predicate) and filters rows **before ** the geometry
104+ WKB is decoded, in one of two modes chosen per query:
105+
106+ * **Reject pre-filter ** — for any spatial predicate, drops rows whose bbox cannot overlap
107+ the query envelope and passes the survivors to the engine's exact ``ST_* `` residual. A
108+ sound, non-destructive pre-filter: only rows failing a *necessary * overlap condition are
109+ dropped, so no matching row is removed and the exact predicate still runs on every
110+ survivor. Its win is the WKB decode it avoids on rejected rows.
111+ * **Short-circuit ** — for a rectangle ``ST_Intersects `` on a Z2/point geometry column the
112+ bbox proves the result exactly, so ``applyFilter() `` claims the predicate enforced
113+ (replacing only the ``st_intersects `` node with ``TRUE `` — never ``is_visible `` or any
114+ other conjunct) and the page source becomes authoritative: rows whose bbox is inside are
115+ **accepted with no decode **, rows outside are rejected, and only the thin
116+ envelope-boundary shell is decoded and tested exactly. For point columns the stored bbox
117+ is degenerate (``xmin == xmax ``, ``ymin == ymax ``), so the classifier reads two coordinate
118+ columns and the box test collapses to a point-in-range check.
119+
120+ Directional float32 rounding keeps both modes exact: the reject box is rounded outward and
121+ the accept box inward, so any row a nearest-rounded float32 bbox could misclassify falls
122+ through to the exact test. The mode each predicate uses:
100123
101124.. list-table ::
102125 :header-rows: 1
103- :widths: 20 45 35
126+ :widths: 30 22 48
104127
105- * - CQL filter
106- - Emitted SQL pattern
128+ * - Predicate (emitted by the data store / recognized in SQL)
129+ - Page-source mode
107130 - Soundness
108- * - ``INTERSECTS(geom, axis-aligned rectangle) ``
109- - ``(bbox-overlap) AND CASE WHEN bbox-contained THEN TRUE ELSE ST_Intersects(geom, rect) END ``
110- - bbox⊆rect ⇒ geom⊆rect ⇒ ST_Intersects=TRUE (sufficient; the rectangle IS its envelope)
111- * - ``INTERSECTS(geom, non-rectangular polygon) ``
112- - ``(bbox-overlap) AND ST_Intersects(geom, polygon) ``
113- - no shortcut: bbox⊆env(polygon) does NOT imply intersection (holes, concavity)
114- * - ``WITHIN(geom, axis-aligned rectangle) ``
115- - ``(bbox-overlap) AND CASE WHEN bbox-in-shrunk-rect THEN TRUE ELSE ST_Within(geom, rect) END ``
116- - WITHIN is boundary-exclusive and the stored bbox is float32, so the shortcut
117- rectangle is shrunk by two float ulps per side (containment then proves the
118- geometry is strictly interior); boundary-adjacent rows take the exact test
119- * - ``WITHIN(geom, non-rectangular polygon) ``
120- - ``(bbox-overlap) AND ST_Within(geom, polygon) ``
121- - bbox⊆env(polygon) does NOT imply geom⊆polygon
122- * - ``DWITHIN(geom, ref, d) ``
123- - ``(outer-bbox) AND CASE WHEN bbox-in-inner-inscribed-rect THEN TRUE ELSE ST_Distance(...) ≤ d END ``
124- - outer bbox = env(ref) expanded by d (covers extended references end to end);
125- the inscribed-rect shortcut applies to point references only — for lines and
126- polygons the exact spherical check runs via the planar nearest-points pair
127- (Trino's spherical ``ST_Distance `` is point-only)
128- * - ``BBOX(geom, env) ``
129- - same as ``INTERSECTS(geom, envelope-rectangle) ``
130- - a bare float32 bbox-overlap is not exact (the stored bbox is rounded to
131- nearest, admitting rows up to ½ ulp outside the envelope), so BBOX takes the
132- rectangle-intersects shape: overlap prefilter + shrunk-contained shortcut +
133- exact ``ST_Intersects `` fallback
134- * - ``CROSSES `` / ``TOUCHES `` / ``OVERLAPS `` / ``EQUALS ``
135- - ``(bbox-overlap) AND ST_<op>(geom, g) ``
136- - each implies a non-empty intersection ⇒ bbox-overlap is a valid necessary prefilter
137- * - ``CONTAINS(geom, g) ``
138- - ``(bbox-covers) AND ST_Contains(geom, g) ``
139- - geom ⊇ g ⇒ bbox(geom) ⊇ env(g) (necessary); reversed operands reuse the WITHIN paths
140- * - ``DISJOINT `` / ``BEYOND ``
141- - exact ``ST_Disjoint `` / spherical distance > d, **no prefilter **
142- - the matching rows lie outside the query neighborhood — an overlap prefilter would prune the answer
143-
144- CASE WHEN — not OR. Trino's optimizer distributes OR over AND, causing the
145- expensive predicate to evaluate up to 4× per row (3.3× wall-clock slowdown
146- measured). CASE WHEN survives the optimizer intact and short-circuits cleanly.
131+ * - ``ST_Intersects(geom, axis-aligned rectangle) `` on a point/Z2 column
132+ - short-circuit
133+ - bbox⊆rect ⇒ geom⊆rect ⇒ intersects (the rectangle IS its own envelope); the
134+ boundary shell falls to the exact test
135+ * - ``ST_Intersects `` on a non-rectangular polygon, or on a non-point (XZ2) column
136+ - reject-only
137+ - bbox⊆env(polygon) does NOT imply intersection (holes, concavity); the exact
138+ ``ST_Intersects `` runs on survivors
139+ * - ``ST_Within `` / ``ST_Contains `` / ``ST_Crosses `` / ``ST_Touches `` /
140+ ``ST_Overlaps `` / ``ST_Equals ``
141+ - reject-only
142+ - each implies a non-empty intersection ⇒ bbox-overlap is a valid necessary
143+ prefilter; the exact predicate decides membership on survivors
144+ * - ``DWITHIN `` — emitted as ``ST_Intersects(geom, outer rectangle) AND ST_Distance(...) ≤ d ``
145+ - reject-only (on the outer rectangle)
146+ - the outer rectangle (reference expanded by ``d ``) is a necessary bound on the
147+ distance neighborhood; the exact spherical ``ST_Distance `` runs on survivors
148+ * - ``ST_Disjoint `` / ``BEYOND `` (distance > d)
149+ - none
150+ - matching rows lie *outside * the query neighborhood — a bbox-overlap prefilter would
151+ prune the answer, so no bbox pushdown is derived
152+
153+ Doing this in the connector rather than as ``CASE WHEN `` SQL is deliberate: it keeps the
154+ data store's emitted SQL pure ``ST_* `` (so plans are identical however a query arrives),
155+ and it sidesteps Trino's optimizer distributing an ``OR `` form over ``AND `` — which would
156+ evaluate the expensive predicate up to 4× per row (a 3.3× wall-clock slowdown was measured
157+ with the old SQL forms). The filter is a pure performance layer: disabling it
158+ (``geomesa.spatial.bbox-page-filter=false ``) changes runtime, never results. See
159+ :ref: `trino_configuration ` for the cost/benefit and when to disable.
147160
148161.. _trino_partitioning :
149162
0 commit comments