From 428fad25028e3d35b4f95bfc0ace540ad6918d5a Mon Sep 17 00:00:00 2001 From: Curt Hagenlocher Date: Wed, 29 Apr 2026 05:19:59 -0700 Subject: [PATCH 1/3] Unambiguously equate decima16 with SqlDecimal in Variant API. --- .../Shredding/VariantShredder.cs | 4 +- .../VariantJson/VariantJsonConverter.cs | 5 +- .../Variant/VariantValue.cs | 61 ++++++++----------- .../Shredding/ShreddedVariantReaderTests.cs | 2 +- .../ParquetTestingVectorTests.cs | 2 +- .../VariantSqlDecimalTests.cs | 43 ++++--------- .../VariantValueTests.cs | 30 +++------ 7 files changed, 50 insertions(+), 97 deletions(-) diff --git a/src/Apache.Arrow.Operations/Shredding/VariantShredder.cs b/src/Apache.Arrow.Operations/Shredding/VariantShredder.cs index 0a825b3f..0a3048c9 100644 --- a/src/Apache.Arrow.Operations/Shredding/VariantShredder.cs +++ b/src/Apache.Arrow.Operations/Shredding/VariantShredder.cs @@ -283,8 +283,8 @@ internal static object ExtractTypedValue(VariantValue value, ShredType shredType case ShredType.Float: return value.AsFloat(); case ShredType.Double: return value.AsDouble(); case ShredType.Decimal4: - case ShredType.Decimal8: - case ShredType.Decimal16: return value.AsDecimal(); + case ShredType.Decimal8: return value.AsDecimal(); + case ShredType.Decimal16: return value.AsSqlDecimal().Value; case ShredType.Date: return value.AsDateDays(); case ShredType.Timestamp: return value.AsTimestampMicros(); case ShredType.TimestampNtz: return value.AsTimestampNtzMicros(); diff --git a/src/Apache.Arrow.Operations/VariantJson/VariantJsonConverter.cs b/src/Apache.Arrow.Operations/VariantJson/VariantJsonConverter.cs index 843cd691..acc9a21f 100644 --- a/src/Apache.Arrow.Operations/VariantJson/VariantJsonConverter.cs +++ b/src/Apache.Arrow.Operations/VariantJson/VariantJsonConverter.cs @@ -243,10 +243,7 @@ internal static void WriteValue(Utf8JsonWriter writer, VariantValue value) writer.WriteNumberValue(value.AsDecimal()); break; case VariantPrimitiveType.Decimal16: - if (value.IsSqlDecimalStorage) - writer.WriteRawValue(value.AsSqlDecimal().ToString()); - else - writer.WriteNumberValue(value.AsDecimal()); + writer.WriteRawValue(value.AsSqlDecimal().ToString()); break; case VariantPrimitiveType.Date: DateTime date = value.AsDate(); diff --git a/src/Apache.Arrow.Scalars/Variant/VariantValue.cs b/src/Apache.Arrow.Scalars/Variant/VariantValue.cs index 8848ca70..a1ab0a71 100644 --- a/src/Apache.Arrow.Scalars/Variant/VariantValue.cs +++ b/src/Apache.Arrow.Scalars/Variant/VariantValue.cs @@ -109,17 +109,12 @@ public static VariantValue FromDecimal4(decimal value) => public static VariantValue FromDecimal8(decimal value) => new VariantValue(VariantPrimitiveType.Decimal8, (object)value); - /// Creates a Decimal16 variant value. - public static VariantValue FromDecimal16(decimal value) => - new VariantValue(VariantPrimitiveType.Decimal16, (object)value); - /// - /// Creates a Decimal16 variant value from a , always - /// producing . Values exceeding - /// range are stored as . - /// Use this when the target type is known (e.g. materializing a Decimal16 - /// shredded column); use when you - /// want the smallest decimal type that fits the value. + /// Creates a Decimal16 variant value. Decimal16 covers the full Parquet + /// 38-digit decimal range, which exceeds the 96-bit + /// mantissa, so the API uses uniformly. Use + /// when you want the smallest + /// decimal type that fits the value. /// public static VariantValue FromDecimal16(SqlDecimal value) { @@ -152,14 +147,13 @@ public static VariantValue FromDecimal(decimal value) { return FromDecimal8(value); } - return FromDecimal16(value); + return new VariantValue(VariantPrimitiveType.Decimal16, (object)value); } /// - /// Creates a decimal variant value from a . - /// Values fitting in are stored as Decimal4/8/16 using - /// the smallest type. Values exceeding range are stored - /// as Decimal16 with storage. + /// Creates a decimal variant value from a , choosing + /// the smallest decimal type that fits. Values exceeding + /// range produce a Decimal16. /// public static VariantValue FromSqlDecimal(SqlDecimal value) { @@ -182,7 +176,7 @@ public static VariantValue FromSqlDecimal(SqlDecimal value) { return FromDecimal8(d); } - return FromDecimal16(d); + return new VariantValue(VariantPrimitiveType.Decimal16, (object)d); } /// Creates a Date variant value from days since epoch. @@ -355,27 +349,30 @@ public double AsDouble() return BitConverter.Int64BitsToDouble(_inlineValue); } - /// Gets a decimal value (works for Decimal4, Decimal8, and Decimal16). - /// - /// For Decimal16 values stored as (exceeding 96 bits), - /// this will throw . Use instead. - /// + /// + /// Gets a decimal value. Supported for Decimal4 and Decimal8 only. + /// Decimal16 can hold 38-digit values that exceed + /// range, so use for Decimal16 instead. + /// public decimal AsDecimal() { if (_primitiveType == VariantPrimitiveType.Decimal4 || - _primitiveType == VariantPrimitiveType.Decimal8 || - _primitiveType == VariantPrimitiveType.Decimal16) + _primitiveType == VariantPrimitiveType.Decimal8) { - if (_objectValue is SqlDecimal sd) - return sd.Value; return (decimal)_objectValue; } + if (_primitiveType == VariantPrimitiveType.Decimal16) + { + throw new InvalidOperationException( + "Cannot read Decimal16 as System.Decimal; use AsSqlDecimal() instead."); + } throw new InvalidOperationException($"Cannot read decimal from variant type {_primitiveType}."); } /// - /// Gets a decimal value as a (works for Decimal4, Decimal8, and Decimal16). - /// Unlike , this method does not throw for large Decimal16 values. + /// Gets a decimal value as a . Works for Decimal4, + /// Decimal8, and Decimal16 (including values that exceed + /// range). /// public SqlDecimal AsSqlDecimal() { @@ -390,16 +387,6 @@ public SqlDecimal AsSqlDecimal() throw new InvalidOperationException($"Cannot read decimal from variant type {_primitiveType}."); } - /// - /// Returns true when the decimal value is stored internally as - /// (i.e., it exceeds the range of ). - /// - internal bool IsSqlDecimalStorage => - (_primitiveType == VariantPrimitiveType.Decimal4 || - _primitiveType == VariantPrimitiveType.Decimal8 || - _primitiveType == VariantPrimitiveType.Decimal16) && - _objectValue is SqlDecimal; - /// Gets the Date value as days since epoch. public int AsDateDays() { diff --git a/test/Apache.Arrow.Operations.Tests/Shredding/ShreddedVariantReaderTests.cs b/test/Apache.Arrow.Operations.Tests/Shredding/ShreddedVariantReaderTests.cs index 1cf52f6d..3b1fcf43 100644 --- a/test/Apache.Arrow.Operations.Tests/Shredding/ShreddedVariantReaderTests.cs +++ b/test/Apache.Arrow.Operations.Tests/Shredding/ShreddedVariantReaderTests.cs @@ -481,7 +481,7 @@ public void GetSqlDecimal_BackedByDecimal128Array_ExceedingSystemDecimalRange() // is retained with SqlDecimal storage inside the VariantValue. VariantValue v = array.GetLogicalVariantValue(0); Assert.Equal(expected, v.AsSqlDecimal()); - Assert.Throws(() => v.AsDecimal()); + Assert.Throws(() => v.AsDecimal()); } /// diff --git a/test/Apache.Arrow.Scalars.Tests/ParquetTestingVectorTests.cs b/test/Apache.Arrow.Scalars.Tests/ParquetTestingVectorTests.cs index a3c6ee38..b2b35292 100644 --- a/test/Apache.Arrow.Scalars.Tests/ParquetTestingVectorTests.cs +++ b/test/Apache.Arrow.Scalars.Tests/ParquetTestingVectorTests.cs @@ -331,7 +331,7 @@ private static void AssertNumericValue(VariantValue actual, JsonElement expected // JSON may lose precision for large decimals (e.g. 1.2345678912345678e+16 // loses the fractional part). Use relative tolerance comparison. double expectedD16 = expected.GetDouble(); - double actualD16 = (double)actual.AsDecimal(); + double actualD16 = actual.AsSqlDecimal().ToDouble(); double relError = Math.Abs(expectedD16 - actualD16) / Math.Max(1.0, Math.Abs(expectedD16)); Assert.True(relError < 1e-10, $"{context}: decimal16 relative error {relError:E3} exceeds tolerance. " + diff --git a/test/Apache.Arrow.Scalars.Tests/VariantSqlDecimalTests.cs b/test/Apache.Arrow.Scalars.Tests/VariantSqlDecimalTests.cs index a0be0e90..b5f81958 100644 --- a/test/Apache.Arrow.Scalars.Tests/VariantSqlDecimalTests.cs +++ b/test/Apache.Arrow.Scalars.Tests/VariantSqlDecimalTests.cs @@ -173,7 +173,7 @@ public void FromSqlDecimal_LargeValue_ProducesDecimal16() SqlDecimal sd = SqlDecimal.Parse("99999999999999999999999999999999999999"); VariantValue vv = VariantValue.FromSqlDecimal(sd); Assert.Equal(VariantPrimitiveType.Decimal16, vv.PrimitiveType); - Assert.True(vv.IsSqlDecimalStorage); + Assert.Equal(sd, vv.AsSqlDecimal()); } // --------------------------------------------------------------- @@ -198,15 +198,15 @@ public void AsSqlDecimal_FromSqlDecimalStored() } // --------------------------------------------------------------- - // AsDecimal from SqlDecimal-stored Decimal16 throws OverflowException + // AsDecimal on Decimal16 always throws (regardless of internal storage) // --------------------------------------------------------------- [Fact] - public void AsDecimal_FromSqlDecimalStored_Throws() + public void AsDecimal_OnLargeDecimal16_Throws() { SqlDecimal sd = SqlDecimal.Parse("99999999999999999999999999999999999999"); VariantValue vv = VariantValue.FromSqlDecimal(sd); - Assert.Throws(() => vv.AsDecimal()); + Assert.Throws(() => vv.AsDecimal()); } // --------------------------------------------------------------- @@ -271,7 +271,6 @@ public void MaterializePrimitive_LargeDecimal16_NoException() // Should not throw — should use SqlDecimal path VariantValue vv = reader.ToVariantValue(); Assert.Equal(VariantPrimitiveType.Decimal16, vv.PrimitiveType); - Assert.True(vv.IsSqlDecimalStorage); SqlDecimal result = vv.AsSqlDecimal(); Assert.Equal(SqlDecimal.Parse("79228162514264337593543950336"), result); @@ -293,15 +292,14 @@ public void RoundTrip_Materialize_LargeDecimal16() VariantValue vv2 = reader.ToVariantValue(); Assert.Equal(VariantPrimitiveType.Decimal16, vv2.PrimitiveType); - Assert.True(vv2.IsSqlDecimalStorage); Assert.Equal(original, vv2.AsSqlDecimal()); } [Fact] - public void RoundTrip_Materialize_SmallDecimal16_UsesDecimalStorage() + public void RoundTrip_Materialize_SmallDecimal16() { - decimal d = 12345.67m; - VariantValue vv1 = VariantValue.FromDecimal16(d); + SqlDecimal sd = new SqlDecimal(12345.67m); + VariantValue vv1 = VariantValue.FromDecimal16(sd); VariantBuilder builder = new VariantBuilder(); (byte[] metadata, byte[] value) = builder.Encode(vv1); @@ -309,8 +307,7 @@ public void RoundTrip_Materialize_SmallDecimal16_UsesDecimalStorage() VariantValue vv2 = reader.ToVariantValue(); Assert.Equal(VariantPrimitiveType.Decimal16, vv2.PrimitiveType); - Assert.False(vv2.IsSqlDecimalStorage); - Assert.Equal(d, vv2.AsDecimal()); + Assert.Equal(sd, vv2.AsSqlDecimal()); } // --------------------------------------------------------------- @@ -434,7 +431,6 @@ public void FromSqlDecimal_Zero() SqlDecimal sd = new SqlDecimal(0m); VariantValue vv = VariantValue.FromSqlDecimal(sd); Assert.Equal(VariantPrimitiveType.Decimal4, vv.PrimitiveType); - Assert.False(vv.IsSqlDecimalStorage); Assert.Equal(0m, vv.AsDecimal()); } @@ -443,27 +439,13 @@ public void FromSqlDecimal_Zero() // --------------------------------------------------------------- [Fact] - public void FromSqlDecimal_96BitBoundary_ProducesDecimal16WithDecimalStorage() + public void FromSqlDecimal_96BitBoundary_ProducesDecimal16() { // A value that needs all 96 bits: data[2] != 0 but data[3] == 0 SqlDecimal sd = new SqlDecimal(79228162514264337593543950335m); VariantValue vv = VariantValue.FromSqlDecimal(sd); Assert.Equal(VariantPrimitiveType.Decimal16, vv.PrimitiveType); - Assert.False(vv.IsSqlDecimalStorage); // stored as decimal, not SqlDecimal - Assert.Equal(79228162514264337593543950335m, vv.AsDecimal()); - } - - // --------------------------------------------------------------- - // AsDecimal on Decimal16 that was created via FromSqlDecimal but fits - // --------------------------------------------------------------- - - [Fact] - public void AsDecimal_FromSqlDecimalThatFits() - { - // 96-bit value goes through FromSqlDecimal -> stored as decimal - SqlDecimal sd = new SqlDecimal(79228162514264337593543950335m); - VariantValue vv = VariantValue.FromSqlDecimal(sd); - Assert.Equal(79228162514264337593543950335m, vv.AsDecimal()); + Assert.Equal(sd, vv.AsSqlDecimal()); } // --------------------------------------------------------------- @@ -512,7 +494,6 @@ public void RoundTrip_ObjectWithSqlDecimal16Field() Assert.True(materialized.IsObject); IReadOnlyDictionary fields = materialized.AsObject(); Assert.Equal(sd, fields["big"].AsSqlDecimal()); - Assert.True(fields["big"].IsSqlDecimalStorage); Assert.Equal(42.5m, fields["small"].AsDecimal()); } @@ -534,10 +515,8 @@ public void RoundTrip_ArrayWithMixedDecimal16() Assert.True(materialized.IsArray); IReadOnlyList elements = materialized.AsArray(); Assert.Equal(3, elements.Count); - Assert.True(elements[0].IsSqlDecimalStorage); Assert.Equal(large, elements[0].AsSqlDecimal()); - Assert.False(elements[1].IsSqlDecimalStorage); - Assert.Equal(42.5m, elements[1].AsDecimal()); + Assert.Equal(new SqlDecimal(42.5m), elements[1].AsSqlDecimal()); Assert.Equal(1.23m, elements[2].AsDecimal()); } diff --git a/test/Apache.Arrow.Scalars.Tests/VariantValueTests.cs b/test/Apache.Arrow.Scalars.Tests/VariantValueTests.cs index e5187dc9..6c4d8298 100644 --- a/test/Apache.Arrow.Scalars.Tests/VariantValueTests.cs +++ b/test/Apache.Arrow.Scalars.Tests/VariantValueTests.cs @@ -152,10 +152,17 @@ public void Decimal8() [Fact] public void Decimal16() { - decimal d = 79228162514264337593543950335m; - VariantValue v = VariantValue.FromDecimal16(d); + SqlDecimal sd = new SqlDecimal(79228162514264337593543950335m); + VariantValue v = VariantValue.FromDecimal16(sd); Assert.Equal(VariantPrimitiveType.Decimal16, v.PrimitiveType); - Assert.Equal(d, v.AsDecimal()); + Assert.Equal(sd, v.AsSqlDecimal()); + } + + [Fact] + public void AsDecimal_OnDecimal16_Throws() + { + VariantValue v = VariantValue.FromDecimal16(new SqlDecimal(42.5m)); + Assert.Throws(() => v.AsDecimal()); } [Fact] @@ -458,23 +465,6 @@ public void Equality_SqlDecimal_FittingInDecimal_UsesDecimalStorage() Assert.Equal(fromDecimal4.GetHashCode(), fromSqlDecimal.GetHashCode()); } - [Fact] - public void IsSqlDecimalStorage_FalseForNonDecimalTypes() - { - Assert.False(VariantValue.FromInt32(42).IsSqlDecimalStorage); - Assert.False(VariantValue.FromString("hello").IsSqlDecimalStorage); - Assert.False(VariantValue.Null.IsSqlDecimalStorage); - Assert.False(VariantValue.FromDouble(3.14).IsSqlDecimalStorage); - } - - [Fact] - public void IsSqlDecimalStorage_FalseForDecimalStoredValues() - { - Assert.False(VariantValue.FromDecimal4(42.5m).IsSqlDecimalStorage); - Assert.False(VariantValue.FromDecimal8(123456789.12m).IsSqlDecimalStorage); - Assert.False(VariantValue.FromDecimal16(42.5m).IsSqlDecimalStorage); - } - // --------------------------------------------------------------- // ToString with SqlDecimal storage // --------------------------------------------------------------- From e5303fd51d46154f0c0b2e07b559d68741b94fef Mon Sep 17 00:00:00 2001 From: Curt Hagenlocher Date: Wed, 29 Apr 2026 06:59:35 -0700 Subject: [PATCH 2/3] Apply Copilot feedback and remove some allocations under net8.0+ --- .../Shredding/VariantShredder.cs | 2 +- .../VariantJson/VariantJsonConverter.cs | 1 + .../Variant/VariantValue.cs | 57 +++++++++++-------- .../Variant/VariantValueWriter.cs | 16 ++++-- 4 files changed, 46 insertions(+), 30 deletions(-) diff --git a/src/Apache.Arrow.Operations/Shredding/VariantShredder.cs b/src/Apache.Arrow.Operations/Shredding/VariantShredder.cs index 0a3048c9..ef2ed11d 100644 --- a/src/Apache.Arrow.Operations/Shredding/VariantShredder.cs +++ b/src/Apache.Arrow.Operations/Shredding/VariantShredder.cs @@ -284,7 +284,7 @@ internal static object ExtractTypedValue(VariantValue value, ShredType shredType case ShredType.Double: return value.AsDouble(); case ShredType.Decimal4: case ShredType.Decimal8: return value.AsDecimal(); - case ShredType.Decimal16: return value.AsSqlDecimal().Value; + case ShredType.Decimal16: return value.AsSqlDecimal(); case ShredType.Date: return value.AsDateDays(); case ShredType.Timestamp: return value.AsTimestampMicros(); case ShredType.TimestampNtz: return value.AsTimestampNtzMicros(); diff --git a/src/Apache.Arrow.Operations/VariantJson/VariantJsonConverter.cs b/src/Apache.Arrow.Operations/VariantJson/VariantJsonConverter.cs index acc9a21f..de11c97b 100644 --- a/src/Apache.Arrow.Operations/VariantJson/VariantJsonConverter.cs +++ b/src/Apache.Arrow.Operations/VariantJson/VariantJsonConverter.cs @@ -243,6 +243,7 @@ internal static void WriteValue(Utf8JsonWriter writer, VariantValue value) writer.WriteNumberValue(value.AsDecimal()); break; case VariantPrimitiveType.Decimal16: + // SqlDecimal.ToString() is culture-agnostic and produces a plain decimal string without exponent writer.WriteRawValue(value.AsSqlDecimal().ToString()); break; case VariantPrimitiveType.Date: diff --git a/src/Apache.Arrow.Scalars/Variant/VariantValue.cs b/src/Apache.Arrow.Scalars/Variant/VariantValue.cs index a1ab0a71..87ed9fbe 100644 --- a/src/Apache.Arrow.Scalars/Variant/VariantValue.cs +++ b/src/Apache.Arrow.Scalars/Variant/VariantValue.cs @@ -19,6 +19,9 @@ using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; +#if !NET8_0_OR_GREATER +using System.Runtime.InteropServices; +#endif namespace Apache.Arrow.Scalars.Variant { @@ -116,15 +119,8 @@ public static VariantValue FromDecimal8(decimal value) => /// when you want the smallest /// decimal type that fits the value. /// - public static VariantValue FromDecimal16(SqlDecimal value) - { - if (value.Data[3] != 0) - { - SqlDecimal normalized = SqlDecimal.ConvertToPrecScale(value, 38, value.Scale); - return new VariantValue(VariantPrimitiveType.Decimal16, (object)normalized); - } - return new VariantValue(VariantPrimitiveType.Decimal16, (object)value.Value); - } + public static VariantValue FromDecimal16(SqlDecimal value) => + new VariantValue(VariantPrimitiveType.Decimal16, (object)value); /// /// Creates a decimal variant value, choosing the smallest decimal type @@ -157,26 +153,37 @@ public static VariantValue FromDecimal(decimal value) /// public static VariantValue FromSqlDecimal(SqlDecimal value) { - int[] data = value.Data; +#if NET8_0_OR_GREATER + Span data = stackalloc uint[4]; + value.WriteTdsValue(data); +#else + ReadOnlySpan data = MemoryMarshal.Cast(value.Data); +#endif + // SqlDecimal.Data: [0]=least-significant, [3]=most-significant - if (data[3] != 0) + if (data[3] == 0 && value.Precision - value.Scale <= 28) { - // Exceeds 96 bits — must store as SqlDecimal - SqlDecimal normalized = SqlDecimal.ConvertToPrecScale(value, 38, value.Scale); - return new VariantValue(VariantPrimitiveType.Decimal16, (object)normalized); + try + { + // Fits in decimal — convert and dispatch + decimal d = value.Value; + if (data[2] == 0 && data[1] == 0) + { + return FromDecimal4(d); + } + if (data[2] == 0) + { + return FromDecimal8(d); + } + } + catch (OverflowException) + { + // Value exceeds decimal range — fall back to Decimal16 + } } - // Fits in decimal — convert and dispatch - decimal d = value.Value; - if (data[2] == 0 && data[1] == 0) - { - return FromDecimal4(d); - } - if (data[2] == 0) - { - return FromDecimal8(d); - } - return new VariantValue(VariantPrimitiveType.Decimal16, (object)d); + // Exceeds 96 bits — must store as SqlDecimal + return new VariantValue(VariantPrimitiveType.Decimal16, (object)value); } /// Creates a Date variant value from days since epoch. diff --git a/src/Apache.Arrow.Scalars/Variant/VariantValueWriter.cs b/src/Apache.Arrow.Scalars/Variant/VariantValueWriter.cs index 781f4a4f..7adc36ba 100644 --- a/src/Apache.Arrow.Scalars/Variant/VariantValueWriter.cs +++ b/src/Apache.Arrow.Scalars/Variant/VariantValueWriter.cs @@ -17,6 +17,9 @@ using System.Buffers; using System.Collections.Generic; using System.Data.SqlTypes; +#if !NET8_0_OR_GREATER +using System.Runtime.InteropServices; +#endif using System.Text; namespace Apache.Arrow.Scalars.Variant @@ -397,12 +400,17 @@ public void WriteDecimal16(SqlDecimal value) buf.Append(VariantEncodingHelper.MakePrimitiveHeader(VariantPrimitiveType.Decimal16)); bool positive = value.IsPositive; - byte scale = (byte)value.Scale; - int[] data = value.Data; + byte scale = value.Scale; +#if NET8_0_OR_GREATER + Span data = stackalloc uint[4]; + value.WriteTdsValue(data); +#else + ReadOnlySpan data = MemoryMarshal.Cast(value.Data); +#endif // SqlDecimal.Data: [0]=least-significant, [3]=most-significant - long lo = ((long)(uint)data[1] << 32) | (uint)data[0]; - long hi = ((long)(uint)data[3] << 32) | (uint)data[2]; + long lo = ((long)data[1] << 32) | data[0]; + long hi = ((long)data[3] << 32) | data[2]; if (!positive) { From 80e778d88e86dc4616ea0e3e86a37af1cc84499c Mon Sep 17 00:00:00 2001 From: Curt Hagenlocher Date: Wed, 29 Apr 2026 16:37:27 -0700 Subject: [PATCH 3/3] Bugfix from PR feedback --- .../Variant/VariantValue.cs | 11 ++++++----- .../VariantSqlDecimalTests.cs | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/Apache.Arrow.Scalars/Variant/VariantValue.cs b/src/Apache.Arrow.Scalars/Variant/VariantValue.cs index 87ed9fbe..005a4e93 100644 --- a/src/Apache.Arrow.Scalars/Variant/VariantValue.cs +++ b/src/Apache.Arrow.Scalars/Variant/VariantValue.cs @@ -143,7 +143,7 @@ public static VariantValue FromDecimal(decimal value) { return FromDecimal8(value); } - return new VariantValue(VariantPrimitiveType.Decimal16, (object)value); + return new VariantValue(VariantPrimitiveType.Decimal16, (object)new SqlDecimal(value)); } /// @@ -384,13 +384,14 @@ public decimal AsDecimal() public SqlDecimal AsSqlDecimal() { if (_primitiveType == VariantPrimitiveType.Decimal4 || - _primitiveType == VariantPrimitiveType.Decimal8 || - _primitiveType == VariantPrimitiveType.Decimal16) + _primitiveType == VariantPrimitiveType.Decimal8) { - if (_objectValue is SqlDecimal sd) - return sd; return new SqlDecimal((decimal)_objectValue); } + if (_primitiveType == VariantPrimitiveType.Decimal16) + { + return (SqlDecimal)_objectValue; + } throw new InvalidOperationException($"Cannot read decimal from variant type {_primitiveType}."); } diff --git a/test/Apache.Arrow.Scalars.Tests/VariantSqlDecimalTests.cs b/test/Apache.Arrow.Scalars.Tests/VariantSqlDecimalTests.cs index b5f81958..f2e539fc 100644 --- a/test/Apache.Arrow.Scalars.Tests/VariantSqlDecimalTests.cs +++ b/test/Apache.Arrow.Scalars.Tests/VariantSqlDecimalTests.cs @@ -167,6 +167,23 @@ public void FromSqlDecimal_MediumValue_ProducesDecimal8() // FromSqlDecimal — large values produce Decimal16 // --------------------------------------------------------------- + [Fact] + public void Decimal16_FromDecimalAndFromSqlDecimal_SameValue_AreEqual() + { + // A value requiring all 96 bits — auto-sizes to Decimal16 via FromDecimal, + // and constructs the same Decimal16 via FromDecimal16(SqlDecimal). The two + // must be equal and have the same hash code regardless of how _objectValue + // is boxed internally. + decimal d = 79228162514264337593543950335m; + VariantValue fromDecimal = VariantValue.FromDecimal(d); + VariantValue fromSqlDecimal = VariantValue.FromDecimal16(new SqlDecimal(d)); + + Assert.Equal(VariantPrimitiveType.Decimal16, fromDecimal.PrimitiveType); + Assert.Equal(VariantPrimitiveType.Decimal16, fromSqlDecimal.PrimitiveType); + Assert.Equal(fromDecimal, fromSqlDecimal); + Assert.Equal(fromDecimal.GetHashCode(), fromSqlDecimal.GetHashCode()); + } + [Fact] public void FromSqlDecimal_LargeValue_ProducesDecimal16() {