Visitar URL original
fix(databases): use the spatial index for distance queries by HarshMN2345 · Pull Request #14199 · appwrite/appwrite · GitHub
Skip to content

fix(databases): use the spatial index for distance queries - #14199

Open
HarshMN2345 wants to merge 8 commits into
mainfrom
cursor/spatial-index-distance-eefb
Open

HarshMN2345 wants to merge 8 commits into
mainfrom
cursor/spatial-index-distance-eefb

Conversation

@HarshMN2345

@HarshMN2345 HarshMN2345 commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

What does this PR do?

Distance filters were compiled as ST_Distance(...) < radius. PostGIS cannot serve that predicate from a GIST index, so a distanceLessThan query walked the primary key and got slower as fewer rows matched (#14177).

The predicate lives in utopia-php/database. This PR pins dev-fix/postgres-distance-index (c5fc67729e3cabd397e534be3bd5c7431ffbe4b7, aliased as 7.4.999) until that fix is tagged. Replace the branch pin with the release when it ships.

distanceLessThan now leads with an index-served predicate on the geometry column (USING GIST (column)). The original comparison stays in the statement, so results do not change: the boundary stays exclusive, and meter queries still use spheroidal distance.

  • Without meters, both sides are planar, so ST_DWithin(column, geometry, radius) is a superset of ST_Distance(...) < radius for points, lines, and polygons.
  • With meters, a degree box (column && ST_Expand(...)) contains the meter circle only when both sides are points: the column type is point and the query value is a point. Longitude uses the poleward latitude, so the box stays large enough near the poles. The box is not applied when it would reach a pole or the antimeridian.
  • A line or polygon on either side stays on the exact geography comparison. A planar box misses geodesic edges, so those queries are not given a prefilter that could drop rows. A column whose type was not resolved stays exact for the same reason.
  • distanceGreaterThan, distanceEqual, and distanceNotEqual stay on ST_Distance. Those predicates cannot use a spatial index.

Test Plan

  • testSpatialDistanceInMeter checks returned rows: 1500 m includes both points, 500 m only the origin, and the same split in degrees (0.02 vs 0.001).
  • On PostgreSQL, a linestring from [-60, 60] to [60, 60] is returned by a 20 km query at [0, 74]. That point sits on the great-circle arc and outside the straight segment, so a planar prefilter would drop it.
  • On PostgreSQL, EXPLAIN of the adapter's meter and degree distanceLessThan predicates must use the spatial index rather than the primary key. Sequential scans are disabled so a two-row table still reveals whether the predicate can use the index. The statement targets this test's collection, resolved from the project, database, and collection sequences, so a parallel run cannot delete another table out from under the plan.
  • distanceGreaterThan, distanceEqual, and distanceNotEqual row results are unchanged.

Related PRs and Issues

Checklist

  • Have you read the Contributing Guidelines on issues?
  • If the PR includes a change to an API's metadata (desc, label, params, etc.), does it also include updated API specs and example docs?
Open in Web Open in Cursor 

PostGIS cannot serve ST_Distance comparisons from a GIST index, so radius
queries walked the primary key. distanceLessThan now leads with ST_DWithin
on the geometry column, which the existing spatial index can serve, and
keeps the strict meter comparison.

Fixes #14177
@tenki-reviewer

tenki-reviewer Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Review complete. 🟠 1 high

💬 Inline comments (1)

🧹 Nitpicks (1) — 🟢 1 low
  • 🟢 Antimeridian check skips ring-closing edge (Postgres.php:97) — degreesCoveringMeters detects antimeridian-crossing geometry only via consecutive vertex pairs (src/Appwrite/Database/Adapter/Postgres.php:104), but a ring's boundary also includes the closing edge from its last vertex back to its first, which is never checked.

This PR adds a project-level Postgres adapter that overrides handleDistanceSpatialQueries to prepend an index-eligible ST_DWithin radius prefilter before the inherited exact ST_Distance comparison, computing a conservative degree bounding radius from meters with pole and antimeridian fallbacks. The DI binding in app/init/registers.php now instantiates the subclass, and unit tests cover the SQL generation and fallback paths.

Files Change
src/Appwrite/Database/Adapter/Postgres.php New adapter subclass adding the ST_DWithin prefilter with degrees-covering-meters math, antimeridian and pole fallbacks to the parent exact-distance query.
app/init/registers.php Swaps the Postgres adapter binding from Utopia\Database\Adapter\Postgres to the new Appwrite\Database\Adapter\Postgres subclass.
tests/unit/Database/Adapter/PostgresTest.php Unit tests for the generated SQL, bind placeholders, and fallback behavior of the new adapter.

Reviewed commit: af8d91d

@hansi-codes

hansi-codes Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

🟢 Tier S · Ready to merge

The latest changes resolve the parallel-test table-discovery race, and no new defects were found.

Pins utopia-php/database to the PostgreSQL distance-index fix and locks it to commit c5fc677. Extends spatial E2E coverage with degree-distance results, a geodesic linestring regression, and PostgreSQL query-plan checks scoped to the current test collection.

Latest changes: Replaces broad PostgreSQL table discovery with project, database, and collection metadata lookups, including shared-table namespace and tenant handling.

Verdict New comments Fixed Still open
✅ Approved 0 1 0
📂 Walkthrough · 3
File Change
composer.json Pins the database distance-index fix branch with a 7.4.999 version alias.
composer.lock Locks the database dependency to the specified distance-index fix commit.
tests/e2e/Services/Databases/DatabasesBase.php Adds spatial distance regressions and index-plan assertions, resolving only the current test's collection through metadata.
✅ Fixed since the last review · 1
  • Restrict plan checks to this test's collection · tests/e2e/Services/Databases/DatabasesBase.php:10859

Reviewed the commits since be44085 · Details · Comment @hansi-codes review to re-run, or mention @hansi-codes with a question.

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Tier B · 1 blocking finding to address. Summary

Comment thread src/Appwrite/Database/Adapter/Postgres.php Outdated
Comment thread src/Appwrite/Database/Adapter/Postgres.php Outdated
Comment thread src/Appwrite/Database/Adapter/Postgres.php Outdated
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

Security rules

No new WARNING or ERROR findings from security rules.

32 existing findings tracked in .semgrep/baseline.json
  • php.appwrite.guest-write-without-abuse-limit (17)
  • php.appwrite.permissive-write-permission (8)
  • php.appwrite.secret-compare-timing (5)
  • php.appwrite.weak-secret-env-default (2)

Posted by Checks / Rules. Re-runs update this comment in place. Rule details and baseline: .semgrep/README.md.

@tenki-reviewer tenki-reviewer Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Introduces an Appwrite Postgres adapter subclass that adds a GIST-index-servable ST_DWithin bounding prefilter to distance queries, and rebinds the Postgres adapter in DI registrations.

Key findings

Comment on lines +113 to +123
$latitude = min(89.9, $maxAbsLatitude + $latitudeDelta);
$cosine = cos(deg2rad($latitude));
if ($cosine < 0.0001) {
$cosine = 0.0001;
}

$longitudeDelta = $meters / (self::LONGITUDE_METERS_PER_DEGREE * $cosine);
$degrees = hypot($latitudeDelta, $longitudeDelta) * self::DEGREE_MARGIN;

if ($minLongitude - $degrees < -180.0 || $maxLongitude + $degrees > 180.0) {
return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 bug · high

Latitude clamp breaks superset near poles

The clamp $latitude = min(89.9, $maxAbsLatitude + $latitudeDelta) moves the cosine input the wrong way (src/Appwrite/Database/Adapter/Postgres.php:113). Cosine shrinks toward 90°, so clamping a worst-case latitude above 89.9° down to 89.9° makes longitudeDelta (line 119) smaller than the true longitudinal span of the meter circle there — at 89.99° it is ~10x too small. A row within the geodesic meter radius can then lie outside the planar ST_DWithin bound and be silently dropped before the exact geography ST_Distance filter runs, so distanceLessThan(..., meters: true) misses matching documents. Additionally, when $maxAbsLatitude + $latitudeDelta >= 90 the meter circle encloses a pole and the matching region wraps every longitude, which no planar degree box can contain; the correct result there is the null exact-distance fallback, not a clamped box.

📋 Prompt for AI Agents

In src/Appwrite/Database/Adapter/Postgres.php, in degreesCoveringMeters() around lines 112-120: remove the min(89.9, ...) clamp and instead return null when $maxAbsLatitude + $latitudeDelta >= 90.0 (the meter circle reaches a pole, so a planar degree box cannot bound it and the parent's exact geography ST_Distance fallback must be used). Otherwise compute $latitude = $maxAbsLatitude + $latitudeDelta directly — it is then guaranteed below 90°, so cos(deg2rad($latitude)) is positive and the computed degree radius is a genuine superset of the meter radius. Without this, geodesic matches near the poles (e.g. vertex at lat 89.95 lon 0 vs a row at lat 89.95 lon 8, ~0.78 km apart) are excluded by the ST_DWithin prefilter while the parent ST_Distance predicate matches them, silently dropping rows from distanceLessThan queries.

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

✨ Benchmark results

Comparing main (before) → cursor/spatial-index-distance-eefb (after).

Metric Before After Change
🚀 Requests/sec 190.26 205.82 🟢 +8.2%
⏱️ Latency P50 91.14 ms 84.97 ms 🟢 -6.8%
⏱️ Latency P95 212.37 ms 196.31 ms 🟢 -7.6%
Per-scenario breakdown & investigation details

Metrics below reflect the current branch (after). Δ P95 compares against the base.

Scenario P50 (ms) P95 (ms) Requests RPS Δ P95 (ms)
API total 84.97 196.31 12,768 205.82 -16.06
Account 157.22 297.65 672 11.16 -15.48
TablesDB 82.54 153.19 6,944 113.51 -11.95
Storage 76.98 168.14 3,360 57.07 -14.15
Functions 119.62 228.48 1,792 31.14 -26.82

Top API waits (after)

API request Max wait (ms)
account.name.update 442.81
account.prefs.update 429.8
account.get 356.35
functions.variables.update 342.03
storage.buckets.create 333.73

A planar degree radius around a line or polygon misses geodesic edges, and
clamping latitude near a pole made that radius too small. Point queries
still use ST_DWithin on the geometry index. Lines, polygons, polar caps,
and the antimeridian keep the exact geography comparison.

Fixes #14177
A stored line or polygon can sit closer on the geodesic than on the
planar segment, so a degree radius around a point query can drop those
rows. Meter queries use ST_DWithin only when the column is a point too.
Degree queries stay on ST_DWithin for every geometry type.

Fixes #14177
…atabase

The Postgres adapter in utopia-php/database now leads distanceLessThan
with an index-served predicate: ST_DWithin without meters, and a degree
box around point columns with meters. Drop the Appwrite subclass, whose
degree radius dropped valid rows near the poles and around geodesic
edges, and pin the library branch until it is released.

Fixes #14177

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Tier A · Looks good to merge. Summary

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Tier S · Looks good to merge. Summary

The index predicate now comes from the pinned database package.
Keep the SQL contract here so a point query still uses the geometry
index, and a line or polygon on either side stays on geography distance.

Fixes #14177

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Tier A · Looks good to merge. Summary

Comment thread tests/unit/Database/Adapter/PostgresTest.php Outdated
@cursor
cursor Bot force-pushed the cursor/spatial-index-distance-eefb branch from 604268c to f94986f Compare October 7, 2026 09:48

Copy link
Copy Markdown
Member Author

@hansi-codes review

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Tier A · Looks good to merge. Summary

The unit suite was locking the adapter's SQL text. Distance results stay
on the existing spatial end-to-end test, including a geodesic line that a
planar prefilter would drop. On PostgreSQL, EXPLAIN must use the spatial
index for both meter and degree distanceLessThan.

Fixes #14177

Copy link
Copy Markdown
Member Author

@hansi-codes review

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Tier A · Looks good to merge. Summary

Comment thread tests/e2e/Services/Databases/DatabasesBase.php Outdated

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Tier A · Looks good to merge. Summary

The plan check scanned every table with loc and _uid, including parallel
runs that also use document id p0. Resolve this collection from project
metadata and EXPLAIN that table only.

Fixes #14177

Copy link
Copy Markdown
Member Author

@hansi-codes review

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Tier S · Looks good to merge. Summary

@hansi-codes

hansi-codes Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Already reviewed 9376e93. No new commits since then, so I didn't post another review.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐛 Bug Report: Distance queries never use the spatial index

1 participant