Repository navigation
feat: 6.3.0 — shape predicates, collection expressions, spans, measure, enumeration and the set/range bridge - #18
Merged
Merged
Conversation
Three gaps in the existing surface, each something one half of the library had
and the other half did not. No breaking changes.
RangeSet.IsInfinity() / IsFinite() — the two shape predicates a range had and
its multirange counterpart did not. IsInfinity() is deliberately not
IsUnboundedStart() && IsUnboundedEnd(): that equivalence holds for a contiguous
range and fails for a set, since {(,5],[10,)} is unbounded at both ends and does
not contain 7. It translates to equality against the infinite multirange rather
than to lower_inf AND upper_inf, which is exact because PostgreSQL canonicalizes
multiranges the way the model does — verified against live PostgreSQL, where the
gapped set answers both lower_inf and upper_inf true and the equality false.
Collection expressions for RangeSet<TRange, T>, matching the fifteen value set
types. The builder is a non-generic RangeSet.Create<TRange, T>, since a
[CollectionBuilder] target cannot be generic; a From(params ReadOnlySpan<TRange>)
overload comes with it and shares the normalization tail with the sequence
overload. Note that C# does not infer type arguments from constraints, so a
direct Create call must name both — the collection expression is the ergonomic
path, not the factory.
ISpanParsable<T> on the eleven range types, RangeSet and all nineteen set types
and arities. The literal grammars were already parsed over spans internally, so
each overload is the existing call minus the .AsSpan() widening. One consequence
documented at the RangeSet.Parse call site: where a type parameter is constrained
to IRangeFactory/IValueSetFactory both overloads are now visible and a string
argument binds to the span one — harmless because every type's two overloads are
the same call.
PackageValidationBaselineVersion moves to 6.2.1, the release this supersedes, per
the convention in Directory.Build.props. Restore therefore fails until 6.2.1 is
published to NuGet; verified green against the 6.2.0 baseline in the meantime.
Co-Authored-By: Claude Opus 5 <[email protected]>
Length on every range type. The convention follows the domain: a discrete one
counts its values inclusive of both bounds ([2024-01-01, 2024-01-31] measures 31
days, [1,10] measures 10 integers), a continuous one measures the span. Empty
measures zero and unbounded measures null — different answers that stay
distinguishable. The type follows too: long? for the integer ranges, int? days
for DateRange, TimeSpan? for the timestamp ranges, decimal? for DecimalRange,
and Duration?/Period? for the NodaTime ranges, which separate exact elapsed time
from a calendar quantity. Int64Range is the one type whose count can exceed what
reports it — the near-full domain holds more than long.MaxValue values — so it is
computed in decimal and refused rather than wrapping to a plausible negative.
Values() on the discrete range types only. Declaring it per type rather than as
an extension over IRange<T> makes asking a DecimalRange for its values a compile
error instead of a runtime throw, which is the same bargain the five sealed
variants already make. Validation is eager: an iterator would defer the unbounded
refusal to the first MoveNext and surface it at the foreach rather than at the
call that was wrong.
A bridge between the two type families over the discrete domains: ToRangeSet()
collapses runs of consecutive values ({1,2,3,7} to {[1,3],[7,7]}) and
ToInt32Set()/ToDateSet()/... expand back. The shapes describe the same membership
and differ only in density — a thousand consecutive dates are one daterange or a
thousand-element array, and @> against the range is the cheaper question. Both
directions are client-side; PostgreSQL converts between arrays and multiranges
only through unnest and a custom aggregate.
Clamp(value) on every range, and an indexer on the nineteen value set types,
matching what RangeSet already offered.
IRangeFactory.IsDiscrete, a defaulted virtual static, because the property cannot
be derived from NextValueAfter — that returns null both for a continuous domain
and for the last value of a discrete one. RangeFactoryContractTests holds the two
to agreement and checks that a type claiming a step also canonicalizes closed,
with a discovery floor so it cannot pass by matching nothing.
Each guard verified by seeding its defect: dropping an IsDiscrete override, making
Length measure a span instead of a count, letting the bridge merge across gaps, and
deferring the eager validation each turn the relevant tests red.
Co-Authored-By: Claude Opus 5 <[email protected]>
… label The parse-rejection change was filed under "## [Unreleased]" and then shipped in 6.2.1 — the tag was cut from a main that already contained it. Consumers got a changed exception-message shape with no 6.2.1 entry describing it, and the version-is-documented tests passed throughout, because 6.2.1 did have an entry, just not the whole one. Folded into 6.2.1 where it belongs, in the root and core changelogs. Guarded so it cannot recur: ChangelogConventionTests now fails on any heading that names no version. Every commit on main is a tag away from publishing, and the release workflow ships whatever Directory.Build.props names, so a heading meaning "not yet" is false the moment someone tags. Verified by putting an Unreleased heading back and watching the test name it. Added "What runs where" to the README: one table for the translated surface with its PostgreSQL operators, one for the client-side operations with the reason each cannot translate. The information existed but was scattered across six places in prose, and the 6.3.0 additions made the client-side half large enough to be worth stating in one place. The EF package README gains the same client-side table, since that is where an EF consumer looks. Fixed a count that contradicted itself: collection expressions were described as supported by "fifteen value set types" in one place and nineteen in another. It is nineteen — fifteen closed types plus four wrapper arities. Every remaining count claim was cross-checked against the code. Co-Authored-By: Claude Opus 5 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Seven additions, no breaking changes. Three closed gaps where one half of the library had something the other did not; four came from a follow-up pass over the same list.
Round one
RangeSet.IsInfinity()/IsFinite()— the shape predicates a range had and its multirange counterpart did not.The design decision worth reviewing:
IsInfinity()is notIsUnboundedStart() && IsUnboundedEnd(). That equivalence holds for a range, which is contiguous, and fails for a set —{(,5],[10,)}is unbounded at both ends and does not contain 7. It translates tox = '{(,)}'::datemultirange, exact because PostgreSQL canonicalizes multiranges the way the model does. Verified against live PostgreSQL, where the gapped set answerslower_infandupper_inftrue and the equality false.Collection expressions for
RangeSet<TRange, T>— via a non-genericRangeSet.Createbuilder plusFrom(params ReadOnlySpan<TRange>). Note this does not also buyRangeSet.From(a, b)inference: C# does not infer type arguments from constraints, so a directCreatecall still names both.ISpanParsable<T>on both factory interfaces,RangeSet, and all 30 concrete types. The parsers were already span-based internally, so each overload is the existing call minus.AsSpan().Round two
Lengthon all eleven range types — a count for discrete domains inclusive of both bounds, a span for continuous ones. Empty measures zero, unbounded measuresnull; the two stay distinguishable.Int64Rangeis the one type whose count can exceed what reports it, so it is computed indecimaland refused rather than wrapping to a plausible negative.Values()on the discrete range types only. Declaring it per type rather than as an extension makes asking aDecimalRangefor its values a compile error instead of a runtime throw. Validation is eager, so an unbounded range fails at the call rather than at theforeach.A bridge between the two type families —
{1,2,3,7}↔{[1,3],[7,7]}for the discrete domains. Client-side: PostgreSQL converts between arrays and multiranges only throughunnestand a custom aggregate.Clamp(value)on every range, and an indexer on the nineteen value set types.Supporting this:
IRangeFactory.IsDiscrete, a defaulted virtual static. It cannot be derived fromNextValueAfter, which returnsnullboth for a continuous domain and for the last value of a discrete one.Documentation
Adds "What runs where" to the README — one table for the translated surface with its PostgreSQL operators, one for the client-side operations with the reason each cannot translate, built by enumerating the translators rather than from memory. The EF package README gains the same client-side table.
Also folds a
## [Unreleased]changelog section into 6.2.1, where it had already shipped — the tag was cut from amainthat already contained the parse-rejection change, so consumers got a changed exception-message shape with no 6.2.1 entry describing it.ChangelogConventionTestsnow fails on any heading naming no version, so a tag cannot outrun its own release notes again.Verification
1117 tests, 0 warnings under
TreatWarningsAsErrors, packing clean through PackageValidation against the published 6.2.1 baseline.Every guard was verified by seeding its defect and watching it go red:
IsInfinityweakened tolower_inf AND upper_infRangeSet.CreatethrowsIsDiscreteoverrideLengthmeasures a span instead of a count[Unreleased]heading restoredJSON needed no change: the additions are members on existing types, and
NamedJsonConverterTestsalready fails on a new family without a converter.🤖 Generated with Claude Code