fix(spend): coerce Decimal and numeric-string aggregates in the benchmarks read path

This commit is contained in:
Tin Chi Lo 2026-08-04 20:06:43 -07:00
parent 7eeafa908e
commit 798e54ddc7
2 changed files with 41 additions and 1 deletions

View file

@ -10,6 +10,7 @@ from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import astuple, dataclass, fields
from datetime import date, datetime, time, timedelta, timezone
from decimal import Decimal
from functools import reduce
from typing import TYPE_CHECKING, Final
@ -136,7 +137,16 @@ def _per(numerator: float, denominator: float) -> float:
def _number(value: object) -> float:
return float(value) if isinstance(value, (int, float)) else 0.0
"""Postgres aggregates surface through the driver as int, float, Decimal or a numeric
string depending on the column type (SUM over BIGINT and EXTRACT both yield NUMERIC)."""
if isinstance(value, (int, float, Decimal)):
return float(value)
if not isinstance(value, str):
return 0.0
try:
return float(value)
except ValueError:
return 0.0
def _counters_from_row(row: Mapping[str, object]) -> _Counters:

View file

@ -6,6 +6,7 @@ is exercised here without a database.
import datetime as dt
from dataclasses import fields
from decimal import Decimal
import pytest
@ -233,6 +234,35 @@ class TestTotals:
assert response.groups == ()
assert response.totals.turns == 0
def test_decimal_and_string_aggregates_survive_the_driver(self):
"""SUM over BIGINT and EXTRACT(EPOCH) yield NUMERIC, which the driver may hand
back as Decimal or a numeric string; neither may read as zero traffic."""
response = build_response(
rows=[
_row(
"a",
sessions=Decimal("2"),
turns=Decimal("64"),
total_tokens=Decimal("10000"),
total_session_seconds="7200.5",
)
],
start_date=dt.date(2026, 8, 1),
end_date=dt.date(2026, 8, 3),
)
assert response.totals.turns == 64
assert response.totals.total_tokens == 10_000
assert response.totals.avg_tokens_per_session == pytest.approx(5000.0)
assert response.totals.avg_session_seconds == pytest.approx(3600.25)
def test_a_malformed_aggregate_reads_as_zero_rather_than_failing_the_page(self):
response = build_response(
rows=[_row("a", total_tokens="not-a-number")],
start_date=dt.date(2026, 8, 1),
end_date=dt.date(2026, 8, 3),
)
assert response.totals.total_tokens == 0
def test_group_identity_is_carried_through(self):
response = build_response(
rows=[_row("claude-auto")], start_date=dt.date(2026, 8, 1), end_date=dt.date(2026, 8, 3)