diff --git a/CHANGELOG.md b/CHANGELOG.md index 28711b3b4..01f15d9ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed +- Fixed "list index out of range" error when using BETWEEN operator in CQL2-text filters. [#521](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/521) + ### Removed ### Updated diff --git a/stac_fastapi/core/stac_fastapi/core/core.py b/stac_fastapi/core/stac_fastapi/core/core.py index 0c3beedc5..42106761a 100644 --- a/stac_fastapi/core/stac_fastapi/core/core.py +++ b/stac_fastapi/core/stac_fastapi/core/core.py @@ -830,7 +830,7 @@ async def post_search( search = await self.database.apply_cql2_filter(search, cql2_filter) except Exception as e: raise HTTPException( - status_code=400, detail=f"Error with cql2_json filter: {e}" + status_code=400, detail=f"Error with cql2 filter: {e}" ) if hasattr(search_request, "q"): diff --git a/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/transform.py b/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/transform.py index c78b19e59..6945a359e 100644 --- a/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/transform.py +++ b/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter/transform.py @@ -92,7 +92,19 @@ def to_es(queryables_mapping: Dict[str, Any], query: Dict[str, Any]) -> Dict[str elif query["op"] == AdvancedComparisonOp.BETWEEN: field = to_es_field(queryables_mapping, query["args"][0]["property"]) - gte, lte = query["args"][1], query["args"][2] + + # Handle both formats: [property, [lower, upper]] or [property, lower, upper] + if len(query["args"]) == 2 and isinstance(query["args"][1], list): + # Format: [{'property': '...'}, [lower, upper]] + gte, lte = query["args"][1][0], query["args"][1][1] + elif len(query["args"]) == 3: + # Format: [{'property': '...'}, lower, upper] + gte, lte = query["args"][1], query["args"][2] + else: + raise ValueError( + f"BETWEEN operator expects 2 or 3 args, got {len(query['args'])}" + ) + if isinstance(gte, dict) and "timestamp" in gte: gte = gte["timestamp"] if isinstance(lte, dict) and "timestamp" in lte: diff --git a/stac_fastapi/tests/extensions/test_filter.py b/stac_fastapi/tests/extensions/test_filter.py index d60a13be1..e597f285f 100644 --- a/stac_fastapi/tests/extensions/test_filter.py +++ b/stac_fastapi/tests/extensions/test_filter.py @@ -410,7 +410,7 @@ async def test_search_filter_extension_in_no_list(app_client, ctx): assert resp.status_code == 400 assert resp.json() == { - "detail": f"Error with cql2_json filter: Arg {product_id} is not a list" + "detail": f"Error with cql2 filter: Arg {product_id} is not a list" } @@ -440,6 +440,24 @@ async def test_search_filter_extension_between(app_client, ctx): assert len(resp.json()["features"]) == 1 +@pytest.mark.asyncio +async def test_search_filter_extension_between_get(app_client, ctx): + """Test BETWEEN operator with GET request using CQL2-text format.""" + sun_elevation = ctx.item["properties"]["view:sun_elevation"] + lower_bound = sun_elevation - 0.01 + upper_bound = sun_elevation + 0.01 + + # Use CQL2-text format for GET request + filter_expr = f"properties.view:sun_elevation BETWEEN {lower_bound} AND {upper_bound} AND id = '{ctx.item['id']}'" + + resp = await app_client.get( + "/search", params={"filter": filter_expr, "filter_lang": "cql2-text"} + ) + + assert resp.status_code == 200 + assert len(resp.json()["features"]) == 1 + + @pytest.mark.asyncio async def test_search_filter_extension_isnull_post(app_client, ctx): # Test for a property that is not null