Non-prepared plan cache reuses statement carriers across SQL parser context changes
- 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)
TiDB's non-prepared plan cache statement LRU is keyed by parameterized SQL text. A preserved string literal can therefore reuse a statement carrier that was parsed with a different SQL parser context.
#### Reproducer A: `sql_mode`
```sql
CREATE TABLE test.t_pc_context (a INT PRIMARY KEY);
INSERT INTO test.t_pc_context VALUES (1), (2);
SET tidb_enable_non_prepared_plan_cache = ON;
SET sql_mode = '';
SELECT HEX('a\n') AS v, a FROM test.t_pc_context WHERE a = 1;
-- first result: 610A | 1
SET sql_mode = 'NO_BACKSLASH_ESCAPES';
SELECT HEX('a\n') AS v, a FROM test.t_pc_context WHERE a = 2;
-- expected: 615C6E | 2
-- observed: 610A | 2
```
The WHERE literal is parameterized, while the projection string is preserved in the statement identity. Both executions produce the same parameterized SQL, so the second execution hits the first statement carrier.
The physical plan cache hit flag is not sufficient to protect this case because the incompatible object is the parsed statement carrier itself.
Reproducer B: character_set_client
The following TestKit-style snippet sends the same raw SQL bytes (C4 E3) under two client character sets. C4 E3 is 你 in GBK and Äã in latin1:
```go
raw := string([]byte{0xc4, 0xe3})
query := "SELECT '" + raw + "' AS s, a FROM test.t_pc_context WHERE a = 1"
tk.MustExec("SET character_set_client = 'gbk'")
tk.MustQuery(query) // expected: 你 | 1; first carrier is created
query = "SELECT '" + raw + "' AS s, a FROM test.t_pc_context WHERE a = 2"
tk.MustExec("SET character_set_client = 'latin1'")
tk.MustQuery(query) // expected: Äã | 2; observed: 你 | 2, with a statement-LRU hit
```
The same issue can be reproduced through a SQL client by making the quoted literal contain the raw bytes 0xC4 0xE3 rather than the four ASCII characters \xC4\xE3.
### 2. What did you expect to see? (Required)
Changing sql_mode or character_set_client must not reuse a statement carrier parsed under the previous context.
Each execution should preserve the literal semantics of its current parser context.
### 3. What did you see instead (Required)
The parameterized SQL text remains the same, and the statement LRU returns the carrier created by the previous execution.
The cached carrier retains the previous literal interpretation, producing stale or incorrect results. In the examples above, the second result is 610A instead of 615C6E, and 你 instead of Äã.
### 4. What is your TiDB version? (Required)
```text
Release Version: v9.0.0-beta.2.pre
Edition: Community
Git Commit Hash: f52d19273b447c08061632fde70e288f24b324ba
Git Branch: HEAD
UTC Build Time: 2026-08-27 12:34:27
GoVersion: go1.26.4
Race Enabled: false
Check Table Before Drop: false
Store: unistore
Kernel Type: Classic
```
Contributor guide
Research direction
Start with the non-prepared plan cache statement LRU and the TestKit-style repro described in the issue. Reproduce both sql_mode and character_set_client cases, then trace how the parameterized SQL key returns a statement carrier. Done means parser-context changes no longer reuse an incompatible carrier and the shown results match each current context.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, sql
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100