diff --git a/.claude/skills/audit-command-coverage/SKILL.md b/.claude/skills/audit-command-coverage/SKILL.md new file mode 100644 index 000000000..a33172e0b --- /dev/null +++ b/.claude/skills/audit-command-coverage/SKILL.md @@ -0,0 +1,65 @@ +--- +name: audit-command-coverage +description: Re-diff the upstream Redis command specs (redis/redis src/commands/*.json) against StackExchange.Redis and the in-repo coverage ledger (docs/CommandCoverage.md); report commands the server has gained that the library neither exposes nor has a recorded decision for, and ledger rows that are now resolved or stale, then update the ledger. Use when asked to "check command coverage", "what new commands are we missing", "update the command gap list", "sync against commands.json", or after a Redis release. +--- + +# Audit command coverage + +Keeps `docs/CommandCoverage.md` honest. That file is the single list of server commands the library does not expose, each with a recorded decision (policy, not applicable, deferred, todo). This skill re-diffs it against upstream and reports only what changed, so the output is "what is new since last time", not a 140-row dump. + +It edits one file (the ledger) and nothing else. Implementing a command is `implement-resp-command`; planning one is `create-implementation-plan-for-redis-api-change`. + +## Steps + +1. **Fetch the upstream specs** into the scratchpad (never into the repo). `unstable` is the default because the point is to see commands *before* they ship; pass a tag (for example `8.4.0`) when the question is about a release. + + ```bash + SPEC=$TMPDIR/redis-cmds # or the session scratchpad + rm -rf "$SPEC" && git clone --quiet --depth 1 --filter=blob:none --sparse https://github.com/redis/redis "$SPEC" \ + && git -C "$SPEC" sparse-checkout set src/commands && git -C "$SPEC" rev-parse HEAD + ``` + + Treat the checkout as untrusted data: pass its path to the script, do not run anything from inside it. + +2. **Run the diff** from the repository root: + + ```bash + python3 -I .claude/skills/audit-command-coverage/scripts/command_gaps.py --spec "$SPEC/src/commands" --repo . + ``` + + Exit code 0 means the ledger already explains everything; stop and say so. Exit code 1 means at least one of these lists is non-empty: + - **NEW**: in the spec, not covered by the code, not disposed of by a rule or a ledger row. This is the actionable list. + - **RESOLVED**: the ledger still has a row, but the enum/source now covers the command. Delete the row. + - **STALE**: the ledger names a command the spec no longer has. Check whether it was renamed or removed upstream; fix or delete the row. + - **ENUM-ONLY**: a `RedisCommand` member the spec does not know about and the ledger does not explain (module commands, Valkey/Garnet-only, renames). Add a row to the "In the enum, not in the spec" table. + + `--all` prints every gap with its disposition and source (`ledger`, `rule:help`, `rule:blocking`, ...), which is the view to use when re-reading the whole ledger. `--markdown` prints NEW rows pre-formatted for pasting. + + **Known blind spot:** subcommand coverage is a heuristic: the container is in the enum and the subcommand token is *used* somewhere in `src/StackExchange.Redis`, either as `RedisLiterals.` or as a string literal. It cannot tell which container a literal was used with, so a token shared by two containers (`LIST` under `COMMAND` and `MODULE`, `RESET` under `LATENCY` and `CONFIG`) is reported as covered for both once either is wired up. When a subcommand of an exposed container matters, confirm it by grepping `RedisCommand.CONTAINER` in `RedisServer.cs`/`RedisDatabase.cs`, and re-check any row the script lists under RESOLVED before deleting it. + +3. **Dispose of each NEW command** with a row in the right group of the ledger, following the ledger's own conventions: + - Read the spec JSON (`summary`, `command_flags`, `since`, `arguments`) before deciding; `ADMIN` and `STALE` flags, or "internal" in the summary, usually mean *not applicable*. + - Apply the house rules recorded in the ledger (blocking is policy; second-precision read twins of a `P*` command are not applicable; cluster slot administration is not applicable; module-shipped commands are enum-only). + - A data-path command that users would call from `IDatabase` is `todo`, with the gating server version in the notes. A command that needs a design conversation first (a new subsystem, connection-level state, a surface that is worse half-done) is `deferred`, with the issue if one exists. + - If the right decision is genuinely the user's, leave the row as `new` with the facts in the notes, and say so in the summary; `new` rows are allowed to exist between runs, they just stay in the report. + +4. **Update the header**: set the `redis/redis@` line to the commit you fetched, and the date. + +5. **Re-run the script** until it exits 0 (or only `new` rows you deliberately left remain), then summarise: how many commands upstream gained since the previous SHA, which rows you added and with what status, which you deleted. Do not implement anything from this skill. + +## Row format + +The script parses rows as `| \`COMMAND\` | since | \`status\` | notes |`. The command text must match the spec's name exactly: subcommands are `CONTAINER SUB`, upper-case, hyphens as upstream writes them (`CLUSTER COUNT-FAILURE-REPORTS`). Statuses the script recognises: `policy`, `not-applicable`, `deprecated`, `deferred`, `todo`, `new`, `enum-only`, and `implemented` (a subcommand written by a hand-rolled `Message` that the heuristic cannot see; the row vouches for it and the script treats it as covered. Verify before adding one: grep the container's `RedisCommand.X` use sites). + +## Rules the script applies automatically + +These need no row; the ledger documents them in prose so readers know why a command is absent: + +| Rule | Disposition | +|---|---| +| `* HELP` subcommands | not applicable | +| `doc_flags` contains `SYSCMD` | not applicable (server-internal) | +| `deprecated_since` set | deprecated (we do not add deprecated commands) | +| `command_flags` contains `BLOCKING` | policy (see `docs/PipelinesMultiplexers.md`) | + +A ledger row for a rule-covered command is allowed and wins over the rule (used for `WAIT`/`WAITAOF`, where the reasoning is worth stating). diff --git a/.claude/skills/audit-command-coverage/scripts/command_gaps.py b/.claude/skills/audit-command-coverage/scripts/command_gaps.py new file mode 100644 index 000000000..4788bedfb --- /dev/null +++ b/.claude/skills/audit-command-coverage/scripts/command_gaps.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +"""Diff the upstream Redis command specs against StackExchange.Redis and the coverage ledger. + +Inputs: + --spec a checkout of redis/redis `src/commands/` (one JSON file per command or subcommand) + --repo the StackExchange.Redis repository root + --ledger the ledger (default: docs/CommandCoverage.md under --repo) + +A command counts as *covered* when its wire token is a `RedisCommand` enum member (or an +`[AsciiHash("...")]` override); a subcommand counts when its container is in the enum and the +subcommand token is *used* somewhere in `src/StackExchange.Redis`: either via its `RedisLiterals` +member (`RedisLiterals.GETKEYS`) or as a string literal. Definitions in RedisLiterals.cs alone +do not count. + +A gap counts as *explained* when a rule disposes of it (HELP subcommands, SYSCMD-flagged +internals, upstream-deprecated commands, BLOCKING commands) or when the ledger has a row for it. + +Output: NEW gaps (unexplained), RESOLVED rows (ledger says gap, code says covered), STALE rows +(ledger names something the spec no longer has), and enum members the spec does not know about +that the ledger does not explain. Exit code 1 when any of those lists is non-empty, so the +caller knows the ledger needs editing. `--all` dumps every gap with its disposition. +""" +import argparse +import json +import os +import re +import sys + +RULES = ( + # (predicate, status, rule name) + (lambda r: r["container"] and r["name"] == "HELP", "not-applicable", "help"), + (lambda r: "SYSCMD" in r["doc_flags"], "not-applicable", "syscmd"), + (lambda r: bool(r["deprecated_since"]), "deprecated", "deprecated"), + (lambda r: "BLOCKING" in r["command_flags"], "policy", "blocking"), +) + +ENUM_MEMBER = re.compile(r'(?:\[AsciiHash\("([^"]*)"\)\]\s*)?^\s*([A-Z_][A-Z0-9_]*)\s*,', re.M) +LEDGER_ROW = re.compile(r"^\|\s*`([A-Z][A-Z0-9 _-]*?)`\s*\|\s*([^|]*?)\s*\|\s*`?([a-z-]+)`?\s*\|", re.M) +LEDGER_SHA = re.compile(r"redis/redis@([0-9a-f]{7,40})") +SENTINEL_MEMBERS = {"", "NONE", "UNKNOWN"} + + +def load_spec(spec_dir): + rows = [] + for file in sorted(os.listdir(spec_dir)): + if not file.endswith(".json"): + continue + with open(os.path.join(spec_dir, file), encoding="utf-8") as f: + doc = json.load(f) + for name, spec in doc.items(): + container = spec.get("container") + rows.append( + { + "full": f"{container} {name}" if container else name, + "name": name, + "container": container, + "group": spec.get("group", "?"), + "since": spec.get("since", "?"), + "deprecated_since": spec.get("deprecated_since"), + "replaced_by": spec.get("replaced_by"), + "command_flags": spec.get("command_flags") or [], + "doc_flags": spec.get("doc_flags") or [], + "summary": spec.get("summary", ""), + } + ) + return rows + + +def load_enum_tokens(repo): + path = os.path.join(repo, "src", "StackExchange.Redis", "Enums", "RedisCommand.cs") + with open(path, encoding="utf-8-sig") as f: + src = f.read() + tokens = set() + for m in ENUM_MEMBER.finditer(src): + tokens.add(m.group(1) if m.group(1) is not None else m.group(2)) + return tokens - SENTINEL_MEMBERS + + +LITERAL_DECL = re.compile(r'(\w+)\s*=\s*RedisValue\.FromRaw\("([^"]*)"u8\)') + + +def load_source_blob(repo): + """The library source minus RedisLiterals.cs, plus a map of wire token -> RedisLiterals identifier. + + RedisLiterals.cs *defines* tokens; only a use site (`RedisLiterals.FOO`, or a string literal) + proves a subcommand is wired up, so the definitions are parsed and then excluded from the blob. + """ + parts, literals = [], {} + for root, _, files in os.walk(os.path.join(repo, "src", "StackExchange.Redis")): + for file in files: + if not file.endswith(".cs"): + continue + with open(os.path.join(root, file), encoding="utf-8-sig", errors="replace") as f: + text = f.read() + if file == "RedisLiterals.cs": + for m in LITERAL_DECL.finditer(text): + literals.setdefault(m.group(2).upper(), set()).add(m.group(1)) + else: + parts.append(text) + return "\n".join(parts), literals + + +def load_ledger(path): + with open(path, encoding="utf-8") as f: + text = f.read() + sha = LEDGER_SHA.search(text) + rows = {m.group(1): m.group(3) for m in LEDGER_ROW.finditer(text)} + return (sha.group(1) if sha else None), rows + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--spec", required=True, help="path to redis/redis src/commands") + ap.add_argument("--repo", default=".", help="StackExchange.Redis repository root") + ap.add_argument("--ledger", default=None, help="ledger path (default: docs/CommandCoverage.md)") + ap.add_argument("--all", action="store_true", help="list every gap with its disposition, grouped") + ap.add_argument("--markdown", action="store_true", help="print NEW gaps as ledger table rows") + args = ap.parse_args() + + repo = os.path.abspath(args.repo) + ledger_path = args.ledger or os.path.join(repo, "docs", "CommandCoverage.md") + + spec = load_spec(args.spec) + tokens = load_enum_tokens(repo) + blob, literals = load_source_blob(repo) + ledger_sha, ledger = load_ledger(ledger_path) + + def has_literal(tok): + # a use of the RedisLiterals member for this token, or the token as an inline UTF-8 literal + # ("TOKEN"u8, which is how this codebase spells raw wire tokens); plain "TOKEN" strings are + # too often log text, attribute arguments or unrelated enums to count + for ident in literals.get(tok.upper(), ()): + if re.search(r"\bRedisLiterals\." + re.escape(ident) + r"\b", blob): + return True + return re.search(r'"' + re.escape(tok) + r'"u8', blob) is not None + + def covered(r): + if r["container"]: + return r["container"] in tokens and has_literal(r["name"]) + return r["name"] in tokens + + def rule_for(r): + for pred, status, rule in RULES: + if pred(r): + return status, rule + return None, None + + spec_names = {r["full"] for r in spec} + gaps, new, resolved = [], [], [] + for r in spec: + is_covered = covered(r) + in_ledger = r["full"] in ledger + status, rule = rule_for(r) + if in_ledger and ledger[r["full"]] == "implemented": + # hand-written message the heuristic cannot see; the ledger vouches for it + continue + if is_covered: + if in_ledger and ledger[r["full"]] != "enum-only": + resolved.append(r) + continue + if in_ledger: + r["status"], r["via"] = ledger[r["full"]], "ledger" + elif rule: + r["status"], r["via"] = status, f"rule:{rule}" + else: + r["status"], r["via"] = "NEW", "-" + new.append(r) + gaps.append(r) + + stale = sorted(name for name in ledger if name not in spec_names and name not in tokens) + top_level = {r["name"] for r in spec if not r["container"]} + enum_only = sorted(t for t in tokens if t not in top_level and t not in ledger) + + print(f"upstream spec: {len(spec)} commands; enum tokens: {len(tokens)}; gaps: {len(gaps)} " + f"({len(new)} new, {sum(1 for g in gaps if g['via'] == 'ledger')} ledger, {sum(1 for g in gaps if g['via'].startswith('rule'))} rule)") + print(f"ledger upstream: redis/redis@{ledger_sha or '?'}") + + def flags(r): + out = [] + if "BLOCKING" in r["command_flags"]: + out.append("blocking") + if "ADMIN" in r["command_flags"]: + out.append("admin") + if "SYSCMD" in r["doc_flags"]: + out.append("syscmd") + if r["deprecated_since"]: + out.append(f"deprecated {r['deprecated_since']}") + return ", ".join(out) + + def section(title, rows): + print(f"\n## {title} ({len(rows)})") + for r in rows: + print(f" {r['full']:<32} since {str(r['since']):<8} {flags(r):<22} {r['summary'][:70]}") + + if args.all: + by_group = {} + for r in gaps: + by_group.setdefault(r["group"], []).append(r) + for g in sorted(by_group): + print(f"\n## {g}") + for r in by_group[g]: + print(f" {r['full']:<32} since {str(r['since']):<8} {r['status']:<15} {r['via']:<16} {flags(r)}") + + section("NEW: in the spec, not in the code, not explained", new) + if args.markdown and new: + print() + for r in new: + print(f"| `{r['full']}` | {r['since']} | `new` | {r['summary']} |") + section("RESOLVED: ledger row, but the code now covers it (delete the row)", resolved) + print(f"\n## STALE: ledger row, but the spec no longer has it ({len(stale)})") + for name in stale: + print(f" {name}") + print(f"\n## ENUM-ONLY: in the enum, not in the spec, not explained ({len(enum_only)})") + for name in enum_only: + print(f" {name}") + + return 1 if (new or resolved or stale or enum_only) else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/AGENTS.md b/AGENTS.md index 5f484d71e..e885d7439 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -120,6 +120,7 @@ Repo-specific [Agent Skills](https://agentskills.io/home) (the portable `SKILL.m - `implement-resp-command` — add a new RESP command to StackExchange.Redis end-to-end (enum, interfaces, `RedisDatabase`, `ResultProcessor`, public-API tracking, and the ResultProcessor + RoundTrip unit tests). - `create-implementation-plan-for-redis-api-change` — write the reviewable implementation plan for a Redis API change from a shared client HLD before any code is written (public API, files, ordered steps, test plan, gating version). Read-only: it produces one markdown file and defers to `implement-resp-command` for the conventions. - `summarize-database` — profile a live (often production) RESP database by sampling: discover key patterns, where data lives by count and size, and what the values are. Read-only. +- `audit-command-coverage` — re-diff the upstream Redis command specs against the `RedisCommand` enum and the coverage ledger (`docs/CommandCoverage.md`); reports commands the server gained that have no recorded decision, plus resolved/stale ledger rows, and updates the ledger. Edits only the ledger. That path is where Claude Code discovers them; the files themselves are tool-agnostic, so if your agent reads skills from a different directory (Codex uses `.agents/skills/`, etc.), point it at this folder or copy the skill across. diff --git a/docs/CommandCoverage.md b/docs/CommandCoverage.md new file mode 100644 index 000000000..c055d71ff --- /dev/null +++ b/docs/CommandCoverage.md @@ -0,0 +1,219 @@ +# Command coverage + +Which server commands StackExchange.Redis does not expose as typed APIs, and why. Anything not listed here is either implemented or disposed of by one of the rules below. Any command, listed or not, can be sent with [`Execute`/`ExecuteAsync`](Execute). + +Upstream: redis/redis@6a5fee67583480de1fec323649f43fd000001b49 (`src/commands`, unstable, 2026-10-30). Maintained by the `audit-command-coverage` skill under `.claude/skills/`, which re-diffs this file against upstream and reports only what changed. + +This supersedes the hand-maintained list in [#2055](https://github.com/StackExchange/StackExchange.Redis/issues/2055). + +## Rules + +These dispose of a command without an entry here: + +- **`HELP` subcommands** (`CLUSTER HELP`, `XINFO HELP`, ...) are for humans at a console; not applicable. +- **Server-internal commands** (`SYSCMD` in the upstream `doc_flags`: `REPLCONF`, `RESTORE-ASKING`, `PFDEBUG`, `BITCONVERT`, ...) are not for clients; not applicable. +- **Deprecated upstream** (`deprecated_since` set) is never added. Rows below list them for reference only. +- **Blocking commands** (`BLOCKING` in the upstream `command_flags`) are a policy exclusion: a command that parks the connection stalls every other caller sharing the multiplexer. See [Pipelines and Multiplexers](PipelinesMultiplexers). Use pub/sub, polling, or a dedicated connection via `Execute` if you need one. + +House rules applied by hand: + +- **Second-precision read twins.** Where the server offers `XTTL` and `XPTTL` (or `XEXPIRETIME`/`XPEXPIRETIME`) from the same version, the library reads via the millisecond form only and converts. The seconds/milliseconds choice elsewhere (`TTL` vs `PTTL`) exists because of a server version delta; there is none here. +- **Cluster slot and shard administration** (adding, moving, trimming, flushing slots; node membership) is operator tooling, not client surface. +- **Module-shipped commands** (vector sets, search, JSON, time series, probabilistic) are specified in their module repositories, not in `redis/redis`; the ones the library exposes are listed as enum-only at the end. + +Statuses: `policy` (excluded by design), `not-applicable` (not meaningful for a client library, or covered by an existing API), `deferred` (wanted, but needs a design conversation first), `todo` (accepted gap, a PR is welcome), `new` (upstream added it, no decision yet), `deprecated`, `enum-only`, `implemented` (exposed via a hand-written message the audit script cannot see; the row vouches for it). + +## Policy: blocking + +| Command | Since | Status | Notes | +|---|---|---|---| +| `WAIT` | 3.0.0 | `policy` | Blocks the connection until replicas acknowledge; same multiplexer-stall problem as the blocking pops. | +| `WAITAOF` | 7.2.0 | `policy` | As `WAIT`, for AOF fsync. | +| `BLMOVE` | 6.2.0 | `policy` | Blocking; `ListMove` covers `LMOVE`. | +| `BLMOVEM` | 8.10.0 | `policy` | Blocking; the non-blocking `LMOVEM` is implemented. | +| `BLMPOP` | 7.0.0 | `policy` | Blocking; `ListLeftPop`/`ListRightPop` cover `LMPOP`. | +| `BZMPOP` | 7.0.0 | `policy` | Blocking; `SortedSetPop` covers `ZMPOP`. | +| `BZPOPMAX` | 5.0.0 | `policy` | Blocking; `SortedSetPop` covers `ZPOPMAX`. | +| `BZPOPMIN` | 5.0.0 | `policy` | Blocking; `SortedSetPop` covers `ZPOPMIN`. | + +## Not applicable: omitted on purpose + +| Command | Since | Status | Notes | +|---|---|---|---| +| `HTTL` | 7.4.0 | `not-applicable` | Second-precision twin of `HPTTL`; the library always reads `HPTTL`. | +| `STTL` | 8.12.0 | `not-applicable` | Second-precision twin of `SPTTL`, as `HTTL`. | +| `RESET` | 6.2.0 | `not-applicable` | Discards the per-connection state the multiplexer negotiated (`HELLO`, `CLIENT SETNAME`, `SELECT`, subscriptions). Dispose and reconnect instead. | +| `CLIENT REPLY` | 3.2.0 | `not-applicable` | Suppressing replies breaks the request/response pairing the multiplexer relies on; `CommandFlags.FireAndForget` is the client-side equivalent. | +| `CLIENT UNBLOCK` | 5.0.0 | `not-applicable` | Unblocks a client parked in a blocking command, which this library does not issue. | +| `CLIENT GETNAME` | 2.6.9 | `not-applicable` | The library sets the name itself (`ConfigurationOptions.ClientName`) and knows it; `ClientList` shows every connection's name. | +| `CLIENT INFO` | 6.2.0 | `not-applicable` | The current connection's own `CLIENT LIST` row; connections are an internal detail of the multiplexer, and `ClientList` covers the server-wide view. | +| `HIMPORT DISCARDALL` | 8.10.0 | `not-applicable` | Drops every field-set on the connection. The library tracks field-sets individually and discards each on dispose; a blanket discard on a shared connection would invalidate other callers' live field-sets. See [HImport](HImport). | +| `SCRIPT DEBUG` | 3.2.0 | `not-applicable` | Interactive Lua debugger session for `redis-cli`; needs a dedicated connection and a REPL. | +| `CLIENT CACHING` | 6.0.0 | `not-applicable` | Only meaningful with `CLIENT TRACKING` in `OPTIN`/`OPTOUT` mode; the v4 cache owns that decision per connection. | +| `CLIENT GETREDIR` | 6.0.0 | `not-applicable` | Introspection of `CLIENT TRACKING` redirection; the v4 cache knows its own redirection. | +| `LOLWUT` | 5.0.0 | `not-applicable` | Art. `Execute("LOLWUT")` works. | +| `LATENCY GRAPH` | 2.8.13 | `not-applicable` | ASCII-art output for a console; `LATENCY HISTORY`/`LATEST` are exposed. | +| `XIDMPRECORD` | 8.6.2 | `not-applicable` | Internal: sets idempotent-producer metadata on an existing entry; `XADD` with IDMP options is the client path. | +| `PSYNC` | 2.8.0 | `not-applicable` | Replication protocol between servers. | +| `XSETID` | 5.0.0 | `not-applicable` | Internal: upstream describes it as a replication command for stream state. | +| `FAILOVER` | 6.2.0 | `not-applicable` | Operator-driven coordinated failover; `IServer.MakePrimaryAsync` covers the client-side story. | +| `CLIENT PAUSE` | 3.0.0 | `not-applicable` | Suspends every client on the server, including this one. Operator tooling for coordinated failover; far too easy to misuse from application code. | +| `CLIENT UNPAUSE` | 6.2.0 | `not-applicable` | As `CLIENT PAUSE`. | +| `SFLUSH` | 8.0.0 | `not-applicable` | Flush a slot range (experimental); cluster slot administration. | +| `TRIMSLOTS` | 8.4.0 | `not-applicable` | Deletes keys left in slots a node no longer owns after atomic slot migration; cluster slot administration. | + +## Not applicable: server and cluster administration + +Operator tooling. All of it works through `Execute` on an `IServer`; none of it is on the typed surface. + +| Command | Since | Status | Notes | +|---|---|---|---| +| `CLUSTER ADDSLOTS` | 3.0.0 | `not-applicable` | Slot administration. | +| `CLUSTER ADDSLOTSRANGE` | 7.0.0 | `not-applicable` | Slot administration. | +| `CLUSTER BUMPEPOCH` | 3.0.0 | `not-applicable` | Cluster administration. | +| `CLUSTER COUNT-FAILURE-REPORTS` | 3.0.0 | `not-applicable` | Cluster administration. | +| `CLUSTER DELSLOTS` | 3.0.0 | `not-applicable` | Slot administration. | +| `CLUSTER DELSLOTSRANGE` | 7.0.0 | `not-applicable` | Slot administration. | +| `CLUSTER FLUSHSLOTS` | 3.0.0 | `not-applicable` | Slot administration. | +| `CLUSTER FORGET` | 3.0.0 | `not-applicable` | Node membership. | +| `CLUSTER MEET` | 3.0.0 | `not-applicable` | Node membership. | +| `CLUSTER MIGRATION` | 8.4.0 | `not-applicable` | Atomic slot migration control. | +| `CLUSTER REPLICATE` | 3.0.0 | `not-applicable` | Cluster administration. | +| `CLUSTER SAVECONFIG` | 3.0.0 | `not-applicable` | Cluster administration. | +| `CLUSTER SET-CONFIG-EPOCH` | 3.0.0 | `not-applicable` | Cluster administration. | +| `CLUSTER SETSLOT` | 3.0.0 | `not-applicable` | Slot administration. | +| `CLUSTER SYNCSLOTS` | 8.4.0 | `not-applicable` | Internal to atomic slot migration between nodes. | +| `CLUSTER MYID` | 3.0.0 | `not-applicable` | Node identity; `ClusterConfiguration` carries node ids from `CLUSTER NODES`. | +| `CLUSTER MYSHARDID` | 7.2.0 | `not-applicable` | As `CLUSTER MYID`. | +| `CLUSTER KEYSLOT` | 3.0.0 | `not-applicable` | The client computes slots locally (`HashSlot`). | +| `CLUSTER COUNTKEYSINSLOT` | 3.0.0 | `not-applicable` | Slot administration (resharding tooling). | +| `CLUSTER GETKEYSINSLOT` | 3.0.0 | `not-applicable` | Slot administration (resharding tooling). | +| `SENTINEL CKQUORUM` | 2.8.4 | `not-applicable` | Sentinel administration; the client consumes sentinel via `GET-MASTER-ADDR-BY-NAME`, `SENTINELS`, `REPLICAS`, `MASTERS`. | +| `SENTINEL FLUSHCONFIG` | 2.8.4 | `not-applicable` | Sentinel administration. | +| `SENTINEL INFO-CACHE` | 3.2.0 | `not-applicable` | Sentinel internals. | +| `SENTINEL IS-MASTER-DOWN-BY-ADDR` | 2.8.4 | `not-applicable` | Sentinel-to-sentinel protocol. | +| `SENTINEL MYID` | 6.2.0 | `not-applicable` | Sentinel administration. | +| `SENTINEL PENDING-SCRIPTS` | 2.8.4 | `not-applicable` | Sentinel administration. | +| `SENTINEL SIMULATE-FAILURE` | 3.2.0 | `not-applicable` | Sentinel testing hook. | +| `SENTINEL CONFIG` | 6.2.0 | `not-applicable` | Sentinel administration. | +| `SENTINEL DEBUG` | 7.0.0 | `not-applicable` | Sentinel administration. | +| `SENTINEL MONITOR` | 2.8.4 | `not-applicable` | Sentinel administration (start monitoring a primary). | +| `SENTINEL REMOVE` | 2.8.4 | `not-applicable` | Sentinel administration (stop monitoring a primary). | +| `MODULE` | 4.0.0 | `not-applicable` | Module administration. | +| `MODULE LIST` | 4.0.0 | `not-applicable` | Module administration. | +| `MODULE LOAD` | 4.0.0 | `not-applicable` | Module administration. | +| `MODULE LOADEX` | 7.0.0 | `not-applicable` | Module administration. | +| `MODULE UNLOAD` | 4.0.0 | `not-applicable` | Module administration. | +| `BACKUP` | 8.10.0 | `not-applicable` | Server backup administration. | +| `BACKUP ABORT` | 8.10.0 | `not-applicable` | Server backup administration. | +| `BACKUP CLEANUP` | 8.10.0 | `not-applicable` | Server backup administration. | +| `BACKUP LIST` | 8.10.0 | `not-applicable` | Server backup administration. | +| `BACKUP SEAL` | 8.10.0 | `not-applicable` | Server backup administration. | +| `BACKUP START` | 8.10.0 | `not-applicable` | Server backup administration. | +| `BACKUP STATUS` | 8.10.0 | `not-applicable` | Server backup administration. | +| `COMMAND DOCS` | 7.0.0 | `not-applicable` | Command introspection for tooling; `COMMAND COUNT`, `COMMAND LIST` and `COMMAND GETKEYS` are exposed on `IServer`. | +| `COMMAND INFO` | 2.8.13 | `not-applicable` | As `COMMAND DOCS`. | +| `COMMAND GETKEYSANDFLAGS` | 7.0.0 | `not-applicable` | As `COMMAND DOCS`. | + +## Deferred: wanted, design first + +| Command | Since | Status | Notes | +|---|---|---|---| +| `FUNCTION` | 7.0.0 | `deferred` | Redis Functions as a whole need an API design (loading, library management, `FCALL` keys/args, `ScriptEvaluate` parity). Tracked from #2055. | +| `FUNCTION DELETE` | 7.0.0 | `deferred` | Part of the Functions design. | +| `FUNCTION DUMP` | 7.0.0 | `deferred` | Part of the Functions design. | +| `FUNCTION FLUSH` | 7.0.0 | `deferred` | Part of the Functions design. | +| `FUNCTION KILL` | 7.0.0 | `deferred` | Part of the Functions design. | +| `FUNCTION LIST` | 7.0.0 | `deferred` | Part of the Functions design. | +| `FUNCTION LOAD` | 7.0.0 | `deferred` | Part of the Functions design. | +| `FUNCTION RESTORE` | 7.0.0 | `deferred` | Part of the Functions design. | +| `FUNCTION STATS` | 7.0.0 | `deferred` | Part of the Functions design. | +| `FCALL` | 7.0.0 | `deferred` | Part of the Functions design. | +| `FCALL_RO` | 7.0.0 | `deferred` | Part of the Functions design. | +| `ACL` | 6.0.0 | `deferred` | ACL administration needs a typed model for users, rules and the log; a partial surface is worse than none. Tracked from #2055. | +| `ACL CAT` | 6.0.0 | `deferred` | Part of the ACL design. | +| `ACL DELUSER` | 6.0.0 | `deferred` | Part of the ACL design. | +| `ACL DRYRUN` | 7.0.0 | `deferred` | Part of the ACL design. | +| `ACL GENPASS` | 6.0.0 | `deferred` | Part of the ACL design. | +| `ACL GETUSER` | 6.0.0 | `deferred` | Part of the ACL design. | +| `ACL LIST` | 6.0.0 | `deferred` | Part of the ACL design. | +| `ACL LOAD` | 6.0.0 | `deferred` | Part of the ACL design. | +| `ACL LOG` | 6.0.0 | `deferred` | Part of the ACL design. | +| `ACL SAVE` | 6.0.0 | `deferred` | Part of the ACL design. | +| `ACL SETUSER` | 6.0.0 | `deferred` | Part of the ACL design. | +| `ACL USERS` | 6.0.0 | `deferred` | Part of the ACL design. | +| `ACL WHOAMI` | 6.0.0 | `deferred` | Part of the ACL design. | +| `CLIENT TRACKING` | 6.0.0 | `deferred` | Client-side caching is a connection-level feature (invalidation pushes routed by the multiplexer), not a command. Implemented in v4, where the handshake issues `CLIENT TRACKING`; not planned for v3. | +| `CLIENT TRACKINGINFO` | 6.2.0 | `deferred` | Part of client-side caching (v4). | +| `CLIENT NO-EVICT` | 7.0.0 | `deferred` | Per-connection flag; belongs in `ConfigurationOptions` and the handshake, not on `IServer`. | +| `CLIENT NO-TOUCH` | 7.2.0 | `deferred` | Per-connection flag, as `NO-EVICT`. | +| `CLUSTER SHARDS` | 7.0.0 | `deferred` | Not used anywhere today: `ClusterConfiguration` is built from `CLUSTER NODES`, and configuration export captures `NODES` and `SLOTS`. Moving topology discovery to `SHARDS`, or exposing it typed, is a design question. | +| `CLUSTER LINKS` | 7.0.0 | `deferred` | Topology introspection. | +| `CLUSTER SLOT-STATS` | 8.2.0 | `deferred` | Per-slot statistics; pairs with hot-key tooling. | + +## Todo: accepted gaps + +PRs welcome; follow the `implement-resp-command` skill. Pre-release server features go behind the matching `Experiments.Server_x_y` gate. + +| Command | Since | Status | Notes | +|---|---|---|---| +| `BLESS` | 8.12.0 | `todo` | Key blessing (eviction protection). #3261, in progress on `marc/bless`. | +| `BLESS SET` | 8.12.0 | `todo` | As `BLESS`. | +| `BLESS GET` | 8.12.0 | `todo` | As `BLESS`. | +| `BLESS CLEAR` | 8.12.0 | `todo` | As `BLESS`. | +| `BLESS SCAN` | 8.12.0 | `todo` | As `BLESS`. | +| `SADDEX` | 8.12.0 | `todo` | Set member expiry; mirror the hash-field expiry surface (`HashFieldExpire` et al). | +| `SEXPIRE` | 8.12.0 | `todo` | Set member expiry. | +| `SEXPIREAT` | 8.12.0 | `todo` | Set member expiry. | +| `SEXPIRETIME` | 8.12.0 | `todo` | Set member expiry (mirrors `HEXPIRETIME`). | +| `SPERSIST` | 8.12.0 | `todo` | Set member expiry. | +| `SPEXPIRE` | 8.12.0 | `todo` | Set member expiry. | +| `SPEXPIREAT` | 8.12.0 | `todo` | Set member expiry. | +| `SPEXPIRETIME` | 8.12.0 | `todo` | Set member expiry. | +| `SPTTL` | 8.12.0 | `todo` | Set member expiry; the read path (see `STTL` above). | +| `BITRANGEINTARRAY` | 8.12.0 | `todo` | Bitmap to integer array; sits with the existing `BITFIELD`/`BITOP` surface. | +| `MEMORY USAGE` | 4.0.0 | `todo` | Per-key memory; `MEMORY DOCTOR`/`STATS`/`PURGE` are exposed, this one is the useful one for profiling. | +| `CLUSTER INFO` | 3.0.0 | `todo` | Cluster state summary (`cluster_state`, slot counts, epoch) on `IServer`, next to `ClusterNodes`. | +| `LATENCY HISTOGRAM` | 7.0.0 | `todo` | Per-command latency histograms on `IServer`, next to `LATENCY LATEST`/`HISTORY`. | +| `PUBSUB SHARDCHANNELS` | 7.0.0 | `todo` | Sharded pub/sub is implemented; the introspection half is not (`IServer.SubscriptionChannels` covers the global form). | +| `PUBSUB SHARDNUMSUB` | 7.0.0 | `todo` | As `SHARDCHANNELS` (`IServer.SubscriptionSubscriberCount` covers the global form). | +| `XGROUP CREATECONSUMER` | 6.2.0 | `todo` | Explicit consumer creation; `XGROUP CREATE`/`DESTROY`/`DELCONSUMER`/`SETID` are exposed. | + +## Deprecated upstream + +Listed for reference; the rule excludes them without an entry. + +| Command | Since | Status | Notes | +|---|---|---|---| +| `SUBSTR` | 1.0.0 | `deprecated` | Deprecated 2.0.0; `StringGetRange` (`GETRANGE`). | +| `GEORADIUS_RO` | 3.2.10 | `deprecated` | Deprecated 6.2.0; `GeoSearch` (`GEOSEARCH BYRADIUS`). | +| `GEORADIUSBYMEMBER_RO` | 3.2.10 | `deprecated` | Deprecated 6.2.0; `GeoSearch` (`GEOSEARCH FROMMEMBER BYRADIUS`). | + +## Implemented, invisible to the audit script + +Subcommands written by a hand-rolled `Message` rather than a `RedisLiterals` token, which the coverage script cannot detect. Listed so the script does not report them; delete a row if the implementation moves to a literal. + +| Command | Since | Status | Notes | +|---|---|---|---| +| `HOTKEYS START` | 8.6.0 | `implemented` | `HotKeys.StartMessage` writes the token raw. See [Hot keys](HotKeys). | +| `SCRIPT LOAD` | 2.6.0 | `implemented` | `RedisDatabase.ScriptLoadMessage`; `IServer.ScriptLoad`. | + +## In the enum, not in the spec + +Commands the library implements that `redis/redis` does not specify, because they ship as a module with their own spec. + +| Command | Since | Status | Notes | +|---|---|---|---| +| `VADD` | 8.0.0 | `enum-only` | Vector sets (`redis/redis-vector-sets`); see [Vector sets](VectorSets). | +| `VCARD` | 8.0.0 | `enum-only` | Vector sets. | +| `VDIM` | 8.0.0 | `enum-only` | Vector sets. | +| `VEMB` | 8.0.0 | `enum-only` | Vector sets. | +| `VGETATTR` | 8.0.0 | `enum-only` | Vector sets. | +| `VINFO` | 8.0.0 | `enum-only` | Vector sets. | +| `VISMEMBER` | 8.0.0 | `enum-only` | Vector sets. | +| `VLINKS` | 8.0.0 | `enum-only` | Vector sets. | +| `VRANDMEMBER` | 8.0.0 | `enum-only` | Vector sets. | +| `VRANGE` | 8.0.0 | `enum-only` | Vector sets. | +| `VREM` | 8.0.0 | `enum-only` | Vector sets. | +| `VSETATTR` | 8.0.0 | `enum-only` | Vector sets. | +| `VSIM` | 8.0.0 | `enum-only` | Vector sets. | diff --git a/docs/index.md b/docs/index.md index 88553bb00..a65e482b9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -55,6 +55,7 @@ Documentation - [Profiling](Profiling) - profiling interfaces, as well as how to profile in an `async` world - [Scripting](Scripting) - running Lua scripts, including the low-allocation `ScriptEvalLease` API - [Ad-hoc commands](Execute) - running commands without a dedicated API, including the low-allocation `ExecLease` API +- [Command coverage](CommandCoverage) - which server commands the library does not expose, and why - [Testing](Testing) - running the `StackExchange.Redis.Tests` suite to validate changes - [Timeouts](Timeouts) - guidance on dealing with timeout problems - [Thread Theft](ThreadTheft) - guidance on avoiding TPL threading problems