fix(client-engine-runtime): preserve Decimal precision in increment/decrement operations#29284
fix(client-engine-runtime): preserve Decimal precision in increment/decrement operations#29284Not-Sarthak wants to merge 6 commits intoprisma:mainfrom
Conversation
…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.
|
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds 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
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
packages/client-engine-runtime/src/interpreter/query-interpreter.test.tspackages/client-engine-runtime/src/interpreter/query-interpreter.ts
packages/client-engine-runtime/src/interpreter/query-interpreter.test.ts
Show resolved
Hide resolved
packages/client-engine-runtime/src/interpreter/query-interpreter.test.ts
Show resolved
Hide resolved
packages/client-engine-runtime/src/interpreter/query-interpreter.test.ts
Show resolved
Hide resolved
…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.
Follow-up fix: High-precision Decimal cloneDuring thorough testing, I discovered that the initial implementation using Example of the problem with default precision: The fix: Use
The original issue scenario ( |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
packages/client-engine-runtime/src/interpreter/query-interpreter.test.tspackages/client-engine-runtime/src/interpreter/query-interpreter.ts
packages/client-engine-runtime/src/interpreter/query-interpreter.test.ts
Show resolved
Hide resolved
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.
There was a problem hiding this comment.
♻️ Duplicate comments (2)
packages/client-engine-runtime/src/interpreter/query-interpreter.test.ts (2)
25-28: 🧹 Nitpick | 🔵 TrivialReword 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 | 🔵 TrivialExtract 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.
As per coding guidelines "Use `interface` for defining object shapes in TypeScript".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 {
|
This doesn't actually fix #29160, although it might still be a valid change. |
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.
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (3)
packages/client/tests/functional/decimal/increment-precision/_matrix.tspackages/client/tests/functional/decimal/increment-precision/prisma/_schema.tspackages/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
|
ohh yes, you're right the real issue is mysql treating fixed in ea8dd8c by wrapping decimal typed params with the earlier |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
packages/client-engine-runtime/src/interpreter/query-interpreter.test.ts (2)
25-28: 🧹 Nitpick | 🔵 TrivialReword/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 | 🟠 MajorUse a named
interfacefor 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
interfacefor defining object shapes in TypeScript".
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (3)
packages/client-engine-runtime/src/interpreter/query-interpreter.test.tspackages/client-engine-runtime/src/interpreter/query-interpreter.tspackages/client-engine-runtime/src/interpreter/render-query.ts
packages/client-engine-runtime/src/interpreter/query-interpreter.ts
Outdated
Show resolved
Hide resolved
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))` |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
#29160 has already been fixed and covered with a test in another PR
Fixes #29160
Problem
When using
incrementordecrementoperations onDecimalfields with large values (>2^53), Prisma loses precision because the query interpreter'sevalFieldOperation()converts all operands to JavaScriptnumberviaNumber(). JavaScript numbers are IEEE 754 doubles and cannot safely represent integers larger thanNumber.MAX_SAFE_INTEGER(2^53 - 1).For example, incrementing
5000000000000000000000000000by1000000000000000000000produces 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 thedecimal.jslibrary (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.tscovering:Summary by CodeRabbit
Bug Fixes
Tests