Skip to content

Get rid of unsafe code from TruncateToNetDecimal() implemetation #397

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Jun 25, 2025

Conversation

SergeiPavlov
Copy link
Collaborator

@SergeiPavlov SergeiPavlov commented Jun 24, 2025

We can use UInt128 type for big-integer arithmetic instead of pinning arrays.
DivHasRem() can be replaced by UInt128.DivRem()

Try sqlDecimal.Value inside try/catch first to avoid array allocaton by SqDecimal.Data

@snaumenko-st
Copy link

@snaumenko-st snaumenko-st requested a review from Copilot June 24, 2025 19:33
Copy link

@Copilot 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

The PR removes unsafe code in TruncateToNetDecimal() by leveraging the new UInt128 type for big-integer arithmetic and replaces manual division logic with UInt128.DivRem(). Key changes include updating the PowersOf10 array to UInt128[], introducing Max96bitValue and Ten constants, and refactoring the scaling/truncation logic to use UInt128.

  • Replace unsafe pointer-based operations with safe UInt128 arithmetic
  • Remove custom DivHasRem() and use UInt128.DivRem() for division and remainder
  • Add constants (PowersOf10, Max96bitValue, Ten) to support new arithmetic
Comments suppressed due to low confidence (4)

Orm/Xtensive.Orm.SqlServer/Sql.Drivers.SqlServer/InternalHelpers.cs:15

  • Consider adding a brief comment above the PowersOf10 declaration explaining that these are precomputed 10^n values used for scaling optimizations.
  private static readonly UInt128[] PowersOf10 = [

Orm/Xtensive.Orm.SqlServer/Sql.Drivers.SqlServer/InternalHelpers.cs:28

  • [nitpick] It may help maintainers to include a comment describing that Max96bitValue represents the maximum value storable in 96 bits (2^96-1).
  private static readonly UInt128 Max96bitValue = new(0xFFFFFFFFUL, ulong.MaxValue);

Orm/Xtensive.Orm.SqlServer/Sql.Drivers.SqlServer/InternalHelpers.cs:167

  • [nitpick] The name dividerPow is a bit terse; consider renaming to powerOfTenIndex or maxDivisionPower for clarity.
    var dividerPow = maxZeroCount > 9 ? (byte) 9 : maxZeroCount;

Orm/Xtensive.Orm.SqlServer/Sql.Drivers.SqlServer/InternalHelpers.cs:182

  • Add unit tests covering scenarios where the original value exceeds 96 bits and triggers the overflow branch, to ensure behavior matches the previous implementation.
    for (; realScale > 0 && data > Max96bitValue; realScale--) {

@SergeiPavlov
Copy link
Collaborator Author

Why not to use https://learn.microsoft.com/en-us/dotnet/api/system.data.sqltypes.sqldecimal.round for example?

I want to keep the original DO's logic
It tries to re-scale out-of-range SqlDecimal values

SergeiPavlov and others added 2 commits June 24, 2025 13:28
…rs.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
SergeiPavlov and others added 5 commits June 24, 2025 14:49
…rs.cs

Co-authored-by: Sergey Naumenko <152863015+snaumenko-st@users.noreply.github.com>
…rs.cs

Co-authored-by: Sergey Naumenko <152863015+snaumenko-st@users.noreply.github.com>
Copy link

@snaumenko-st snaumenko-st left a comment

Choose a reason for hiding this comment

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

This code is still hard to follow. But it's already better than before.
Maybe we can even get rid of PowersOf10 array for simplicity.

@SergeiPavlov
Copy link
Collaborator Author

Maybe we can even get rid of PowersOf10 array for simplicity.

I agree and have removed it.

Comment on lines +139 to +142
try {
return sqlDecimal.Value; // throws an OverflowException if the value is out of the decimal range.
}
catch (OverflowException) {

Choose a reason for hiding this comment

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

What is the probability of this in real use cases?
Exceptions are not cheap if it's not a rare case.

Copy link
Collaborator Author

@SergeiPavlov SergeiPavlov Jun 25, 2025

Choose a reason for hiding this comment

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

I never seen OverflowException here during local App runs.
I suppose that money values in the real DB don't have > 28 decimal places.
Some aggregations like AVG() can, but their invocation is a rare case comparing
field access when we avoid array allocation by SqlDecimal.Data

It looks as processing of an extreme case.

@SergeiPavlov SergeiPavlov merged commit 72524cc into master-servicetitan Jun 25, 2025
5 checks passed
@SergeiPavlov SergeiPavlov deleted the TruncateToNetDecimal branch June 25, 2025 02:35
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.

2 participants