diff --git a/herbapi-api/src/db/species.rs b/herbapi-api/src/db/species.rs index 33067f5..dcdb8f1 100644 --- a/herbapi-api/src/db/species.rs +++ b/herbapi-api/src/db/species.rs @@ -10,6 +10,13 @@ pub struct SpeciesListParams { pub per_page: Option, pub search: Option, pub family: Option, + pub plant_layer: Option, + pub nitrogen_fixer: Option, + pub dynamic_accumulator: Option, + pub drought_tolerance: Option, + pub native_status: Option, + pub min_nectar: Option, + pub min_bees: Option, } impl SpeciesListParams { @@ -21,74 +28,57 @@ pub async fn list(pool: &PgPool, params: &SpeciesListParams) -> Result { - let tsquery = search.split_whitespace().collect::>().join(" & "); - let rows = sqlx::query_as::<_, Species>( - "SELECT s.* FROM species s JOIN families f ON s.family_id = f.id - WHERE f.slug = $1 - AND to_tsvector('english', coalesce(s.name_scientific,'') || ' ' || coalesce(s.name_en,'') || ' ' || coalesce(s.name_de,'') || ' ' || coalesce(s.description,'')) - @@ to_tsquery('english', $2) - ORDER BY s.name_scientific LIMIT $3 OFFSET $4" - ) - .bind(family_slug).bind(&tsquery).bind(limit).bind(offset) - .fetch_all(pool).await?; + // Always bind all 11 params ($1-$11) using IS NULL pattern for inactive filters. + // $1=family, $2=search, $3=plant_layer, $4=nitrogen_fixer, $5=dynamic_accumulator, + // $6=drought_tolerance, $7=native_status, $8=min_nectar, $9=min_bees, $10=limit, $11=offset + let tsquery = params.search.as_ref().map(|s| s.split_whitespace().collect::>().join(" & ")); + let family = params.family.clone(); + let search = tsquery.clone(); + let layer = params.plant_layer.clone(); + let nfixer = params.nitrogen_fixer; + let dynacc = params.dynamic_accumulator; + let drought = params.drought_tolerance.clone(); + let native = params.native_status.clone(); + let nectar = params.min_nectar; + let bees = params.min_bees; - let (count,): (i64,) = sqlx::query_as( - "SELECT COUNT(*) FROM species s JOIN families f ON s.family_id = f.id - WHERE f.slug = $1 - AND to_tsvector('english', coalesce(s.name_scientific,'') || ' ' || coalesce(s.name_en,'') || ' ' || coalesce(s.name_de,'') || ' ' || coalesce(s.description,'')) - @@ to_tsquery('english', $2)" - ) - .bind(family_slug).bind(&tsquery) - .fetch_one(pool).await?; - (rows, count) - } - (Some(family_slug), None) => { - let rows = sqlx::query_as::<_, Species>( - "SELECT s.* FROM species s JOIN families f ON s.family_id = f.id - WHERE f.slug = $1 ORDER BY s.name_scientific LIMIT $2 OFFSET $3" - ) - .bind(family_slug).bind(limit).bind(offset) - .fetch_all(pool).await?; + let data_sql = "SELECT s.* FROM species s LEFT JOIN families f ON s.family_id = f.id WHERE \ + ($1::text IS NULL OR f.slug = $1) AND \ + ($2::text IS NULL OR to_tsvector('english', coalesce(s.name_scientific,'') || ' ' || coalesce(s.name_en,'') || ' ' || coalesce(s.name_de,'') || ' ' || coalesce(s.description,'')) @@ to_tsquery('english', $2)) AND \ + ($3::text IS NULL OR s.plant_layer = $3) AND \ + ($4::bool IS NULL OR s.nitrogen_fixer = $4) AND \ + ($5::bool IS NULL OR s.dynamic_accumulator = $5) AND \ + ($6::text IS NULL OR s.drought_tolerance = $6) AND \ + ($7::text IS NULL OR s.native_status = $7) AND \ + ($8::smallint IS NULL OR s.nectar_value >= $8) AND \ + ($9::integer IS NULL OR s.wild_bee_count >= $9) \ + ORDER BY s.name_scientific LIMIT $10 OFFSET $11"; - let (count,): (i64,) = sqlx::query_as( - "SELECT COUNT(*) FROM species s JOIN families f ON s.family_id = f.id WHERE f.slug = $1" - ) - .bind(family_slug).fetch_one(pool).await?; - (rows, count) - } - (None, Some(search)) => { - let tsquery = search.split_whitespace().collect::>().join(" & "); - let rows = sqlx::query_as::<_, Species>( - "SELECT * FROM species - WHERE to_tsvector('english', coalesce(name_scientific,'') || ' ' || coalesce(name_en,'') || ' ' || coalesce(name_de,'') || ' ' || coalesce(description,'')) - @@ to_tsquery('english', $1) - ORDER BY name_scientific LIMIT $2 OFFSET $3" - ) - .bind(&tsquery).bind(limit).bind(offset) - .fetch_all(pool).await?; + let count_sql = "SELECT COUNT(*) FROM species s LEFT JOIN families f ON s.family_id = f.id WHERE \ + ($1::text IS NULL OR f.slug = $1) AND \ + ($2::text IS NULL OR to_tsvector('english', coalesce(s.name_scientific,'') || ' ' || coalesce(s.name_en,'') || ' ' || coalesce(s.name_de,'') || ' ' || coalesce(s.description,'')) @@ to_tsquery('english', $2)) AND \ + ($3::text IS NULL OR s.plant_layer = $3) AND \ + ($4::bool IS NULL OR s.nitrogen_fixer = $4) AND \ + ($5::bool IS NULL OR s.dynamic_accumulator = $5) AND \ + ($6::text IS NULL OR s.drought_tolerance = $6) AND \ + ($7::text IS NULL OR s.native_status = $7) AND \ + ($8::smallint IS NULL OR s.nectar_value >= $8) AND \ + ($9::integer IS NULL OR s.wild_bee_count >= $9)"; - let (count,): (i64,) = sqlx::query_as( - "SELECT COUNT(*) FROM species - WHERE to_tsvector('english', coalesce(name_scientific,'') || ' ' || coalesce(name_en,'') || ' ' || coalesce(name_de,'') || ' ' || coalesce(description,'')) - @@ to_tsquery('english', $1)" - ) - .bind(&tsquery).fetch_one(pool).await?; - (rows, count) - } - (None, None) => { - let rows = sqlx::query_as::<_, Species>( - "SELECT * FROM species ORDER BY name_scientific LIMIT $1 OFFSET $2" - ) - .bind(limit).bind(offset) - .fetch_all(pool).await?; + let rows: Vec = sqlx::query_as(data_sql) + .bind(family.as_deref()).bind(search.as_deref()) + .bind(layer.as_deref()).bind(nfixer).bind(dynacc) + .bind(drought.as_deref()).bind(native.as_deref()) + .bind(nectar).bind(bees) + .bind(limit).bind(offset) + .fetch_all(pool).await?; - let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM species") - .fetch_one(pool).await?; - (rows, count) - } - }; + let (total,): (i64,) = sqlx::query_as(count_sql) + .bind(family.as_deref()).bind(search.as_deref()) + .bind(layer.as_deref()).bind(nfixer).bind(dynacc) + .bind(drought.as_deref()).bind(native.as_deref()) + .bind(nectar).bind(bees) + .fetch_one(pool).await?; Ok(PaginatedResponse { data: rows,