From 2e326b8cd72aea3bcfb3a3ffce2a706878b7ac56 Mon Sep 17 00:00:00 2001 From: Shautvast Date: Fri, 3 Apr 2026 17:47:31 +0200 Subject: [PATCH] one query for pois --- backend/src/models/poi.rs | 50 +++++++++++++++++++++++++++++++++++--- backend/src/routes/pois.rs | 46 +++++++---------------------------- 2 files changed, 56 insertions(+), 40 deletions(-) diff --git a/backend/src/models/poi.rs b/backend/src/models/poi.rs index 2db3655..058a249 100644 --- a/backend/src/models/poi.rs +++ b/backend/src/models/poi.rs @@ -16,10 +16,21 @@ pub struct PoiRow { pub wheelchair: Option, } -/// Count row for total matching POIs. +/// POI row with window-function total (replaces separate COUNT query). #[derive(Debug, sqlx::FromRow)] -pub struct CountRow { - pub count: Option, +pub struct PoiRowWithTotal { + pub osm_id: i64, + pub osm_type: String, + pub name: String, + pub category: String, + pub geometry: serde_json::Value, + pub address: Option, + pub tags: Option, + pub opening_hours: Option, + pub phone: Option, + pub website: Option, + pub wheelchair: Option, + pub total: Option, } /// Address sub-object in the API response. @@ -86,6 +97,39 @@ pub struct PaginationMetadata { } /// Converts a database row into a GeoJSON Feature. +impl PoiRowWithTotal { + pub fn into_feature(self) -> PoiFeature { + let address: Option = self + .address + .as_ref() + .and_then(|v| serde_json::from_value(v.clone()).ok()); + + let opening_hours_parsed = self.opening_hours.as_ref().map(|_oh| OpeningHoursParsed { + is_open: true, + today: None, + next_change: None, + }); + + PoiFeature { + r#type: "Feature".into(), + geometry: self.geometry, + properties: PoiProperties { + osm_id: self.osm_id, + osm_type: self.osm_type, + name: self.name, + category: self.category, + address, + opening_hours: self.opening_hours, + opening_hours_parsed, + phone: self.phone, + website: self.website, + wheelchair: self.wheelchair, + tags: self.tags, + }, + } + } +} + impl PoiRow { pub fn into_feature(self) -> PoiFeature { let address: Option = self diff --git a/backend/src/routes/pois.rs b/backend/src/routes/pois.rs index 6647b76..2a25065 100644 --- a/backend/src/routes/pois.rs +++ b/backend/src/routes/pois.rs @@ -72,43 +72,13 @@ pub async fn list_pois( None }; - // Build and execute the count query - let total = if let Some(ref cats) = categories { - sqlx::query_as::<_, CountRow>( - "SELECT COUNT(*) as count FROM pois \ - WHERE geometry && ST_MakeEnvelope($1, $2, $3, $4, 4326) \ - AND category = ANY($5)", - ) - .bind(min_lon) - .bind(min_lat) - .bind(max_lon) - .bind(max_lat) - .bind(cats) - .fetch_one(pool.get_ref()) - .await? - .count - .unwrap_or(0) - } else { - sqlx::query_as::<_, CountRow>( - "SELECT COUNT(*) as count FROM pois \ - WHERE geometry && ST_MakeEnvelope($1, $2, $3, $4, 4326)", - ) - .bind(min_lon) - .bind(min_lat) - .bind(max_lon) - .bind(max_lat) - .fetch_one(pool.get_ref()) - .await? - .count - .unwrap_or(0) - }; - - // Build and execute the data query - let rows: Vec = if let Some(ref cats) = categories { - sqlx::query_as::<_, PoiRow>( + // Single query: window function computes total within the same spatial scan. + let rows: Vec = if let Some(ref cats) = categories { + sqlx::query_as::<_, PoiRowWithTotal>( "SELECT osm_id, osm_type, name, category, \ ST_AsGeoJSON(geometry)::json AS geometry, \ - address, tags, opening_hours, phone, website, wheelchair \ + address, tags, opening_hours, phone, website, wheelchair, \ + COUNT(*) OVER() AS total \ FROM pois \ WHERE geometry && ST_MakeEnvelope($1, $2, $3, $4, 4326) \ AND category = ANY($5) \ @@ -125,10 +95,11 @@ pub async fn list_pois( .fetch_all(pool.get_ref()) .await? } else { - sqlx::query_as::<_, PoiRow>( + sqlx::query_as::<_, PoiRowWithTotal>( "SELECT osm_id, osm_type, name, category, \ ST_AsGeoJSON(geometry)::json AS geometry, \ - address, tags, opening_hours, phone, website, wheelchair \ + address, tags, opening_hours, phone, website, wheelchair, \ + COUNT(*) OVER() AS total \ FROM pois \ WHERE geometry && ST_MakeEnvelope($1, $2, $3, $4, 4326) \ ORDER BY name \ @@ -144,6 +115,7 @@ pub async fn list_pois( .await? }; + let total = rows.first().and_then(|r| r.total).unwrap_or(0); let features: Vec = rows.into_iter().map(|r| r.into_feature()).collect(); let response = PoiFeatureCollection {