8000 docs(storage): state where encode_value puts NaN (#688) — backport by adityamukho · Pull Request #705 · project-minigraf/minigraf · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ Auto-migrates v1/v2/v3/v4/v5/v6 → v7 on open/checkpoint.

## Test Coverage

**1325 tests** (1316 passing, 9 ignored; unit + integration + doc).
**1326 tests** (1317 passing, 9 ignored; unit + integration + doc).
See `docs/TEST_COVERAGE.md` for the full per-file breakdown.

**Testing conventions** — see the Testing Conventions section below before writing any tests.
Expand Down
2 changes: 1 addition & 1 deletion docs/TEST_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

**Verified**: 2026-10-23 with `cargo test`

**Result**: 1,316 passing tests and 9 ignored tests (1,325 total), including the golden-file corpus (#391)
**Result**: 1,317 passing tests and 9 ignored tests (1,326 total), including the golden-file corpus (#391)

For v2.0.5, `tests/edge_cases_test.rs` checks that a file of 1 to 4,095 bytes is `STG-045`, unchanged and with no WAL, when it is a database cut below one page or foreign bytes, and opens as a new database that takes a write and reads it back after reopen when it is a prefix of the initial header or all zeros (#506). Rule-body tests in `tests/recursive_rules_test.rs` check that a `:db/valid-at` pattern in a rule definition, in its `not` and `not-join` bodies and in its `or` branches binds the query's valid time, on the semi-naive, stratified and magic-sets paths (#617).

Expand Down
35 changes: 32 additions & 3 deletions src/storage/index.rs
6338
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ pub struct FactRef {
///
/// Within each type, big-endian layout ensures byte-wise comparison matches
/// the natural order of the type.
///
/// Except for NaN (#688). Every NaN encodes to `0x03` followed by
/// `0x7FF8_0000_0000_0000`, without the sign transform the other floats get.
/// Those are also the bytes of the negative subnormal
/// `f64::from_bits(0x8007_FFFF_FFFF_FFFF)` (about `-1.1125e-308`), so NaN sorts
/// among the negative subnormals, before `-0.0` and every positive float, and
/// is byte-equal to that one value. `Value::cmp` instead puts NaN after `+∞`.
/// The encoding is the `value_bytes` of on-disk v7 AVET keys, so it stays as it
/// is: don't build a range scan by value on these bytes that assumes NaN sorts
/// last, or an equality that must tell NaN from that subnormal.
pub fn encode_value(v: &Value) -> Vec<u8> {
match v {
Value::Null => vec![0x00],
Expand All @@ -65,7 +75,8 @@ pub fn encode_value(v: &Value) -> Vec<u8> {
let mut bytes = Vec::with_capacity(9);
bytes.push(0x03);
let bits = if f.is_nan() {
// Canonicalize all NaN to a single bit pattern (quiet NaN, positive)
// Canonicalize all NaN to a single bit pattern. It is not
// sign-transformed, so NaN sorts among the negative subnormals.
0x7FF8_0000_0000_0000u64
} else {
let raw = f.to_bits();
Expand Down Expand Up @@ -560,11 +571,29 @@ mod tests {
let nan2 = encode_value(&Value::Float(f64::NAN));
// All NaN values produce the same bytes
assert_eq!(nan1, nan2);
// NaN sorts above all positive finite values (it uses quiet NaN bit pattern)
// Just verify it doesn't panic and produces a fixed-length result
assert_eq!(nan1.len(), 9);
}

/// The NaN position `encode_value`'s doc comment states (#688). These are
/// v7 on-disk bytes, so this pins them as they are rather than as they
/// should sort.
#[test]
fn test_encode_value_nan_sorts_among_negative_subnormals() {
let nan = encode_value(&Value::Float(f64::NAN));
assert_eq!(nan, [0x03, 0x7F, 0xF8, 0, 0, 0, 0, 0, 0]);
let twin = encode_value(&Value::Float(f64::from_bits(0x8007_FFFF_FFFF_FFFF)));
assert_eq!(nan, twin, "NaN is byte-equal to one negative subnormal");
let below = encode_value(&Value::Float(f64::from_bits(0x8008_0000_0000_0000)));
let above = encode_value(&Value::Float(f64::from_bits(0x8007_FFFF_FFFF_FFFE)));
assert!(below < nan, "NaN sorts after a smaller negative subnormal");
assert!(nan < above, "NaN sorts before a larger negative subnormal");
assert!(nan < encode_value(&Value::Float(-0.0)), "NaN < -0.0");
assert!(
nan < encode_value(&Value::Float(f64::INFINITY)),
"NaN < +inf"
);
}

/// #445 widened `slot_index` from `u16` to `u32`. postcard must still write
/// and read exactly the bytes a `u16` slot produced, so v7 index pages are
/// unchanged on disk.
Expand Down
Loading
0