feat(commands): introduce subcommand as a primary abstraction#3413
feat(commands): introduce subcommand as a primary abstraction#3413jihuayu wants to merge 8 commits intoapache:unstablefrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces “subcommand” as a first-class command abstraction by adding subcommand registration/resolution to the command table, updating command lookup to resolve root+subcommand attributes, and refactoring NAMESPACE into separate subcommand implementations.
Changes:
- Added subcommand registration and resolution via
CommandTable::Resolve, plus supporting metadata structures. - Updated command creation paths (connection execution + Lua scripting) to resolve commands using full token vectors (enabling subcommand-aware dispatch and renamed-root support).
- Refactored
NAMESPACEinto a root command that errors on invalid subcommands plus dedicatedget/set/add/del/currentsubcommands, and added tests for subcommand resolution + legacy error behavior.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/gocase/unit/namespace/namespace_test.go | Adds regression coverage for legacy invalid-subcommand error messaging for NAMESPACE. |
| tests/gocase/unit/command/command_test.go | Adds coverage ensuring COMMAND GETKEYS works when the target root command is renamed. |
| tests/cppunit/subcommand_resolution_test.cc | Adds unit tests validating subcommand resolution, arity handling, key-range extraction, and renamed-root behavior. |
| src/storage/scripting.cc | Updates Lua command invocation to resolve commands using full tokens and use canonical root name for execution checks. |
| src/server/server.h | Changes command lookup API to accept full token vectors. |
| src/server/server.cc | Implements token-based command resolution/creation, setting canonical root name on the Commander instance. |
| src/server/redis_connection.cc | Updates connection execution path to resolve commands using full tokens and use canonical root name for downstream logic. |
| src/commands/commander.h | Introduces ResolvedCommand, subcommand maps, MakeSubCmdAttr, and the Resolve API; extends Commander with a stored root name. |
| src/commands/commander.cc | Implements subcommand registration, lookup, and CommandTable::Resolve; updates key extraction to use Commander parsing with set args/attrs. |
| src/commands/cmd_server.cc | Refactors NAMESPACE into root+subcommand implementations and registers `namespace |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| StatusOr<ResolvedCommand> CommandTable::Resolve(const std::vector<std::string> &cmd_tokens) { | ||
| if (cmd_tokens.empty()) { | ||
| return {Status::RedisUnknownCmd}; | ||
| } | ||
|
|
There was a problem hiding this comment.
COMMAND metadata APIs currently only consult commands/original_commands (root commands). With the new subcommand table (sub_commands/redis_subcommand_table) and Resolve returning subcommand attributes like namespace|add, subcommands won’t be discoverable via COMMAND, and COMMAND INFO namespace|add will return nil. Consider extending GetAllCommandsInfo / GetCommandsInfo / Size (and any other command-lookup metadata paths) to include registered subcommands as separate entries using the parent|sub name so clients can introspect them.
There was a problem hiding this comment.
To avoid making this PR too large, I’ll handle this in a follow-up PR.
| MakeSubCmdAttr<CommandNamespaceSet>("namespace", "set", 4, "read-only admin skip-monitor", NO_KEY), | ||
| MakeSubCmdAttr<CommandNamespaceAdd>("namespace", "add", 4, "read-only admin skip-monitor", NO_KEY), | ||
| MakeSubCmdAttr<CommandNamespaceDel>("namespace", "del", 3, "read-only admin skip-monitor", NO_KEY), |
There was a problem hiding this comment.
These subcommands call Namespace::Set/Add/Del which can rewrite config and/or write to the Propagate CF, so they are not actually "read-only". Leaving them tagged as read-only means they’ll bypass Lua no-writes enforcement and other write-related safety checks. Update the flag strings for SET/ADD/DEL to include write (and remove read-only) so they’re treated as mutating operations.
| MakeSubCmdAttr<CommandNamespaceSet>("namespace", "set", 4, "read-only admin skip-monitor", NO_KEY), | |
| MakeSubCmdAttr<CommandNamespaceAdd>("namespace", "add", 4, "read-only admin skip-monitor", NO_KEY), | |
| MakeSubCmdAttr<CommandNamespaceDel>("namespace", "del", 3, "read-only admin skip-monitor", NO_KEY), | |
| MakeSubCmdAttr<CommandNamespaceSet>("namespace", "set", 4, "write admin skip-monitor", NO_KEY), | |
| MakeSubCmdAttr<CommandNamespaceAdd>("namespace", "add", 4, "write admin skip-monitor", NO_KEY), | |
| MakeSubCmdAttr<CommandNamespaceDel>("namespace", "del", 3, "write admin skip-monitor", NO_KEY), |
There was a problem hiding this comment.
@PragmaTwice I have the same question. However, the existing namespace command is read-only. Do we need to modify it?
| // renamed root command GETKEYS | ||
| func TestCommandGetKeysWithRenamedCommand(t *testing.T) { | ||
| srv := util.StartServer(t, map[string]string{ | ||
| "rename-command MGET": "RENAMED_MGET", | ||
| }) |
There was a problem hiding this comment.
The comment says "renamed root command GETKEYS", but this test is renaming the root command MGET and then calling the COMMAND GETKEYS subcommand. Consider updating the comment/test name to avoid confusion about what is being renamed.
|
Hi @git-hulk. I noticed that all Redis CLI-related test cases are failing on the Ubuntu platform, even though they pass just fine on my local Ubuntu machine. Any ideas on what might be causing this? |
It seems like a flaky test in Ubuntu 22.04, but I also cannot tell without diving deep. |
| std::cout << fmt::format("Duplicate command registration for '{}'", attr.name) << std::endl; | ||
| std::abort(); |
There was a problem hiding this comment.
"Are std::cout and std::abort used for debugging?"
There was a problem hiding this comment.
This serves as a reminder for developers to avoid this error, as it can lead to unpredictable behavior.
In standard execution flow, this code should never be reached.
There was a problem hiding this comment.
"Typically, for such cases, we should use FATAL logging and assert_debug for safeguards, rather than std::cout and std::abort. Here are two main reasons:
- For a server service, we don't print std::cout to the terminal, so using std::cout vs. a logging library doesn't make a fundamental difference. Even if we want terminal output, I
think error messages should go to std::cerr rather than std::cout. (PS: I believe the best practice is to log at FATAL level.) - Although this is code that should never be reached, at the server level, nothing is worse than a crash. If this path is somehow triggered in production (meaning kvrocks has a bug),
the application shouldn't crash. A better approach would be to use assert_debug, which only crashes in debug mode. In release mode, the impact should be minimized.
What do you think?"
There was a problem hiding this comment.
"However, it looks like this code path will only be reached during initialization. Both approaches seem acceptable."
| } | ||
| if (IsNamespaceCommandDisabled(srv)) { | ||
| return {Status::RedisExecErr, "namespace command is not allowed when redis-databases > 0"}; | ||
| } |
There was a problem hiding this comment.
"Both IsNamespaceReadOnlyOnSlave and IsNamespaceCommandDisabled checks seem to be needed by all subcommands. Could we consider abstracting them into a common function?"
There was a problem hiding this comment.
Namespace GET not need IsNamespaceReadOnlyOnSlave
| } | ||
|
|
||
| auto normalized_parent = util::ToLower(name.substr(0, delimiter)); | ||
| auto normalized_sub = util::ToLower(name.substr(delimiter + 1)); |
There was a problem hiding this comment.
The check here looks redundant. If delimiter + 1 >= name.size() is true, then name.substr(delimiter + 1) must be empty.
|



This PR introduces subcommand as a first-class abstraction.
Key changes include:
AI Disclosure