Skip to content

Commit 882ce66

Browse files
docs: add architecture and testing documentation
Added detailed `architecture.md` to explain core concepts, design patterns, and internal structure of range types and `RangeSet`. Introduced `testing.md` to outline test organization, patterns, and guidelines, ensuring consistent and efficient testing practices.
1 parent 5403334 commit 882ce66

3 files changed

Lines changed: 208 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# CodoMetis.ValueRanges
2+
3+
In-memory range and multirange types for .NET 10, mirroring PostgreSQL's six built-in range domains (`int4range`, `int8range`, `numrange`, `daterange`, `tsrange`, `tstzrange`). Each type is a discriminated union of five sealed variants with exhaustive pattern matching.
4+
5+
## Stack
6+
.NET 10 · C# 14 (extension methods) · MSTest 4.x · EF Core + Npgsql (PostgreSQL bridge)
7+
8+
## Structure
9+
- `CodoMetis.ValueRanges/` — Core library: range types, interfaces, set ops
10+
- `CodoMetis.ValueRanges.EFCore.PostgreSQL/` — EF Core provider for LINQ-to-SQL translation
11+
- `CodoMetis.ValueRanges.Tests/` — Unit tests (one file per operation)
12+
- `CodoMetis.ValueRanges.EFCore.PostgreSQL.Tests/` — EF Core integration tests
13+
- `docs/` — Agent docs (read relevant doc before starting work)
14+
15+
## Commands
16+
```bash
17+
dotnet build # Build everything
18+
dotnet test # Run all tests
19+
dotnet test --filter "ClassName=RangeContainsTests" # Single test class
20+
dotnet test --filter "FullyQualifiedName~Contains_FiniteRange" # Single method
21+
dotnet pack # Pack NuGet packages
22+
```
23+
24+
## Workflow
25+
1. Read `docs/architecture.md` before modifying range types or interfaces
26+
2. Explore the codebase — range operations are per-shape (Finite, UnboundedStart, etc.)
27+
3. Run `dotnet test` after each change; tests are method-level parallel
28+
4. Commit with conventional format: `feat:`, `fix:`, `refactor:`, `docs:`
29+
30+
## Docs
31+
- `docs/architecture.md` — Discriminated union pattern, interface hierarchy, RangeSet internals
32+
- `docs/testing.md` — Test organization, patterns, shape-combination matrix
33+
34+
## Critical Rules
35+
- **NEVER** create external subtypes of range base records — the private constructor enforces exhaustive pattern matching; breaking this removes compiler guarantees
36+
- **ALWAYS** preserve RangeSet's invariant (sorted, disjoint, non-adjacent, no empties) on every code path that constructs or mutates a set
37+
- **Do NOT** add new range types without adding corresponding `IntersectEngine`/`MergeEngine` per-shape implementations in `Internals/`

docs/architecture.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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

docs/testing.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Testing
2+
3+
## Running Tests
4+
5+
```bash
6+
dotnet test # All tests (method-level parallel)
7+
dotnet test --filter "ClassName=RangeContainsTests" # Single class
8+
dotnet test --filter "FullyQualifiedName~Contains_FiniteRange" # Single method
9+
```
10+
11+
Tests run in parallel at the **method level** (`MSTestSettings.cs`). No shared state between tests.
12+
13+
## Organization
14+
15+
One test file per operation, named `Range[Operation]Tests.cs`:
16+
17+
| File | Covers |
18+
|---|---|
19+
| `RangeContainsTests.cs` | Point and range containment |
20+
| `RangeOverlapsTests.cs` | Overlap detection |
21+
| `RangeIsAdjacentTests.cs` | Adjacency (discrete step-aware, continuous complementary inclusiveness) |
22+
| `RangeIntersectTests.cs` | Intersection across all shape combinations |
23+
| `RangeUnionTests.cs` | Union (merge overlapping, keep disjoint) |
24+
| `RangeExceptTests.cs` | Set difference (0/1/2 element results) |
25+
| `RangeContainedByTests.cs` | Symmetric containment alias |
26+
| `RangeDoesNotExtendLeftOfTests.cs` / `RightOfTests.cs` | PostgreSQL `&<`/`&>` |
27+
| `RangeStrictlyLeftOrRightOfTests.cs` | PostgreSQL `<<`/`>>` |
28+
| `RangeParseFormatTests.cs` | PostgreSQL range literal round-trips |
29+
| `RangeSetTests.cs` | RangeSet construction, normalization, bulk ops |
30+
| `RangeSetOptimizationTests.cs` | Performance-critical invariants |
31+
| `RangeJsonConverterTests.cs` | JSON serialization round-trips |
32+
33+
EF Core tests live in `CodoMetis.ValueRanges.EFCore.PostgreSQL.Tests/`.
34+
35+
## Patterns
36+
37+
### Shape-Combination Matrix
38+
39+
Every binary operation test must cover all five range shapes: `Finite`, `UnboundedStart`, `UnboundedEnd`, `EmptyRange`, `Infinity`. Tests instantiate the "other" operand in each shape and verify the result type and value.
40+
41+
Example from `RangeContainsTests.cs`:
42+
```csharp
43+
// Tests Finite vs each shape of "other" — interior, left-of, right-of, overlapping
44+
```
45+
46+
### Boundary Inclusiveness Permutations
47+
48+
For `Finite` ranges, tests cover all four inclusiveness combinations:
49+
- `[start, end]` — both inclusive (default for discrete)
50+
- `(start, end)` — both exclusive
51+
- `[start, end)` — lower inclusive (default for continuous)
52+
- `(start, end]` — upper inclusive
53+
54+
### Discrete vs Continuous Split
55+
56+
Tests are parameterized by range type:
57+
- **Discrete** (`Int32Range`, `Int64Range`, `DateRange`) — canonicalization, adjacency with step awareness
58+
- **Continuous** (`DecimalRange`, `DateTimeRange`, `DateTimeOffsetRange`) — half-open defaults, equality via `IEquatable`
59+
60+
### Assertion Style
61+
62+
Use MSTest assertions (`Assert.IsTrue`, `Assert.IsFalse`, `Assert.AreEqual`, `Assert.AreSame`). For structural shape checks, cast to the specific interface (e.g., `IFiniteRange<int>`) and verify properties.
63+
64+
### RangeSet Type Aliases
65+
66+
Use local type aliases for readability in tests:
67+
```csharp
68+
using IntSet = CodoMetis.ValueRanges.RangeSet<CodoMetis.ValueRanges.Int32Range, int>;
69+
using DecimalSet = CodoMetis.ValueRanges.RangeSet<CodoMetis.ValueRanges.DecimalRange, decimal>;
70+
```
71+
72+
## What to Test
73+
74+
- **Every public method** on range types and `RangeSet`
75+
- **All shape combinations** for binary operations (5×5 = 25 cases per operation)
76+
- **Boundary inclusiveness** permutations for `Finite` ranges
77+
- **Normalization invariants**: empty filtering, overlap merging, adjacency merging, Infinity collapse
78+
- **Round-trips**: parse → format should produce equivalent ranges (exact string match for same-shape inputs)
79+
- **Edge cases**: empty input to `RangeSet.From()`, single-element fast path, `Infinite.Except()` complement
80+
81+
## What Not to Test
82+
83+
- Framework internals (MSTest, System.Text.Json)
84+
- Simple property getters on range variants
85+
- Code paths that are provably unreachable (private constructors, sealed subtypes)

0 commit comments

Comments
 (0)