Skip to content

v4 overload minimalism: SortedSetRange, Expiration on RESTORE, GeoCenter - #3356

Merged
mgravell merged 1 commit into
v4from
marc/v4-overload-minimalism
Nov 13, 2026
Merged

mgravell merged 1 commit into
v4from
marc/v4-overload-minimalism

Conversation

@mgravell

Copy link
Copy Markdown
Member

Implements the three findings from the v4 group-surface overload audit (now checked in as notes/api-surface/overload-audit.md, with an implementation postscript).

1. SortedSets: one method per operation, the axis is data

A SortedSetRange operand in the Expiration mould - ByRank(start, stop), ByScore(min, max, exclude), ByValue(min, max, exclude), with default meaning the whole set by rank - and five methods where there were eleven:

await db.SortedSets.RangeAsync(key, SortedSetRange.ByScore(1, 10), Order.Descending);
await db.SortedSets.RangeWithScoresAsync(key, SortedSetRange.ByRank(0, 9));
await db.SortedSets.RangeAndStoreAsync(src, dst, SortedSetRange.ByValue("a", "j"), take: 3);
await db.SortedSets.RemoveRangeAsync(key, SortedSetRange.ByScore(max: 0));
await db.SortedSets.RangeLengthAsync(key, SortedSetRange.ByValue("a", "j"));

LengthAsync slims to bare ZCARD (ZCOUNT/ZLEXCOUNT live under RangeLengthAsync), and the SortedSetOrder axis enum disappears from RangeAndStoreAsync. The per-axis methods demote to internal - the wire spelling, bound-swapping and the legacy IDatabase adapter all stay put.

Where the server has no command for a cell, the dispatcher throws rather than quietly answering a different question: WITHSCORES over a lexical range (every member of such a set shares one score - the audit originally mis-called this cell "arbitrarily missing"), LIMIT over a rank range, and a count of a rank range. A by-rank exclusivity, which the old surface rejected at runtime, is now unrepresentable: ByRank has no exclude parameter.

Two things the operand store path gains over the legacy shape: open (-/+) lexical bounds, and descending bound-swapping done for you (with REV the server wants high-then-low, exclusivity travelling with its bound - the same normalization the reads have always applied).

2. Keys.RestoreAsync takes an Expiration

RESTORE has ABSTTL, so the operand that already spells relative/absolute/none is the right one. Relative renders as the positional millisecond ttl; absolute adds the ABSTTL trailer; KEEPTTL/PERSIST/ENX are rejected (a restored key has no expiry to keep). The TimeSpan.MaxValue-means-none wart survives only in the internal sibling that serves the legacy surface.

3. GeoCenter folds the geo search pairs 4 → 2

SearchAsync and SearchAndStoreAsync each differed only in how the centre was spelled (member vs longitude/latitude). GeoCenter is that choice as data, implicit from RedisValue/string/GeoPosition - not from a (double, double) tuple, which the full-suite run caught: SanityChecks.ValueTupleNotReferenced pins that this library never references System.ValueTuple (net461 only gets it from an extra package). A default centre is rejected rather than silently searching (0, 0).

En route

RangeByScoreCore and CombineCore accepted a CancellationToken from their public callers and dropped it before SendAsync; both now forward it. Geo's SearchCore (and possibly others) have the same drop - left alone here, worth a sweep of its own.

Tests

Wire tests rewritten against the operand surface with identical byte expectations, plus new coverage: rank-axis dispatch, descending stores, the refusal matrix, RESTORE's three expiry spellings, and the default-GeoCenter rejection. Full net10.0 suite: 7532 passed / 167 skipped; the only failures are the 8 known-environmental SortedSetScanLarge ones (dev-server btree ZSCAN duplicates, fixed in the server 8.12 branch). Docs (Groups.md, LegacyApi.md mapping tables) updated.

The three findings from the v4 group-surface overload audit
(notes/api-surface/overload-audit.md):

- SortedSets: one method per operation - RangeAsync, RangeWithScoresAsync,
  RangeAndStoreAsync, RemoveRangeAsync, RangeLengthAsync - each taking a
  SortedSetRange operand (ByRank/ByScore/ByValue) instead of eleven per-axis
  methods; LengthAsync slims to bare ZCARD. The per-axis methods demote to
  internal, keeping the wire spelling and the legacy IDatabase adapter. Cells
  the server cannot spell (WITHSCORES over BYLEX, LIMIT or a count over ranks)
  throw rather than quietly answering a different question; a by-rank
  exclusivity is no longer representable at all. The operand store path also
  gains open lexical bounds and descending bound-swapping, which the
  SortedSetOrder-shaped legacy method left to the caller.
- Keys.RestoreAsync takes an Expiration: relative stays positional
  milliseconds, absolute adds ABSTTL, KEEPTTL/PERSIST/ENX are rejected, and
  the TimeSpan.MaxValue-means-none wart survives only in the internal legacy
  sibling.
- Geospatial: a GeoCenter operand (implicit from RedisValue/string/GeoPosition;
  deliberately not from a tuple - ValueTupleNotReferenced pins that) folds the
  member/lon-lat SearchAsync and SearchAndStoreAsync pairs 4 -> 2.

En route: RangeByScoreCore and CombineCore accepted a CancellationToken from
their public callers and dropped it before SendAsync; both now forward it.
Geo's SearchCore has the same drop, left for a sweep of its own.
@mgravell
mgravell merged commit 0d18c57 into v4 Nov 13, 2026
3 checks passed
@mgravell
mgravell deleted the marc/v4-overload-minimalism branch November 13, 2026 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant