This is an automated email from the ASF dual-hosted git repository.

ptupitsyn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 3175d442ee IGNITE-23340 .NET: Add BigDecimal type (#4573)
3175d442ee is described below

commit 3175d442eedc4df0d7dbf6e3f5624b1541d23c49
Author: Pavel Tupitsyn <[email protected]>
AuthorDate: Wed Oct 16 15:52:36 2024 +0300

    IGNITE-23340 .NET: Add BigDecimal type (#4573)
    
    Add `BigDecimal` type to support full range of values that Ignite can 
handle.
    
    .NET `decimal` type has ~29 digit precision, which is less than 32767 max 
precision in Ignite, so a custom type is required.
    
    * Native `decimal` can still be used with POCO mappers, LINQ, Compute, etc
    * Tuple-based APIs return `BigDecimal` (which can be converted to `decimal` 
with `ToDecimal` call)
---
 .../dotnet/Apache.Ignite.Tests/BigDecimalTests.cs  | 164 ++++++++++++++++++++
 .../Apache.Ignite.Tests/Compute/ComputeTests.cs    |  11 +-
 .../dotnet/Apache.Ignite.Tests/IgniteTestsBase.cs  |   3 +
 .../Linq/LinqTests.AsyncMaterialization.cs         |  19 +++
 .../dotnet/Apache.Ignite.Tests/Linq/LinqTests.cs   |   9 +-
 .../Proto/BinaryTuple/BinaryTupleTests.cs          |  80 +++++++---
 .../Sql/IgniteDbDataReaderTests.cs                 |   7 +-
 .../dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs     |  18 ++-
 .../Apache.Ignite.Tests/Table/DataStreamerTests.cs |   6 +
 .../Table/KeyValueViewPrimitiveTests.cs            |   1 +
 .../Table/PocoAllColumnsBigDecimal.cs              |  40 +++++
 .../Table/RecordViewBinaryTests.cs                 |   2 +-
 .../Table/RecordViewPocoTests.cs                   |  32 ++++
 .../Table/RecordViewPrimitiveTests.cs              |   1 +
 .../platforms/dotnet/Apache.Ignite/BigDecimal.cs   | 168 +++++++++++++++++++++
 .../Apache.Ignite/Internal/Linq/ResultSelector.cs  |  10 +-
 .../Proto/BinaryTuple/BinaryTupleBuilder.cs        |  77 +++++++---
 .../Proto/BinaryTuple/BinaryTupleReader.cs         |  43 +++---
 .../Internal/Sql/ColumnTypeExtensions.cs           |  39 +++--
 .../dotnet/Apache.Ignite/Internal/Sql/Sql.cs       |   2 +-
 .../Table/Serialization/BinaryTupleMethods.cs      |  26 +++-
 .../Table/Serialization/ObjectSerializerHandler.cs |   4 +-
 22 files changed, 671 insertions(+), 91 deletions(-)

diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/BigDecimalTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/BigDecimalTests.cs
new file mode 100644
index 0000000000..69108a5db7
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/BigDecimalTests.cs
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Apache.Ignite.Tests;
+
+using System;
+using System.Globalization;
+using System.Numerics;
+using NUnit.Framework;
+
+/// <summary>
+/// Add tests for <see cref="BigDecimal"/>.
+/// </summary>
+public class BigDecimalTests
+{
+    [Test]
+    [TestCase(0, 0, 0)]
+    [TestCase(1, 0, 1)]
+    [TestCase(1, 1, 0.1)]
+    [TestCase(1, -1, 10)]
+    [TestCase(1, 5, 0.00001)]
+    [TestCase(12345678912345, 6, 12345678.912345)]
+    [TestCase(987, -6, 987000000)]
+    public void TestToDecimal(long unscaled, short scale, decimal expected)
+    {
+        var bigDecimal = new BigDecimal(new BigInteger(unscaled), scale);
+
+        Assert.AreEqual(expected, bigDecimal.ToDecimal());
+    }
+
+    [Test]
+    public void TestFromDecimalToDecimal()
+    {
+        for (int i = 0; i < 100; i++)
+        {
+            var unscaled = Random.Shared.NextInt64(long.MinValue, 
long.MaxValue);
+            var scale = Random.Shared.Next(0, 25);
+
+            var decimalVal = new decimal(unscaled) / (decimal)Math.Pow(10, 
scale);
+            var bigDecimal = new BigDecimal(decimalVal);
+            var result = bigDecimal.ToDecimal();
+
+            Assert.AreEqual(decimalVal, result, $"Unscaled={unscaled}, 
Scale={scale}");
+        }
+    }
+
+    [Test]
+    public void TestToDecimalOutOfRange()
+    {
+        var bigDecimal = new 
BigDecimal(BigInteger.Parse("123456789123456789123456789123456789123456789"), 
3);
+
+        var ex = Assert.Throws<OverflowException>(() => 
bigDecimal.ToDecimal());
+        Assert.AreEqual("Value was either too large or too small for a 
Decimal.", ex.Message);
+    }
+
+    [Test]
+    [TestCase("0", 0, null, "0")]
+    [TestCase("0", 1, null, "0")]
+    [TestCase("0", -1, null, "0")]
+    [TestCase("1", 0, null, "1")]
+    [TestCase("1", 1, null, "0.1")]
+    [TestCase("1", -1, null, "10")]
+    [TestCase("1", 5, null, "0.00001")]
+    [TestCase("123", -2, null, "12300")]
+    [TestCase("123", 0, null, "123")]
+    [TestCase("123", 1, null, "12.3")]
+    [TestCase("123", 2, null, "1.23")]
+    [TestCase("123", 3, null, "0.123")]
+    [TestCase("123", 4, null, "0.0123")]
+    [TestCase("123", 5, null, "0.00123")]
+    [TestCase("123456789", 5, null, "1234.56789")]
+    [TestCase("123456789", 5, "", "1234.56789")]
+    [TestCase("123456789", 5, "en-US", "1234.56789")]
+    [TestCase("123456789", 5, "de-DE", "1234,56789")]
+    [TestCase("123456789123456789123456789123456789123456789", 35, "de-DE", 
"1234567891,23456789123456789123456789123456789")]
+    public void TestToString(string unscaled, short scale, string? 
cultureName, string expected)
+    {
+        var bigDecimal = new BigDecimal(BigInteger.Parse(unscaled), scale);
+
+        var str = cultureName == null
+            ? bigDecimal.ToString()
+            : bigDecimal.ToString(CultureInfo.GetCultureInfo(cultureName));
+
+        Assert.AreEqual(expected, str);
+    }
+
+    [Test]
+    [TestCase(0, 0, 0)]
+    [TestCase(1, 1, 0)]
+    [TestCase(12.3456, 123456, 4)]
+    [TestCase(12.34560, 123456, 4)]
+    [TestCase(.1, 1, 1)]
+    public void TestUnscaledValueAndScale(decimal val, long expectedUnscaled, 
short expectedScale)
+    {
+        var bigDecimal = new BigDecimal(val);
+
+        Assert.AreEqual(expectedUnscaled, (long)bigDecimal.UnscaledValue);
+        Assert.AreEqual(expectedScale, bigDecimal.Scale);
+    }
+
+    [Test]
+    public void TestEquality([Values(0, 1, -1, 0.1, -0.1, 1234567, -456.789)] 
decimal d)
+    {
+        var x = new BigDecimal(d);
+        var y = new BigDecimal(d);
+
+        Assert.AreEqual(x, y);
+        Assert.AreEqual(x.ToDecimal(), y.ToDecimal());
+
+        Assert.AreEqual(x.UnscaledValue, y.UnscaledValue);
+        Assert.AreEqual(x.Scale, y.Scale);
+
+        Assert.AreEqual(0, x.CompareTo(y));
+        Assert.AreEqual(0, y.CompareTo(x));
+
+        Assert.IsFalse(x < y);
+        Assert.IsFalse(x > y);
+
+        Assert.IsTrue(x <= y);
+        Assert.IsTrue(x >= y);
+    }
+
+    [Test]
+    public void TestSameValueDifferentScaleAreNotEqual()
+    {
+        var x = new BigDecimal(100, 0);
+        var y = new BigDecimal(10, -1);
+
+        // Similar to Java BigDecimal.
+        Assert.AreNotEqual(x, y);
+        Assert.AreEqual(0, x.CompareTo(y));
+        Assert.AreEqual(x.ToString(), y.ToString());
+        Assert.AreEqual(100, x.ToDecimal());
+        Assert.AreEqual(100, y.ToDecimal());
+    }
+
+    [Test]
+    public void TestCompareTo()
+    {
+        var x = new BigDecimal(1234, 1000);
+        var y = new BigDecimal(1235, 1000);
+
+        Assert.AreEqual(-1, x.CompareTo(y));
+        Assert.AreEqual(1, y.CompareTo(x));
+        Assert.IsTrue(x < y);
+        Assert.IsTrue(x <= y);
+        Assert.IsTrue(y > x);
+        Assert.IsTrue(y >= x);
+    }
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeTests.cs
index 9189a6f33a..530a5eb874 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeTests.cs
@@ -59,7 +59,7 @@ namespace Apache.Ignite.Tests.Compute
 
         public static readonly JobDescriptor<int, string> SleepJob = 
new(ItThinClientComputeTest + "$SleepJob");
 
-        public static readonly JobDescriptor<string, decimal> DecimalJob = 
new(ItThinClientComputeTest + "$DecimalJob");
+        public static readonly JobDescriptor<string, BigDecimal> DecimalJob = 
new(ItThinClientComputeTest + "$DecimalJob");
 
         public static readonly JobDescriptor<string, string> CreateTableJob = 
new(PlatformTestNodeRunner + "$CreateTableJob");
 
@@ -241,6 +241,8 @@ namespace Apache.Ignite.Tests.Compute
             await Test(-123.456m);
             await Test(decimal.MinValue);
             await Test(decimal.MaxValue);
+            await Test(new BigDecimal(long.MinValue, 10));
+            await Test(new BigDecimal(long.MaxValue, 20));
 
             await Test(new byte[] { 1, 255 }, "[1, -1]");
             await Test("Ignite 🔥");
@@ -260,6 +262,11 @@ namespace Apache.Ignite.Tests.Compute
                 IJobExecution<object> resExec = await 
Client.Compute.SubmitAsync(nodes, EchoJob, val);
                 object res = await resExec.GetResultAsync();
 
+                if (res is BigDecimal bigDecimal && val is decimal)
+                {
+                    res = bigDecimal.ToDecimal();
+                }
+
                 Assert.AreEqual(val, res);
 
                 var strExec = await Client.Compute.SubmitAsync(nodes, 
ToStringJob, val);
@@ -709,7 +716,7 @@ namespace Apache.Ignite.Tests.Compute
                 expected = decimal.Round(expected, scale);
             }
 
-            Assert.AreEqual(expected, resVal);
+            Assert.AreEqual(expected, resVal.ToDecimal());
         }
 
         [Test]
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/IgniteTestsBase.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/IgniteTestsBase.cs
index d70fecbb7b..dd6ff0b127 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/IgniteTestsBase.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/IgniteTestsBase.cs
@@ -88,6 +88,8 @@ namespace Apache.Ignite.Tests
 
         protected IRecordView<PocoAllColumns> PocoAllColumnsView { get; 
private set; } = null!;
 
+        protected IRecordView<PocoAllColumnsBigDecimal> 
PocoAllColumnsBigDecimalView { get; private set; } = null!;
+
         protected IRecordView<PocoAllColumnsNullable> 
PocoAllColumnsNullableView { get; private set; } = null!;
 
         protected IRecordView<PocoAllColumnsSql> PocoAllColumnsSqlView { get; 
private set; } = null!;
@@ -110,6 +112,7 @@ namespace Apache.Ignite.Tests
 
             var tableAllColumnsNotNull = await 
Client.Tables.GetTableAsync(TableAllColumnsNotNullName);
             PocoAllColumnsView = 
tableAllColumnsNotNull!.GetRecordView<PocoAllColumns>();
+            PocoAllColumnsBigDecimalView = 
tableAllColumnsNotNull.GetRecordView<PocoAllColumnsBigDecimal>();
 
             var tableAllColumnsSql = await 
Client.Tables.GetTableAsync(TableAllColumnsSqlName);
             PocoAllColumnsSqlView = 
tableAllColumnsSql!.GetRecordView<PocoAllColumnsSql>();
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Linq/LinqTests.AsyncMaterialization.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Linq/LinqTests.AsyncMaterialization.cs
index df78becacc..8eff6a8396 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Linq/LinqTests.AsyncMaterialization.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Linq/LinqTests.AsyncMaterialization.cs
@@ -297,4 +297,23 @@ public partial class LinqTests
 
         Assert.AreEqual("Sequence contains no elements", ex!.Message);
     }
+
+    [Test]
+    public void TestDecimalMaterialization()
+    {
+        var query = PocoBigDecimalView.AsQueryable();
+
+        var key = new BigDecimal(6);
+
+        BigDecimal? primitive = query
+            .Where(x => x.Key == key)
+            .Select(x => x.Val)
+            .Single();
+
+        PocoBigDecimal poco = query.Single(x => x.Key == key);
+
+        Assert.AreEqual(key, primitive);
+        Assert.AreEqual(key, poco.Key);
+        Assert.AreEqual(key, poco.Val);
+    }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Linq/LinqTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Linq/LinqTests.cs
index 4b66655358..af12336cc3 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Linq/LinqTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Linq/LinqTests.cs
@@ -57,6 +57,8 @@ public partial class LinqTests : IgniteTestsBase
 
     private IRecordView<PocoDecimal> PocoDecimalView { get; set; } = null!;
 
+    private IRecordView<PocoBigDecimal> PocoBigDecimalView { get; set; } = 
null!;
+
     private IRecordView<PocoString> PocoStringView { get; set; } = null!;
 
     [OneTimeSetUp]
@@ -80,9 +82,12 @@ public partial class LinqTests : IgniteTestsBase
         PocoLongView = (await 
Client.Tables.GetTableAsync(TableInt64Name))!.GetRecordView<PocoLong>();
         PocoFloatView = (await 
Client.Tables.GetTableAsync(TableFloatName))!.GetRecordView<PocoFloat>();
         PocoDoubleView = (await 
Client.Tables.GetTableAsync(TableDoubleName))!.GetRecordView<PocoDouble>();
-        PocoDecimalView = (await 
Client.Tables.GetTableAsync(TableDecimalName))!.GetRecordView<PocoDecimal>();
         PocoStringView = (await 
Client.Tables.GetTableAsync(TableStringName))!.GetRecordView<PocoString>();
 
+        var tableDecimal = await Client.Tables.GetTableAsync(TableDecimalName);
+        PocoDecimalView = tableDecimal!.GetRecordView<PocoDecimal>();
+        PocoBigDecimalView = tableDecimal.GetRecordView<PocoBigDecimal>();
+
         for (int i = 0; i < Count; i++)
         {
             await PocoView.UpsertAsync(null, new Poco { Key = i, Val = "v-" + 
i });
@@ -737,6 +742,8 @@ public partial class LinqTests : IgniteTestsBase
 
     private record PocoDecimal(decimal Key, decimal? Val);
 
+    private record PocoBigDecimal(BigDecimal Key, BigDecimal? Val);
+
     private record PocoString(string Key, string? Val);
 
     private record PocoDate(LocalDate Key, LocalDate? Val);
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/BinaryTuple/BinaryTupleTests.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/BinaryTuple/BinaryTupleTests.cs
index 6b0ad94cbb..c99d05e726 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/BinaryTuple/BinaryTupleTests.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/BinaryTuple/BinaryTupleTests.cs
@@ -462,7 +462,39 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
             static void Test(decimal val, int scale, decimal? expected = null)
             {
                 var reader = BuildAndRead((ref BinaryTupleBuilder b) => 
b.AppendDecimal(val, scale));
-                var res = reader.GetDecimal(0, scale);
+                var res = reader.GetBigDecimal(0, scale).ToDecimal();
+
+                Assert.AreEqual(expected ?? val, res);
+            }
+        }
+
+        [Test]
+        public void TestBigDecimal()
+        {
+            Test(0, 3);
+            Test(0, 0);
+
+            Test(decimal.MaxValue, 0);
+            Test(decimal.MinValue, 0);
+
+            Test(12345.6789m, 4);
+            Test(12345.678m, 4);
+            Test(12345.67m, 4);
+
+            Test(-12345.6789m, 4);
+            Test(-12345.678m, 4);
+            Test(-12345.67m, 4);
+
+            Test(12345.6789m, 2, 12345.67m);
+            Test(12345.6789m, 0, 12345m);
+
+            Test(-12345.6789m, 2, -12345.67m);
+            Test(-12345.6789m, 0, -12345m);
+
+            static void Test(decimal val, int scale, decimal? expected = null)
+            {
+                var reader = BuildAndRead((ref BinaryTupleBuilder b) => 
b.AppendBigDecimal(new BigDecimal(val), scale));
+                var res = reader.GetBigDecimal(0, scale).ToDecimal();
 
                 Assert.AreEqual(expected ?? val, res);
             }
@@ -473,9 +505,9 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
         {
             const int scale = 100;
 
-            var ex = Assert.Throws<OverflowException>(
-                () => BuildAndRead((ref BinaryTupleBuilder b) => 
b.AppendBytes(new byte[] { 64, 64, 64 })).GetDecimal(0, scale));
+            var res = BuildAndRead((ref BinaryTupleBuilder b) => 
b.AppendBytes(new byte[] { 64, 64, 64 })).GetBigDecimal(0, scale);
 
+            var ex = Assert.Throws<OverflowException>(() => res.ToDecimal());
             Assert.AreEqual("Value was either too large or too small for a 
Decimal.", ex!.Message);
         }
 
@@ -484,9 +516,9 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
         {
             var magnitude = Enumerable.Range(1, 100).Select(_ => 
(byte)250).ToArray();
 
-            var ex = Assert.Throws<OverflowException>(
-                () => BuildAndRead((ref BinaryTupleBuilder b) => 
b.AppendBytes(magnitude)).GetDecimal(0, 0));
+            var res = BuildAndRead((ref BinaryTupleBuilder b) => 
b.AppendBytes(magnitude)).GetBigDecimal(0, 0);
 
+            var ex = Assert.Throws<OverflowException>(() => res.ToDecimal());
             Assert.AreEqual("Value was either too large or too small for a 
Decimal.", ex!.Message);
         }
 
@@ -655,7 +687,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
             Assert.IsNull(reader.GetLongNullable(0));
             Assert.IsNull(reader.GetDoubleNullable(0));
             Assert.IsNull(reader.GetFloatNullable(0));
-            Assert.IsNull(reader.GetDecimalNullable(0, 123));
+            Assert.IsNull(reader.GetBigDecimalNullable(0, 123));
             Assert.IsNull(reader.GetStringNullable(0));
             Assert.IsNull(reader.GetGuidNullable(0));
             Assert.IsNull(reader.GetBytesNullable(0));
@@ -699,6 +731,8 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
                     b.AppendGuidNullable(null);
                     b.AppendDecimalNullable(1, 3);
                     b.AppendDecimalNullable(null, 3);
+                    b.AppendBigDecimalNullable(new BigDecimal(1), 3);
+                    b.AppendBigDecimalNullable(null, 3);
                     b.AppendDateNullable(date);
                     b.AppendDateNullable(null);
                     b.AppendTimeNullable(dateTime.TimeOfDay, 
TemporalTypes.MaxTimePrecision);
@@ -732,20 +766,22 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
             Assert.IsNull(reader.GetBytesNullable(15));
             Assert.AreEqual(guid, reader.GetGuidNullable(16));
             Assert.IsNull(reader.GetGuidNullable(17));
-            Assert.AreEqual(1, reader.GetDecimalNullable(18, 3));
-            Assert.IsNull(reader.GetDecimalNullable(19, 3));
-            Assert.AreEqual(date, reader.GetDateNullable(20));
-            Assert.IsNull(reader.GetDateNullable(21));
-            Assert.AreEqual(dateTime.TimeOfDay, reader.GetTimeNullable(22));
-            Assert.IsNull(reader.GetTimeNullable(23));
-            Assert.AreEqual(dateTime, reader.GetDateTimeNullable(24));
-            Assert.IsNull(reader.GetDateTimeNullable(25));
-            Assert.AreEqual(Instant.FromDateTimeUtc(utcNow), 
reader.GetTimestampNullable(26));
-            Assert.IsNull(reader.GetTimestampNullable(27));
-            Assert.AreEqual(Duration.FromMinutes(1), 
reader.GetDurationNullable(28));
-            Assert.IsNull(reader.GetDurationNullable(29));
-            Assert.AreEqual(Period.FromDays(1), reader.GetPeriodNullable(30));
-            Assert.IsNull(reader.GetPeriodNullable(31));
+            Assert.AreEqual(1, reader.GetBigDecimalNullable(18, 
3)!.Value.ToDecimal());
+            Assert.IsNull(reader.GetBigDecimalNullable(19, 3));
+            Assert.AreEqual(1, reader.GetBigDecimalNullable(20, 
3)!.Value.ToDecimal());
+            Assert.IsNull(reader.GetBigDecimalNullable(21, 3));
+            Assert.AreEqual(date, reader.GetDateNullable(22));
+            Assert.IsNull(reader.GetDateNullable(23));
+            Assert.AreEqual(dateTime.TimeOfDay, reader.GetTimeNullable(24));
+            Assert.IsNull(reader.GetTimeNullable(25));
+            Assert.AreEqual(dateTime, reader.GetDateTimeNullable(26));
+            Assert.IsNull(reader.GetDateTimeNullable(27));
+            Assert.AreEqual(Instant.FromDateTimeUtc(utcNow), 
reader.GetTimestampNullable(28));
+            Assert.IsNull(reader.GetTimestampNullable(29));
+            Assert.AreEqual(Duration.FromMinutes(1), 
reader.GetDurationNullable(30));
+            Assert.IsNull(reader.GetDurationNullable(31));
+            Assert.AreEqual(Period.FromDays(1), reader.GetPeriodNullable(32));
+            Assert.IsNull(reader.GetPeriodNullable(33));
         }
 
         [Test]
@@ -785,7 +821,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
             Assert.AreEqual(long.MaxValue, reader.GetObject(4, 
ColumnType.Int64));
             Assert.AreEqual(float.MaxValue, reader.GetObject(5, 
ColumnType.Float));
             Assert.AreEqual(double.MaxValue, reader.GetObject(6, 
ColumnType.Double));
-            Assert.AreEqual(decimal.One, reader.GetObject(7, 
ColumnType.Decimal));
+            Assert.AreEqual(new BigDecimal(1), reader.GetObject(7, 
ColumnType.Decimal));
             Assert.AreEqual("foo", reader.GetObject(8, ColumnType.String));
             Assert.AreEqual(guid, reader.GetObject(9, ColumnType.Uuid));
             Assert.AreEqual(bytes, reader.GetObject(10, ColumnType.ByteArray));
@@ -835,7 +871,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
             Assert.AreEqual(long.MaxValue, reader.GetObject(12));
             Assert.AreEqual(float.MaxValue, reader.GetObject(15));
             Assert.AreEqual(double.MaxValue, reader.GetObject(18));
-            Assert.AreEqual(decimal.One, reader.GetObject(21));
+            Assert.AreEqual(new BigDecimal(1), reader.GetObject(21));
             Assert.AreEqual("foo", reader.GetObject(24));
             Assert.AreEqual(guid, reader.GetObject(27));
             Assert.AreEqual(bytes, reader.GetObject(30));
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbDataReaderTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbDataReaderTests.cs
index 3a3527f97f..013651ab28 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbDataReaderTests.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbDataReaderTests.cs
@@ -139,6 +139,7 @@ public class IgniteDbDataReaderTests : IgniteTestsBase
         Assert.AreEqual(new DateTime(2023, 01, 18, 09, 29, 0), 
reader.GetDateTime("DATETIME"));
         Assert.AreEqual(Instant.ToDateTimeUtc(), 
reader.GetDateTime("TIMESTAMP"));
         Assert.AreEqual(8.7m, reader.GetDecimal("DECIMAL"));
+        Assert.AreEqual(new BigDecimal( 8.7m), reader.GetValue("DECIMAL"));
         Assert.AreEqual(2, reader.GetBytes("BLOB", 0, null!, 0, 0));
         Assert.AreEqual(Guid, reader.GetGuid("UUID"));
         Assert.IsTrue(reader.GetBoolean("BOOLEAN"));
@@ -439,7 +440,7 @@ public class IgniteDbDataReaderTests : IgniteTestsBase
         Assert.AreEqual(LocalTime, reader.GetValue("TIME"));
         Assert.AreEqual(LocalDateTime, reader.GetValue("DATETIME"));
         Assert.AreEqual(Instant, reader.GetValue("TIMESTAMP"));
-        Assert.AreEqual(8.7m, reader.GetValue("DECIMAL"));
+        Assert.AreEqual(new BigDecimal(8.7m), reader.GetValue("DECIMAL"));
         Assert.AreEqual(Bytes, reader.GetValue("BLOB"));
         Assert.IsNull(reader.GetValue("NULL"));
     }
@@ -454,7 +455,7 @@ public class IgniteDbDataReaderTests : IgniteTestsBase
 
         var expected = new object?[]
         {
-            1, "v-1", 2, 3, 4, 5, 6.5f, 7.5d, LocalDate, LocalTime, 
LocalDateTime, Instant, Bytes, 8.7m, Guid, true, null
+            1, "v-1", 2, 3, 4, 5, 6.5f, 7.5d, LocalDate, LocalTime, 
LocalDateTime, Instant, Bytes, new BigDecimal(8.7m), Guid, true, null
         };
 
         CollectionAssert.AreEqual(expected, values);
@@ -630,7 +631,7 @@ public class IgniteDbDataReaderTests : IgniteTestsBase
         Assert.AreEqual(typeof(LocalDateTime), reader.GetFieldType(10));
         Assert.AreEqual(typeof(Instant), reader.GetFieldType(11));
         Assert.AreEqual(typeof(byte[]), reader.GetFieldType(12));
-        Assert.AreEqual(typeof(decimal), reader.GetFieldType(13));
+        Assert.AreEqual(typeof(BigDecimal), reader.GetFieldType(13));
     }
 
     [Test]
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
index fe5a079a9c..251b20a447 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/SqlTests.cs
@@ -519,7 +519,23 @@ namespace Apache.Ignite.Tests.Sql
             await using var resultSet = await Client.Sql.ExecuteAsync(null, 
"select cast((10 / ?) as decimal(20, 5))", 3m);
             IIgniteTuple res = await resultSet.SingleAsync();
 
-            Assert.AreEqual(3.33333m, res[0]);
+            var bigDecimal = (BigDecimal)res[0]!;
+
+            Assert.AreEqual(3.33333m, bigDecimal.ToDecimal());
+            Assert.AreEqual(5, bigDecimal.Scale);
+        }
+
+        [Test]
+        public async Task TestMaxDecimalScale()
+        {
+            await using var resultSet = await Client.Sql.ExecuteAsync(null, 
"select (10 / ?)", 3m);
+            IIgniteTuple res = await resultSet.SingleAsync();
+
+            var bigDecimal = (BigDecimal)res[0]!;
+
+            Assert.AreEqual(32757, bigDecimal.Scale);
+            Assert.AreEqual(13603, bigDecimal.UnscaledValue.GetByteCount());
+            StringAssert.StartsWith("3.3333333333", bigDecimal.ToString());
         }
 
         [Test]
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/DataStreamerTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/DataStreamerTests.cs
index 644b55cbcb..61735e9d19 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/DataStreamerTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/DataStreamerTests.cs
@@ -66,6 +66,7 @@ public class DataStreamerTests : IgniteTestsBase
         float.MaxValue,
         double.MinValue,
         decimal.One,
+        new BigDecimal(1234, 2),
         new LocalDate(1234, 5, 6),
         new LocalTime(12, 3, 4, 567),
         new LocalDateTime(1234, 5, 6, 7, 8, 9),
@@ -693,6 +694,11 @@ public class DataStreamerTests : IgniteTestsBase
             EchoArgsReceiver,
             receiverArg: arg).SingleAsync();
 
+        if (arg is decimal dec)
+        {
+            arg = new BigDecimal(dec);
+        }
+
         Assert.AreEqual(arg, res);
     }
 
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPrimitiveTests.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPrimitiveTests.cs
index 6a4306bacf..d0c9fba9f3 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPrimitiveTests.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPrimitiveTests.cs
@@ -367,6 +367,7 @@ public class KeyValueViewPrimitiveTests : IgniteTestsBase
         await TestKey(1.1f, (float?)1.1f, TableFloatName);
         await TestKey(1.1d, (double?)1.1d, TableDoubleName);
         await TestKey(1.234m, (decimal?)1.234m, TableDecimalName);
+        await TestKey(new BigDecimal(1.234m), (BigDecimal?)new 
BigDecimal(1.234m), TableDecimalName);
         await TestKey("foo", "foo", TableStringName);
 
         var localDateTime = new LocalDateTime(2022, 10, 13, 8, 4, 42);
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/PocoAllColumnsBigDecimal.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/PocoAllColumnsBigDecimal.cs
new file mode 100644
index 0000000000..6a88773bec
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/PocoAllColumnsBigDecimal.cs
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Apache.Ignite.Tests.Table
+{
+    using System;
+    using System.Diagnostics.CodeAnalysis;
+
+    /// <summary>
+    /// Test user object.
+    /// </summary>
+    [SuppressMessage("Microsoft.Naming", "CA1720:AvoidTypeNamesInParameters", 
Justification = "POCO mapping.")]
+    [SuppressMessage("Microsoft.Usage", 
"CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "POCO mapping.")]
+    [SuppressMessage("Microsoft.Performance", 
"CA1819:PropertiesShouldNotReturnArrays", Justification = "POCO mapping.")]
+    public record PocoAllColumnsBigDecimal(
+        long Key,
+        string? Str,
+        sbyte Int8,
+        short Int16,
+        int Int32,
+        long Int64,
+        float Float,
+        double Double,
+        Guid Uuid,
+        BigDecimal Decimal);
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewBinaryTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewBinaryTests.cs
index edb3fd879e..c8fe1527fc 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewBinaryTests.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewBinaryTests.cs
@@ -560,7 +560,7 @@ namespace Apache.Ignite.Tests.Table
                 ["DateTime"] = dt,
                 ["Timestamp"] = Instant.FromDateTimeUtc(DateTime.UtcNow),
                 ["Blob"] = new byte[] { 1, 2, 3 },
-                ["Decimal"] = 123.456m,
+                ["Decimal"] = new BigDecimal(123.456m),
                 ["Boolean"] = true
             };
 
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPocoTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPocoTests.cs
index 0461fc942f..926c8390e2 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPocoTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPocoTests.cs
@@ -638,6 +638,38 @@ namespace Apache.Ignite.Tests.Table
             Assert.AreEqual(poco.Uuid, res.Uuid);
         }
 
+        [Test]
+        public async Task TestAllColumnsPocoBigDecimal()
+        {
+            var pocoView = PocoAllColumnsBigDecimalView;
+
+            var poco = new PocoAllColumnsBigDecimal(
+                Key: 123,
+                Str: "str",
+                Int8: 8,
+                Int16: 16,
+                Int32: 32,
+                Int64: 64,
+                Float: 32.32f,
+                Double: 64.64,
+                Uuid: Guid.NewGuid(),
+                Decimal: new BigDecimal(123456789, 2));
+
+            await pocoView.UpsertAsync(null, poco);
+
+            var res = (await pocoView.GetAsync(null, poco)).Value;
+
+            Assert.AreEqual(poco.Decimal, res.Decimal);
+            Assert.AreEqual(poco.Double, res.Double);
+            Assert.AreEqual(poco.Float, res.Float);
+            Assert.AreEqual(poco.Int8, res.Int8);
+            Assert.AreEqual(poco.Int16, res.Int16);
+            Assert.AreEqual(poco.Int32, res.Int32);
+            Assert.AreEqual(poco.Int64, res.Int64);
+            Assert.AreEqual(poco.Str, res.Str);
+            Assert.AreEqual(poco.Uuid, res.Uuid);
+        }
+
         [Test]
         public async Task TestAllColumnsPocoNullableNotNull()
         {
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPrimitiveTests.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPrimitiveTests.cs
index eaf3f63279..72a7ea80c9 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPrimitiveTests.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPrimitiveTests.cs
@@ -43,6 +43,7 @@ public class RecordViewPrimitiveTests : IgniteTestsBase
         await TestKey(1.1f, TableFloatName);
         await TestKey(1.1d, TableDoubleName);
         await TestKey(1.234m, TableDecimalName);
+        await TestKey(new BigDecimal(1.234m), TableDecimalName);
         await TestKey("foo", TableStringName);
         await TestKey(new LocalDateTime(2022, 10, 13, 8, 4, 42), 
TableDateTimeName);
         await TestKey(new LocalTime(3, 4, 5), TableTimeName);
diff --git a/modules/platforms/dotnet/Apache.Ignite/BigDecimal.cs 
b/modules/platforms/dotnet/Apache.Ignite/BigDecimal.cs
new file mode 100644
index 0000000000..98913db6ee
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite/BigDecimal.cs
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Apache.Ignite;
+
+using System;
+using System.Globalization;
+using System.Numerics;
+using Internal.Common;
+using Internal.Proto.BinaryTuple;
+
+/// <summary>
+/// Big decimal.
+/// <para />
+/// Ignite supports values with up to <see cref="short.MaxValue"/> precision 
(in tables, SQL, Compute, and other APIs).
+/// .NET <see cref="decimal"/> has 28-29 digit precision and can not represent 
all values that Ignite supports.
+/// This type fills the gap.
+/// </summary>
+public readonly record struct BigDecimal : IComparable<BigDecimal>, IComparable
+{
+    /// <summary>
+    /// Initializes a new instance of the <see cref="BigDecimal"/> struct.
+    /// </summary>
+    /// <param name="unscaledValue">Unscaled value.</param>
+    /// <param name="scale">Scale.</param>
+    public BigDecimal(BigInteger unscaledValue, short scale)
+    {
+        UnscaledValue = unscaledValue;
+        Scale = scale;
+    }
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="BigDecimal"/> struct.
+    /// </summary>
+    /// <param name="value">Decimal value.</param>
+    public BigDecimal(decimal value) =>
+        (UnscaledValue, Scale) = 
BinaryTupleCommon.DecimalToUnscaledBigInteger(value, maxScale: short.MaxValue);
+
+    /// <summary>
+    /// Gets the unscaled value.
+    /// </summary>
+    public BigInteger UnscaledValue { get; }
+
+    /// <summary>
+    /// Gets the scale.
+    /// </summary>
+    public short Scale { get; }
+
+#pragma warning disable CS1591 // Missing XML comment for publicly visible 
type or member
+
+    public static bool operator <(BigDecimal left, BigDecimal right) => 
left.CompareTo(right) < 0;
+
+    public static bool operator <=(BigDecimal left, BigDecimal right) => 
left.CompareTo(right) <= 0;
+
+    public static bool operator >(BigDecimal left, BigDecimal right) => 
left.CompareTo(right) > 0;
+
+    public static bool operator >=(BigDecimal left, BigDecimal right) => 
left.CompareTo(right) >= 0;
+
+#pragma warning restore CS1591 // Missing XML comment for publicly visible 
type or member
+
+    /// <summary>
+    /// Converts the value of this instance to its equivalent decimal 
representation.
+    /// </summary>
+    /// <returns>Decimal representation of the current value.</returns>
+    /// <exception cref="OverflowException">Value was either too large or too 
small for a Decimal.</exception>
+    public decimal ToDecimal()
+    {
+        decimal res = (decimal)UnscaledValue;
+
+        if (Scale > 0)
+        {
+            res /= (decimal)BigInteger.Pow(10, Scale);
+        }
+        else if (Scale < 0)
+        {
+            res *= (decimal)BigInteger.Pow(10, -Scale);
+        }
+
+        return res;
+    }
+
+    /// <inheritdoc />
+    public int CompareTo(BigDecimal other)
+    {
+        if (Equals(this, other))
+        {
+            return 0;
+        }
+
+        if (Scale == other.Scale)
+        {
+            return UnscaledValue.CompareTo(other.UnscaledValue);
+        }
+
+        if (Scale > other.Scale)
+        {
+            var otherVal = other.UnscaledValue * BigInteger.Pow(10, Scale - 
other.Scale);
+            return UnscaledValue.CompareTo(otherVal);
+        }
+
+        var thisVal = UnscaledValue * BigInteger.Pow(10, other.Scale - Scale);
+        return thisVal.CompareTo(other.UnscaledValue);
+    }
+
+    /// <inheritdoc />
+    public int CompareTo(object? obj)
+    {
+        if (obj is null)
+        {
+            return 1;
+        }
+
+        if (obj is BigDecimal other)
+        {
+            return CompareTo(other);
+        }
+
+        throw new ArgumentException($"Unexpected object type: {obj.GetType()}. 
Object must be of type {nameof(BigDecimal)}.");
+    }
+
+    /// <inheritdoc />
+    public override string ToString() => ToString(null);
+
+    /// <summary>
+    /// Converts the numeric value of this object to its equivalent string 
representation.
+    /// </summary>
+    /// <param name="provider">An object that supplies culture-specific 
formatting information.</param>
+    /// <returns>The string representation of the current value.</returns>
+    public string ToString(IFormatProvider? provider)
+    {
+        var numberFormatInfo = NumberFormatInfo.GetInstance(provider);
+
+        var res = UnscaledValue.ToString("D", provider);
+
+        if (Scale == 0 || res == "0")
+        {
+            return res;
+        }
+
+        if (Scale < 0)
+        {
+            return res + new string('0', -Scale);
+        }
+
+        res = res.PadLeft(Scale, '0');
+
+        if (res.Length == Scale)
+        {
+            return "0." + res;
+        }
+
+        return res.Insert(res.Length - Scale, 
numberFormatInfo.NumberDecimalSeparator);
+    }
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Linq/ResultSelector.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Linq/ResultSelector.cs
index 11333b4971..8ad3f7b5f0 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Linq/ResultSelector.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Linq/ResultSelector.cs
@@ -365,9 +365,10 @@ internal static class ResultSelector
         }
 
         var colType = col.Type.ToClrType(col.Nullable);
-        il.Emit(OpCodes.Call, BinaryTupleMethods.GetReadMethod(colType));
+        var readMethod = BinaryTupleMethods.GetReadMethod(colType, targetType);
+        il.Emit(OpCodes.Call, readMethod);
 
-        il.EmitConv(colType, targetType, col.Name);
+        il.EmitConv(readMethod.ReturnType, targetType, col.Name);
         il.MarkLabel(endParamLabel);
     }
 
@@ -405,9 +406,10 @@ internal static class ResultSelector
         }
 
         var colType = col.Type.ToClrType(col.Nullable);
-        il.Emit(OpCodes.Call, BinaryTupleMethods.GetReadMethod(colType));
+        var readMethod = BinaryTupleMethods.GetReadMethod(colType, 
columnInfo.Field.FieldType);
+        il.Emit(OpCodes.Call, readMethod);
 
-        il.EmitConv(colType, columnInfo.Field.FieldType, col.Name);
+        il.EmitConv(readMethod.ReturnType, columnInfo.Field.FieldType, 
col.Name);
         il.Emit(OpCodes.Stfld, columnInfo.Field); // res.field = value
 
         il.MarkLabel(endFieldLabel);
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleBuilder.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleBuilder.cs
index 32e7853147..986d1d9ec8 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleBuilder.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleBuilder.cs
@@ -544,24 +544,31 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         }
 
         /// <summary>
-        /// Appends a decimal.
+        /// Appends a big decimal.
         /// </summary>
         /// <param name="value">Value.</param>
         /// <param name="scale">Decimal scale from schema.</param>
-        public void AppendDecimal(decimal value, int scale)
+        public void AppendBigDecimal(BigDecimal value, int scale)
         {
-            var (unscaled, actualScale) = 
BinaryTupleCommon.DecimalToUnscaledBigInteger(value, scale);
+            var valueScale = value.Scale;
+            var unscaledValue = value.UnscaledValue;
 
-            PutShort(actualScale);
-            PutNumber(unscaled);
+            if (valueScale > scale)
+            {
+                unscaledValue /= BigInteger.Pow(10, valueScale - scale);
+                valueScale = (short)scale;
+            }
+
+            PutShort(valueScale);
+            PutNumber(unscaledValue);
         }
 
         /// <summary>
-        /// Appends a decimal.
+        /// Appends a nullable big decimal.
         /// </summary>
         /// <param name="value">Value.</param>
         /// <param name="scale">Decimal scale from schema.</param>
-        public void AppendDecimalNullable(decimal? value, int scale)
+        public void AppendBigDecimalNullable(BigDecimal? value, int scale)
         {
             if (value == null)
             {
@@ -569,10 +576,26 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
             }
             else
             {
-                AppendDecimal(value.Value, scale);
+                AppendBigDecimal(value.Value, scale);
             }
         }
 
+        /// <summary>
+        /// Appends a decimal.
+        /// </summary>
+        /// <param name="value">Value.</param>
+        /// <param name="scale">Decimal scale from schema.</param>
+        public void AppendDecimal(decimal value, int scale) =>
+            AppendBigDecimal(new BigDecimal(value), scale);
+
+        /// <summary>
+        /// Appends a nullable decimal.
+        /// </summary>
+        /// <param name="value">Value.</param>
+        /// <param name="scale">Decimal scale from schema.</param>
+        public void AppendDecimalNullable(decimal? value, int scale) =>
+            AppendBigDecimalNullable(value == null ? null : new 
BigDecimal(value.Value), scale);
+
         /// <summary>
         /// Appends a date.
         /// </summary>
@@ -824,7 +847,15 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
                     break;
 
                 case ColumnType.Decimal:
-                    AppendDecimal((decimal)value, scale);
+                    if (value is decimal dec)
+                    {
+                        AppendDecimal(dec, scale);
+                    }
+                    else
+                    {
+                        AppendBigDecimal((BigDecimal)value, scale);
+                    }
+
                     break;
 
                 case ColumnType.Date:
@@ -922,9 +953,14 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
                     break;
 
                 case decimal dec:
-                    var scale = GetDecimalScale(dec);
-                    AppendTypeAndScale(ColumnType.Decimal, scale);
-                    AppendDecimal(dec, scale);
+                    var bigDec0 = new BigDecimal(dec);
+                    AppendTypeAndScale(ColumnType.Decimal, bigDec0.Scale);
+                    AppendBigDecimal(bigDec0, bigDec0.Scale);
+                    break;
+
+                case BigDecimal bigDec:
+                    AppendTypeAndScale(ColumnType.Decimal, bigDec.Scale);
+                    AppendBigDecimal(bigDec, bigDec.Scale);
                     break;
 
                 case LocalDate localDate:
@@ -1072,6 +1108,15 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
 
                     break;
 
+                case BigDecimal:
+                    AppendTypeAndSize(ColumnType.Decimal, collection.Length);
+                    foreach (var item in collection)
+                    {
+                        AppendBigDecimal((BigDecimal)(object)item!, 
int.MaxValue);
+                    }
+
+                    break;
+
                 case LocalDate:
                     AppendTypeAndSize(ColumnType.Date, collection.Length);
                     foreach (var item in collection)
@@ -1194,14 +1239,6 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
             _buffer.Dispose();
         }
 
-        private static int GetDecimalScale(decimal value)
-        {
-            Span<int> bits = stackalloc int[4];
-            decimal.GetBits(value, bits);
-
-            return (bits[3] & 0x00FF0000) >> 16;
-        }
-
         private void PutByte(sbyte value) => 
_buffer.WriteByte(unchecked((byte)value));
 
         private void PutShort(short value) => _buffer.WriteShort(value);
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleReader.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleReader.cs
index 8568793522..9151d237c1 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleReader.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleReader.cs
@@ -253,7 +253,23 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         /// <param name="index">Index.</param>
         /// <param name="scale">Decimal scale.</param>
         /// <returns>Value.</returns>
-        public decimal GetDecimal(int index, int scale) => 
GetDecimalNullable(index, scale) ?? ThrowNullElementException<decimal>(index);
+        public BigDecimal GetBigDecimal(int index, int scale) => 
GetBigDecimalNullable(index, scale) ?? 
ThrowNullElementException<BigDecimal>(index);
+
+        /// <summary>
+        /// Gets a big decimal value.
+        /// </summary>
+        /// <param name="index">Index.</param>
+        /// <param name="scale">Decimal scale.</param>
+        /// <returns>Value.</returns>
+        public BigDecimal? GetBigDecimalNullable(int index, int scale) => 
ReadDecimal(Seek(index), scale);
+
+        /// <summary>
+        /// Gets a big decimal value.
+        /// </summary>
+        /// <param name="index">Index.</param>
+        /// <param name="scale">Decimal scale.</param>
+        /// <returns>Value.</returns>
+        public decimal GetDecimal(int index, int scale) => 
GetBigDecimal(index, scale).ToDecimal();
 
         /// <summary>
         /// Gets a decimal value.
@@ -261,7 +277,7 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         /// <param name="index">Index.</param>
         /// <param name="scale">Decimal scale.</param>
         /// <returns>Value.</returns>
-        public decimal? GetDecimalNullable(int index, int scale) => 
ReadDecimal(Seek(index), scale);
+        public decimal? GetDecimalNullable(int index, int scale) => 
GetBigDecimalNullable(index, scale)?.ToDecimal();
 
         /// <summary>
         /// Gets a local date value.
@@ -439,7 +455,7 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
                 ColumnType.Double => GetDoubleNullable(index),
                 ColumnType.Uuid => GetGuidNullable(index),
                 ColumnType.String => GetStringNullable(index),
-                ColumnType.Decimal => GetDecimalNullable(index, scale),
+                ColumnType.Decimal => GetBigDecimalNullable(index, scale),
                 ColumnType.ByteArray => GetBytesNullable(index),
                 ColumnType.Date => GetDateNullable(index),
                 ColumnType.Time => GetTimeNullable(index),
@@ -552,7 +568,7 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         }
 
         [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = 
"Schema scale is not required for deserialization.")]
-        private static decimal? ReadDecimal(ReadOnlySpan<byte> span, int scale)
+        private static BigDecimal? ReadDecimal(ReadOnlySpan<byte> span, int 
scale)
         {
             if (span.IsEmpty)
             {
@@ -560,24 +576,9 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
             }
 
             var valScale = BinaryPrimitives.ReadInt16LittleEndian(span[..2]);
-            return ReadDecimalUnscaled(span[2..], valScale);
-        }
-
-        private static decimal? ReadDecimalUnscaled(ReadOnlySpan<byte> span, 
int scale)
-        {
-            var unscaled = new BigInteger(span, isBigEndian: true);
-            var res = (decimal)unscaled;
-
-            if (scale > 0)
-            {
-                res /= (decimal)BigInteger.Pow(10, scale);
-            }
-            else if (scale < 0)
-            {
-                res *= (decimal)BigInteger.Pow(10, -scale);
-            }
+            var unscaled = new BigInteger(span[2..], isBigEndian: true);
 
-            return res;
+            return new BigDecimal(unscaled, valScale);
         }
 
         private static T ThrowNullElementException<T>(int index) => throw 
GetNullElementException(index);
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/ColumnTypeExtensions.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/ColumnTypeExtensions.cs
index 1723e59663..f900d953da 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/ColumnTypeExtensions.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/ColumnTypeExtensions.cs
@@ -19,7 +19,6 @@ namespace Apache.Ignite.Internal.Sql;
 
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using Ignite.Sql;
 using NodaTime;
 
@@ -28,13 +27,7 @@ using NodaTime;
 /// </summary>
 internal static class ColumnTypeExtensions
 {
-    private static readonly IReadOnlyDictionary<Type, ColumnType> ClrToSql =
-        Enum.GetValues<ColumnType>()
-            .ToDictionary(x => x.ToClrType(), x => x);
-
-    private static readonly IReadOnlyDictionary<Type, string> ClrToSqlName =
-        Enum.GetValues<ColumnType>()
-            .ToDictionary(x => x.ToClrType(), x => x.ToSqlTypeName());
+    private static readonly IReadOnlyDictionary<Type, ColumnType> ClrToSql = 
GetClrToSqlMap();
 
     /// <summary>
     /// Gets corresponding .NET type.
@@ -51,7 +44,7 @@ internal static class ColumnTypeExtensions
         ColumnType.Int64 => typeof(long),
         ColumnType.Float => typeof(float),
         ColumnType.Double => typeof(double),
-        ColumnType.Decimal => typeof(decimal),
+        ColumnType.Decimal => typeof(BigDecimal),
         ColumnType.Date => typeof(LocalDate),
         ColumnType.Time => typeof(LocalTime),
         ColumnType.Datetime => typeof(LocalDateTime),
@@ -64,6 +57,14 @@ internal static class ColumnTypeExtensions
         _ => throw new InvalidOperationException($"Invalid 
{nameof(ColumnType)}: {columnType}")
     };
 
+    /// <summary>
+    /// Gets alternative CLR type for a give column type.
+    /// </summary>
+    /// <param name="columnType">Column type.</param>
+    /// <returns>CLR type, or null when there is no alternative type.</returns>
+    public static Type? ToClrTypeAlternative(this ColumnType columnType) =>
+        columnType == ColumnType.Decimal ? typeof(decimal) : null;
+
     /// <summary>
     /// Gets corresponding .NET type.
     /// </summary>
@@ -111,8 +112,8 @@ internal static class ColumnTypeExtensions
     /// <param name="type">CLR type.</param>
     /// <returns>SQL type name.</returns>
     public static string ToSqlTypeName(this Type type) =>
-        ClrToSqlName.TryGetValue(Nullable.GetUnderlyingType(type) ?? type, out 
var sqlTypeName)
-            ? sqlTypeName
+        ClrToSql.TryGetValue(Nullable.GetUnderlyingType(type) ?? type, out var 
columnType)
+            ? columnType.ToSqlTypeName()
             : throw new InvalidOperationException($"Type is not supported in 
SQL: {type}");
 
     /// <summary>
@@ -138,4 +139,20 @@ internal static class ColumnTypeExtensions
     /// <returns>Whether the type is floating point.</returns>
     public static bool IsAnyFloat(this ColumnType columnType) =>
         columnType is ColumnType.Float or ColumnType.Double;
+
+    private static Dictionary<Type, ColumnType> GetClrToSqlMap()
+    {
+        var columnTypes = Enum.GetValues<ColumnType>();
+        var clrToSql = new Dictionary<Type, ColumnType>(columnTypes.Length + 
1);
+
+        foreach (var columnType in columnTypes)
+        {
+            var clrType = columnType.ToClrType();
+            clrToSql[clrType] = columnType;
+        }
+
+        clrToSql[typeof(decimal)] = ColumnType.Decimal;
+
+        return clrToSql;
+    }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/Sql.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/Sql.cs
index ac8cd34dd8..f4766fe6cf 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/Sql.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Sql/Sql.cs
@@ -124,7 +124,7 @@ namespace Apache.Ignite.Internal.Sql
                 ColumnType.Int64 => reader.GetLong(idx),
                 ColumnType.Float => reader.GetFloat(idx),
                 ColumnType.Double => reader.GetDouble(idx),
-                ColumnType.Decimal => reader.GetDecimal(idx, col.Scale),
+                ColumnType.Decimal => reader.GetBigDecimal(idx, col.Scale),
                 ColumnType.Date => reader.GetDate(idx),
                 ColumnType.Time => reader.GetTime(idx),
                 ColumnType.Datetime => reader.GetDateTime(idx),
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/BinaryTupleMethods.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/BinaryTupleMethods.cs
index 3cba2fb036..3cc5bbf451 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/BinaryTupleMethods.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/BinaryTupleMethods.cs
@@ -67,6 +67,8 @@ namespace Apache.Ignite.Internal.Table.Serialization
         private static readonly MethodInfo AppendTimestampNullable = 
typeof(BinaryTupleBuilder).GetMethod(nameof(BinaryTupleBuilder.AppendTimestampNullable))!;
         private static readonly MethodInfo AppendDecimal = 
typeof(BinaryTupleBuilder).GetMethod(nameof(BinaryTupleBuilder.AppendDecimal))!;
         private static readonly MethodInfo AppendDecimalNullable = 
typeof(BinaryTupleBuilder).GetMethod(nameof(BinaryTupleBuilder.AppendDecimalNullable))!;
+        private static readonly MethodInfo AppendBigDecimal = 
typeof(BinaryTupleBuilder).GetMethod(nameof(BinaryTupleBuilder.AppendBigDecimal))!;
+        private static readonly MethodInfo AppendBigDecimalNullable = 
typeof(BinaryTupleBuilder).GetMethod(nameof(BinaryTupleBuilder.AppendBigDecimalNullable))!;
         private static readonly MethodInfo AppendBytes =
             
typeof(BinaryTupleBuilder).GetMethod(nameof(BinaryTupleBuilder.AppendBytesNullable),
 new[] { typeof(byte[]) })!;
 
@@ -97,6 +99,8 @@ namespace Apache.Ignite.Internal.Table.Serialization
         private static readonly MethodInfo GetTimestampNullable = 
typeof(BinaryTupleReader).GetMethod(nameof(BinaryTupleReader.GetTimestampNullable))!;
         private static readonly MethodInfo GetDecimal = 
typeof(BinaryTupleReader).GetMethod(nameof(BinaryTupleReader.GetDecimal))!;
         private static readonly MethodInfo GetDecimalNullable = 
typeof(BinaryTupleReader).GetMethod(nameof(BinaryTupleReader.GetDecimalNullable))!;
+        private static readonly MethodInfo GetBigDecimal = 
typeof(BinaryTupleReader).GetMethod(nameof(BinaryTupleReader.GetBigDecimal))!;
+        private static readonly MethodInfo GetBigDecimalNullable = 
typeof(BinaryTupleReader).GetMethod(nameof(BinaryTupleReader.GetBigDecimalNullable))!;
         private static readonly MethodInfo GetBytes = 
typeof(BinaryTupleReader).GetMethod(nameof(BinaryTupleReader.GetBytesNullable))!;
 
         private static readonly IReadOnlyDictionary<Type, MethodInfo> 
WriteMethods = new Dictionary<Type, MethodInfo>
@@ -129,6 +133,8 @@ namespace Apache.Ignite.Internal.Table.Serialization
             { typeof(byte[]), AppendBytes },
             { typeof(decimal), AppendDecimal },
             { typeof(decimal?), AppendDecimalNullable },
+            { typeof(BigDecimal), AppendBigDecimal },
+            { typeof(BigDecimal?), AppendBigDecimalNullable }
         };
 
         private static readonly IReadOnlyDictionary<Type, MethodInfo> 
ReadMethods = new Dictionary<Type, MethodInfo>
@@ -158,6 +164,8 @@ namespace Apache.Ignite.Internal.Table.Serialization
             { typeof(LocalDateTime?), GetDateTimeNullable },
             { typeof(Instant), GetTimestamp },
             { typeof(Instant?), GetTimestampNullable },
+            { typeof(BigDecimal), GetBigDecimal },
+            { typeof(BigDecimal?), GetBigDecimalNullable },
             { typeof(decimal), GetDecimal },
             { typeof(decimal?), GetDecimalNullable },
             { typeof(byte[]), GetBytes }
@@ -182,9 +190,23 @@ namespace Apache.Ignite.Internal.Table.Serialization
         /// Gets the read method.
         /// </summary>
         /// <param name="type">Type of the value to read.</param>
+        /// <param name="targetTypeHint">User-requested type.</param>
         /// <returns>Read method for the specified value type.</returns>
-        public static MethodInfo GetReadMethod(Type type) =>
-            ReadMethods.TryGetValue(Unwrap(type), out var method) ? method : 
throw GetUnsupportedTypeException(type);
+        public static MethodInfo GetReadMethod(Type type, Type? targetTypeHint 
= null)
+        {
+            if (targetTypeHint != null)
+            {
+                if (type == typeof(BigDecimal) || type == typeof(BigDecimal?))
+                {
+                    if (targetTypeHint != typeof(BigDecimal) && targetTypeHint 
!= typeof(BigDecimal?))
+                    {
+                        type = Nullable.GetUnderlyingType(type) != null ? 
typeof(decimal?) : typeof(decimal);
+                    }
+                }
+            }
+
+            return ReadMethods.TryGetValue(Unwrap(type), out var method) ? 
method : throw GetUnsupportedTypeException(type);
+        }
 
         /// <summary>
         /// Gets the read method.
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
index 35bcf52947..5707e82f81 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
@@ -427,7 +427,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
 
             type = type.UnwrapEnum();
 
-            if (type != columnType)
+            if (type != columnType && type != 
column.Type.ToClrTypeAlternative())
             {
                 var message = $"Can't map field 
'{fieldInfo.DeclaringType?.Name}.{fieldInfo.Name}' of type 
'{fieldInfo.FieldType}' " +
                               $"to column '{column.Name}' of type 
'{columnType}' - types do not match.";
@@ -455,7 +455,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 type = nullableType;
             }
 
-            if (type != columnType)
+            if (type != columnType && type != 
column.Type.ToClrTypeAlternative())
             {
                 var message = $"Can't map '{type}' to column '{column.Name}' 
of type '{columnType}' - types do not match.";
 

Reply via email to