From 5fbc809417e1077deb2eac3f621d270819cf7ff2 Mon Sep 17 00:00:00 2001 From: DG-Elom Date: Wed, 14 Oct 2026 12:02:25 +0200 Subject: [PATCH 1/2] Exact filters match integer columns without type affinity, closes #2073 --- datasette/filters.py | 20 +++++++++++++++++++- tests/test_facets.py | 22 ++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/datasette/filters.py b/datasette/filters.py index 0499d086..4754b676 100644 --- a/datasette/filters.py +++ b/datasette/filters.py @@ -259,6 +259,24 @@ class TemplatedFilter(Filter): return template.format(c=column, v=value) +class ExactFilter(TemplatedFilter): + def where_clause(self, table, column, value, param_counter): + sql, param = super().where_clause(table, column, value, param_counter) + try: + as_int = int(value) + except ValueError: + return sql, param + if str(as_int) != value: + return sql, param + # Columns without type affinity (such as expressions in a view) do not + # convert "1" to 1 for the comparison, so match both representations + quoted = _quote_sqlite_identifier(column) + return ( + f"{quoted} in (:p{param_counter}, :p{param_counter + 1})", + [value, as_int], + ) + + def _quote_sqlite_identifier(identifier): # Preserve the historic always-quoted SQL generated by TemplatedFilter. escaped = escape_sqlite(identifier) @@ -305,7 +323,7 @@ class Filters: _filters = ( [ # key, display, sql_template, human_template, format=, numeric=, no_argument= - TemplatedFilter( + ExactFilter( "exact", "=", "{c} = :{p}", diff --git a/tests/test_facets.py b/tests/test_facets.py index 10325f9a..03a39fe9 100644 --- a/tests/test_facets.py +++ b/tests/test_facets.py @@ -829,3 +829,25 @@ async def test_facet_only_considers_first_x_rows(): assert data2["suggested_facets"] == [] finally: Facet.suggest_consider = original_suggest_consider + + +@pytest.mark.asyncio +async def test_facet_selection_against_integer_column_in_view(): + # https://github.com/simonw/datasette/issues/2073 + # View columns computed from expressions have no column affinity, so + # comparing them to the string "1" from the querystring does not match 1 + ds = Datasette() + db = ds.add_memory_database("test_facet_integer_view") + await db.execute_write("create table t (id integer primary key, n integer)") + await db.execute_write_many( + "insert into t (n) values (:n)", [{"n": 1}, {"n": 1}, {"n": 2}] + ) + await db.execute_write("create view v as select id, n + 0 as n from t") + response = await ds.client.get( + "/test_facet_integer_view/v.json?_facet=n&n=1&_extra=facet_results" + ) + assert response.status_code == 200 + data = response.json() + assert [row["id"] for row in data["rows"]] == [1, 2] + results = data["facet_results"]["results"]["n"]["results"] + assert [(r["value"], r["count"], r["selected"]) for r in results] == [(1, 2, True)] From 912c24fb0acd0a063235d7fb2a21fc41c14e0715 Mon Sep 17 00:00:00 2001 From: DG-Elom Date: Wed, 14 Oct 2026 12:11:11 +0200 Subject: [PATCH 2/2] Skip integer coercion outside SQLite 64-bit range in exact filter --- datasette/filters.py | 2 +- tests/test_facets.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/datasette/filters.py b/datasette/filters.py index 4754b676..b4a6fb09 100644 --- a/datasette/filters.py +++ b/datasette/filters.py @@ -266,7 +266,7 @@ class ExactFilter(TemplatedFilter): as_int = int(value) except ValueError: return sql, param - if str(as_int) != value: + if str(as_int) != value or not -(2**63) <= as_int < 2**63: return sql, param # Columns without type affinity (such as expressions in a view) do not # convert "1" to 1 for the comparison, so match both representations diff --git a/tests/test_facets.py b/tests/test_facets.py index 03a39fe9..0effeec4 100644 --- a/tests/test_facets.py +++ b/tests/test_facets.py @@ -851,3 +851,17 @@ async def test_facet_selection_against_integer_column_in_view(): assert [row["id"] for row in data["rows"]] == [1, 2] results = data["facet_results"]["results"]["n"]["results"] assert [(r["value"], r["count"], r["selected"]) for r in results] == [(1, 2, True)] + + +@pytest.mark.asyncio +async def test_exact_filter_integer_outside_sqlite_range(): + # Values beyond SQLite 64-bit integers must not be bound as integers + ds = Datasette() + db = ds.add_memory_database("test_exact_filter_big_int") + await db.execute_write("create table t (id integer primary key, code text)") + await db.execute_write("insert into t (code) values (?)", ["9223372036854775808"]) + response = await ds.client.get( + "/test_exact_filter_big_int/t.json?code=9223372036854775808" + ) + assert response.status_code == 200 + assert [row["id"] for row in response.json()["rows"]] == [1]