Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions docs/user/trino/configuration.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
.. _trino_configuration:

Trino Connector Configuration
=============================

The ``spatial_iceberg`` connector is configured through **catalog properties** in its
``.properties`` file (see :ref:`trino_install`). Any property that is not in a
``geomesa.*`` namespace is passed through unchanged to the stock Iceberg connector the
plugin wraps, so all of Iceberg's own catalog properties are available as-is. The
GeoMesa-specific properties below are consumed by the plugin and stripped before the
config reaches the Iceberg delegate.

.. list-table::
:header-rows: 1
:widths: 32 12 12 44

* - Catalog property
- Default
- Required
- Description
* - ``geomesa.spatial.bbox-page-filter``
- ``true``
- no
- Enables connector-side, pre-decode bbox filtering in the page source (see below).
* - ``geomesa.security.*``
- —
- no
- Trino-layer row-visibility enforcement; see :ref:`trino_security`.

.. _trino_bbox_page_filter:

Bounding-box page filtering
---------------------------

``geomesa.spatial.bbox-page-filter`` controls whether the connector wraps the Iceberg
page source with a bounding-box filter that runs **before** each row's geometry WKB is
decoded. The filter reads only the cheap ``__<geom>_bbox__`` sub-field columns (which the
connector already injects as REAL domains for per-file stat pruning, and which ride to the
worker in the table handle's unenforced predicate) and operates in one of two modes,
chosen per query:

* **Reject pre-filter** (all spatial predicates) — drops any row whose bbox *cannot*
overlap the query envelope, then hands the survivors to the engine's exact ``ST_*``
residual. This is a sound, non-destructive pre-filter: only rows failing a *necessary*
overlap condition are removed, so no matching row is ever dropped and the engine's exact
predicate still runs on every survivor. It saves the WKB decode on the rejected rows for
any spatial predicate — including non-rectangular geometries, ``ST_Contains``, and the
outer envelope of a ``DWITHIN`` distance query.

* **Short-circuit** (rectangle ``ST_Intersects`` on a Z2/point geometry column) — here the
connector can claim the predicate *enforced*, so the page source is the authoritative
filter: rows whose bbox lies fully inside the query rectangle are **accepted** with no
WKB decode, rows outside are **rejected**, and only rows on the thin envelope boundary
fall through to an exact decode-and-test. For point geometry columns the stored bbox is
degenerate (``xmin == xmax``, ``ymin == ymax``), so the test reads two coordinate columns
and collapses to a point-in-range check.

Both modes use directional float32 rounding — the reject box is rounded outward and the
accept box inward — so any row a nearest-rounded float32 bbox could misclassify falls
through to the exact test, and the result is identical to the engine's exact predicate.
Non-spatial queries carry no bbox domains and delegate to Iceberg unchanged.

When to disable
~~~~~~~~~~~~~~~

The filter's value is inversely proportional to spatial partition quality. On a
well-``truncate``-partitioned Z2/XZ2 table, Layer 1 and Layer 2 pruning
(see :ref:`trino_design`) already eliminate most non-matching rows before the page source
runs, so the extra bbox-column read can cost more than the decodes it saves — this is most
noticeable for ``DWITHIN``, where the geometry must be materialized for the exact distance
test regardless. On a coarsely-partitioned table the row-level rejection catches what file
pruning misses and is a clear win. Set ``geomesa.spatial.bbox-page-filter=false`` to turn
it off; the connector then relies on partition and per-file stat pruning alone and the
engine's exact ``ST_*`` predicate decodes every surviving row.
111 changes: 62 additions & 49 deletions docs/user/trino/design.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Architecture
│ applyFilter() → bbox + Z2 TupleDomain │
│ │
│ CQL clients ──► geomesa-trino-datastore ──► TrinoFilterToSQL │
│ (CQL filter ──► bbox-overlap + CASE WHEN shortcut SQL) │
│ (CQL filter ──► pure row-level ST_* SQL, column-first) │
└──────────────┬──────────────────────────────────────┬──────────────────┘
│ │
┌───────▼──────────┐ ┌────────▼─────────────────┐
Expand Down Expand Up @@ -91,59 +91,72 @@ set — files in non-overlapping Z2 cells have non-overlapping bbox stats, so
benchmark deltas between the two connectors read ~0% on rectangular queries:
both land at the same file count via different mechanisms.

Layer 3: Row-Level CASE WHEN bbox-Contained Shortcut
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

For surviving rows, the GeoMesa data store's ``TrinoFilterToSQL`` emits SQL that
short-circuits the expensive geometry test when the row's bbox is fully inside
the query envelope. The form differs by spatial filter type:
Layer 3: Connector-Side Page-Source bbox Filter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The data store emits only row-level ``ST_*`` predicates (no ``CASE WHEN``), so the
row-level saving that shortcut used to provide is recovered *inside the connector*, where
it applies uniformly to data store queries and hand-written Trino SQL alike. When
``geomesa.spatial.bbox-page-filter`` is enabled (the default), ``SpatialPageSourceProvider``
wraps the Iceberg page source with ``BboxFilteringPageSource``. It reads only the cheap
``__<geom>_bbox__`` sub-field columns (the REAL domains injected in Layer 2, carried to the
worker in the table handle's unenforced predicate) and filters rows **before** the geometry
WKB is decoded, in one of two modes chosen per query:

* **Reject pre-filter** — for any spatial predicate, drops rows whose bbox cannot overlap
the query envelope and passes the survivors to the engine's exact ``ST_*`` residual. A
sound, non-destructive pre-filter: only rows failing a *necessary* overlap condition are
dropped, so no matching row is removed and the exact predicate still runs on every
survivor. Its win is the WKB decode it avoids on rejected rows.
* **Short-circuit** — for a rectangle ``ST_Intersects`` on a Z2/point geometry column the
bbox proves the result exactly, so ``applyFilter()`` claims the predicate enforced
(replacing only the ``st_intersects`` node with ``TRUE`` — never ``is_visible`` or any
other conjunct) and the page source becomes authoritative: rows whose bbox is inside are
**accepted with no decode**, rows outside are rejected, and only the thin
envelope-boundary shell is decoded and tested exactly. For point columns the stored bbox
is degenerate (``xmin == xmax``, ``ymin == ymax``), so the classifier reads two coordinate
columns and the box test collapses to a point-in-range check.

Directional float32 rounding keeps both modes exact: the reject box is rounded outward and
the accept box inward, so any row a nearest-rounded float32 bbox could misclassify falls
through to the exact test. The mode each predicate uses:

.. list-table::
:header-rows: 1
:widths: 20 45 35
:widths: 30 22 48

* - CQL filter
- Emitted SQL pattern
* - Predicate (emitted by the data store / recognized in SQL)
- Page-source mode
- Soundness
* - ``INTERSECTS(geom, axis-aligned rectangle)``
- ``(bbox-overlap) AND CASE WHEN bbox-contained THEN TRUE ELSE ST_Intersects(geom, rect) END``
- bbox⊆rect ⇒ geom⊆rect ⇒ ST_Intersects=TRUE (sufficient; the rectangle IS its envelope)
* - ``INTERSECTS(geom, non-rectangular polygon)``
- ``(bbox-overlap) AND ST_Intersects(geom, polygon)``
- no shortcut: bbox⊆env(polygon) does NOT imply intersection (holes, concavity)
* - ``WITHIN(geom, axis-aligned rectangle)``
- ``(bbox-overlap) AND CASE WHEN bbox-in-shrunk-rect THEN TRUE ELSE ST_Within(geom, rect) END``
- WITHIN is boundary-exclusive and the stored bbox is float32, so the shortcut
rectangle is shrunk by two float ulps per side (containment then proves the
geometry is strictly interior); boundary-adjacent rows take the exact test
* - ``WITHIN(geom, non-rectangular polygon)``
- ``(bbox-overlap) AND ST_Within(geom, polygon)``
- bbox⊆env(polygon) does NOT imply geom⊆polygon
* - ``DWITHIN(geom, ref, d)``
- ``(outer-bbox) AND CASE WHEN bbox-in-inner-inscribed-rect THEN TRUE ELSE ST_Distance(...) ≤ d END``
- outer bbox = env(ref) expanded by d (covers extended references end to end);
the inscribed-rect shortcut applies to point references only — for lines and
polygons the exact spherical check runs via the planar nearest-points pair
(Trino's spherical ``ST_Distance`` is point-only)
* - ``BBOX(geom, env)``
- same as ``INTERSECTS(geom, envelope-rectangle)``
- a bare float32 bbox-overlap is not exact (the stored bbox is rounded to
nearest, admitting rows up to ½ ulp outside the envelope), so BBOX takes the
rectangle-intersects shape: overlap prefilter + shrunk-contained shortcut +
exact ``ST_Intersects`` fallback
* - ``CROSSES`` / ``TOUCHES`` / ``OVERLAPS`` / ``EQUALS``
- ``(bbox-overlap) AND ST_<op>(geom, g)``
- each implies a non-empty intersection ⇒ bbox-overlap is a valid necessary prefilter
* - ``CONTAINS(geom, g)``
- ``(bbox-covers) AND ST_Contains(geom, g)``
- geom ⊇ g ⇒ bbox(geom) ⊇ env(g) (necessary); reversed operands reuse the WITHIN paths
* - ``DISJOINT`` / ``BEYOND``
- exact ``ST_Disjoint`` / spherical distance > d, **no prefilter**
- the matching rows lie outside the query neighborhood — an overlap prefilter would prune the answer

CASE WHEN — not OR. Trino's optimizer distributes OR over AND, causing the
expensive predicate to evaluate up to 4× per row (3.3× wall-clock slowdown
measured). CASE WHEN survives the optimizer intact and short-circuits cleanly.
* - ``ST_Intersects(geom, axis-aligned rectangle)`` on a point/Z2 column
- short-circuit
- bbox⊆rect ⇒ geom⊆rect ⇒ intersects (the rectangle IS its own envelope); the
boundary shell falls to the exact test
* - ``ST_Intersects`` on a non-rectangular polygon, or on a non-point (XZ2) column
- reject-only
- bbox⊆env(polygon) does NOT imply intersection (holes, concavity); the exact
``ST_Intersects`` runs on survivors
* - ``ST_Within`` / ``ST_Contains`` / ``ST_Crosses`` / ``ST_Touches`` /
``ST_Overlaps`` / ``ST_Equals``
- reject-only
- each implies a non-empty intersection ⇒ bbox-overlap is a valid necessary
prefilter; the exact predicate decides membership on survivors
* - ``DWITHIN`` — emitted as ``ST_Intersects(geom, outer rectangle) AND ST_Distance(...) ≤ d``
- reject-only (on the outer rectangle)
- the outer rectangle (reference expanded by ``d``) is a necessary bound on the
distance neighborhood; the exact spherical ``ST_Distance`` runs on survivors
* - ``ST_Disjoint`` / ``BEYOND`` (distance > d)
- none
- matching rows lie *outside* the query neighborhood — a bbox-overlap prefilter would
prune the answer, so no bbox pushdown is derived

Doing this in the connector rather than as ``CASE WHEN`` SQL is deliberate: it keeps the
data store's emitted SQL pure ``ST_*`` (so plans are identical however a query arrives),
and it sidesteps Trino's optimizer distributing an ``OR`` form over ``AND`` — which would
evaluate the expensive predicate up to 4× per row (a 3.3× wall-clock slowdown was measured
with the old SQL forms). The filter is a pure performance layer: disabling it
(``geomesa.spatial.bbox-page-filter=false``) changes runtime, never results. See
:ref:`trino_configuration` for the cost/benefit and when to disable.

.. _trino_partitioning:

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

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

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

design
install
configuration
usage
security
6 changes: 4 additions & 2 deletions docs/user/trino/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ Glue example:
fs.s3.enabled=true
s3.region=<region>
Optionally add the ``geomesa.security.*`` catalog properties for Trino-layer row
visibility (see :ref:`trino_security`).
The connector passes any non-``geomesa.*`` property straight through to the Iceberg
delegate. For the GeoMesa-specific catalog properties — including
``geomesa.spatial.bbox-page-filter`` and the ``geomesa.security.*`` row-visibility
keys — see :ref:`trino_configuration`.

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