fixes
This commit is contained in:
+88
-57
@@ -1,4 +1,3 @@
|
||||
// src/api.rs
|
||||
use crate::db;
|
||||
use crate::types::{
|
||||
Config, LandingDto, LookupDto, MonthlyAggregate, PriceTrend, SpeciesDto, SummaryDto, TopSpecies,
|
||||
@@ -52,6 +51,9 @@ const MAX_LIMIT: u32 = 10000;
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct SummaryQuery {
|
||||
pub species: Option<String>,
|
||||
pub month: Option<String>,
|
||||
pub month_from: Option<String>,
|
||||
pub month_to: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -255,17 +257,38 @@ async fn get_summary(
|
||||
Query(params): Query<SummaryQuery>,
|
||||
) -> ApiResult<Json<SummaryDto>> {
|
||||
let conn = state.conn.clone();
|
||||
let species_filter = params.species.clone();
|
||||
|
||||
let result = tokio::task::spawn_blocking(move || -> db::Result<SummaryDto> {
|
||||
let conn = conn.blocking_lock();
|
||||
|
||||
let where_clause = if species_filter.is_some() {
|
||||
" AND species_code = ?".to_string()
|
||||
} else {
|
||||
let mut conditions: Vec<String> = Vec::new();
|
||||
let mut args: Vec<Box<dyn duckdb::ToSql>> = Vec::new();
|
||||
|
||||
if let Some(ref month) = params.month {
|
||||
conditions.push("month = ?".to_string());
|
||||
args.push(Box::new(month.clone()));
|
||||
}
|
||||
if let Some(ref month_from) = params.month_from {
|
||||
conditions.push("month >= ?".to_string());
|
||||
args.push(Box::new(month_from.clone()));
|
||||
}
|
||||
if let Some(ref month_to) = params.month_to {
|
||||
conditions.push("month <= ?".to_string());
|
||||
args.push(Box::new(month_to.clone()));
|
||||
}
|
||||
if let Some(ref species) = params.species {
|
||||
conditions.push("species_code = ?".to_string());
|
||||
args.push(Box::new(species.clone()));
|
||||
}
|
||||
|
||||
let where_clause = if conditions.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" AND {}", conditions.join(" AND "))
|
||||
};
|
||||
|
||||
let arg_refs: Vec<&dyn duckdb::ToSql> = args.iter().map(|b| b.as_ref()).collect();
|
||||
|
||||
let monthly_sql = format!(
|
||||
"SELECT month, \
|
||||
SUM(CASE WHEN measure_code = 'MASS' THEN value END) AS total_mass, \
|
||||
@@ -274,31 +297,16 @@ async fn get_summary(
|
||||
GROUP BY month ORDER BY month"
|
||||
);
|
||||
|
||||
let monthly: Vec<MonthlyAggregate> = if let Some(ref sp) = species_filter {
|
||||
let mut stmt = conn.prepare(&monthly_sql)?;
|
||||
let rows = stmt.query_map(params![sp], |row| {
|
||||
let mut stmt = conn.prepare(&monthly_sql)?;
|
||||
let monthly: Vec<MonthlyAggregate> = stmt
|
||||
.query_map(arg_refs.as_slice(), |row| {
|
||||
Ok(MonthlyAggregate {
|
||||
month: row.get(0)?,
|
||||
total_mass: row.get(1)?,
|
||||
total_value: row.get(2)?,
|
||||
})
|
||||
})?;
|
||||
Ok::<Vec<MonthlyAggregate>, duckdb::Error>(
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()?,
|
||||
)?
|
||||
} else {
|
||||
let mut stmt = conn.prepare(&monthly_sql)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(MonthlyAggregate {
|
||||
month: row.get(0)?,
|
||||
total_mass: row.get(1)?,
|
||||
total_value: row.get(2)?,
|
||||
})
|
||||
})?;
|
||||
Ok::<Vec<MonthlyAggregate>, duckdb::Error>(
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()?,
|
||||
)?
|
||||
};
|
||||
})?
|
||||
.collect::<std::result::Result<Vec<_>, duckdb::Error>>()?;
|
||||
|
||||
let top_sql = format!(
|
||||
"SELECT species_code, \
|
||||
@@ -311,29 +319,17 @@ async fn get_summary(
|
||||
LIMIT 10"
|
||||
);
|
||||
|
||||
let top_species: Vec<TopSpecies> = if let Some(ref sp) = species_filter {
|
||||
let mut stmt = conn.prepare(&top_sql)?;
|
||||
let rows = stmt.query_map(params![sp], |row| {
|
||||
let mut stmt = conn.prepare(&top_sql)?;
|
||||
let top_species: Vec<TopSpecies> = stmt
|
||||
.query_map(arg_refs.as_slice(), |row| {
|
||||
Ok(TopSpecies {
|
||||
species_code: row.get(0)?,
|
||||
species_label: row.get(1)?,
|
||||
total_value: row.get(2)?,
|
||||
total_mass: row.get(3)?,
|
||||
})
|
||||
})?;
|
||||
Ok::<Vec<TopSpecies>, duckdb::Error>(rows.collect::<std::result::Result<Vec<_>, _>>()?)?
|
||||
} else {
|
||||
let mut stmt = conn.prepare(&top_sql)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(TopSpecies {
|
||||
species_code: row.get(0)?,
|
||||
species_label: row.get(1)?,
|
||||
total_value: row.get(2)?,
|
||||
total_mass: row.get(3)?,
|
||||
})
|
||||
})?;
|
||||
Ok::<Vec<TopSpecies>, duckdb::Error>(rows.collect::<std::result::Result<Vec<_>, _>>()?)?
|
||||
};
|
||||
})?
|
||||
.collect::<std::result::Result<Vec<_>, duckdb::Error>>()?;
|
||||
|
||||
let price_sql = format!(
|
||||
"WITH monthly_mass AS ( \
|
||||
@@ -354,25 +350,23 @@ async fn get_summary(
|
||||
ORDER BY m.month"
|
||||
);
|
||||
|
||||
let price_trend: Vec<PriceTrend> = if let Some(ref sp) = species_filter {
|
||||
let mut stmt = conn.prepare(&price_sql)?;
|
||||
let rows = stmt.query_map(params![sp, sp], |row| {
|
||||
let mut price_args: Vec<&dyn duckdb::ToSql> = Vec::with_capacity(args.len() * 2);
|
||||
for a in &args {
|
||||
price_args.push(a.as_ref());
|
||||
}
|
||||
for a in &args {
|
||||
price_args.push(a.as_ref());
|
||||
}
|
||||
|
||||
let mut stmt = conn.prepare(&price_sql)?;
|
||||
let price_trend: Vec<PriceTrend> = stmt
|
||||
.query_map(price_args.as_slice(), |row| {
|
||||
Ok(PriceTrend {
|
||||
month: row.get(0)?,
|
||||
price_per_kg: row.get(1)?,
|
||||
})
|
||||
})?;
|
||||
Ok::<Vec<PriceTrend>, duckdb::Error>(rows.collect::<std::result::Result<Vec<_>, _>>()?)?
|
||||
} else {
|
||||
let mut stmt = conn.prepare(&price_sql)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(PriceTrend {
|
||||
month: row.get(0)?,
|
||||
price_per_kg: row.get(1)?,
|
||||
})
|
||||
})?;
|
||||
Ok::<Vec<PriceTrend>, duckdb::Error>(rows.collect::<std::result::Result<Vec<_>, _>>()?)?
|
||||
};
|
||||
})?
|
||||
.collect::<std::result::Result<Vec<_>, duckdb::Error>>()?;
|
||||
|
||||
Ok(SummaryDto {
|
||||
monthly,
|
||||
@@ -862,6 +856,43 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_summary_with_month_range_filter() {
|
||||
let state = test_state();
|
||||
let base = spawn_test_server(state).await;
|
||||
|
||||
let resp = reqwest::get(format!(
|
||||
"{base}/api/summary?month_from=2024M01&month_to=2024M01"
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let body: SummaryDto = resp.json().await.unwrap();
|
||||
|
||||
assert_eq!(body.monthly.len(), 1);
|
||||
assert_eq!(body.monthly[0].month, "2024M01");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_summary_with_combined_filters() {
|
||||
let state = test_state();
|
||||
let base = spawn_test_server(state).await;
|
||||
|
||||
let resp = reqwest::get(format!(
|
||||
"{base}/api/summary?month_from=2024M01&month_to=2024M01&species=COD"
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let body: SummaryDto = resp.json().await.unwrap();
|
||||
|
||||
assert_eq!(body.monthly.len(), 1);
|
||||
assert_eq!(body.monthly[0].month, "2024M01");
|
||||
assert!((body.monthly[0].total_mass.unwrap() - 1000.0).abs() < f64::EPSILON);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_landings_faroese_label_preserved() {
|
||||
let state = test_state();
|
||||
|
||||
Reference in New Issue
Block a user