X Tutup
using System; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text; // ReSharper disable once CheckNamespace namespace NpgsqlTypes; /// /// Represents a PostgreSQL range type. /// /// The element type of the values in the range. /// /// See: https://www.postgresql.org/docs/current/static/rangetypes.html /// public readonly struct NpgsqlRange : IEquatable> { // ----------------------------------------------------------------------------------------------- // Regarding bitwise flag checks via @roji: // // > Note that Flags.HasFlag() used to be very inefficient compared to simply doing the // > bit operation - this is why I've always avoided it. .NET Core 2.1 adds JIT intrinstics // > for this, making Enum.HasFlag() fast, but I honestly don't see the value over just doing // > a bitwise and operation, which would also be fast under .NET Core 2.0 and .NET Framework. // // See: // - https://github.com/npgsql/npgsql/pull/1939#pullrequestreview-121308396 // - https://blogs.msdn.microsoft.com/dotnet/2018/04/18/performance-improvements-in-net-core-2-1 // ----------------------------------------------------------------------------------------------- /// /// Defined by PostgreSQL to represent an empty range. /// const string EmptyLiteral = "empty"; /// /// Defined by PostgreSQL to represent an infinite lower bound. /// Some element types may have specific handling for this value distinct from a missing or null value. /// const string LowerInfinityLiteral = "-infinity"; /// /// Defined by PostgreSQL to represent an infinite upper bound. /// Some element types may have specific handling for this value distinct from a missing or null value. /// const string UpperInfinityLiteral = "infinity"; /// /// Defined by PostgreSQL to represent an null bound. /// Some element types may have specific handling for this value distinct from an infinite or missing value. /// const string NullLiteral = "null"; /// /// Defined by PostgreSQL to represent a lower inclusive bound. /// const char LowerInclusiveBound = '['; /// /// Defined by PostgreSQL to represent a lower exclusive bound. /// const char LowerExclusiveBound = '('; /// /// Defined by PostgreSQL to represent an upper inclusive bound. /// const char UpperInclusiveBound = ']'; /// /// Defined by PostgreSQL to represent an upper exclusive bound. /// const char UpperExclusiveBound = ')'; /// /// Defined by PostgreSQL to separate the values for the upper and lower bounds. /// const char BoundSeparator = ','; /// /// The used by to convert bounds into . /// static TypeConverter? BoundConverter; /// /// True if implements ; otherwise, false. /// static readonly bool HasEquatableBounds = typeof(IEquatable).IsAssignableFrom(typeof(T)); /// /// Represents the empty range. This field is read-only. /// public static readonly NpgsqlRange Empty = new(default, default, RangeFlags.Empty); /// /// The lower bound of the range. Only valid when is false. /// [MaybeNull, AllowNull] public T LowerBound { get; } /// /// The upper bound of the range. Only valid when is false. /// [MaybeNull, AllowNull] public T UpperBound { get; } /// /// The characteristics of the boundaries. /// internal readonly RangeFlags Flags; /// /// True if the lower bound is part of the range (i.e. inclusive); otherwise, false. /// public bool LowerBoundIsInclusive => (Flags & RangeFlags.LowerBoundInclusive) != 0; /// /// True if the upper bound is part of the range (i.e. inclusive); otherwise, false. /// public bool UpperBoundIsInclusive => (Flags & RangeFlags.UpperBoundInclusive) != 0; /// /// True if the lower bound is indefinite (i.e. infinite or unbounded); otherwise, false. /// public bool LowerBoundInfinite => (Flags & RangeFlags.LowerBoundInfinite) != 0; /// /// True if the upper bound is indefinite (i.e. infinite or unbounded); otherwise, false. /// public bool UpperBoundInfinite => (Flags & RangeFlags.UpperBoundInfinite) != 0; /// /// True if the range is empty; otherwise, false. /// public bool IsEmpty => (Flags & RangeFlags.Empty) != 0; /// /// Constructs an with inclusive and definite bounds. /// /// The lower bound of the range. /// The upper bound of the range. public NpgsqlRange([AllowNull] T lowerBound, [AllowNull] T upperBound) : this(lowerBound, true, false, upperBound, true, false) { } /// /// Constructs an with definite bounds. /// /// The lower bound of the range. /// True if the lower bound is is part of the range (i.e. inclusive); otherwise, false. /// The upper bound of the range. /// True if the upper bound is part of the range (i.e. inclusive); otherwise, false. public NpgsqlRange( [AllowNull] T lowerBound, bool lowerBoundIsInclusive, [AllowNull] T upperBound, bool upperBoundIsInclusive) : this(lowerBound, lowerBoundIsInclusive, false, upperBound, upperBoundIsInclusive, false) { } /// /// Constructs an . /// /// The lower bound of the range. /// True if the lower bound is is part of the range (i.e. inclusive); otherwise, false. /// True if the lower bound is indefinite (i.e. infinite or unbounded); otherwise, false. /// The upper bound of the range. /// True if the upper bound is part of the range (i.e. inclusive); otherwise, false. /// True if the upper bound is indefinite (i.e. infinite or unbounded); otherwise, false. public NpgsqlRange( [AllowNull] T lowerBound, bool lowerBoundIsInclusive, bool lowerBoundInfinite, [AllowNull] T upperBound, bool upperBoundIsInclusive, bool upperBoundInfinite) : this( lowerBound, upperBound, EvaluateBoundaryFlags( lowerBoundIsInclusive, upperBoundIsInclusive, lowerBoundInfinite, upperBoundInfinite)) { } /// /// Constructs an . /// /// The lower bound of the range. /// The upper bound of the range. /// The characteristics of the range boundaries. internal NpgsqlRange([AllowNull] T lowerBound, [AllowNull] T upperBound, RangeFlags flags) : this() { // TODO: We need to check if the bounds are implicitly empty. E.g. '(1,1)' or '(0,0]'. // See: https://github.com/npgsql/npgsql/issues/1943. LowerBound = (flags & RangeFlags.LowerBoundInfinite) != 0 ? default : lowerBound; UpperBound = (flags & RangeFlags.UpperBoundInfinite) != 0 ? default : upperBound; Flags = flags; if (IsEmptyRange(LowerBound, UpperBound, Flags)) { LowerBound = default!; UpperBound = default!; Flags = RangeFlags.Empty; } } /// /// Attempts to determine if the range is malformed or implicitly empty. /// /// The lower bound of the range. /// The upper bound of the range. /// The characteristics of the range boundaries. /// /// True if the range is implicitly empty; otherwise, false. /// static bool IsEmptyRange([AllowNull] T lowerBound, [AllowNull] T upperBound, RangeFlags flags) { // --------------------------------------------------------------------------------- // We only want to check for those conditions that are unambiguously erroneous: // 1. The bounds must not be default values (including null). // 2. The bounds must be definite (non-infinite). // 3. The bounds must be inclusive. // 4. The bounds must be considered equal. // // See: // - https://github.com/npgsql/npgsql/pull/1939 // - https://github.com/npgsql/npgsql/issues/1943 // --------------------------------------------------------------------------------- if ((flags & RangeFlags.Empty) == RangeFlags.Empty) return true; if ((flags & RangeFlags.Infinite) == RangeFlags.Infinite) return false; if ((flags & RangeFlags.Inclusive) == RangeFlags.Inclusive) return false; if (lowerBound is null || upperBound is null) return false; if (!HasEquatableBounds) return lowerBound?.Equals(upperBound) ?? false; var lower = (IEquatable)lowerBound; var upper = (IEquatable)upperBound; return !lower.Equals(default!) && !upper.Equals(default!) && lower.Equals(upperBound); } /// /// Evaluates the boundary flags. /// /// True if the lower bound is is part of the range (i.e. inclusive); otherwise, false. /// True if the lower bound is indefinite (i.e. infinite or unbounded); otherwise, false. /// True if the upper bound is part of the range (i.e. inclusive); otherwise, false. /// True if the upper bound is indefinite (i.e. infinite or unbounded); otherwise, false. /// /// The boundary characteristics. /// static RangeFlags EvaluateBoundaryFlags(bool lowerBoundIsInclusive, bool upperBoundIsInclusive, bool lowerBoundInfinite, bool upperBoundInfinite) { var result = RangeFlags.None; // This is the only place flags are calculated. if (lowerBoundIsInclusive) result |= RangeFlags.LowerBoundInclusive; if (upperBoundIsInclusive) result |= RangeFlags.UpperBoundInclusive; if (lowerBoundInfinite) result |= RangeFlags.LowerBoundInfinite; if (upperBoundInfinite) result |= RangeFlags.UpperBoundInfinite; // PostgreSQL automatically converts inclusive-infinities. // See: https://www.postgresql.org/docs/current/static/rangetypes.html#RANGETYPES-INFINITE if ((result & RangeFlags.LowerInclusiveInfinite) == RangeFlags.LowerInclusiveInfinite) result &= ~RangeFlags.LowerBoundInclusive; if ((result & RangeFlags.UpperInclusiveInfinite) == RangeFlags.UpperInclusiveInfinite) result &= ~RangeFlags.UpperBoundInclusive; return result; } /// /// Indicates whether the on the left is equal to the on the right. /// /// The on the left. /// The on the right. /// /// True if the on the left is equal to the on the right; otherwise, false. /// public static bool operator ==(NpgsqlRange x, NpgsqlRange y) => x.Equals(y); /// /// Indicates whether the on the left is not equal to the on the right. /// /// The on the left. /// The on the right. /// /// True if the on the left is not equal to the on the right; otherwise, false. /// public static bool operator !=(NpgsqlRange x, NpgsqlRange y) => !x.Equals(y); /// public override bool Equals(object? o) => o is NpgsqlRange range && Equals(range); /// public bool Equals(NpgsqlRange other) { if (Flags != other.Flags) return false; if (HasEquatableBounds) { var lowerEqual = LowerBound is null ? other.LowerBound is null : !(other.LowerBound is null) && ((IEquatable)LowerBound).Equals(other.LowerBound); if (!lowerEqual) return false; return UpperBound is null ? other.UpperBound is null : !(other.UpperBound is null) && ((IEquatable)UpperBound).Equals(other.UpperBound); } return (LowerBound?.Equals(other.LowerBound) ?? other.LowerBound is null) && (UpperBound?.Equals(other.UpperBound) ?? other.UpperBound is null); } /// public override int GetHashCode() => unchecked((397 * (int)Flags) ^ (397 * (LowerBound?.GetHashCode() ?? 0)) ^ (397 * (UpperBound?.GetHashCode() ?? 0))); /// public override string ToString() { if (IsEmpty) return EmptyLiteral; var sb = new StringBuilder(); sb.Append(LowerBoundIsInclusive ? LowerInclusiveBound : LowerExclusiveBound); if (!LowerBoundInfinite) sb.Append(LowerBound); sb.Append(BoundSeparator); if (!UpperBoundInfinite) sb.Append(UpperBound); sb.Append(UpperBoundIsInclusive ? UpperInclusiveBound : UpperExclusiveBound); return sb.ToString(); } // TODO: rewrite this to use ReadOnlySpan for the 4.1 release /// /// Parses the well-known text representation of a PostgreSQL range type into a . /// /// A PosgreSQL range type in a well-known text format. /// /// The represented by the . /// /// /// Malformed range literal. /// /// /// Malformed range literal. Missing left parenthesis or bracket. /// /// /// Malformed range literal. Missing right parenthesis or bracket. /// /// /// Malformed range literal. Missing comma after lower bound. /// /// /// See: https://www.postgresql.org/docs/current/static/rangetypes.html /// [RequiresUnreferencedCode("Parse implementations for certain types of T may require members that have been trimmed.")] public static NpgsqlRange Parse(string value) { ArgumentNullException.ThrowIfNull(value); value = value.Trim(); if (value.Length < 3) throw new FormatException("Malformed range literal."); if (string.Equals(value, EmptyLiteral, StringComparison.OrdinalIgnoreCase)) return Empty; var lowerInclusive = value[0] == LowerInclusiveBound; var lowerExclusive = value[0] == LowerExclusiveBound; if (!lowerInclusive && !lowerExclusive) throw new FormatException("Malformed range literal. Missing left parenthesis or bracket."); var upperInclusive = value[^1] == UpperInclusiveBound; var upperExclusive = value[^1] == UpperExclusiveBound; if (!upperInclusive && !upperExclusive) throw new FormatException("Malformed range literal. Missing right parenthesis or bracket."); var separator = value.IndexOf(BoundSeparator); if (separator == -1) throw new FormatException("Malformed range literal. Missing comma after lower bound."); if (separator != value.LastIndexOf(BoundSeparator)) // TODO: this should be replaced to handle quoted commas. throw new NotSupportedException("Ranges with embedded commas are not currently supported."); // Skip the opening bracket and stop short of the separator. var lowerSegment = value.Substring(1, separator - 1).Trim(); // Skip past the separator and stop short of the closing bracket. var upperSegment = value.Substring(separator + 1, value.Length - separator - 2).Trim(); // TODO: infinity literals have special meaning to some types (e.g. daterange), we should consider a flag to track them. var lowerInfinite = lowerSegment.Length == 0 || string.Equals(lowerSegment, string.Empty, StringComparison.OrdinalIgnoreCase) || string.Equals(lowerSegment, NullLiteral, StringComparison.OrdinalIgnoreCase) || string.Equals(lowerSegment, LowerInfinityLiteral, StringComparison.OrdinalIgnoreCase); var upperInfinite = upperSegment.Length == 0 || string.Equals(upperSegment, string.Empty, StringComparison.OrdinalIgnoreCase) || string.Equals(upperSegment, NullLiteral, StringComparison.OrdinalIgnoreCase) || string.Equals(upperSegment, UpperInfinityLiteral, StringComparison.OrdinalIgnoreCase); BoundConverter ??= TypeDescriptor.GetConverter(typeof(T)); var lower = lowerInfinite ? default : (T?)BoundConverter.ConvertFromString(lowerSegment); var upper = upperInfinite ? default : (T?)BoundConverter.ConvertFromString(upperSegment); return new NpgsqlRange(lower, lowerInclusive, lowerInfinite, upper, upperInclusive, upperInfinite); } /// /// Represents a type converter for . /// [RequiresUnreferencedCode("ConvertFrom implementations for certain types of T may require members that have been trimmed.")] public class RangeTypeConverter : TypeConverter { /// /// Adds a to the closed form . /// public static void Register() => TypeDescriptor.AddAttributes( typeof(NpgsqlRange), new TypeConverterAttribute(typeof(RangeTypeConverter))); /// public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(string); /// public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) => destinationType == typeof(string); /// public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) => value is string s ? Parse(s) : base.ConvertFrom(context, culture, value); /// public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) => value is null ? string.Empty : value.ToString(); } } /// /// Represents characteristics of range type boundaries. /// /// /// See: https://www.postgresql.org/docs/current/static/rangetypes.html /// [Flags] enum RangeFlags : byte { /// /// The default flag. The range is not empty and has boundaries that are definite and exclusive. /// None = 0, /// /// The range is empty. E.g. '(0,0)', 'empty'. /// Empty = 1, /// /// The lower bound is inclusive. E.g. '[0,5]', '[0,5)', '[0,)'. /// LowerBoundInclusive = 2, /// /// The upper bound is inclusive. E.g. '[0,5]', '(0,5]', '(,5]'. /// UpperBoundInclusive = 4, /// /// The lower bound is infinite or indefinite. E.g. '(null,5]', '(-infinity,5]', '(,5]'. /// LowerBoundInfinite = 8, /// /// The upper bound is infinite or indefinite. E.g. '[0,null)', '[0,infinity)', '[0,)'. /// UpperBoundInfinite = 16, /// /// Both the lower and upper bounds are inclusive. /// Inclusive = LowerBoundInclusive | UpperBoundInclusive, /// /// Both the lower and upper bounds are indefinite. /// Infinite = LowerBoundInfinite | UpperBoundInfinite, /// /// The lower bound is both inclusive and indefinite. This represents an error condition. /// LowerInclusiveInfinite = LowerBoundInclusive | LowerBoundInfinite, /// /// The upper bound is both inclusive and indefinite. This represents an error condition. /// UpperInclusiveInfinite = UpperBoundInclusive | UpperBoundInfinite }
X Tutup