Repository navigation
Cull non-finite projected positions to fix orthographic OOM - #593
Merged
teunbrand merged 4 commits intoOct 15, 2026
Merged
Conversation
…on-finite domains Fixes posit-dev#592. ST_Transform returns (inf, inf) for points behind the horizon of an azimuthal projection (orthographic, stereographic, gnomonic). The spatial geom clips geometries to the horizon, but position geoms (point, line, path, ...) projected positions without culling, so inf coordinates flowed into the materialized layer data. A mixed-visibility point set (one point visible, one behind the horizon) then produced a scale domain of [min, inf], and graticule_breaks iterated (start..=i64::MAX), allocating unboundedly — OOM-killing the process (observed via querychat's maps capability, where any world-spanning point set under PROJECT TO orthographic killed the Shiny server). Two-layer fix: - project_position_columns now filters rows whose projected coordinates are non-finite, via a new SqlDialect::sql_is_finite helper (portable default predicate; DuckDB/Postgres/Trino/Snowflake NaN ordering and three-valued logic make it cull NaN/inf/NULL uniformly). This culls behind-horizon points for all position geoms. - ScaleTypeTrait::resolve_breaks now returns None for non-finite domains, so no break generator can iterate an unbounded range regardless of how infinities reach the domain (e.g. inf in user data). Regression tests: mixed-visibility and all-behind point sets under PROJECT TO orthographic execute and return 1 and 0 rows respectively; resolve_breaks returns None for inf/NaN domains. Golden SQL for the five spatial-capable dialects blessed with the new predicate.
Graticules never span more than the globe, so clamp the input range to ±180 before picking a step. Besides being the right semantics, this prevents the (start..=end) collect from iterating ~i64::MAX times when the domain is non-finite (e.g. infinite coordinates from degenerate projections) or merely huge (as i64 saturates), either of which allocates unboundedly. The ±180 boundary conditions below the clamp are now equality checks.
Degenerate projections (e.g. points behind an orthographic horizon) can yield infinite extents. Every bbox in the map pipeline flows through BBox::from_df, so rejecting non-finite components there drops a poisoned layer extent and lets the frame fall back to the world/clip bbox instead.
DuckDB (isfinite), Trino (is_finite), and ClickHouse (isFinite) have a dedicated predicate; BigQuery has IS_INF/IS_NAN but no single function. These avoid the portable default's false positive on legitimate finite values in [1e308, 1.8e308]. Other dialects keep the ABS(x) < 1e308 default (Spark's isnan alone doesn't cover infinity). Golden SQL for duckdb/trino re-blessed with the new predicate.
teunbrand
approved these changes
Oct 15, 2026
teunbrand
left a comment
Collaborator
There was a problem hiding this comment.
Thanks for this Carson! I've added a few commits that I think are perhaps superfluously defensive, but probably can't hurt. If you could check at your end that this works as you expect, then I think this can be merged.
Collaborator
Author
|
LGTM, thanks! 👍 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #592.
Problem
ST_Transformreturns(inf, inf)for points behind the horizon of an azimuthal projection (orthographic,stereographic,gnomonic). Thespatialgeom clips geometries to the horizon, but position geoms (point, line, path, polygon, …) projected positions without culling, soinfcoordinates flowed into the materialized layer data.A point set split across the horizon (one point visible, one behind) then produced a geographic scale domain of
[min, inf], andgraticule_breaksiterated(start..=end)withendsaturated toi64::MAX— allocating unboundedly until the process was OOM-killed. Any world-spanning point dataset underPROJECT TO orthographicwas fatal (found via querychat's maps capability, where it killed the Shiny server with exit 137).Fix
Two layers:
project_position_columns(the shared projection path for all position geoms): rows whose projected x/y are non-finite are filtered via a newSqlDialect::sql_is_finitehelper. The default predicate (ABS(x) < 1e308) is portable — three-valued logic plus NaN-above-all ordering on DuckDB/Postgres/Trino/Snowflake (and NaN→NULL on SQLite) makes it cull NULL/NaN/±inf uniformly; dialects with a native predicate can override.ScaleTypeTrait::resolve_breaksagainst non-finite domains (returnsNone), so no break generator can iterate an unbounded range no matter how infinities reach a domain (e.g.infin user data on any scale).Tests
PROJECT TO orthographicexecutes and returns exactly the visible point; an all-behind set executes and returns 0 rows (previously: OOM /min >= maxearly-return accident).resolve_breaksreturnsNoneforinf/NaNdomains.WHERE ABS(ST_X(...)) < 1e308 AND ...predicate on projected-layer materialization.--features spatial).Verified end-to-end through ggsql-python + querychat: the Python minimal repro from the issue executes at 0.11 GB peak RSS (previously killed at a 3 GB supervisor cap, ~9 GB unsupervised).