Skip to content

fix(graph): stop coercing oversized integer strings into lossy floats - #740

Merged
TinDang97 merged 1 commit into
mainfrom
fix/graph-addnode-numeric-coercion-724
Aug 26, 2026
Merged

fix(graph): stop coercing oversized integer strings into lossy floats#740
TinDang97 merged 1 commit into
mainfrom
fix/graph-addnode-numeric-coercion-724

Conversation

@TinDang97

@TinDang97 TinDang97 commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Closes #724.

What

GRAPH.ADDNODE and GRAPH.ADDEDGE share parse_property_value, which coerced any bulk string that parsed as i64 or f64 into a number. A value too long for i64 fell through to f64 — 15–17 significant digits kept, the rest zeroed:

GRAPH.ADDNODE g Doc _key 12345678901234567890123456789012
MATCH (n:Doc) RETURN n._key
  -> 12345678901234567000000000000000        # corrupted
MATCH (n:Doc {_key: '12345678901234567890123456789012'}) RETURN n._key
  -> (empty)                                  # unfindable by its own key

Irreversible, and silent.

How

RESP puts no type tag on a bulk string, so guessing is reasonable — but a guess that changes the value is not. An integer-syntax string that does not fit i64 is an identifier, not a number, so it stays a string.

Why not the fix the issue suggested

#724 proposed refusing whenever the round-trip is inexact (format!("{f}") != s). I did not use that rule, because it also demotes "3.0""3" and "1e5""100000", turning legitimate float properties into strings and breaking MATCH (n {x: 3.0}). Withholding only the integer case fixes the reported data loss with no change to float behaviour.

Unchanged: "42"Int(42), "2.5"Float(2.5), "3.0"Float(3.0), "1e5"Float(1e5), i64::MAXInt.
Changed: i64::MAX + 1 and -9223372036854775809String instead of a lossy Float.

Evidence

Red first. The unit test failed with Float(1.2345678901234567e31) against the expected string — reproducing the issue's measured corruption exactly, not a proxy for it.

Green, then end-to-end through the real command path:

-- ADDNODE then read back --
12345678901234567890123456789012        # exact
-- point lookup by the original key (was empty before the fix) --
12345678901234567890123456789012        # found
-- control: genuine numbers must still be numbers --
MATCH (n:Num) WHERE n.age > 20 RETURN n.age  ->  30

No regressions: 610 lib tests and 641 graph/cypher/property integration tests pass.

Notes

  • is_integer_syntax borrows only and allocates nothing — this sits on the command path.
  • The Cypher path was never affected; it is fed by the grammar, which already knows the type.
  • parse_property_value was widened to pub(super) so the test can address it directly. Test lives in mod.rs per the split convention.
  • Stored data is untouched: WAL replay carries explicit type tags, so existing values replay as whatever they were written as.

Summary by CodeRabbit

  • Bug Fixes
    • Oversized integer-like property values added through GRAPH.ADDNODE and GRAPH.ADDEDGE are now preserved exactly as strings instead of being converted inaccurately.
    • Valid integers, floating-point values, scientific notation, and Cypher parsing continue to behave as expected.
  • Documentation
    • Added an Unreleased changelog entry describing the improved numeric value handling.

@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@TinDang97

Copy link
Copy Markdown
Collaborator Author

Re-triggering CI: the pull_request run never fired on open, so Lint and Check (which the dispatch deliberately skips per #733) had no gate.

@TinDang97 TinDang97 closed this Aug 26, 2026
@TinDang97 TinDang97 reopened this Aug 26, 2026
@TinDang97

Copy link
Copy Markdown
Collaborator Author

Note for reviewers: this PR has no pull_request CI run

GitHub never created one — not on opened, and not on reopened after I closed/reopened to re-trigger. gh run list for this branch shows only the workflow_dispatch run:

32988007174 workflow_dispatch in_progress a8a98743 CI

Verified not to be a configuration problem on this PR's side:

  • gh pr diff 740 --name-only returns CHANGELOG.md, src/command/graph/graph_write.rs, src/command/graph/mod.rs — two .rs files, so paths-ignore (docs/**, **.md, .planning/**, .add/**, mkdocs.yml, LICENSE*) does not match everything
  • base is main, which pull_request: targets
  • both CI and Supply Chain workflows report active
  • main is not branch-protected, so no required-check config is involved

Why this matters beyond this PR

Since #733, the dispatch matrix deliberately skips lint and check (if: github.event_name != 'workflow_dispatch'), on the assumption that the PR run always provides them. When the PR run does not fire, those two gates run nowhere — and nothing surfaces that, because a PR with zero checks reports no failures.

For this PR the gap is covered in substance: scripts/ci-local.sh runs fmt --check, audit-unsafe, audit-unwrap, the CHANGELOG gate, clippy x3, and both full suites — a superset of hosted Lint + Check. So the merge bar is met; it is the hosted record that is missing, not the verification.

Worth deciding separately whether the dispatch should stop skipping lint/check when it is the only run for a SHA, or whether the merge procedure should assert a pull_request run exists before merging. Filing that thought here rather than silently working around it.

@TinDang97

Copy link
Copy Markdown
Collaborator Author

The red macOS leg that has been blocking this PR is not this PR. Diagnosed and fixed in #742 (issue #741).

used_memory_lua_tracks_real_lua_allocation fails because both #[test]s in
lua_vm_memory_published can be handed the same --dir — the path was
moon-506-lua-{pid}-{nanos}, both tests are threads of one process, and macOS SystemTime
resolves to microseconds. moon's instance flock then correctly refuses the second server, which
exits 1 before binding. Instrumented proof: on a failing run both tests printed
moon-506-lua-49140-1787763396820255000, byte-identical. 14 failures / 120 runs before the fix,
0 / 120 after.

Independently, this PR cannot affect that leg: default = ["runtime-monoio", "jemalloc", "graph", "text-index"], and the macOS leg runs --no-default-features --features runtime-tokio,jemalloc — the graph module, where this entire change lives, is not compiled
there. The tokio suite count stayed at 5309 across the run while monoio went 6107 -> 6108.

Plan: merge #742 first, rebase this on it, then the bar should be green for the right reason.

@TinDang97

Copy link
Copy Markdown
Collaborator Author

Correction to my earlier comment about this PR having no hosted checks — that was wrong. The
pull_request run exists and is green:

  • pull_request run 32990091788, head a8a98743 (= current PR head): Lint ✓, Check ✓,
    MSRV ✓, Memory steady-state ✓
  • workflow_dispatch run 32988007174: MSRV ✓, Check (Windows) ✓, Memory ✓

gh pr view --json statusCheckRollup returned length 0 at the moment I sampled it and I read
"empty now" as "never fired". An empty rollup means not observed yet, not did not happen
gh api repos/.../commits/<sha>/check-suites shows GitHub Actions | queued while the rollup is
still empty. Also worth recording: concurrency: group: ci-${{ github.ref }} does not let
the dispatch cancel the PR run — the refs differ (refs/pull/N/merge vs refs/heads/<branch>).

So the hosted bar for this PR is complete. The only outstanding item is the local macOS leg,
which is #741 and is fixed in #742.

GRAPH.ADDNODE and GRAPH.ADDEDGE share parse_property_value, which coerced any
bulk string parsing as i64 or f64 into a number. A value too long for i64 fell
through to f64, which keeps 15-17 significant digits and zeroes the rest. A
32-digit key stored through GRAPH.ADDNODE came back as
12345678901234567000000000000000, and the node could no longer be found by the
key it was created with -- irreversible, and silent.

RESP puts no type tag on a bulk string, so guessing is reasonable; a guess that
CHANGES the value is not. An integer-syntax string that does not fit i64 is an
identifier, not a number, so it now stays a string.

Only the integer case is withheld. moon#724 suggested refusing whenever the
round-trip is inexact (format!("{f}") != s), but that also demotes "3.0" and
"1e5" to strings and would break MATCH (n {x: 3.0}), so genuine float syntax
still coerces exactly as before. "42" is still Int(42), "2.5" still Float(2.5);
i64::MAX still coerces and i64::MAX + 1 no longer does.

Red first: the unit test failed with Float(1.2345678901234567e31) against the
expected string, reproducing the issue's measured corruption exactly. Verified
end-to-end afterwards through the real command path -- the 32-digit key
round-trips and MATCH (n:Doc {_key: '<key>'}) finds it, where before it returned
nothing -- with `WHERE n.age > 20` as the control that numbers still coerce.

is_integer_syntax borrows only and allocates nothing, since this sits on the
command path.

The Cypher path was never affected: it is fed by the grammar, which already
knows the type.

Closes #724

author: Tin Dang
@TinDang97
TinDang97 force-pushed the fix/graph-addnode-numeric-coercion-724 branch from a8a9874 to 98c8042 Compare August 26, 2026 19:24
@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 47edf2b5-7f57-44bb-94b4-95eb1d9f22cb

📥 Commits

Reviewing files that changed from the base of the PR and between fadfbe5 and 98c8042.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/command/graph/graph_write.rs
  • src/command/graph/mod.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

GRAPH.ADDNODE and GRAPH.ADDEDGE now preserve integer-like strings outside the i64 range instead of converting them through f64. Existing integer, float, scientific notation, ordinary string, and Cypher behaviors remain unchanged.

Changes

Graph property parsing

Layer / File(s) Summary
Property parsing behavior
src/command/graph/graph_write.rs
parse_property_value detects optional-sign integer syntax. Integer-like values that exceed i64 remain strings. Float syntax continues to parse as f64.
Regression coverage and changelog
src/command/graph/mod.rs, CHANGELOG.md
Tests cover oversized values, boundary integers, floats, scientific notation, and ordinary strings. The changelog documents the behavior change.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 98c80

The change preserves valid numeric parsing while preventing oversized integer strings from being silently corrupted, with no actionable merge-blocking risk remaining after normal checks and review.

Suggested reviewers: pilotspacex-byte

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: preventing oversized integer strings from being coerced into lossy floats in graph commands.
Description check ✅ Passed The description is mostly complete. It explains the problem, implementation, rationale, evidence, regression coverage, and notes. It does not use the template headings for Summary, Checklist, or Perfo…
Linked Issues check ✅ Passed The changes satisfy issue #724. Oversized integer-syntax strings remain exact strings, preventing irreversible f64 corruption and failed key lookups. Existing integer and float coercion remains unchan…
Out of Scope Changes check ✅ Passed The changes are within scope. The implementation, regression tests, changelog entry, and test-only visibility change directly support the linked issue and stated objectives.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 2 files. (1 skipped: 1 u…
Full details: Description check

Explanation

The description is mostly complete. It explains the problem, implementation, rationale, evidence, regression coverage, and notes. It does not use the template headings for Summary, Checklist, or Performance Impact, and it does not explicitly report the checklist results, but the required technical context is present.

Full details: Linked Issues check

Explanation

The changes satisfy issue #724. Oversized integer-syntax strings remain exact strings, preventing irreversible f64 corruption and failed key lookups. Existing integer and float coercion remains unchanged, and the shared helper covers both GRAPH.ADDNODE and GRAPH.ADDEDGE.

Full details: Docstring Coverage

Explanation

Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 2 files. (1 skipped: 1 unsupported.)

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/graph-addnode-numeric-coercion-724

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@TinDang97
TinDang97 merged commit 7931809 into main Aug 26, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GRAPH.ADDNODE silently coerces string properties to numbers, losing data beyond f64 precision

1 participant