Skip to content

fix(client-engine-runtime): preserve Decimal precision in increment/decrement operations#29284

Open
Not-Sarthak wants to merge 6 commits intoprisma:mainfrom
Not-Sarthak:fix/decimal-precision-field-operations
Open

fix(client-engine-runtime): preserve Decimal precision in increment/decrement operations#29284
Not-Sarthak wants to merge 6 commits intoprisma:mainfrom
Not-Sarthak:fix/decimal-precision-field-operations

Conversation

@Not-Sarthak
Copy link
Copy Markdown
Contributor

@Not-Sarthak Not-Sarthak commented Mar 2, 2026

Fixes #29160

Problem

When using increment or decrement operations on Decimal fields with large values (>2^53), Prisma loses precision because the query interpreter's evalFieldOperation() converts all operands to JavaScript number via Number(). JavaScript numbers are IEEE 754 doubles and cannot safely represent integers larger than Number.MAX_SAFE_INTEGER (2^53 - 1).

For example, incrementing 5000000000000000000000000000 by 1000000000000000000000 produces an incorrect result due to floating-point precision loss. This is critical for financial and blockchain applications that require precise arithmetic with large numbers.

Fix

Added evalArithmetic() which detects when either operand is a string (the representation used for Decimal database columns) and uses the decimal.js library (already a dependency via @prisma/client-runtime-utils) for the computation. When both operands are regular numbers (integer/float columns), the existing JavaScript arithmetic is preserved unchanged.

The key insight is that database drivers return Decimal column values as strings (because they can't be represented as JS numbers), while integer and float columns return numbers. This makes typeof value === 'string' a reliable indicator for when Decimal-safe arithmetic is needed.

Tests

Added unit tests in packages/client-engine-runtime/src/interpreter/query-interpreter.test.ts covering:

  • Add/subtract/multiply/divide with large decimal strings (precision preserved)
  • Division by zero returns null for both decimal and number paths
  • Mixed operands (string value + number operand) use Decimal path
  • Regular number arithmetic is unchanged (integers, floats)
  • All existing tests (100/100) continue to pass

Summary by CodeRabbit

  • Bug Fixes

    • Improved decimal arithmetic (add, subtract, multiply, divide) to preserve very large and long-decimal values and handle mixed string/number operands.
    • Division-by-zero now consistently returns null.
    • Preserved decimal precision when sending parameters to MySQL.
  • Tests

    • Added comprehensive unit and functional tests covering field-level arithmetic, mixed-type scenarios, decimal precision edge cases, and division-by-zero across multiple providers.

…s in field operations

When increment/decrement operations are used on Decimal fields, the values
from the database come as strings. Previously, `asNumber()` converted these
to JavaScript numbers via `Number()`, causing precision loss for values
larger than 2^53 (Number.MAX_SAFE_INTEGER).

This adds `evalArithmetic()` which detects string operands (indicating
Decimal columns) and uses the `decimal.js` library for arithmetic,
preserving full precision. Regular number arithmetic is unchanged.
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 2, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds high-precision Decimal arithmetic to the SQL interpreter, propagates provider context into SQL rendering (MySQL decimal CAST for parameters), and introduces unit and functional tests validating decimal precision, mixed operand arithmetic, and edge cases including division-by-zero and very large Decimals.

Changes

Cohort / File(s) Summary
Query Interpreter Implementation
packages/client-engine-runtime/src/interpreter/query-interpreter.ts
Imports Decimal, creates an ArithmeticDecimal clone and adds internal evalArithmetic to perform add/subtract/multiply/divide with high-precision Decimal logic for mixed string/number operands; replaces numeric field ops with evalArithmetic. Division-by-zero returns null.
Interpreter Unit Tests
packages/client-engine-runtime/src/interpreter/query-interpreter.test.ts
Adds comprehensive tests for mapRecord field operations exercising Decimal/number/string mixes, large-precision arithmetic, division-by-zero, and edge cases via MockDriverAdapter and interpreter runtime options.
SQL Rendering / Provider Context
packages/client-engine-runtime/src/interpreter/render-query.ts
Adds optional provider parameter through render call chain and a hasDecimalArgType helper; when provider is MySQL and arg type is decimal, wraps placeholders with CAST(... AS DECIMAL(65,30)) to preserve precision.
Functional Decimal Test Matrix
packages/client/tests/functional/decimal/increment-precision/_matrix.ts
Adds test matrix exporting provider configurations for MYSQL, POSTGRESQL, COCKROACHDB, and SQLSERVER.
Functional Decimal Schema
packages/client/tests/functional/decimal/increment-precision/prisma/_schema.ts
Adds test schema setup via testMatrix.setupSchema with generator, provider datasource, and Account model where amount is Decimal @db.Decimal(30, 0) and id is provider-aware.
Functional Decimal Tests
packages/client/tests/functional/decimal/increment-precision/tests.ts
Adds functional tests using Prisma.Decimal to validate precision preservation for large Decimal increment/decrement/multiply operations; opts out sqlite/mongodb where applicable.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: fixing Decimal precision loss in increment/decrement operations, which is the core objective addressed across all file modifications.
Linked Issues check ✅ Passed The PR successfully addresses all coding requirements from issue #29160: client-side evalArithmetic() handles large Decimal arithmetic with proper precision [29160], server-side CAST wrapping preserves MySQL parameter precision [29160], and comprehensive unit and functional tests validate the fixes [29160].
Out of Scope Changes check ✅ Passed All changes directly support the core objectives: query interpreter arithmetic enhancements, render-query provider/decimal parameter handling, comprehensive test coverage for precision scenarios, and test infrastructure setup—all within scope of fixing #29160.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3


ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6738298 and 511ec1d.

📒 Files selected for processing (2)
  • packages/client-engine-runtime/src/interpreter/query-interpreter.test.ts
  • packages/client-engine-runtime/src/interpreter/query-interpreter.ts

…hmetic

The default Decimal.js precision of 20 significant digits silently
truncates values with more than 20 significant digits during arithmetic
operations. This uses Decimal.clone() with precision 1000 to support
the full range of database decimal types.
@Not-Sarthak
Copy link
Copy Markdown
Contributor Author

Follow-up fix: High-precision Decimal clone

During thorough testing, I discovered that the initial implementation using new Decimal() had a subtle but critical issue: Decimal.js defaults to only 20 significant digits of precision for arithmetic operations. This means values with 21+ unique significant digits would silently lose precision during computation — the exact same class of bug we're trying to fix.

Example of the problem with default precision:

new Decimal('1234567890123456789012345678').plus('1').toFixed()
// Returns: '1234567890123456789000000000' (WRONG — last 9 digits zeroed out)
// Expected: '1234567890123456789012345679'

The fix: Use Decimal.clone({ precision: 1000 }) to create a module-level high-precision Decimal constructor (ArithmeticDecimal). This:

  • Is created once at module level (zero per-operation overhead)
  • Supports up to 1000 significant digits (covers PostgreSQL NUMERIC's ~131K+16K range, MySQL DECIMAL's 65, and SQL Server's 38 in all practical scenarios)
  • Added a dedicated test case for the >20 significant digit scenario

The original issue scenario (5000000000000000000000000000 + 1000000000000000000000) happened to work with default precision because the non-zero significant digits fit within 20, but any balance with many unique digits (like 1234567890123456789012345678) would have been silently corrupted.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 511ec1d and 3472698.

📒 Files selected for processing (2)
  • packages/client-engine-runtime/src/interpreter/query-interpreter.test.ts
  • packages/client-engine-runtime/src/interpreter/query-interpreter.ts

Covers the case where the database returns a number but the operation
value is a string, which triggers the Decimal path and returns a string
result.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
packages/client-engine-runtime/src/interpreter/query-interpreter.test.ts (2)

25-28: 🧹 Nitpick | 🔵 Trivial

Reword or remove this helper comment; it explains mechanics rather than rationale.

This docblock currently describes what the helper does. Please remove it or restate the Why for keeping this local plan-builder in tests.

As per coding guidelines "Avoid adding useless code comments that do not add new information. Only write inline comments explaining Why (context, background, GitHub issues, decisions), not What or How."


31-31: 🧹 Nitpick | 🔵 Trivial

Extract the inline field-operation type into a named interface.

Using a named interface here improves reuse/readability and aligns with the TypeScript shape-definition rule.

Proposed refactor
+interface MapRecordFieldOperation {
+  type: 'add' | 'subtract' | 'multiply' | 'divide'
+  value: unknown
+}
+
 function makeMapRecordPlan(
   selectSql: string,
-  fields: Record<string, { type: 'add' | 'subtract' | 'multiply' | 'divide'; value: unknown }>,
+  fields: Record<string, MapRecordFieldOperation>,
 ): QueryPlanNode {
As per coding guidelines "Use `interface` for defining object shapes in TypeScript".

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3472698 and 04723e2.

📒 Files selected for processing (1)
  • packages/client-engine-runtime/src/interpreter/query-interpreter.test.ts

@jacek-prisma
Copy link
Copy Markdown
Contributor

jacek-prisma commented Mar 3, 2026

This doesn't actually fix #29160, although it might still be a valid change.
The issue in #29160 is a loss of precision when serializing the number into the MySQL protocol (and I already opened a PR to fix it: #29285)
You should try adding a functional test (in packages/client/tests/functional/) that actually demonstrates a real loss of precision rather than adding query interpreter test.

Reproduces the precision loss from prisma#29160 with an end-to-end test
that performs sequential increment/decrement operations on a
Decimal(30, 0) field with values beyond JS safe integer range.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04723e2 and 76d85f9.

📒 Files selected for processing (3)
  • packages/client/tests/functional/decimal/increment-precision/_matrix.ts
  • packages/client/tests/functional/decimal/increment-precision/prisma/_schema.ts
  • packages/client/tests/functional/decimal/increment-precision/tests.ts

…recision

MySQL treats parameterized values as doubles in arithmetic expressions
(e.g. `SET amount = amount + ?`), silently losing precision for large
decimals. This wraps decimal-typed parameters with CAST(? AS DECIMAL(65,30))
when the provider is MySQL, ensuring full precision is preserved.

prisma#29160
@Not-Sarthak
Copy link
Copy Markdown
Contributor Author

ohh yes, you're right

the real issue is mysql treating ? params as doubles in arithmetic expressions like SET amount = amount + ?, not the js side arithmetic.

fixed in ea8dd8c by wrapping decimal typed params with CAST(? AS DECIMAL(65,30)) when provider is mysql, inside renderFragment() in render-query.ts. also added a functional test in packages/client/tests/functional/decimal/increment-precision/ that reproduces the exact precision loss from #29160. verified locally against mysql with --adapter js_mariadb.

the earlier evalArithmetic changes are still valid for the mapRecord path where arithmetic happens in js rather than in the database.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
packages/client-engine-runtime/src/interpreter/query-interpreter.test.ts (2)

25-28: 🧹 Nitpick | 🔵 Trivial

Reword/remove the helper comment to capture rationale, not mechanics.

Line 25 comment mostly explains what the helper does rather than why it exists.

As per coding guidelines "Avoid adding useless code comments that do not add new information. Only write inline comments explaining Why (context, background, GitHub issues, decisions), not What or How."


29-32: 🛠️ Refactor suggestion | 🟠 Major

Use a named interface for the field operation shape.

Line 31 currently uses an inline object type inside Record, which is harder to reuse and violates the TS shape guideline.

Proposed refactor
+interface MapRecordFieldOperation {
+  type: 'add' | 'subtract' | 'multiply' | 'divide'
+  value: unknown
+}
+
 function makeMapRecordPlan(
   selectSql: string,
-  fields: Record<string, { type: 'add' | 'subtract' | 'multiply' | 'divide'; value: unknown }>,
+  fields: Record<string, MapRecordFieldOperation>,
 ): QueryPlanNode {

As per coding guidelines "Use interface for defining object shapes in TypeScript".


ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 76d85f9 and ea8dd8c.

📒 Files selected for processing (3)
  • packages/client-engine-runtime/src/interpreter/query-interpreter.test.ts
  • packages/client-engine-runtime/src/interpreter/query-interpreter.ts
  • packages/client-engine-runtime/src/interpreter/render-query.ts

Ensures MySQL-specific decimal CAST is applied even when the schema
provider option is not explicitly set, matching the existing fallback
pattern used in withQuerySpanAndEvent.
// interprets the value as DECIMAL with full precision.
// https://github.com/prisma/prisma/issues/29160
if (provider === 'mysql' && hasDecimalArgType(fragment)) {
return `CAST(${placeholder} AS DECIMAL(65,30))`
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.

I think is no longer needed now that #29160 has been fixed already

declare let prisma: PrismaClient
declare let Prisma: typeof PrismaNamespace

// Reproduces https://github.com/prisma/prisma/issues/29160
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.

#29160 has already been fixed and covered with a test in another PR

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.

Precision loss when using increment/decrement with large Decimal values

3 participants