Skip to content

tools: add show region by keyspace-id and table-id#10519

Closed
bufferflies wants to merge 4 commits intotikv:masterfrom
bufferflies:pr-merge/c93399a1-pdctl-region-keyspace-table
Closed

tools: add show region by keyspace-id and table-id#10519
bufferflies wants to merge 4 commits intotikv:masterfrom
bufferflies:pr-merge/c93399a1-pdctl-region-keyspace-table

Conversation

@bufferflies
Copy link
Copy Markdown
Contributor

@bufferflies bufferflies commented Mar 31, 2026

What problem does this PR solve?

Issue Number: ref #10516, closed #10531

This PR ports the pd-ctl region lookup enhancement tracked in tikv/pd#10516 onto the current master branch so operators can query regions by keyspace ID and optional table ID.

Source change: bufferflies/pd@c93399a176f8ffc13580fe7ef35c43aa3830902e.

What is changed and how does it work?

add a pd-ctl region subcommand to query regions by keyspace id and
optional table id.

build the encoded region key range from the keyspace and table values
and send the request through the existing region key API.

adapt the encoded key construction to the current codec helper
signature on master.

Check List

Tests

  • Unit test

Release note

Support querying regions by keyspace ID and table ID in pd-ctl.

Summary by CodeRabbit

  • New Features
    • Added a region query subcommand that accepts keyspace-id with optional table-id and optional limit.
  • Improvements
    • Validates arguments more strictly with clearer error messages.
    • Builds precise start/end key ranges for keyspace- and table-scoped queries.
    • Supports JSON filtering (--jq) or raw response printing.
  • Tests
    • Added tests for keyspace/table ranges, limit handling, request URL construction, and invalid inputs.

@ti-chi-bot ti-chi-bot bot added release-note Denotes a PR that will be considered when it comes time to generate release notes. dco-signoff: yes Indicates the PR's author has signed the dco. labels Mar 31, 2026
@ti-chi-bot
Copy link
Copy Markdown
Contributor

ti-chi-bot bot commented Mar 31, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign lhy1024 for approval. For more information see the Code Review Process.
Please ensure that each of them provides their approval before proceeding.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@bufferflies
Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@ti-chi-bot ti-chi-bot bot added the size/M Denotes a PR that changes 30-99 lines, ignoring generated files. label Mar 31, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

📝 Walkthrough

Walkthrough

Adds a new regions keyspace-id [table-id] [limit] subcommand that validates args, builds encoded start/end keys (optionally scoped by table-id), issues a GET to /pd/api/v1/regions/key with key, end_key and optional limit, and prints the PD JSON response (supports --jq).

Changes

Cohort / File(s) Summary
Region Command
tools/pd-ctl/pdctl/command/region_command.go
Added NewRegionsByKeyspaceTableIDCommand() and showRegionsByKeyspaceTableIDCommandFunc; validate keyspace-id (0–2^24-1), optional table-id and limit; construct encoded key/end_key (with optional table prefix and codec.EncodeInt); send request to /pd/api/v1/regions/key; integrate --jq output; registered subcommand in NewRegionCommand().
Region Command Tests
tools/pd-ctl/pdctl/command/region_test.go
Added tests capturing outbound HTTP requests via captureRoundTripper; assert request path and key/end_key query values (using codec.EncodeBytes/codec.EncodeInt), limit handling, stdout/stderr outputs, and validation error messages; added mustQueryUnescape helper and temporary dialClient override.

Sequence Diagram(s)

sequenceDiagram
    participant User as "User (CLI)"
    participant Cmd as "pd-ctl: regions cmd"
    participant HTTP as "HTTP Client"
    participant PD as "PD Server\n(/pd/api/v1/regions/key)"
    User->>Cmd: invoke subcommand with args
    Cmd->>Cmd: validate args (keyspace-id, optional table-id, limit)
    Cmd->>Cmd: encode startKey / endKey (codec.EncodeBytes / EncodeInt)
    Cmd->>HTTP: GET /pd/api/v1/regions/key?key=...&end_key=...(&limit=...)
    HTTP->>PD: send HTTP request
    PD-->>HTTP: respond (JSON)
    HTTP-->>Cmd: return response
    Cmd->>User: print response (or pipe through --jq)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

lgtm, approved

Suggested reviewers

  • lhy1024
  • okJiang
  • rleungx

Poem

🐇 I hop on bytes and nibble keys so neat,
I bound the table trail with tiny feet,
I whisper limits, encode each start and end,
I bring back regions — a message I send,
Carrots for tests, a CLI treat! 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description includes the problem statement, what is changed, commit message, and release note, but is missing the full checklist attestations and does not explicitly list or check HTTP API changes. Clarify whether HTTP APIs were changed and ensure all applicable checklist items are explicitly addressed or marked as not applicable.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding a pd-ctl region subcommand to query regions by keyspace-id and table-id.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tools/pd-ctl/pdctl/command/region_command.go`:
- Around line 440-447: The code currently parses keyspaceID with
strconv.ParseInt and blindly splits it into 3 bytes (used to build
startKey/endKey), which allows negative or >24-bit values to be silently
truncated; change parsing/validation so keyspaceID is validated as an unsigned
24-bit value (0 <= keyspaceID <= 0xFFFFFF) before byte-splitting: either parse
with strconv.ParseUint or parse then check the int64 range, return a clear error
via cmd.Println if out of range, and only then construct startKey and endKey
using the validated keyspaceID variable (refer to the strconv.ParseInt call,
keyspaceID variable, and startKey/endKey construction).
- Around line 459-463: The endKey is incorrectly built by appending to startKey
(producing a key containing tableID then nextTableID), so change construction to
append to a fresh slice or to the original base prefix instead of startKey:
build endKey from the same base prefix used for startKey (e.g., use a new slice
or the base variable before codec.EncodeInt) and then call
codec.EncodeInt(endKey, nextTableID) so endKey becomes the correct prefix +
nextTableID; update the code paths around startKey, endKey, codec.EncodeInt,
tableID and nextTableID accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a7255fd5-89c5-4b45-b457-2ab2318bf701

📥 Commits

Reviewing files that changed from the base of the PR and between 0a0dcfd and 4ef4351.

📒 Files selected for processing (1)
  • tools/pd-ctl/pdctl/command/region_command.go

zeminzhou and others added 2 commits March 31, 2026 10:08
Signed-off-by: zeminzhou <zhouzemin@pingcap.com>
Signed-off-by: yongman <yming0221@gmail.com>
Co-authored-by: Ray Yan <yming0221@gmail.com>
(cherry picked from commit c93399a)
Signed-off-by: bufferflies <1045931706@qq.com>
@bufferflies
Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@bufferflies bufferflies force-pushed the pr-merge/c93399a1-pdctl-region-keyspace-table branch from 4ef4351 to c5405b3 Compare March 31, 2026 10:09
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

✅ Actions performed

Full review triggered.

Signed-off-by: bufferflies <1045931706@qq.com>
@bufferflies
Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

✅ Actions performed

Full review triggered.

@ti-chi-bot ti-chi-bot bot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. and removed size/M Denotes a PR that changes 30-99 lines, ignoring generated files. labels Mar 31, 2026
Signed-off-by: bufferflies <1045931706@qq.com>
@bufferflies
Copy link
Copy Markdown
Contributor Author

Addressed the latest CodeRabbit findings in 0f0d54c:

  • validate keyspace-id as unsigned 24-bit
  • build table-scope endKey from a fresh prefix
  • extend unit tests for both paths

Validation: cd tools && go test ./pd-ctl/pdctl/...

@coderabbitai full review
@PR-Review

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tools/pd-ctl/pdctl/command/region_test.go`:
- Around line 191-196: The test constructs endKey by appending to startKey which
masks a malformed range construction; change the test in region_test.go so
endKey is built from the intended prefix (e.g., construct the original prefix
bytes, encode the start int with codec.EncodeInt to form startKey, then
independently construct endKey by encoding the end int against that prefix
rather than appending to startKey) and assert against codec.EncodeBytes(...) and
mustQueryUnescape as before; also update showRegionsByKeyspaceTableIDCommandFunc
to build endKey from the prefix/namespace correctly (not by appending to the
computed startKey) so the command and test use the same correct construction
logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dc24ec5d-eb1e-4e50-8711-8d6d03dc8cad

📥 Commits

Reviewing files that changed from the base of the PR and between c5405b3 and dc00044.

📒 Files selected for processing (1)
  • tools/pd-ctl/pdctl/command/region_test.go

Comment on lines +191 to +196
startKey := []byte{'x', 0, 0, 17, 't'}
startKey = codec.EncodeInt(startKey, 45)
endKey := append(startKey, 't')
endKey = codec.EncodeInt(endKey, 46)
re.Equal(string(codec.EncodeBytes(nil, startKey)), mustQueryUnescape(t, requestURL.Query().Get("key")))
re.Equal(string(codec.EncodeBytes(nil, endKey)), mustQueryUnescape(t, requestURL.Query().Get("end_key")))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

endKey expectation is derived from startKey, which masks malformed range construction.

At Line 193, endKey := append(startKey, 't') bakes in the same bug pattern as the command implementation, so the test passes while validating an incorrect end_key shape.

🔧 Proposed test fix
 	startKey := []byte{'x', 0, 0, 17, 't'}
 	startKey = codec.EncodeInt(startKey, 45)
-	endKey := append(startKey, 't')
+	endKey := []byte{'x', 0, 0, 17, 't'}
 	endKey = codec.EncodeInt(endKey, 46)
 	re.Equal(string(codec.EncodeBytes(nil, startKey)), mustQueryUnescape(t, requestURL.Query().Get("key")))
 	re.Equal(string(codec.EncodeBytes(nil, endKey)), mustQueryUnescape(t, requestURL.Query().Get("end_key")))

Also update showRegionsByKeyspaceTableIDCommandFunc to construct endKey from the intended prefix instead of startKey, otherwise this test will still mirror the bug.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/pd-ctl/pdctl/command/region_test.go` around lines 191 - 196, The test
constructs endKey by appending to startKey which masks a malformed range
construction; change the test in region_test.go so endKey is built from the
intended prefix (e.g., construct the original prefix bytes, encode the start int
with codec.EncodeInt to form startKey, then independently construct endKey by
encoding the end int against that prefix rather than appending to startKey) and
assert against codec.EncodeBytes(...) and mustQueryUnescape as before; also
update showRegionsByKeyspaceTableIDCommandFunc to build endKey from the
prefix/namespace correctly (not by appending to the computed startKey) so the
command and test use the same correct construction logic.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the main implementation file
fd "region_command.go" --type f | xargs cat

echo "---SEPARATOR---"

# Read the test file
fd "region_command_test.go" --type f | xargs cat

echo "---SEPARATOR---"

# Look at the codec helpers being used
fd "codec" --type f -e go | head -20

echo "---SEPARATOR---"

# Look for EncodeInt and codec usage patterns
rg -n "EncodeInt|EncodeBytes|GetKeyspacePrefix\|codecpb\|codec\." --type go -A2 -B2 tools/pd-ctl/

echo "---SEPARATOR---"

# Check the codec package being imported
rg -n "\"github.com/tikv/pd/pkg/codec\"" --type go

echo "---SEPARATOR---"

# Look at what codec functions are available
fd "codec" --type f -e go -g "pkg/codec*" 
rg -n "^func " pkg/codec/

echo "---SEPARATOR---"

# Look at how other commands use the region key API
rg -n "regionsKeyPrefix\|regions/key" --type go -A3 -B3 tools/pd-ctl/

Length of output: 43593


🏁 Script executed:

#!/bin/bash
# Read the test file specifically for region_command_test.go
fd "region_command_test.go" --type f | xargs cat

echo "---SEPARATOR---"

# Check the codec EncodeBytes signature difference between pkg/codec and tools/pd-ctl/helper/tidb/codec
echo "=== pkg/codec/codec.go EncodeBytes ==="
rg -n "^func EncodeBytes" pkg/codec/codec.go

echo "=== tools pd-ctl helper codec EncodeBytes ==="
rg -n "^func EncodeBytes" tools/pd-ctl/helper/tidb/codec/

echo "---SEPARATOR---"

# Look at the keyspace prefix - is 'x' correct for txn keyspace?
echo "=== keyspace prefix bytes ==="
rg -n "keyspacePrefix\|txnPrefix\|rawPrefix\|'x'\|\"x\"\|0x78" --type go pkg/keyspace/ -A1 -B1

echo "---SEPARATOR---"
# Check if there's a keyspace meta or constant defining the prefix bytes
rg -n "const.*=.*'x'\|const.*txnKey\|TxnKeyspacePrefix\|RawKeyspacePrefix" --type go -A2 -B2

echo "---SEPARATOR---"
# Check how keyspace prefix is constructed (for 'r' raw vs 'x' txn) in the existing keyspace command
rg -n "keyspaceID\|'r'\|'x'\|0x78\|0x72" tools/pd-ctl/pdctl/command/keyspace_command.go -A2 -B2
rg -n "keyspaceID\|'r'\|'x'\|0x78\|0x72" tools/pd-ctl/pdctl/command/keyspace_command_test.go -A2 -B2

echo "---SEPARATOR---"
# Check the existing NewRegionWithKeyspaceCommand for how keyspace is handled
rg -n "keyspace" tools/pd-ctl/pdctl/command/region_command.go -A2 -B2

echo "---SEPARATOR---"
# What are the specific tests for the new keyspace-id subcommand?
rg -n "KeyspaceTableID\|keyspace-id\|keyspace_table" tools/pd-ctl/pdctl/command/region_command_test.go -A10 -B2

echo "---SEPARATOR---"
# Check for overflow edge case: keyspaceID = 16777215 (max 24-bit), nextKeyspaceID would be 16777216
# which is > 24 bits - is this handled?
echo "=== endKey overflow check ==="
rg -n "nextKeyspaceID\|keyspaceID \+ 1" tools/pd-ctl/pdctl/command/ -A2 -B2

echo "---SEPARATOR---"
# Check: does the 'x' prefix cover both txn write and txn read key types or just one?
rg -n "txn\|raw\|'x'\|'r'\|keyspace.*prefix\|prefix.*keyspace" pkg/keyspace/util.go -A2 -B2 | head -60

Length of output: 3038


🏁 Script executed:

#!/bin/bash
# Find region test files
fd "region.*test\|test.*region" --type f -e go

echo "---SEPARATOR---"

# Look for tests related to the new command in any test file
rg -rn "KeyspaceTableID\|keyspace-id\|showRegionsByKeyspace" --type go -l

echo "---SEPARATOR---"

# Find the test file containing the region command tests
rg -rn "NewRegionsByKeyspace\|keyspace-id\|table-id.*table_id" --type go -A5 -B5

echo "---SEPARATOR---"

# Check for overflow: when keyspaceID == 16777215 (max 24-bit = 0xFFFFFF), nextKeyspaceID = 16777216
# byte(16777216>>16) = byte(0x100) = 0x00, byte(16777216>>8) = byte(0x100 * 256) = ...
# Let's verify the exact bytes produced
python3 -c "
ks = 16777215
next_ks = ks + 1
print(f'keyspaceID = {ks} = {hex(ks)}')
print(f'nextKeyspaceID = {next_ks} = {hex(next_ks)}')
start = ['x', ks >> 16 & 0xFF, ks >> 8 & 0xFF, ks & 0xFF]
end = ['x', next_ks >> 16 & 0xFF, next_ks >> 8 & 0xFF, next_ks & 0xFF]
print(f'startKey prefix bytes: {[hex(b) if isinstance(b, int) else b for b in start]}')
print(f'endKey prefix bytes: {[hex(b) if isinstance(b, int) else b for b in end]}')
print()
print('endKey with overflow:')
print(f'  next_ks >> 16 = {hex(next_ks >> 16)} -> byte = {hex((next_ks >> 16) & 0xFF)}')
print(f'  next_ks >> 8 = {hex(next_ks >> 8)} -> byte = {hex((next_ks >> 8) & 0xFF)}')
print(f'  next_ks = {hex(next_ks)} -> byte = {hex(next_ks & 0xFF)}')
print()
print('In Go, byte() truncates to 8 bits, so:')
import struct
# Simulate Go byte() truncation
end_bytes = bytes([ord('x'), next_ks >> 16 & 0xFF, next_ks >> 8 & 0xFF, next_ks & 0xFF])
print(f'endKey bytes: {list(end_bytes)} = {end_bytes.hex()}')
print('This would be [x, 0, 0, 0] -- same as keyspace 0! That is a bug for max keyspace.')
"

echo "---SEPARATOR---"

# Check if there's a test for the overflow case
rg -rn "16777215\|0xFFFFFF\|maxKeyspace" --type go -A3 -B3

echo "---SEPARATOR---"

# Look at the args parsing logic for limit detection (2 or 4 args)
# args[1] != "table-id" guard: what if args[1] IS "table-id" but len(args) == 2? 
# (i.e., "keyspace-id 17 table-id" without a table ID value)
python3 -c "
# Simulate arg parsing:
# If user passes: keyspace-id 17 table-id (3 args without table value)
# len(args) = 3, so enters the if len(args) >= 3 branch
# args[1] == 'table-id', so passes guard
# args[2] == ... wait, args[2] would be the next token
# Actually user runs: region keyspace-id 17 table-id
# args = ['17', 'table-id'] only 2 args, won't reach >= 3
# But: region keyspace-id 17 table-id 45 -> args = ['17', 'table-id', '45'] - 3 args, correct
# What about: region keyspace-id 17 table-id 45 100 -> args = ['17', 'table-id', '45', '100'] - 4 args
# len(args) == 4 -> enters >= 3 branch, parses args[2]='45' as tableID
# Then len(args) == 4, checks for limit: args[len(args)-1] = args[3] = '100', valid
# What about: region keyspace-id 17 100 -> args = ['17', '100'] - 2 args
# len(args) >= 3 is false, so skips table-id logic
# Then checks len(args) == 2 or len(args) == 4: len==2 -> sets limit = args[1] = '100'
# What about: region keyspace-id 17 notanumber -> args = ['17', 'notanumber'] - 2 args
# len(args) == 2 -> tries to parse args[1] as limit -> fails with error
# So 'table-id' keyword as the 2nd arg without table value?
# args = ['17', 'table-id'] -> tries to parse 'table-id' as limit -> reports 'limit should be a number'
# This is confusing UX but not a bug per se.
print('Arg parsing analysis complete')
print()
print('Edge case: keyspace-id 17 table-id (missing table value)')
print('  -> args = [17, table-id] (2 args)')
print('  -> enters limit branch, tries Atoi(table-id) -> fails with limit error')
print('  -> confusing UX but not strictly wrong')
"

echo "---SEPARATOR---"

# Look for the args count validation: when args[1] == "table-id" and len(args) == 2
# The error says "limit should be a number" rather than "table-id value required"
# Let's check the actual conditional flow
cat tools/pd-ctl/pdctl/command/region_command.go | grep -A40 "func showRegionsByKeyspaceTableIDCommandFunc"

Length of output: 2413


@bufferflies Thanks for the updates — the 24-bit validation and fresh-prefix endKey fixes look correct. One remaining bug surfaced during analysis, plus a minor UX note.


🐛 Bug: endKey wraps to keyspace 0 when keyspace-id = 16777215

strconv.ParseUint(args[0], 10, 24) correctly caps keyspaceID at 0xFFFFFF (16777215). However, the very next line computes nextKeyspaceID = keyspaceID + 1 = 0x1000000, which is 25 bits wide. Go's byte() cast silently truncates the top bits:

# Verified in simulation
nextKeyspaceID = 0x1000000
byte(nextKeyspaceID >> 16) = byte(0x100) = 0x00
byte(nextKeyspaceID >>  8) = byte(0x10000) = 0x00
byte(nextKeyspaceID)       = byte(0x1000000) = 0x00
# → endKey = {'x', 0x00, 0x00, 0x00}  ← same as keyspace 0's startKey!

With endKey < startKey, the range is inverted and the regions API returns zero results for the maximum valid keyspace ID.

Suggested fix — special-case 0xFFFFFF to use the next TiDB key-space prefix ('y') as the open upper bound:

startKey := []byte{'x', byte(keyspaceID >> 16), byte(keyspaceID >> 8), byte(keyspaceID)}
var endKey []byte
if keyspaceID == 0xFFFFFF {
    // nextKeyspaceID would overflow a 3-byte field; use 'y' as the open upper bound.
    endKey = []byte{'y'}
} else {
    nextKeyspaceID := keyspaceID + 1
    endKey = []byte{'x', byte(nextKeyspaceID >> 16), byte(nextKeyspaceID >> 8), byte(nextKeyspaceID)}
}

A test to add alongside the existing ones:

func TestShowRegionsByKeyspaceTableIDCommand_MaxKeyspace(t *testing.T) {
    // keyspaceID = 0xFFFFFF: endKey must NOT wrap to keyspace 0
    // ...
    re.Equal(string(codec.EncodeBytes(nil, []byte{'y'})),
        mustQueryUnescape(t, requestURL.Query().Get("end_key")))
}

⚠️ Minor UX: misleading error when table-id value is omitted

If a user types region keyspace-id 17 table-id (forgetting the numeric value), args = ["17", "table-id"] (len 2). The code falls into the len(args) == 2 limit-parsing branch and reports "limit should be a number" instead of indicating the missing table-id value. This is low-priority but worth a targeted guard:

if len(args) == 2 && args[1] == "table-id" {
    cmd.Println("Error: table-id requires a numeric value")
    return
}

Everything else in the commit looks good: the 24-bit unsigned parse, the fresh-prefix tablePrefix slice for endKey, and the test coverage for both the keyspace-only and keyspace+table paths. The overflow at the maximum boundary is the only correctness issue remaining.


🧠 Learnings used
Learnt from: bufferflies
Repo: tikv/pd PR: 10330
File: pkg/keyspace/util.go:462-485
Timestamp: 2026-03-18T13:01:12.324Z
Learning: In tikv/pd, `GetKeyspaceSplitKeys` in `pkg/keyspace/util.go` is intentionally designed to return split keys for only one keyspace at a time (a single `checker.GetKeyspaceIDInRange` call). The SplitChecker is invoked repeatedly across scheduling cycles to incrementally resolve all cross-keyspace spans, so splitting all boundaries in a single call is not the goal.
✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tools/pd-ctl/pdctl/command/region_command.go`:
- Around line 446-447: The code computes nextKeyspaceID := keyspaceID + 1 and
builds endKey from its low 24 bits, which wraps for the maximum 24-bit value
(keyspaceID == 16777215); change the logic so that when keyspaceID == 0xFFFFFF
you treat the end key as the exclusive end of space (e.g., nil or a suffix that
sorts after any valid key) instead of splitting nextKeyspaceID into 3 bytes;
update the construction of endKey in the region scan code (the variables
nextKeyspaceID and endKey in region_command.go) to check for this boundary and
generate a proper end sentinel, and add a regression test in
tools/pd-ctl/pdctl/command/region_test.go that asserts scans for
keyspace-id=16777215 produce the correct non-wrapping range.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4be769f0-38b7-4ffc-91b4-512b684f7abc

📥 Commits

Reviewing files that changed from the base of the PR and between 0a0dcfd and 0f0d54c.

📒 Files selected for processing (2)
  • tools/pd-ctl/pdctl/command/region_command.go
  • tools/pd-ctl/pdctl/command/region_test.go

Comment on lines +446 to +447
nextKeyspaceID := keyspaceID + 1
endKey := []byte{'x', byte(nextKeyspaceID >> 16), byte(nextKeyspaceID >> 8), byte(nextKeyspaceID)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Handle the max keyspace-id boundary without 24-bit wraparound.

At Line 446 and Line 447, keyspace_id=16777215 (valid) makes nextKeyspaceID overflow the 24-bit keyspace byte range when split into 3 bytes, so endKey becomes smaller than startKey and the scan range is incorrect.

🔧 Proposed fix
 	startKey := []byte{'x', byte(keyspaceID >> 16), byte(keyspaceID >> 8), byte(keyspaceID)}
-	nextKeyspaceID := keyspaceID + 1
-	endKey := []byte{'x', byte(nextKeyspaceID >> 16), byte(nextKeyspaceID >> 8), byte(nextKeyspaceID)}
+	var endKey []byte
+	if keyspaceID == 0xFFFFFF {
+		// next prefix after all 'x'+24bit keyspace keys
+		endKey = []byte{'y'}
+	} else {
+		nextKeyspaceID := keyspaceID + 1
+		endKey = []byte{'x', byte(nextKeyspaceID >> 16), byte(nextKeyspaceID >> 8), byte(nextKeyspaceID)}
+	}

Please also add a regression test for keyspace-id=16777215 in tools/pd-ctl/pdctl/command/region_test.go.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/pd-ctl/pdctl/command/region_command.go` around lines 446 - 447, The
code computes nextKeyspaceID := keyspaceID + 1 and builds endKey from its low 24
bits, which wraps for the maximum 24-bit value (keyspaceID == 16777215); change
the logic so that when keyspaceID == 0xFFFFFF you treat the end key as the
exclusive end of space (e.g., nil or a suffix that sorts after any valid key)
instead of splitting nextKeyspaceID into 3 bytes; update the construction of
endKey in the region scan code (the variables nextKeyspaceID and endKey in
region_command.go) to check for this boundary and generate a proper end
sentinel, and add a regression test in tools/pd-ctl/pdctl/command/region_test.go
that asserts scans for keyspace-id=16777215 produce the correct non-wrapping
range.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
tools/pd-ctl/pdctl/command/region_command.go (1)

445-447: ⚠️ Potential issue | 🔴 Critical

Handle the max keyspace-id boundary without 24-bit wraparound.

When keyspaceID = 0xFFFFFF (16777215), nextKeyspaceID becomes 0x1000000. Truncating to 3 bytes produces endKey = []byte{'x', 0, 0, 0}, which is lexicographically smaller than startKey = []byte{'x', 0xFF, 0xFF, 0xFF}, resulting in an inverted/empty scan range.

🔧 Proposed fix
 	startKey := []byte{'x', byte(keyspaceID >> 16), byte(keyspaceID >> 8), byte(keyspaceID)}
-	nextKeyspaceID := keyspaceID + 1
-	endKey := []byte{'x', byte(nextKeyspaceID >> 16), byte(nextKeyspaceID >> 8), byte(nextKeyspaceID)}
+	var endKey []byte
+	if keyspaceID == 0xFFFFFF {
+		// Use next prefix after all 'x'+24bit keyspace keys
+		endKey = []byte{'y'}
+	} else {
+		nextKeyspaceID := keyspaceID + 1
+		endKey = []byte{'x', byte(nextKeyspaceID >> 16), byte(nextKeyspaceID >> 8), byte(nextKeyspaceID)}
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/pd-ctl/pdctl/command/region_command.go` around lines 445 - 447, The
scan range wraps when keyspaceID == 0xFFFFFF because nextKeyspaceID overflows
the 3-byte representation; update the logic that builds endKey to detect the max
24-bit value and use the next prefix byte instead of truncating: if keyspaceID
== 0xFFFFFF set endKey = []byte{'x' + 1} (or the next top-level prefix used by
your key schema), otherwise build endKey the same way using nextKeyspaceID;
change the code that computes endKey (referencing keyspaceID, nextKeyspaceID,
startKey, endKey) to implement this branch.
🧹 Nitpick comments (2)
tools/pd-ctl/pdctl/command/region_test.go (1)

200-226: Add regression test for keyspace-id=16777215 (max valid value).

The existing test validates out-of-range rejection (16777216), but there's no test for the max valid keyspace ID (16777215). As noted in PR comments, when keyspaceID = 0xFFFFFF, computing nextKeyspaceID = keyspaceID + 1 overflows the 3-byte field, producing an incorrect endKey. Please add a test that verifies end_key uses a non-wrapping bound for this edge case.

💡 Suggested test addition
func TestShowRegionsByKeyspaceTableIDCommandFuncWithMaxKeyspaceID(t *testing.T) {
	re := require.New(t)
	var requestURL *url.URL
	resp := `{"regions":[]}`
	oldClient := dialClient
	dialClient = &http.Client{
		Transport: &captureRoundTripper{
			roundTrip: func(req *http.Request) (*http.Response, error) {
				requestURL = req.URL
				return &http.Response{
					StatusCode: http.StatusOK,
					Body:       io.NopCloser(bytes.NewBufferString(resp)),
				}, nil
			},
		},
	}
	defer func() { dialClient = oldClient }()

	cmd := NewRegionsByKeyspaceTableIDCommand()
	cmd.Flags().String("pd", "http://mock-pd:2379", "")
	var out bytes.Buffer
	cmd.SetOut(&out)
	cmd.SetErr(&out)
	cmd.SetArgs([]string{"16777215"}) // max valid keyspace-id
	re.NoError(cmd.Execute())

	re.NotNil(requestURL)
	expectedStartKey := codec.EncodeBytes(nil, []byte{'x', 0xFF, 0xFF, 0xFF})
	// endKey should be 'y' (next prefix), not 'x',0,0,0
	expectedEndKey := codec.EncodeBytes(nil, []byte{'y'})
	re.Equal(string(expectedStartKey), mustQueryUnescape(t, requestURL.Query().Get("key")))
	re.Equal(string(expectedEndKey), mustQueryUnescape(t, requestURL.Query().Get("end_key")))
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/pd-ctl/pdctl/command/region_test.go` around lines 200 - 226, Add a
regression test that verifies the max valid keyspace ID (16777215) does not wrap
when computing end_key: implement
TestShowRegionsByKeyspaceTableIDCommandFuncWithMaxKeyspaceID which replaces
dialClient with a captureRoundTripper to capture the outgoing request URL, calls
NewRegionsByKeyspaceTableIDCommand with arg "16777215", and asserts the captured
request's "key" equals codec.EncodeBytes(nil, []byte{'x',0xFF,0xFF,0xFF}) and
"end_key" equals codec.EncodeBytes(nil, []byte{'y'}) (use mustQueryUnescape to
decode query values) and restore dialClient in a defer; this ensures the end_key
uses the non-wrapping next prefix for the 3-byte keyspace field.
tools/pd-ctl/pdctl/command/region_command.go (1)

467-474: Improve error message when table-id value is missing.

When a user runs region keyspace-id 17 table-id (missing the table value), len(args) == 2 causes the code to parse "table-id" as a limit, resulting in the confusing error "limit should be a number". Consider detecting this case and emitting a clearer message like "table-id requires a value".

💡 Proposed improvement
 	if len(args) == 2 || len(args) == 4 {
+		// Guard against "keyspace-id <id> table-id" without value
+		if len(args) == 2 && args[1] == "table-id" {
+			cmd.Println("Error: ", "table-id requires a value")
+			return
+		}
 		limit, err := strconv.Atoi(args[len(args)-1])
 		if err != nil {
 			cmd.Println("Error: ", "limit should be a number")
 			return
 		}
 		prefix += "&limit=" + strconv.Itoa(limit)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/pd-ctl/pdctl/command/region_command.go` around lines 467 - 474, The
code is parsing the last arg as a numeric limit in the len(args) == 2 ||
len(args) == 4 branch and misinterprets the literal "table-id" as a number;
before calling strconv.Atoi on args[len(args)-1] (in region_command.go where
args and limit are handled), add a guard that checks if args[len(args)-1] ==
"table-id" and if so print a clear error like "table-id requires a value" and
return; otherwise proceed with strconv.Atoi and the existing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@tools/pd-ctl/pdctl/command/region_command.go`:
- Around line 445-447: The scan range wraps when keyspaceID == 0xFFFFFF because
nextKeyspaceID overflows the 3-byte representation; update the logic that builds
endKey to detect the max 24-bit value and use the next prefix byte instead of
truncating: if keyspaceID == 0xFFFFFF set endKey = []byte{'x' + 1} (or the next
top-level prefix used by your key schema), otherwise build endKey the same way
using nextKeyspaceID; change the code that computes endKey (referencing
keyspaceID, nextKeyspaceID, startKey, endKey) to implement this branch.

---

Nitpick comments:
In `@tools/pd-ctl/pdctl/command/region_command.go`:
- Around line 467-474: The code is parsing the last arg as a numeric limit in
the len(args) == 2 || len(args) == 4 branch and misinterprets the literal
"table-id" as a number; before calling strconv.Atoi on args[len(args)-1] (in
region_command.go where args and limit are handled), add a guard that checks if
args[len(args)-1] == "table-id" and if so print a clear error like "table-id
requires a value" and return; otherwise proceed with strconv.Atoi and the
existing behavior.

In `@tools/pd-ctl/pdctl/command/region_test.go`:
- Around line 200-226: Add a regression test that verifies the max valid
keyspace ID (16777215) does not wrap when computing end_key: implement
TestShowRegionsByKeyspaceTableIDCommandFuncWithMaxKeyspaceID which replaces
dialClient with a captureRoundTripper to capture the outgoing request URL, calls
NewRegionsByKeyspaceTableIDCommand with arg "16777215", and asserts the captured
request's "key" equals codec.EncodeBytes(nil, []byte{'x',0xFF,0xFF,0xFF}) and
"end_key" equals codec.EncodeBytes(nil, []byte{'y'}) (use mustQueryUnescape to
decode query values) and restore dialClient in a defer; this ensures the end_key
uses the non-wrapping next prefix for the 3-byte keyspace field.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2c278144-f282-43de-a635-fa37d1e18311

📥 Commits

Reviewing files that changed from the base of the PR and between 0a0dcfd and 0f0d54c.

📒 Files selected for processing (2)
  • tools/pd-ctl/pdctl/command/region_command.go
  • tools/pd-ctl/pdctl/command/region_test.go

@rleungx
Copy link
Copy Markdown
Member

rleungx commented Apr 1, 2026

duplicated with #9850

@bufferflies
Copy link
Copy Markdown
Contributor Author

/retest

@bufferflies
Copy link
Copy Markdown
Contributor Author

/closed #9850

@bufferflies bufferflies closed this Apr 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: yes Indicates the PR's author has signed the dco. do-not-merge/needs-triage-completed release-note Denotes a PR that will be considered when it comes time to generate release notes. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pd-ctl: add show region by keyspace-id and table-id - PR: #10519

3 participants