Skip to content

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

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

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
Copy link
Copy Markdown

@STiFLeR7 STiFLeR7 commented Jun 2, 2026

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

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.
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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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;
}
}
…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.
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.

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

3 participants