fix(render): trim IEEE-754 noise from displayed decimal arithmetic (#32)
`decimal` is stored as exact TEXT, but SQLite has no native decimal type, so arithmetic/aggregation implicitly coerces it to an IEEE-754 double. The computed result carries no playground type, so `sum(price * qty)` rendered the double's full noise — `298.59999999999997` for `298.60` — a confusing, off-topic float lesson for a teaching tool. Add `format_real_display`: round REAL values to 15 significant figures (a double's reliable precision) then take the shortest round-tripping form, collapsing `298.59999999999997` to `298.6`. Wired into `format_cell` (result-set / `show data` cells) only — the sole surface where the noise appears, since it arises from arithmetic. Every other f64->string path keeps full precision for semantic, not cosmetic, reasons: CSV persistence stays byte-exact for round-trip; `render_value` is a canonical identity key for the uniqueness dry-runs (dry_run_unique, check_uniqueness_collisions), where rounding would report collisions the exact-valued engine wouldn't; FK-key matching and EXPLAIN-SQL literals likewise stay exact. ADR-0005 Amendment 1; +7 tests.
This commit is contained in:
@@ -175,6 +175,67 @@ fn open_project_db() -> (project::Project, Database, tempfile::TempDir) {
|
||||
(project, db, dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decimal_aggregation_display_trims_ieee754_noise() {
|
||||
// Issue #32: `decimal` is stored as exact TEXT, but SQLite
|
||||
// coerces it to an IEEE-754 double for arithmetic/aggregation,
|
||||
// so `sum(price * qty)` would render `298.59999999999997` for
|
||||
// `298.60`. The display layer rounds computed REAL cells to ~15
|
||||
// significant figures, trimming that noise — while raw decimal
|
||||
// columns stay byte-exact (TEXT, untouched).
|
||||
let (_p, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
rt.block_on(async {
|
||||
db.create_table(
|
||||
"Products".to_string(),
|
||||
vec![
|
||||
ColumnSpec::new("id", Type::Serial),
|
||||
ColumnSpec::new("price", Type::Decimal),
|
||||
ColumnSpec::new("qty", Type::Int),
|
||||
],
|
||||
vec!["id".to_string()],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("create");
|
||||
for (price, qty) in [("19.99", 3), ("5.49", 7), ("100.10", 2)] {
|
||||
db.insert(
|
||||
"Products".to_string(),
|
||||
Some(vec!["price".to_string(), "qty".to_string()]),
|
||||
vec![
|
||||
Value::Number(price.to_string()),
|
||||
Value::Number(qty.to_string()),
|
||||
],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("insert");
|
||||
}
|
||||
});
|
||||
|
||||
// The reported case: the aggregate no longer leaks float noise.
|
||||
let agg = rt
|
||||
.block_on(db.run_select("select sum(price * qty) from Products".to_string(), None))
|
||||
.expect("aggregate select");
|
||||
assert_eq!(
|
||||
agg.rows[0][0].as_deref(),
|
||||
Some("298.6"),
|
||||
"sum(price*qty) must trim IEEE-754 noise (298.60), not show 298.59999999999997",
|
||||
);
|
||||
|
||||
// Raw decimal column is still exact — TEXT storage preserves
|
||||
// the input string verbatim, including the trailing zero.
|
||||
let raw = rt
|
||||
.block_on(db.run_select("select price from Products".to_string(), None))
|
||||
.expect("raw decimal select");
|
||||
let prices: Vec<&str> = raw.rows.iter().map(|r| r[0].as_deref().unwrap()).collect();
|
||||
assert_eq!(
|
||||
prices,
|
||||
vec!["19.99", "5.49", "100.10"],
|
||||
"raw decimal column must stay byte-exact (TEXT storage untouched)",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn database_run_select_constant_returns_a_single_row() {
|
||||
let (_p, db, _dir) = open_project_db();
|
||||
|
||||
Reference in New Issue
Block a user