Skip to content

fix(read): preserve Int64 precision for unsafe Longs in tool outputs#1220

Closed
STiFLeR7 wants to merge 2 commits into
mongodb-js:mainfrom
STiFLeR7:fix/int64-precision-loss
Closed

fix(read): preserve Int64 precision for unsafe Longs in tool outputs#1220
STiFLeR7 wants to merge 2 commits into
mongodb-js:mainfrom
STiFLeR7:fix/int64-precision-loss

Conversation

@STiFLeR7

@STiFLeR7 STiFLeR7 commented Jun 2, 2026

Copy link
Copy Markdown

Description

In relaxed mode serialization (EJSON.stringify default), BSON Long values are directly serialized to standard JSON numbers. When these numbers exceed JavaScript's safe integer boundaries (Number.MIN_SAFE_INTEGER and Number.MAX_SAFE_INTEGER), they lose precision (for example, 7583362298413593073 gets serialized as 7583362298413593000). This leads to round-off errors and tool failures when downstream clients/LLMs execute queries using these values.

Changes

  1. Custom EJSON Helper: Introduced stringifyEJSON in src/helpers/ejson.ts to pre-process objects, converting unsafe Long instances (or those identified by _bsontype === "Long") into strict EJSON format (i.e. { $numberLong: "value" }) to preserve exact 64-bit precision. Safe Longs are kept as normal numbers to maintain relaxed mode readability.
  2. Updated Read/Metadata Tools: Updated find, aggregate, aggregate-db, and db-stats tools to use stringifyEJSON instead of default EJSON.stringify.
  3. Unit Tests: Added unit tests in tests/unit/helpers/ejson.test.ts ensuring correct precision behavior for safe/unsafe values and deeply nested objects or arrays.

Closes #728

Copilot AI review requested due to automatic review settings June 2, 2026 06:35
@STiFLeR7 STiFLeR7 requested a review from a team as a code owner June 2, 2026 06:35
@STiFLeR7 STiFLeR7 requested review from gagik and removed request for a team June 2, 2026 06:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Introduces a custom EJSON stringifier that preserves 64-bit integer precision by converting BSON Long values to standard JS numbers when within safe-integer range and to {$numberLong: "..."} otherwise, then wires it into the MongoDB read tools that previously used EJSON.stringify directly.

Changes:

  • New src/helpers/ejson.ts with serializeSafeLongs and stringifyEJSON helpers, plus unit tests.
  • Replaced direct EJSON.stringify(...) usage in find, aggregate, aggregateDB, and dbStats tools with the new stringifyEJSON.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/helpers/ejson.ts New helper that serializes safe Longs as numbers and unsafe Longs as $numberLong strict EJSON.
tests/unit/helpers/ejson.test.ts Unit tests covering primitives, safe/unsafe Longs, and nested structures.
src/tools/mongodb/read/find.ts Switched to stringifyEJSON for result serialization.
src/tools/mongodb/read/aggregate.ts Switched to stringifyEJSON for result serialization.
src/tools/mongodb/read/aggregateDB.ts Switched to stringifyEJSON for result serialization.
src/tools/mongodb/metadata/dbStats.ts Switched to stringifyEJSON for stats serialization.

Comment thread src/helpers/ejson.ts
Comment on lines +17 to +21
const longObj = obj as unknown as Long;
const num = longObj.toNumber();
if (num >= Number.MIN_SAFE_INTEGER && num <= Number.MAX_SAFE_INTEGER) {
return num;
}
Comment thread src/helpers/ejson.ts
Comment on lines +29 to +38
if (typeof obj === "object") {
const proto: unknown = Object.getPrototypeOf(obj);
if (proto === null || proto === Object.prototype) {
const result: Record<string, unknown> = {};
for (const key of Object.keys(obj)) {
result[key] = serializeSafeLongs((obj as Record<string, unknown>)[key]);
}
return result;
}
}
Comment thread src/helpers/ejson.ts
replacer?: ((this: unknown, key: string, value: unknown) => unknown) | (string | number)[] | null,
space?: string | number
): string {
return EJSON.stringify(serializeSafeLongs(value), replacer as Parameters<typeof EJSON.stringify>[1], space);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to not just do EJSON.stringify(value, { relaxed: false })? Yes that gives you verbose EJSON always which we may or may not want, but then we don't need custom code at all.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using { relaxed: false }\ would force all BSON values (including safe \Long\s, \Int32\s, \Double\s, etc.) to serialize into their verbose strict EJSON representations (e.g. {\count: {\: \42}}\ or {\value: {\: \123}}).

This significantly reduces output readability for downstream LLMs and increases client payload sizes. By using this targeted preprocessing helper, we retain the highly readable relaxed representations for all safe numbers and other BSON types, while strictly preventing precision loss for numbers that exceed JS safe integer limits.

@lerouxb lerouxb Jun 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compass has this concept called "semi-relaxed" which does a variation of what you're doing here and that's probably what we should be using everywhere we output JSON. ie. do relaxed: true except for these edge cases. But it does have the problem that something that reads the output could work fine for years and then suddenly encounter an int greater than 2^^53 for the first time which suddenly changes the output format and then their code blows up.

And it still has the readability problem because those values are suddenly in EJSON. So if LLMs don't deal well with seeing { $numberLong: "value" } then it still applies in that case - it just goes from common to rare. Which as I explained above isn't necessarily better.

I think the team should probably deal with all all content and structuredContent in one go. Make a decision and apply it everywhere consistently. Also make sure that output always contains structuredContent while at it.

@lerouxb lerouxb Jun 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gagik gagik removed their request for review June 16, 2026 10:12
STiFLeR7 added 2 commits June 26, 2026 15:31
Introduce a custom EJSON stringifier helper that pre-processes objects to convert BSON Long values exceeding safe JavaScript integer limits into strict numberLong string representations, while keeping safe Long values as standard relaxed numbers. Use this custom stringifier across find, aggregate, aggregate-db, and db-stats tool outputs to prevent precision round-off errors in downstream LLM clients.
…mit check

Compare the Long object directly rather than calling toNumber() first, preventing precision loss for boundary values (e.g. MAX_SAFE_INTEGER + 2) that round back to safe limits as floats. Add clarifying JSDoc documentation about the prototype recursion constraint.
@STiFLeR7 STiFLeR7 force-pushed the fix/int64-precision-loss branch from dc47721 to 31b183d Compare June 26, 2026 10:19
@dudaschar

Copy link
Copy Markdown
Collaborator

Hi @STiFLeR7, thanks for your suggestion! We are adding the support to the long values (and other BSON types) in this PR #1249 along with structured content for the tools output.

We are implementing the approach @lerouxb mentioned in the discussion here, same as how Compass handles those types.

@STiFLeR7

Copy link
Copy Markdown
Author

Thanks for the context @dudaschar — closing this in favor of #1249 since it covers the same Int64/Long precision issue with a broader, more consistent approach across BSON types. Happy to help test/review #1249 if useful.

@STiFLeR7 STiFLeR7 closed this Jul 10, 2026
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.

[Bug]: Int64 Type Query and Result not support

4 participants