Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/skills/add-garnet-command/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,8 @@ new("DEBUG", RespCommand.DEBUG),

Needed for commands that don't exist in standard Redis (e.g., `DELIFGREATER`, `SETIFMATCH`), or standard Redis commands whose info you need to override. Standard Redis commands (e.g., `DEBUG`, `GETDEL`) normally get their metadata from a running RESP server automatically via the CommandInfoUpdater tool — skip this step and Step 8c for those unless you need to override their info.

> **Overriding a command that also exists on the baseline server** (e.g., `OBJECT`): the tool merges **per sub-command, by name** — your `GarnetCommandsInfo.json` / `GarnetCommandsDocs.json` entry wins and the baseline server fills in any sub-commands you did not specify. The override replaces the command's base fields wholesale (it is *not* a field-by-field merge), so include the parent command plus the sub-commands you want to control. This is how `OBJECT` reports Garnet-accurate sub-command summaries while the tool still generates the rest.

Add a JSON entry:

```json
Expand Down Expand Up @@ -539,13 +541,17 @@ Do NOT invent new group names — the JSON deserializer will fail.
dotnet run -f net10.0 --no-build -- --port 6399 --output ../../libs/resources
```
(The `--port` must match the port of the local RESP server.)
3. The tool will prompt `Would you like to continue? (Y/N)` **twice** (once for info, once for docs). Press `Y` for both.
3. The tool will prompt `Would you like to continue? (Y/N)` **twice** (once for info, once for docs). Press `Y` for both, or pass `--yes` (see below) to auto-confirm.
4. Kill the local RESP server afterward.

**⚠️ Caveat:** The tool uses `Console.ReadKey()` which does NOT work with piped input. You must run it interactively (not via `echo "Y" | dotnet run ...`). For AI agents, use an async shell session and send `Y` keystrokes via interactive input (e.g., `write_bash`).
**⚠️ Caveat:** By default the tool prompts via `Console.ReadKey()`, which does NOT work with piped input (`echo "Y" | dotnet run ...` fails). For non-interactive / scripted runs (including AI agents), pass **`--yes`** to auto-confirm both prompts; otherwise run it in a real interactive terminal.

**⚠️ Caveat:** The tool requires a running RESP-compatible server (e.g., Valkey or Redis — **not** Garnet) to query standard command metadata. For Garnet-only commands, the tool reads from `GarnetCommandsInfo.json` and `GarnetCommandsDocs.json` instead.

**⚠️ Caveat — unexpected "commands to remove":** If the tool reports commands *to remove* that you did not intend to delete, those commands exist in the resource files but are missing from `SupportedCommand.cs`. Register them in `SupportedCommand.cs` (and, for Garnet-only commands, in the override JSONs) rather than reaching for the `--ignore` flag. Every command Garnet ships should be registered, so a normal run needs **no `--ignore`**.

> **See also:** [`playground/CommandInfoUpdater/README.md`](../../../playground/CommandInfoUpdater/README.md) documents the tool in full — the inputs you edit, the per-sub-command override/merge behavior, the baseline server (Docker), the `--ignore` escape hatch, and how to surgically regenerate a single command.

---

## Step 9: Add ACL Test
Expand Down
29 changes: 29 additions & 0 deletions libs/common/EnumUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@

namespace Garnet.common
{
/// <summary>
/// Specifies an additional description string that maps to an enum value when parsing from a
/// description (e.g. a wire-protocol alias used by a different or newer RESP server version).
/// Aliases affect parsing only; the primary <see cref="DescriptionAttribute"/> is still used
/// when converting an enum value back to its description.
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
public sealed class EnumDescriptionAliasAttribute : Attribute
{
/// <summary>
/// The alias description string.
/// </summary>
public string Alias { get; }

/// <summary>
/// Creates a new <see cref="EnumDescriptionAliasAttribute"/>.
/// </summary>
/// <param name="alias">The alias description string.</param>
public EnumDescriptionAliasAttribute(string alias) => Alias = alias;
}

/// <summary>
/// Utilities for enums
/// </summary>
Expand Down Expand Up @@ -104,6 +125,14 @@ private static void AddTypeToCache<T>()
descToVals.Add(descAttr.Description, new List<string>());
descToVals[descAttr.Description].Add(flagFieldInfo.Name);
}

// Register any parse-only aliases (e.g. wire names used by other RESP server versions)
foreach (var aliasAttr in flagFieldInfo.GetCustomAttributes(typeof(EnumDescriptionAliasAttribute), false).Cast<EnumDescriptionAliasAttribute>())
{
if (!descToVals.ContainsKey(aliasAttr.Alias))
descToVals.Add(aliasAttr.Alias, new List<string>());
descToVals[aliasAttr.Alias].Add(flagFieldInfo.Name);
}
}

EnumNameToDescriptionCache.Add(typeof(T), valToDesc);
Expand Down
210 changes: 145 additions & 65 deletions libs/resources/RespCommandsDocs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1469,6 +1469,13 @@
}
]
},
{
"Command": "CLUSTER_MLOG_KEY_TIME",
"Name": "CLUSTER|MLOG_KEY_TIME",
"Summary": "Returns sequence number for provided key.",
"Group": "Cluster",
"Complexity": "O(1)"
},
{
"Command": "CLUSTER_MYID",
"Name": "CLUSTER|MYID",
Expand Down Expand Up @@ -1638,13 +1645,6 @@
"Summary": "Processes a forwarded published message from a node in the same shard",
"Group": "Cluster",
"Complexity": "O(1)"
},
{
"Command": "CLUSTER_MLOG_KEY_TIME",
"Name": "CLUSTER|MLOG_KEY_TIME",
"Summary": "Returns sequence number for provided key.",
"Group": "Cluster",
"Complexity": "O(1)"
}
]
},
Expand Down Expand Up @@ -5656,6 +5656,86 @@
"Group": "Transactions",
"Complexity": "O(1)"
},
{
"Command": "OBJECT",
"Name": "OBJECT",
"Summary": "A container for object introspection commands.",
"Group": "Generic",
"Complexity": "Depends on subcommand.",
"SubCommands": [
{
"Command": "OBJECT_ENCODING",
"Name": "OBJECT|ENCODING",
"Summary": "Returns the internal encoding of a Garnet object.",
"Group": "Generic",
"Complexity": "O(1)",
"Arguments": [
{
"TypeDiscriminator": "RespCommandKeyArgument",
"Name": "KEY",
"DisplayText": "key",
"Type": "Key",
"KeySpecIndex": 0
}
]
},
{
"Command": "OBJECT_FREQ",
"Name": "OBJECT|FREQ",
"Summary": "Not supported in Garnet: always returns an error, as access frequency (LFU) is not tracked.",
"Group": "Generic",
"Complexity": "O(1)",
"Arguments": [
{
"TypeDiscriminator": "RespCommandKeyArgument",
"Name": "KEY",
"DisplayText": "key",
"Type": "Key",
"KeySpecIndex": 0
}
]
},
{
"Command": "OBJECT_HELP",
"Name": "OBJECT|HELP",
"Summary": "Returns helpful text about the different subcommands.",
"Group": "Generic",
"Complexity": "O(1)"
},
{
"Command": "OBJECT_IDLETIME",
"Name": "OBJECT|IDLETIME",
"Summary": "Returns the idle time of a Garnet object; always 0, as Garnet does not track per-key idle time.",
"Group": "Generic",
"Complexity": "O(1)",
"Arguments": [
{
"TypeDiscriminator": "RespCommandKeyArgument",
"Name": "KEY",
"DisplayText": "key",
"Type": "Key",
"KeySpecIndex": 0
}
]
},
{
"Command": "OBJECT_REFCOUNT",
"Name": "OBJECT|REFCOUNT",
"Summary": "Returns the reference count of a Garnet object; always 1, as Garnet does not share value objects.",
"Group": "Generic",
"Complexity": "O(1)",
"Arguments": [
{
"TypeDiscriminator": "RespCommandKeyArgument",
"Name": "KEY",
"DisplayText": "key",
"Type": "Key",
"KeySpecIndex": 0
}
]
}
]
},
{
"Command": "PERSIST",
"Name": "PERSIST",
Expand Down Expand Up @@ -6376,6 +6456,22 @@
}
]
},
{
"Command": "RICONFIG",
"Name": "RI.CONFIG",
"Summary": "Returns the configuration of a RangeIndex as alternating field-value pairs.",
"Group": "Generic",
"Complexity": "O(1)",
"Arguments": [
{
"TypeDiscriminator": "RespCommandKeyArgument",
"Name": "KEY",
"DisplayText": "key",
"Type": "Key",
"KeySpecIndex": 0
}
]
},
{
"Command": "RICREATE",
"Name": "RI.CREATE",
Expand Down Expand Up @@ -6454,22 +6550,6 @@
}
]
},
{
"Command": "RICONFIG",
"Name": "RI.CONFIG",
"Summary": "Returns the configuration of a RangeIndex as alternating field-value pairs.",
"Group": "Generic",
"Complexity": "O(1)",
"Arguments": [
{
"TypeDiscriminator": "RespCommandKeyArgument",
"Name": "KEY",
"DisplayText": "key",
"Type": "Key",
"KeySpecIndex": 0
}
]
},
{
"Command": "RIDEL",
"Name": "RI.DEL",
Expand Down Expand Up @@ -7371,11 +7451,13 @@
]
},
{
"Command": "SETWITHETAG",
"Name": "SETWITHETAG",
"Summary": "Sets a key-value pair with an ETag. If the key already exists, the value is overwritten and the ETag is incremented. Returns the ETag.",
"Command": "SETNX",
"Name": "SETNX",
"Summary": "Set the string value of a key only when the key doesn't exist.",
"Group": "String",
"Complexity": "O(1)",
"DocFlags": "Deprecated",
"ReplacedBy": "`SET` with the `NX` argument",
"Arguments": [
{
"TypeDiscriminator": "RespCommandKeyArgument",
Expand All @@ -7389,39 +7471,15 @@
"Name": "VALUE",
"DisplayText": "value",
"Type": "String"
},
{
"TypeDiscriminator": "RespCommandContainerArgument",
"Name": "EXPIRATION",
"Type": "OneOf",
"ArgumentFlags": "Optional",
"Arguments": [
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "SECONDS",
"DisplayText": "seconds",
"Type": "Integer",
"Token": "EX"
},
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "MILLISECONDS",
"DisplayText": "milliseconds",
"Type": "Integer",
"Token": "PX"
}
]
}
]
},
{
"Command": "SETNX",
"Name": "SETNX",
"Summary": "Set the string value of a key only when the key doesn't exist.",
"Command": "SETRANGE",
"Name": "SETRANGE",
"Summary": "Overwrites a part of a string value with another by an offset. Creates the key if it doesn't exist.",
"Group": "String",
"Complexity": "O(1)",
"DocFlags": "Deprecated",
"ReplacedBy": "`SET` with the `NX` argument",
"Complexity": "O(1), not counting the time taken to copy the new string in place. Usually, this string is very small so the amortized complexity is O(1). Otherwise, complexity is O(M) with M being the length of the value argument.",
"Arguments": [
{
"TypeDiscriminator": "RespCommandKeyArgument",
Expand All @@ -7430,6 +7488,12 @@
"Type": "Key",
"KeySpecIndex": 0
},
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "OFFSET",
"DisplayText": "offset",
"Type": "Integer"
},
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "VALUE",
Expand All @@ -7439,11 +7503,11 @@
]
},
{
"Command": "SETRANGE",
"Name": "SETRANGE",
"Summary": "Overwrites a part of a string value with another by an offset. Creates the key if it doesn't exist.",
"Command": "SETWITHETAG",
"Name": "SETWITHETAG",
"Summary": "Sets a key-value pair with an ETag. If the key already exists, the value is overwritten and the ETag is incremented. Returns the ETag.",
"Group": "String",
"Complexity": "O(1), not counting the time taken to copy the new string in place. Usually, this string is very small so the amortized complexity is O(1). Otherwise, complexity is O(M) with M being the length of the value argument.",
"Complexity": "O(1)",
"Arguments": [
{
"TypeDiscriminator": "RespCommandKeyArgument",
Expand All @@ -7452,17 +7516,33 @@
"Type": "Key",
"KeySpecIndex": 0
},
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "OFFSET",
"DisplayText": "offset",
"Type": "Integer"
},
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "VALUE",
"DisplayText": "value",
"Type": "String"
},
{
"TypeDiscriminator": "RespCommandContainerArgument",
"Name": "EXPIRATION",
"Type": "OneOf",
"ArgumentFlags": "Optional",
"Arguments": [
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "SECONDS",
"DisplayText": "seconds",
"Type": "Integer",
"Token": "EX"
},
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "MILLISECONDS",
"DisplayText": "milliseconds",
"Type": "Integer",
"Token": "PX"
}
]
}
]
},
Expand Down Expand Up @@ -10214,4 +10294,4 @@
}
]
}
]
]
Loading
Loading