Skip to content

Commit 8baa15a

Browse files
JuliaEdomclaude
andcommitted
fix(eval): normalise Decimal to float — Postgres numeric scored as a miss
psycopg returns `numeric` — every AVG, SUM and division — as `Decimal`; SQLite returns float. `_hashable` quantised only floats onto the tolerance grid and let Decimal through untouched, so a correct answer of Decimal('1.3070996799810359') never landed in the same bucket as gold 1.307099679981036 and was scored wrong. `_normalise_row`'s docstring has always claimed "Decimal → float"; the line was simply never there. The cost was 4 EA points on the Postgres run (40.8% → 49.0%) — a comparator bug being read as model error. Invisible on SQLite, which never hands back a Decimal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c8742c6 commit 8baa15a

2 files changed

Lines changed: 36 additions & 0 deletions

File tree

‎src/nl_sql/eval/metrics/execution_accuracy.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import re
2424
from collections.abc import Sequence
2525
from dataclasses import dataclass
26+
from decimal import Decimal
2627
from typing import Any
2728

2829
_FLOAT_TOLERANCE = 1e-6
@@ -177,6 +178,14 @@ def _normalise_cell(value: Any) -> Any:
177178
return value
178179
if isinstance(value, int):
179180
return value
181+
if isinstance(value, Decimal):
182+
# Postgres hands back numeric — AVG, SUM, any division — as Decimal, while
183+
# SQLite hands back float. Left as Decimal it skips the float tolerance
184+
# grid in `_hashable`, so a correct answer that differs from gold in the
185+
# 16th decimal lands in a different bucket and scores a miss. This line is
186+
# what the docstring above always claimed; it was never actually here, and
187+
# every Postgres ratio/average question paid for it.
188+
return float(value)
180189
if isinstance(value, float):
181190
# NaN compares unequal to itself; map all NaN to a sentinel so two
182191
# NaN rows from the same query compare equal.

‎tests/eval/test_metrics.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
from decimal import Decimal
6+
57
from nl_sql.eval.metrics.execution_accuracy import (
68
compare_results,
79
execution_accuracy,
@@ -124,6 +126,31 @@ def test_float_outside_tolerance_fails(self) -> None:
124126
c = compare_results(gold, pred, gold_sql="SELECT x FROM t ORDER BY x")
125127
assert not c.match
126128

129+
def test_decimal_compares_equal_to_the_same_number_as_float(self) -> None:
130+
"""Postgres returns numeric (AVG, SUM, division) as Decimal; SQLite as float.
131+
132+
A correct Postgres answer and BIRD's gold therefore come back as
133+
Decimal('1.3070996799810359') vs 1.307099679981036 — the same number to
134+
1e-16. The set path quantised the float onto the tolerance grid but let
135+
the Decimal through untouched, so the two never landed in the same
136+
bucket and a correct answer scored a miss. That silently deflated every
137+
Postgres run computing a ratio or an average (seen on BIRD qids 629
138+
"ratio of votes in 2010 and 2011" and 598 "percentage difference").
139+
"""
140+
gold = [(1.307099679981036,)]
141+
pred = [(Decimal("1.3070996799810359"),)]
142+
143+
assert compare_results(gold, pred, gold_sql="SELECT ratio FROM t").match
144+
assert compare_results(gold, pred, gold_sql="SELECT ratio FROM t ORDER BY 1").match
145+
146+
def test_decimal_outside_tolerance_still_fails(self) -> None:
147+
"""The Decimal path must not become a blanket pass — 1.0 vs 1.001 is wrong."""
148+
gold = [(1.0,)]
149+
pred = [(Decimal("1.001"),)]
150+
151+
assert not compare_results(gold, pred, gold_sql="SELECT x FROM t").match
152+
assert not compare_results(gold, pred, gold_sql="SELECT x FROM t ORDER BY 1").match
153+
127154
def test_bytes_decoded_as_utf8(self) -> None:
128155
gold = [("hello",)]
129156
pred = [(b"hello",)]

0 commit comments

Comments
 (0)