|
| 1 | +# Architecture |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +CodoMetis.ValueRanges is a .NET 10 class library providing type-safe range types that mirror PostgreSQL's six built-in range domains. Each range is a discriminated union of five sealed variants, making invalid states unrepresentable and pattern matching exhaustive by contract. A companion EF Core package (`CodoMetis.ValueRanges.EFCore.PostgreSQL`) bridges these types to `NpgsqlRange<T>` for LINQ-to-SQL translation. |
| 6 | + |
| 7 | +## Range Types |
| 8 | + |
| 9 | +| C# Type | PostgreSQL | Element Type | Discrete | |
| 10 | +|---|---|---|---| |
| 11 | +| `Int32Range` | `int4range` | `int` | ✓ | |
| 12 | +| `Int64Range` | `int8range` | `long` | ✓ | |
| 13 | +| `DateRange` | `daterange` | `DateOnly` | ✓ | |
| 14 | +| `DecimalRange` | `numrange` | `decimal` | — | |
| 15 | +| `DateTimeRange` | `tsrange` | `DateTime` | — | |
| 16 | +| `DateTimeOffsetRange` | `tstzrange` | `DateTimeOffset` | — | |
| 17 | + |
| 18 | +Discrete types (int, long, DateOnly) implement `NextValueAfter`/`PreviousValueBefore` to return the adjacent value. Continuous types leave them returning `null`. Discrete ranges canonicalize to closed `[lower, upper]` at construction (`Internals/DiscreteCanonical.cs`); continuous ranges default to half-open `[lower, upper)`. |
| 19 | + |
| 20 | +## Discriminated Union Pattern |
| 21 | + |
| 22 | +Every range type is an abstract record with five sealed nested variants: |
| 23 | + |
| 24 | +``` |
| 25 | +RangeType (abstract, private ctor) |
| 26 | +├── EmptyRange : IEmptyRange<T> — contains no values |
| 27 | +├── Finite : IFiniteRange<T> — [start, end] (bounded both sides) |
| 28 | +├── UnboundedStart : IUnboundedStartRange<T> — (-∞, end] |
| 29 | +├── UnboundedEnd : IUnboundedEndRange<T> — [start, +∞) |
| 30 | +└── Infinity : IInfinityRange<T> — (-∞, +∞) |
| 31 | +``` |
| 32 | + |
| 33 | +The private base constructor prevents external subtyping, so the compiler guarantees exhaustive switch expressions. Invalid ranges (inverted bounds, degenerate half-open) normalize to `EmptyRange` at construction time. |
| 34 | + |
| 35 | +## Interface Hierarchy (`Core/`) |
| 36 | + |
| 37 | +- **`IRange<T>`** — Marker interface. Carries `internal default methods` `IntersectWith<TRange>()` and `MergeWith<TRange>()` that dispatch per-shape to the engines in `Internals/`. |
| 38 | +- **`IRangeFactory<TRange, T>`** — Abstract static factory: `Empty`, `Infinite`, `CreateFinite()`, `CreateUnboundedStart()`, `CreateUnboundedEnd()`, plus virtual `NextValueAfter`/`PreviousValueBefore`. Also implements `IParsable<TRange>` and `IFormattable` with PostgreSQL range literal syntax. |
| 39 | +- **Structural interfaces** — `IFiniteRange<T>`, `IUnboundedStartRange<T>`, `IUnboundedEndRange<T>`, `IEmptyRange<T>`, `IInfinityRange<T>` — each provides its own concrete `IntersectWith`/`MergeWith` implementations (e.g., `IInfinityRange<T>` always returns the other operand for intersection, always returns `Infinite` for merge). |
| 40 | + |
| 41 | +All type parameters are constrained to `struct, IComparable<T>, IEquatable<T>`. |
| 42 | + |
| 43 | +## Extension Methods (`RangeExtensions.cs`) |
| 44 | + |
| 45 | +Uses the C# 14 `extension` keyword. Two `extension<T>` blocks: |
| 46 | + |
| 47 | +1. **Query operations** on `IRange<T>` — state checks (`IsEmpty`, `IsInfinity`, etc.), containment, overlap, adjacency, directional comparisons |
| 48 | +2. **Set operations** on `IRangeFactory<TRange, T>` — `Intersect` (returns `TRange`), `Union`/`Except` (return `RangeSet<TRange, T>`) |
| 49 | + |
| 50 | +See `CodoMetis.ValueRanges/RangeExtensions.cs` for the full implementation. |
| 51 | + |
| 52 | +## RangeSet<TRange, T> (`RangeSet.cs`) |
| 53 | + |
| 54 | +Immutable multirange counterpart of PostgreSQL's `int4multirange`, etc. A sealed class over `ImmutableArray<TRange>` with a strict invariant: |
| 55 | + |
| 56 | +- Sorted by lower bound |
| 57 | +- Pairwise disjoint, pairwise non-adjacent |
| 58 | +- No empty elements |
| 59 | +- Any `Infinity` input collapses the set to `Infinite` singleton |
| 60 | + |
| 61 | +Key methods: |
| 62 | +- `From(IEnumerable<TRange>)` — normalizes (filter → sort via `Internals/RangeSetHelpers.CompareByLowerBound` → greedy merge) |
| 63 | +- Bulk ops (`Union`, `Intersect`, `Except`) use O(n+m) merge-join instead of nested loops |
| 64 | +- Operators: `\|` for union, `&` for intersect, `-` for except |
| 65 | +- `LowerBoundComparer` — static `IComparer<TRange>` for external sorting |
| 66 | + |
| 67 | +See `CodoMetis.ValueRanges/RangeSet.cs` and `CodoMetis.ValueRanges/RangeLowerBoundComparer.cs`. |
| 68 | + |
| 69 | +## JSON Serialization (`Serialization/`) |
| 70 | + |
| 71 | +- `RangeJsonConverter<TRange, T>` — serializes to/from PostgreSQL range literal strings |
| 72 | +- `RangeJsonConverterFactory` — auto-registers for any type implementing `IRangeFactory<TRange, T>` or `RangeSet<TRange, T>` |
| 73 | +- Extension: `AddRangeConverters()` registers all at once |
| 74 | + |
| 75 | +## EF Core PostgreSQL (`CodoMetis.ValueRanges.EFCore.PostgreSQL/`) |
| 76 | + |
| 77 | +- **`ValueRangesMethodCallTranslator`** — translates LINQ methods to PostgreSQL operators (`@>`, `&&`, `<@`, `<<`, `>>`, `&<`, `&>`, `-|-`, `*`, `+`, `-`) |
| 78 | +- **Type mapping** — maps range types to PostgreSQL range columns, RangeSet to multirange columns |
| 79 | +- **Enable**: `options.UseNpgsql(connectionString, npgsql => npgsql.UseValueRanges());` |
| 80 | + |
| 81 | +## Engine Internals (`Internals/`) |
| 82 | + |
| 83 | +- `IntersectEngine.cs`, `MergeEngine.cs` — per-shape intersection and merge logic |
| 84 | +- `ExceptEngine.cs` — set difference with boundary inversion at cut points |
| 85 | +- `DiscreteCanonical.cs` — canonicalizes discrete ranges to closed form |
| 86 | +- `RangeBoundHelpers.cs`, `RangeFormat.cs`, `RangeSetHelpers.cs` — shared utilities |
0 commit comments