Simplify species list filtering with IS NULL pattern

Replaced dynamic WHERE clause builder + macro with static SQL using
($N::type IS NULL OR column = $N) pattern. Always binds all 9 filter
params + limit/offset. Supports: family, search, plant_layer,
nitrogen_fixer, dynamic_accumulator, drought_tolerance, native_status,
min_nectar, min_bees.
This commit is contained in:
2026-03-15 00:58:47 +01:00
parent 97f651572b
commit 14b63f00af
+54 -64
View File
@@ -10,6 +10,13 @@ pub struct SpeciesListParams {
pub per_page: Option<i64>,
pub search: Option<String>,
pub family: Option<String>,
pub plant_layer: Option<String>,
pub nitrogen_fixer: Option<bool>,
pub dynamic_accumulator: Option<bool>,
pub drought_tolerance: Option<String>,
pub native_status: Option<String>,
pub min_nectar: Option<i16>,
pub min_bees: Option<i32>,
}
impl SpeciesListParams {
@@ -21,74 +28,57 @@ pub async fn list(pool: &PgPool, params: &SpeciesListParams) -> Result<Paginated
let limit = params.limit();
let offset = params.offset();
let (rows, total) = match (&params.family, &params.search) {
(Some(family_slug), Some(search)) => {
let tsquery = search.split_whitespace().collect::<Vec<_>>().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::<Vec<_>>().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::<Vec<_>>().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<Species> = 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,