From bcb3556bf60c1f75cbdf8af59862874132b07241 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Fri, 20 Feb 2026 07:26:29 -0600 Subject: [PATCH] Use PostGIS GiST index for spatial bounds queries --- lib/aprsme/packets/query_builder.ex | 11 ++-- test/aprsme/packets/query_builder_test.exs | 75 +++++++++++++--------- 2 files changed, 49 insertions(+), 37 deletions(-) diff --git a/lib/aprsme/packets/query_builder.ex b/lib/aprsme/packets/query_builder.ex index e8615c4..9617f4d 100644 --- a/lib/aprsme/packets/query_builder.ex +++ b/lib/aprsme/packets/query_builder.ex @@ -121,17 +121,18 @@ defmodule Aprsme.Packets.QueryBuilder do @spec within_bounds(Ecto.Query.t(), list(number())) :: Ecto.Query.t() def within_bounds(query, [west, south, east, north]) when is_number(west) and is_number(south) and is_number(east) and is_number(north) do - # Handle antimeridian crossing (e.g., west=170, east=-170) + # Use ST_MakeEnvelope with && operator to leverage GiST spatial index on location column if west > east do + # Handle antimeridian crossing with two envelopes: [west, 180] and [-180, east] from p in query, where: p.has_position == true, - where: p.lat >= ^south and p.lat <= ^north, - where: p.lon >= ^west or p.lon <= ^east + where: + fragment("? && ST_MakeEnvelope(?, ?, 180, ?, 4326)", p.location, ^west, ^south, ^north) or + fragment("? && ST_MakeEnvelope(-180, ?, ?, ?, 4326)", p.location, ^south, ^east, ^north) else from p in query, where: p.has_position == true, - where: p.lat >= ^south and p.lat <= ^north, - where: p.lon >= ^west and p.lon <= ^east + where: fragment("? && ST_MakeEnvelope(?, ?, ?, ?, 4326)", p.location, ^west, ^south, ^east, ^north) end end diff --git a/test/aprsme/packets/query_builder_test.exs b/test/aprsme/packets/query_builder_test.exs index 9241ce9..bc8d098 100644 --- a/test/aprsme/packets/query_builder_test.exs +++ b/test/aprsme/packets/query_builder_test.exs @@ -5,26 +5,22 @@ defmodule Aprsme.Packets.QueryBuilderTest do alias Aprsme.Packet alias Aprsme.Packets.QueryBuilder + alias Aprsme.PacketsFixtures describe "within_bounds/2" do test "filters packets within normal bounds" do - # Create test packets - {:ok, inside} = - Aprsme.Repo.insert(%Packet{ + inside = + PacketsFixtures.packet_fixture(%{ sender: "TEST-1", - lat: 40.5, - lon: -73.5, - has_position: true, - received_at: DateTime.truncate(DateTime.utc_now(), :second) + lat: Decimal.new("40.5"), + lon: Decimal.new("-73.5") }) - {:ok, _outside} = - Aprsme.Repo.insert(%Packet{ + _outside = + PacketsFixtures.packet_fixture(%{ sender: "TEST-2", - lat: 50.0, - lon: -73.5, - has_position: true, - received_at: DateTime.truncate(DateTime.utc_now(), :second) + lat: Decimal.new("50.0"), + lon: Decimal.new("-73.5") }) # Test normal bounds [west, south, east, north] @@ -36,32 +32,25 @@ defmodule Aprsme.Packets.QueryBuilderTest do end test "handles antimeridian crossing bounds" do - # Create test packets - {:ok, west_side} = - Aprsme.Repo.insert(%Packet{ + west_side = + PacketsFixtures.packet_fixture(%{ sender: "WEST-1", - lat: 0.0, - lon: 175.0, - has_position: true, - received_at: DateTime.truncate(DateTime.utc_now(), :second) + lat: Decimal.new("0.0"), + lon: Decimal.new("175.0") }) - {:ok, east_side} = - Aprsme.Repo.insert(%Packet{ + east_side = + PacketsFixtures.packet_fixture(%{ sender: "EAST-1", - lat: 0.0, - lon: -175.0, - has_position: true, - received_at: DateTime.truncate(DateTime.utc_now(), :second) + lat: Decimal.new("0.0"), + lon: Decimal.new("-175.0") }) - {:ok, _middle} = - Aprsme.Repo.insert(%Packet{ + _middle = + PacketsFixtures.packet_fixture(%{ sender: "MIDDLE-1", - lat: 0.0, - lon: 0.0, - has_position: true, - received_at: DateTime.truncate(DateTime.utc_now(), :second) + lat: Decimal.new("0.0"), + lon: Decimal.new("0.0") }) # Test antimeridian crossing bounds [west=170, south=-10, east=-170, north=10] @@ -87,4 +76,26 @@ defmodule Aprsme.Packets.QueryBuilderTest do assert QueryBuilder.within_bounds(original_query, ["a", "b", "c", "d"]) == original_query end end + + describe "weather_only/1" do + test "filters to weather packets using has_weather column" do + _weather = + PacketsFixtures.packet_fixture(%{ + sender: "WX-QBT", + temperature: 72.0, + has_weather: true + }) + + _non_weather = + PacketsFixtures.packet_fixture(%{ + sender: "NOWX-QBT", + comment: "No weather" + }) + + query = QueryBuilder.weather_only(Packet) + results = Aprsme.Repo.all(query) + + assert Enum.all?(results, fn p -> p.has_weather == true end) + end + end end