RECOVER TABLE table_name N is parsed but ignored, recovering a table outside the requested DDL-job selection
- Dominant language
- Go
- Stars
- 40.5k
- Forks
- 6.2k
- PR merge metrics
- PR metrics pending
Description
## Bug Report
Please answer these questions before submitting your issue. Thanks!
### 1. Minimal reproduce step (Required)
Run on a real-TiKV playground pinned to source `a514a92784c`:
```sql
CREATE DATABASE repro_recover_jobnum_main;
USE repro_recover_jobnum_main;
CREATE TABLE t(a INT);
INSERT INTO t VALUES (5);
DROP TABLE t;
CREATE TABLE marker(x INT);
-- The latest DDL job is now CREATE TABLE marker; the DROP TABLE t job is older.
SELECT JOB_ID, TABLE_NAME, JOB_TYPE
FROM information_schema.ddl_jobs
WHERE DB_NAME = 'repro_recover_jobnum_main'
ORDER BY JOB_ID DESC LIMIT 3;
-- A one-job window contains only CREATE TABLE marker, so this must not recover t.
RECOVER TABLE t 1;
SELECT a FROM t; -- actual: 5
```
Probe output:
```text
RECENT_JOB=171|marker|create table
RECENT_JOB=169|t|drop table
RECENT_JOB=168|t|create table
MAIN_LATEST_JOB_IS_CREATE_MARKER
MAIN_DROP_T_JOB_PRESENT=True
RECOVERED_ROWS=['5']
TIDB_JOB_NUMBER_IGNORED
```
Control in a separate database:
```text
CONTROL_ROWS=['7']
CONTROL_UNBOUNDED_RECOVER_OK
```
The unbounded `RECOVER TABLE t` control proves the recovery path and GC safe point are healthy. The RED is specific to the parsed integer being ignored.
### 2. What did you expect to see? (Required)
Pass `JobNum` into `getRecoverTableByTableName` and limit the history search to the most recent `JobNum` DDL jobs (or implement the intended job-id selection). If the syntax is intentionally unsupported, reject `RECOVER TABLE table_name N` in the parser/planner before executing. Add a regression test where the latest DDL job is a non-matching `CREATE TABLE` and `RECOVER TABLE t 1` must not recover the older dropped table.
### 3. What did you see instead (Required)
Fixture: CREATE TABLE t(a INT); INSERT INTO t VALUES(5); DROP TABLE t; CREATE TABLE marker(x INT). Recent jobs before recovery: CREATE TABLE marker (latest), DROP TABLE t (older). RECOVER TABLE t 1 returns success; information_schema.tables shows t; SELECT a FROM t returns 5. Control: unbounded RECOVER TABLE t recovers the separate control row 7.
### 4. What is your TiDB version? (Required)
```text
Release Version: v8.4.0-this-is-a-placeholder
Edition: Community
Git Commit Hash: None
Git Branch: None
UTC Build Time: None
GoVersion: go1.25.12
Race Enabled: false
Check Table Before Drop: false
Store: unistore
Kernel Type: Classic
```
Built from source commit `a514a92784c9654502686e6ee6efc9e0aeda8afa` (pingcap/tidb master, 2026-09-07).
### 5. Root cause (optional)
- pkg/parser/parser.y:3382-3394 RECOVER TABLE TableName Int64Num sets JobNum
- pkg/parser/ast/ddl.go:5653-5659 RecoverTableStmt.JobID, Table, JobNum
- pkg/executor/ddl.go:438-460 executeRecoverTable ignores s.JobNum and branches only on s.Table
- pkg/executor/ddl.go:535-592 getRecoverTableByTableName searches all history DDL jobs via ddl.IterHistoryDDLJobs and never reads JobNum
- TiDB v3.0.0 executor/ddl.go TODO: only search recent `e.JobNum` DDL jobs
Contributor guide
Research direction
Run the SQL reproduction first, then read pkg/executor/ddl.go at executeRecoverTable and getRecoverTableByTableName, followed by the parser and AST definitions in pkg/parser/parser.y and pkg/parser/ast/ddl.go. Add the regression test requested in the issue so RECOVER TABLE t 1 does not recover an older dropped table when the latest DDL job is unrelated, while unbounded recovery still works.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- database
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100