Skip to content

Commit ddda0db

Browse files
authored
GEOMESA-3582 Trino - Move BBOX predicates from datastore to plugin connector (#3589)
* Adds connector-configurable BBOX-containment short-circuiting logic to the plugin connector, which shortcuts the more expensive WKB decode during row-filtering, provides additional performance improvement for coarsely-pruned datasets.
1 parent ef8fcba commit ddda0db

19 files changed

Lines changed: 2664 additions & 891 deletions

File tree

docs/user/trino/configuration.rst

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
.. _trino_configuration:
2+
3+
Trino Connector Configuration
4+
=============================
5+
6+
The ``spatial_iceberg`` connector is configured through **catalog properties** in its
7+
``.properties`` file (see :ref:`trino_install`). Any property that is not in a
8+
``geomesa.*`` namespace is passed through unchanged to the stock Iceberg connector the
9+
plugin wraps, so all of Iceberg's own catalog properties are available as-is. The
10+
GeoMesa-specific properties below are consumed by the plugin and stripped before the
11+
config reaches the Iceberg delegate.
12+
13+
.. list-table::
14+
:header-rows: 1
15+
:widths: 32 12 12 44
16+
17+
* - Catalog property
18+
- Default
19+
- Required
20+
- Description
21+
* - ``geomesa.spatial.bbox-page-filter``
22+
- ``true``
23+
- no
24+
- Enables connector-side, pre-decode bbox filtering in the page source (see below).
25+
* - ``geomesa.security.*``
26+
- —
27+
- no
28+
- Trino-layer row-visibility enforcement; see :ref:`trino_security`.
29+
30+
.. _trino_bbox_page_filter:
31+
32+
Bounding-box page filtering
33+
---------------------------
34+
35+
``geomesa.spatial.bbox-page-filter`` controls whether the connector wraps the Iceberg
36+
page source with a bounding-box filter that runs **before** each row's geometry WKB is
37+
decoded. The filter reads only the cheap ``__<geom>_bbox__`` sub-field columns (which the
38+
connector already injects as REAL domains for per-file stat pruning, and which ride to the
39+
worker in the table handle's unenforced predicate) and operates in one of two modes,
40+
chosen per query:
41+
42+
* **Reject pre-filter** (all spatial predicates) — drops any row whose bbox *cannot*
43+
overlap the query envelope, then hands the survivors to the engine's exact ``ST_*``
44+
residual. This is a sound, non-destructive pre-filter: only rows failing a *necessary*
45+
overlap condition are removed, so no matching row is ever dropped and the engine's exact
46+
predicate still runs on every survivor. It saves the WKB decode on the rejected rows for
47+
any spatial predicate — including non-rectangular geometries, ``ST_Contains``, and the
48+
outer envelope of a ``DWITHIN`` distance query.
49+
50+
* **Short-circuit** (rectangle ``ST_Intersects`` on a Z2/point geometry column) — here the
51+
connector can claim the predicate *enforced*, so the page source is the authoritative
52+
filter: rows whose bbox lies fully inside the query rectangle are **accepted** with no
53+
WKB decode, rows outside are **rejected**, and only rows on the thin envelope boundary
54+
fall through to an exact decode-and-test. For point geometry columns the stored bbox is
55+
degenerate (``xmin == xmax``, ``ymin == ymax``), so the test reads two coordinate columns
56+
and collapses to a point-in-range check.
57+
58+
Both modes use directional float32 rounding — the reject box is rounded outward and the
59+
accept box inward — so any row a nearest-rounded float32 bbox could misclassify falls
60+
through to the exact test, and the result is identical to the engine's exact predicate.
61+
Non-spatial queries carry no bbox domains and delegate to Iceberg unchanged.
62+
63+
When to disable
64+
~~~~~~~~~~~~~~~
65+
66+
The filter's value is inversely proportional to spatial partition quality. On a
67+
well-``truncate``-partitioned Z2/XZ2 table, Layer 1 and Layer 2 pruning
68+
(see :ref:`trino_design`) already eliminate most non-matching rows before the page source
69+
runs, so the extra bbox-column read can cost more than the decodes it saves — this is most
70+
noticeable for ``DWITHIN``, where the geometry must be materialized for the exact distance
71+
test regardless. On a coarsely-partitioned table the row-level rejection catches what file
72+
pruning misses and is a clear win. Set ``geomesa.spatial.bbox-page-filter=false`` to turn
73+
it off; the connector then relies on partition and per-file stat pruning alone and the
74+
engine's exact ``ST_*`` predicate decodes every surviving row.

docs/user/trino/design.rst

Lines changed: 62 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -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
9191
benchmark deltas between the two connectors read ~0% on rectangular queries:
9292
both 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

docs/user/trino/index.rst

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ end-to-end:
1313
``truncate(N_chars)`` on ``__<geom>_z2__`` / ``__<geom>_xz2__``).
1414
* **Per-file** ``__<geom>_bbox__`` **column-stat pruning** at planning time (works for both
1515
the spatial connector and stock Iceberg).
16-
* **Row-level CASE WHEN bbox-contained shortcut** in the SQL emitted by the GeoMesa
17-
data store (skips ``ST_Intersects``/``ST_Distance`` for rows whose bbox is fully inside
18-
the query envelope).
16+
* **Connector-side page-source bbox filtering** (``geomesa.spatial.bbox-page-filter``,
17+
on by default): before a surviving row's geometry WKB is decoded, the connector
18+
rejects rows whose ``__<geom>_bbox__`` cannot overlap the query envelope, and — for a
19+
rectangle ``ST_Intersects`` on a point/Z2 column — accepts rows whose bbox is fully
20+
inside without decoding at all. See :ref:`trino_configuration`.
1921

20-
For axis-aligned rectangular WITHIN queries the row-level test is eliminated entirely
21-
— the bbox-contained predicate is exactly equivalent to ``ST_Within``.
22+
The connector derives all three layers from the same ``ST_*`` call, so a CQL query
23+
through the data store and the equivalent hand-written Trino SQL get identical plans.
24+
The data store's ``TrinoFilterToSQL`` emits only row-level ``ST_*`` predicates; the
25+
bbox/Z2 pruning and the row-level bbox filter are the connector's responsibility.
2226

2327
``<geom>`` above is a placeholder for the geometry column's name; each geometry
2428
column in a table gets its own companion group, so multi-geometry tables carry
@@ -42,5 +46,6 @@ GeoMesa Trino consists of two modules:
4246

4347
design
4448
install
49+
configuration
4550
usage
4651
security

docs/user/trino/install.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,10 @@ Glue example:
8080
fs.s3.enabled=true
8181
s3.region=<region>
8282
83-
Optionally add the ``geomesa.security.*`` catalog properties for Trino-layer row
84-
visibility (see :ref:`trino_security`).
83+
The connector passes any non-``geomesa.*`` property straight through to the Iceberg
84+
delegate. For the GeoMesa-specific catalog properties — including
85+
``geomesa.spatial.bbox-page-filter`` and the ``geomesa.security.*`` row-visibility
86+
keys — see :ref:`trino_configuration`.
8587

8688
**3. Restart Trino** on all nodes. Verify the catalog is up with ``SHOW CATALOGS;``,
8789
then confirm pruning is active with the EXPLAIN checks under :ref:`trino_verify_pruning`.

0 commit comments

Comments
 (0)