From 64e4a371af44480f540eda2e27530c9ca736c972 Mon Sep 17 00:00:00 2001 From: "Oleg V. Kozlyuk" Date: Sun, 6 Apr 2025 15:36:00 +0200 Subject: [PATCH] Started implementing int128 #614 --- .../Numerics/ClickHouseInt128Tests.cs | 79 ++++++ ClickHouse.Client/FeatureSwitch.cs | 27 +- .../Numerics/ClickHouseInt128.cs | 236 ++++++++++++++++++ 3 files changed, 328 insertions(+), 14 deletions(-) create mode 100644 ClickHouse.Client.Tests/Numerics/ClickHouseInt128Tests.cs create mode 100644 ClickHouse.Client/Numerics/ClickHouseInt128.cs diff --git a/ClickHouse.Client.Tests/Numerics/ClickHouseInt128Tests.cs b/ClickHouse.Client.Tests/Numerics/ClickHouseInt128Tests.cs new file mode 100644 index 00000000..f0dd7f77 --- /dev/null +++ b/ClickHouse.Client.Tests/Numerics/ClickHouseInt128Tests.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using ClickHouse.Client.Numerics; + +namespace ClickHouse.Client.Tests.Numerics; + +public class ClickHouseInt128Tests +{ + public static BigInteger MaxValue = BigInteger.Parse("170141183460469231731687303715884105727"); + public static BigInteger MinValue = BigInteger.Parse("-170141183460469231731687303715884105728"); + + public static IEnumerable Fibonacci + { + get + { + // Base cases + yield return 0; + yield return 1; + yield return int.MaxValue; + yield return uint.MaxValue; + yield return long.MaxValue; + yield return ulong.MaxValue; + + // Fibonacci sequence for very large numbers + bool setLeft = false; + BigInteger right = 1; + BigInteger left = 0; + while (true) + { + var sum = left + right; + if (sum > MaxValue) + yield break; + yield return sum; + if (setLeft) + left = sum; + else + right = sum; + setLeft = !setLeft; + } + } + } + + [Test] + [TestCaseSource(nameof(Fibonacci))] + public void ShouldRoundtripConvert(BigInteger input) + { + var int128 = new ClickHouseInt128(input); + var roundtrip = (BigInteger)int128; + Assert.That(roundtrip, Is.EqualTo(input)); + } + + [Test] + [TestCaseSource(nameof(Fibonacci))] + public void ShouldCompareEquality(BigInteger input) + { + var value1 = new ClickHouseInt128(input); + var value2 = new ClickHouseInt128(input + 1); + var value3 = new ClickHouseInt128(input); + Assert.That(value1, Is.Not.EqualTo(value2)); + Assert.That(value1, Is.EqualTo(value3)); + } + + [Test] + [TestCaseSource(nameof(Fibonacci))] + public void ShouldIncrement(BigInteger input) + { + var value = new ClickHouseInt128(input); + value++; + Assert.That((BigInteger)value, Is.EqualTo(input + 1)); + } + + [Test] + public void ShouldHaveCorrectExtremeValues() + { + Assert.That((BigInteger)ClickHouseInt128.MaxValue, Is.EqualTo(MaxValue)); + Assert.That((BigInteger)ClickHouseInt128.MinValue, Is.EqualTo(MinValue)); + } +} diff --git a/ClickHouse.Client/FeatureSwitch.cs b/ClickHouse.Client/FeatureSwitch.cs index 49cd2f3d..f3df3cd7 100644 --- a/ClickHouse.Client/FeatureSwitch.cs +++ b/ClickHouse.Client/FeatureSwitch.cs @@ -1,22 +1,21 @@ using System; -namespace ClickHouse.Client +namespace ClickHouse.Client; + +internal class FeatureSwitch { - internal class FeatureSwitch - { - private const string Prefix = "ClickHouse.Client."; + private const string Prefix = "ClickHouse.Client."; - public static readonly bool DisableReplacingParameters; + public static readonly bool DisableReplacingParameters; - static FeatureSwitch() - { - DisableReplacingParameters = GetSwitchValue(nameof(DisableReplacingParameters)); - } + static FeatureSwitch() + { + DisableReplacingParameters = GetSwitchValue(nameof(DisableReplacingParameters)); + } - private static bool GetSwitchValue(string switchName) - { - AppContext.TryGetSwitch(Prefix + switchName, out bool switchValue); - return switchValue; - } + private static bool GetSwitchValue(string switchName) + { + AppContext.TryGetSwitch(Prefix + switchName, out bool switchValue); + return switchValue; } } diff --git a/ClickHouse.Client/Numerics/ClickHouseInt128.cs b/ClickHouse.Client/Numerics/ClickHouseInt128.cs new file mode 100644 index 00000000..4b797346 --- /dev/null +++ b/ClickHouse.Client/Numerics/ClickHouseInt128.cs @@ -0,0 +1,236 @@ +using System; +using System.Globalization; +using System.Numerics; +using System.Runtime.InteropServices; + +namespace ClickHouse.Client.Numerics; + +[StructLayout(LayoutKind.Sequential)] +public readonly struct ClickHouseInt128 + : +#if NET8_0_OR_GREATER + ISignedNumber, + IMinMaxValue, +// IBinaryInteger, +// IUtf8SpanFormattable, +// IBinaryIntegerParseAndFormatInfo +#endif + IEquatable, + IComparable +{ +#if BIGENDIAN + private readonly ulong upper; + private readonly ulong lower; +#else + private readonly ulong lower; + private readonly ulong upper; +#endif + +#if NET8_0_OR_GREATER + public ClickHouseInt128(Int128 value) + { + + } +#endif + + public ClickHouseInt128(ulong lower) + { + this.upper = 0; + this.lower = lower; + } + + public ClickHouseInt128(ulong upper, ulong lower) + { + this.upper = upper; + this.lower = lower; + } + + // Very slow and unoptimized constructor - used only for testing/comparison purposes + public ClickHouseInt128(BigInteger value) + { + var bytes = value.ToByteArray(); + var int128bytes = new byte[16]; + if (value.Sign < 0) + { + int128bytes.AsSpan().Fill(byte.MaxValue); + } + else + { + int128bytes.AsSpan().Clear(); + } + + if (bytes.Length > 16) + throw new OverflowException("Value is too large to fit in ClickHouseInt128."); + + bytes.CopyTo(int128bytes.AsSpan()); + + lower = BitConverter.ToUInt64(int128bytes, 0); + upper = BitConverter.ToUInt64(int128bytes, 8); + } + + public static ClickHouseInt128 NegativeOne => new(ulong.MaxValue, ulong.MaxValue); + + public static ClickHouseInt128 One => new(1); + + public static int Radix => 2; + + public static ClickHouseInt128 Zero => new(0); + + public static ClickHouseInt128 AdditiveIdentity => default; + + public static ClickHouseInt128 MultiplicativeIdentity => One; + + internal ulong Lower => lower; + + internal ulong Upper => upper; + + public static ClickHouseInt128 MaxValue => new(long.MaxValue, ulong.MaxValue); + + public static ClickHouseInt128 MinValue => new(0x8000_0000_0000_0000, 0); + + public static ClickHouseInt128 Abs(ClickHouseInt128 value) => throw new NotImplementedException(); + + public static bool IsCanonical(ClickHouseInt128 value) => true; + + public static bool IsComplexNumber(ClickHouseInt128 value) => false; + + public static bool IsEvenInteger(ClickHouseInt128 value) => (value.lower & 1) == 0; + + public static bool IsFinite(ClickHouseInt128 value) => true; + + public static bool IsImaginaryNumber(ClickHouseInt128 value) => false; + + public static bool IsInfinity(ClickHouseInt128 value) => false; + + public static bool IsInteger(ClickHouseInt128 value) => true; + + public static bool IsNaN(ClickHouseInt128 value) => false; + + public static bool IsNegative(ClickHouseInt128 value) => (long)value.Upper < 0; + + public static bool IsNegativeInfinity(ClickHouseInt128 value) => false; + + public static bool IsNormal(ClickHouseInt128 value) => throw new NotImplementedException(); + + public static bool IsOddInteger(ClickHouseInt128 value) => (value.lower & 1) != 0; + + public static bool IsPositive(ClickHouseInt128 value) => (long)value.Upper > 0; + + public static bool IsPositiveInfinity(ClickHouseInt128 value) => false; + + public static bool IsRealNumber(ClickHouseInt128 value) => true; + + public static bool IsSubnormal(ClickHouseInt128 value) => false; + + public static bool IsZero(ClickHouseInt128 value) => value.Upper == 0 && value.Lower == 0; + + public static ClickHouseInt128 MaxMagnitude(ClickHouseInt128 x, ClickHouseInt128 y) => throw new NotImplementedException(); + + public static ClickHouseInt128 MaxMagnitudeNumber(ClickHouseInt128 x, ClickHouseInt128 y) => throw new NotImplementedException(); + + public static ClickHouseInt128 MinMagnitude(ClickHouseInt128 x, ClickHouseInt128 y) => throw new NotImplementedException(); + + public static ClickHouseInt128 MinMagnitudeNumber(ClickHouseInt128 x, ClickHouseInt128 y) => throw new NotImplementedException(); + + public static ClickHouseInt128 Parse(ReadOnlySpan s, NumberStyles style, IFormatProvider provider) => throw new NotImplementedException(); + + public static ClickHouseInt128 Parse(string s, NumberStyles style, IFormatProvider provider) => throw new NotImplementedException(); + + public static ClickHouseInt128 Parse(ReadOnlySpan s, IFormatProvider provider) => throw new NotImplementedException(); + + public static ClickHouseInt128 Parse(string s, IFormatProvider provider) => throw new NotImplementedException(); + +#if NET8_0_OR_GREATER + public static bool TryConvertFromChecked(TOther value, out ClickHouseInt128 result) + where TOther : INumberBase + => throw new NotImplementedException(); + + public static bool TryConvertFromSaturating(TOther value, out ClickHouseInt128 result) + where TOther : INumberBase + => throw new NotImplementedException(); + + public static bool TryConvertFromTruncating(TOther value, out ClickHouseInt128 result) + where TOther : INumberBase + => throw new NotImplementedException(); + + public static bool TryConvertToChecked(ClickHouseInt128 value, out TOther result) + where TOther : INumberBase + => throw new NotImplementedException(); + + public static bool TryConvertToSaturating(ClickHouseInt128 value, out TOther result) + where TOther : INumberBase + => throw new NotImplementedException(); + + public static bool TryConvertToTruncating(ClickHouseInt128 value, out TOther result) + where TOther : INumberBase + => throw new NotImplementedException(); + +#endif + public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatProvider provider, out ClickHouseInt128 result) => throw new NotImplementedException(); + + public static bool TryParse(string s, NumberStyles style, IFormatProvider provider, out ClickHouseInt128 result) => throw new NotImplementedException(); + + public static bool TryParse(ReadOnlySpan s, IFormatProvider provider, out ClickHouseInt128 result) => throw new NotImplementedException(); + + public static bool TryParse(string s, IFormatProvider provider, out ClickHouseInt128 result) => throw new NotImplementedException(); + + public string ToString(string format, IFormatProvider formatProvider) => throw new NotImplementedException(); + + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider) => throw new NotImplementedException(); + + public static ClickHouseInt128 operator +(ClickHouseInt128 value) => value; + + public static ClickHouseInt128 operator +(ClickHouseInt128 left, ClickHouseInt128 right) + { + ulong lower = left.lower + right.lower; + // Carry 1 if sum overflows + ulong carry = (lower < left.lower) ? 1UL : 0UL; + + ulong upper = left.upper + right.upper + carry; + return new ClickHouseInt128(upper, lower); + } + + public static ClickHouseInt128 operator -(ClickHouseInt128 value) => Zero - value; + + public static ClickHouseInt128 operator -(ClickHouseInt128 left, ClickHouseInt128 right) + { + ulong lower = left.lower - right.lower; + // Borrow 1 if subtraction underflows + ulong borrow = (lower > left.lower) ? 1UL : 0UL; + + ulong upper = left.upper - right.upper - borrow; + return new ClickHouseInt128(upper, lower); + } + + public static ClickHouseInt128 operator ++(ClickHouseInt128 value) => value + One; + + public static ClickHouseInt128 operator --(ClickHouseInt128 value) => value - One; + + public static ClickHouseInt128 operator *(ClickHouseInt128 left, ClickHouseInt128 right) => throw new NotImplementedException(); + + public static ClickHouseInt128 operator /(ClickHouseInt128 left, ClickHouseInt128 right) => throw new NotImplementedException(); + + public static bool operator ==(ClickHouseInt128 left, ClickHouseInt128 right) => left.CompareTo(right) == 0; + + public static bool operator !=(ClickHouseInt128 left, ClickHouseInt128 right) => left.CompareTo(right) != 0; + + public static explicit operator BigInteger(ClickHouseInt128 value) + { + var int128bytes = new byte[16]; + BitConverter.GetBytes(value.lower).CopyTo(int128bytes.AsSpan(0)); + BitConverter.GetBytes(value.upper).CopyTo(int128bytes.AsSpan(8)); + return new BigInteger(int128bytes); + } + + public bool Equals(ClickHouseInt128 other) => CompareTo(other) == 0; + + public int CompareTo(ClickHouseInt128 other) + { + var comparison1 = Upper.CompareTo(other.Upper); + if (comparison1 != 0) + return comparison1; + return Lower.CompareTo(other.Lower); + } + + public override bool Equals(object obj) => obj is ClickHouseInt128 other && CompareTo(other) == 0; +}