From 82ed8d47f9c45c3ca3a28f58c66efdcf0e3640fd Mon Sep 17 00:00:00 2001 From: Rami Abdelrazzaq Date: Tue, 15 Sep 2026 15:14:29 -0500 Subject: [PATCH] Fix float coercion for numeric filter parameters (#2876) Closes #1681 --- datasette/filters.py | 16 ++++++++-- tests/test_numeric_filter_values.py | 45 +++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 tests/test_numeric_filter_values.py diff --git a/datasette/filters.py b/datasette/filters.py index 83e51165..0499d086 100644 --- a/datasette/filters.py +++ b/datasette/filters.py @@ -1,4 +1,5 @@ import json +import math from typing import ClassVar from datasette import hookimpl @@ -202,6 +203,17 @@ class Filter: raise NotImplementedError +def _coerce_numeric_filter_value(value): + try: + return int(value) + except ValueError: + try: + converted = float(value) + except ValueError: + return value + return converted if math.isfinite(converted) else value + + class TemplatedFilter(Filter): def __init__( self, @@ -223,8 +235,8 @@ class TemplatedFilter(Filter): def where_clause(self, table, column, value, param_counter): converted = self.format.format(value) - if self.numeric and converted.isdigit(): - converted = int(converted) + if self.numeric: + converted = _coerce_numeric_filter_value(converted) if self.no_argument: kwargs = {"c": _quote_sqlite_identifier(column)} converted = None diff --git a/tests/test_numeric_filter_values.py b/tests/test_numeric_filter_values.py new file mode 100644 index 00000000..69c5cf7e --- /dev/null +++ b/tests/test_numeric_filter_values.py @@ -0,0 +1,45 @@ +import sqlite3 + +import pytest + +from datasette.filters import Filters + + +@pytest.mark.parametrize( + "value,expected", + ( + ("3.5", 3.5), + ("-2", -2), + ("-2.5", -2.5), + ("1e3", 1000.0), + ("not-a-number", "not-a-number"), + ("nan", "nan"), + ("inf", "inf"), + ("-inf", "-inf"), + ), +) +def test_numeric_filter_parameters(value, expected): + filters = Filters((("score__gt", value),)) + sql_bits, params = filters.build_where_clauses("items") + + assert sql_bits == ['"score" > :p0'] + assert params == {"p0": expected} + + +def test_numeric_filter_parameters_against_calculated_view(): + conn = sqlite3.connect(":memory:") + conn.execute("create table searchable(pk integer)") + conn.executemany("insert into searchable(pk) values (?)", [(0,), (1,), (2,)]) + conn.execute( + "create view calculated as " + "select pk + 1 as pk_plus_one, pk / 2.0 as score from searchable" + ) + + sql_bits, params = Filters((("score__gt", "0.1"),)).build_where_clauses( + "calculated" + ) + rows = conn.execute( + "select score from calculated where {}".format(" and ".join(sql_bits)), params + ).fetchall() + + assert rows == [(0.5,), (1.0,)]