diff --git a/Cargo.toml b/Cargo.toml index 85be7191..bf0e783a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ dirsql = { path = "packages/rust" } serde = { version = "1", features = ["derive"] } # `preserve_order`: a serialized row must keep the projection order the SELECT # list asked for, which the default BTreeMap-backed object discards. -serde_json = { version = "1", features = ["preserve_order"] } +serde_json = { version = "1", features = ["preserve_order", "float_roundtrip"] } regex = "1" thiserror = "2" tokio = { version = "1", features = ["rt", "sync"] } diff --git a/packages/python/changelog.d/2026-11-04-exact-json-floats.md b/packages/python/changelog.d/2026-11-04-exact-json-floats.md new file mode 100644 index 00000000..45059b89 --- /dev/null +++ b/packages/python/changelog.d/2026-11-04-exact-json-floats.md @@ -0,0 +1 @@ +**Fixed** JSON numbers from `on-file` hooks and worker functions now parse to the exact double: `123456789.12345679` was stored one ULP off as `123456789.1234568`. diff --git a/packages/python/e2e-attestations/bot-1586-float-roundtrip-1104.json b/packages/python/e2e-attestations/bot-1586-float-roundtrip-1104.json new file mode 100644 index 00000000..cd45a869 --- /dev/null +++ b/packages/python/e2e-attestations/bot-1586-float-roundtrip-1104.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e/on_file_query_test.py tests/e2e/jsonl_parse_speed_test.py tests/e2e/use_case_transcripts_test.py tests/e2e/query_contract_test.py -x -q", + "ran_at": 1793814794, + "exit_code": 0, + "commit": "4696a76d81c3080df86c416ef418d3d9b2dc0062", + "branch": "bot/1586-float-roundtrip-1104" +} diff --git a/packages/python/migrations.d/2026-11-04-exact-json-floats.md b/packages/python/migrations.d/2026-11-04-exact-json-floats.md new file mode 100644 index 00000000..fdf4eabe --- /dev/null +++ b/packages/python/migrations.d/2026-11-04-exact-json-floats.md @@ -0,0 +1,21 @@ +### Exact JSON float parsing + +#### Summary + +A JSON float an `on-file` hook or worker function prints is stored as the correctly rounded REAL, where some were one ULP off before. No API or configuration changes are required. + +#### Required changes + +_None._ + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +A REAL column fed from JSON can hold a value one ULP different from before the upgrade: the correct one. + +#### Verification + +In an empty directory, write `{"v":123456789.12345679}` to `a.ndjson` and run `dirsql query "SELECT v = 123456789.12345679 AS eq FROM './a.ndjson'" --on-file cat --format json`; the output is `[{"eq":1}]`. diff --git a/packages/rust/changelog.d/2026-11-04-exact-json-floats.md b/packages/rust/changelog.d/2026-11-04-exact-json-floats.md new file mode 100644 index 00000000..45059b89 --- /dev/null +++ b/packages/rust/changelog.d/2026-11-04-exact-json-floats.md @@ -0,0 +1 @@ +**Fixed** JSON numbers from `on-file` hooks and worker functions now parse to the exact double: `123456789.12345679` was stored one ULP off as `123456789.1234568`. diff --git a/packages/rust/migrations.d/2026-11-04-exact-json-floats.md b/packages/rust/migrations.d/2026-11-04-exact-json-floats.md new file mode 100644 index 00000000..fdf4eabe --- /dev/null +++ b/packages/rust/migrations.d/2026-11-04-exact-json-floats.md @@ -0,0 +1,21 @@ +### Exact JSON float parsing + +#### Summary + +A JSON float an `on-file` hook or worker function prints is stored as the correctly rounded REAL, where some were one ULP off before. No API or configuration changes are required. + +#### Required changes + +_None._ + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +A REAL column fed from JSON can hold a value one ULP different from before the upgrade: the correct one. + +#### Verification + +In an empty directory, write `{"v":123456789.12345679}` to `a.ndjson` and run `dirsql query "SELECT v = 123456789.12345679 AS eq FROM './a.ndjson'" --on-file cat --format json`; the output is `[{"eq":1}]`. diff --git a/packages/rust/tests/on_file_float.rs b/packages/rust/tests/on_file_float.rs new file mode 100644 index 00000000..635e6160 --- /dev/null +++ b/packages/rust/tests/on_file_float.rs @@ -0,0 +1,40 @@ +//! A float an `on-file` hook prints is stored as the REAL a correctly +//! rounded JSON parser reads. + +use std::fs; + +use dirsql::{DirSQL, Value}; +use tempfile::TempDir; + +#[test] +fn a_real_column_keeps_the_exact_double() { + let doubles = [ + "123456789.12345679", + "0.30000000000000004", + "1.7976931348623157e308", + ]; + let root = TempDir::new().unwrap(); + fs::write( + root.path().join(".dirsql.toml"), + "[[table]]\nname = \"t\"\nddl = \"CREATE TABLE t (v REAL)\"\nglob = \"*.ndjson\"\non-file = \"cat\"\n", + ) + .unwrap(); + let lines: String = doubles.iter().map(|d| format!("{{\"v\":{d}}}\n")).collect(); + fs::write(root.path().join("a.ndjson"), lines).unwrap(); + let db = DirSQL::builder() + .root(root.path()) + .config(root.path().join(".dirsql.toml")) + .build() + .unwrap(); + let stored: Vec = db + .query("SELECT v FROM t ORDER BY rowid") + .unwrap() + .into_iter() + .map(|row| row["v"].clone()) + .collect(); + let expected: Vec = doubles + .iter() + .map(|d| Value::Real(d.parse().unwrap())) + .collect(); + assert_eq!(stored, expected); +} diff --git a/packages/rust/tests/on_file_float_e2e.rs b/packages/rust/tests/on_file_float_e2e.rs new file mode 100644 index 00000000..e0f5081b --- /dev/null +++ b/packages/rust/tests/on_file_float_e2e.rs @@ -0,0 +1,37 @@ +//! End-to-end: a float an `on-file` hook prints reaches SQLite as the same +//! REAL a correctly rounded JSON parser reads, through the real `dirsql` +//! binary. Nothing is mocked. + +#![cfg(feature = "cli")] + +use assert_cmd::prelude::*; +use tempfile::TempDir; + +#[test] +fn an_on_file_float_round_trips_exactly() { + let root = TempDir::new().unwrap(); + std::fs::write(root.path().join("a.ndjson"), "{\"v\":123456789.12345679}\n").unwrap(); + let out = std::process::Command::cargo_bin("dirsql") + .expect("binary must exist") + .args([ + "query", + "SELECT v, v = 123456789.12345679 AS eq FROM './a.ndjson'", + "--on-file", + "cat", + "--format", + "json", + ]) + .env("DIRSQL_PROGRESS", "never") + .current_dir(root.path()) + .output() + .expect("spawning `dirsql` failed"); + assert!( + out.status.success(), + "expected success, stderr: {}", + String::from_utf8_lossy(&out.stderr) + ); + assert_eq!( + String::from_utf8(out.stdout).unwrap(), + "[{\"v\":123456789.12345679,\"eq\":1}]\n" + ); +} diff --git a/packages/ts/changelog.d/2026-11-04-exact-json-floats.md b/packages/ts/changelog.d/2026-11-04-exact-json-floats.md new file mode 100644 index 00000000..45059b89 --- /dev/null +++ b/packages/ts/changelog.d/2026-11-04-exact-json-floats.md @@ -0,0 +1 @@ +**Fixed** JSON numbers from `on-file` hooks and worker functions now parse to the exact double: `123456789.12345679` was stored one ULP off as `123456789.1234568`. diff --git a/packages/ts/e2e-attestations/bot-1586-float-roundtrip-1104.json b/packages/ts/e2e-attestations/bot-1586-float-roundtrip-1104.json new file mode 100644 index 00000000..dc79d184 --- /dev/null +++ b/packages/ts/e2e-attestations/bot-1586-float-roundtrip-1104.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm exec vitest run --dir tests/e2e query-contract query-params", + "ran_at": 1793814888, + "exit_code": 0, + "commit": "0340207c463df4173a0e59f0993bde832ee2c50e", + "branch": "bot/1586-float-roundtrip-1104" +} diff --git a/packages/ts/migrations.d/2026-11-04-exact-json-floats.md b/packages/ts/migrations.d/2026-11-04-exact-json-floats.md new file mode 100644 index 00000000..fdf4eabe --- /dev/null +++ b/packages/ts/migrations.d/2026-11-04-exact-json-floats.md @@ -0,0 +1,21 @@ +### Exact JSON float parsing + +#### Summary + +A JSON float an `on-file` hook or worker function prints is stored as the correctly rounded REAL, where some were one ULP off before. No API or configuration changes are required. + +#### Required changes + +_None._ + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +A REAL column fed from JSON can hold a value one ULP different from before the upgrade: the correct one. + +#### Verification + +In an empty directory, write `{"v":123456789.12345679}` to `a.ndjson` and run `dirsql query "SELECT v = 123456789.12345679 AS eq FROM './a.ndjson'" --on-file cat --format json`; the output is `[{"eq":1}]`.