Skip to content

feat(commands): introduce subcommand as a primary abstraction#3413

Open
jihuayu wants to merge 8 commits intoapache:unstablefrom
jihuayu:subkey
Open

feat(commands): introduce subcommand as a primary abstraction#3413
jihuayu wants to merge 8 commits intoapache:unstablefrom
jihuayu:subkey

Conversation

@jihuayu
Copy link
Copy Markdown
Member

@jihuayu jihuayu commented Apr 1, 2026

This PR introduces subcommand as a first-class abstraction.

Key changes include:

  • Supported standardized registration and parsing processes for subcommands.
  • Implemented metadata management for subcommand structures.
  • Refactored namespace as a reference implementation for this new abstraction.

AI Disclosure

  • The architectural design of this PR was completed independently. Some code segments were developed with AI.
  • All test cases in this PR were either autonomously generated by AI or generated based on natural language descriptions provided by me.

@jihuayu jihuayu changed the title feat(commands): support registered subcommand resolution feat(commands): introduce subcommand as a primary abstraction Apr 2, 2026
@jihuayu jihuayu marked this pull request as ready for review April 2, 2026 07:11
@jihuayu jihuayu requested review from PragmaTwice, Copilot and git-hulk and removed request for PragmaTwice and Copilot April 2, 2026 07:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 NAMESPACE into a root command that errors on invalid subcommands plus dedicated get/set/add/del/current subcommands, 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.

Comment thread src/commands/commander.cc
Comment on lines +152 to +156
StatusOr<ResolvedCommand> CommandTable::Resolve(const std::vector<std::string> &cmd_tokens) {
if (cmd_tokens.empty()) {
return {Status::RedisUnknownCmd};
}

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid making this PR too large, I’ll handle this in a follow-up PR.

Comment on lines +1736 to +1738
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),
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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),

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PragmaTwice I have the same question. However, the existing namespace command is read-only. Do we need to modify it?

Comment on lines +437 to +441
// renamed root command GETKEYS
func TestCommandGetKeysWithRenamedCommand(t *testing.T) {
srv := util.StartServer(t, map[string]string{
"rename-command MGET": "RENAMED_MGET",
})
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@jihuayu
Copy link
Copy Markdown
Member Author

jihuayu commented Apr 3, 2026

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?

@git-hulk
Copy link
Copy Markdown
Member

git-hulk commented Apr 3, 2026

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.

Comment thread src/commands/commander.cc
Comment on lines +51 to +52
std::cout << fmt::format("Duplicate command registration for '{}'", attr.name) << std::endl;
std::abort();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Are std::cout and std::abort used for debugging?"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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:

  1. 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.)
  2. 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?"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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"};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Both IsNamespaceReadOnlyOnSlave and IsNamespaceCommandDisabled checks seem to be needed by all subcommands. Could we consider abstracting them into a common function?"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Namespace GET not need IsNamespaceReadOnlyOnSlave

Comment thread src/commands/commander.cc
}

auto normalized_parent = util::ToLower(name.substr(0, delimiter));
auto normalized_sub = util::ToLower(name.substr(delimiter + 1));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check here looks redundant. If delimiter + 1 >= name.size() is true, then name.substr(delimiter + 1) must be empty.

Comment thread src/commands/commander.cc
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 6, 2026

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.

4 participants