session: repeated binary PREPARE mishandles non-UTF8 character_set_client
- Dominant language
- Go
- Stars
- 40.5k
- Forks
- 6.2k
- PR merge metrics
- PR metrics pending
Description
### 1. Minimal reproduce step (Required)
This issue requires the binary protocol (`COM_STMT_PREPARE`). SQL-layer `PREPARE ...` does not reproduce it.
Use a MySQL client/driver that sends binary prepares, for example `github.com/go-sql-driver/mysql` with `interpolateParams=false`:
```go
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "root:@tcp(127.0.0.1:4000)/test?charset=utf8mb4&collation=utf8mb4_bin&interpolateParams=false")
if err != nil {
log.Fatal(err)
}
defer db.Close()
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
for _, q := range []string{
"SET SESSION tidb_enable_cache_prepare_stmt = ON",
"SET SESSION tidb_enable_prepared_plan_cache = OFF",
"SET SESSION character_set_connection = utf8mb4",
"SET SESSION collation_connection = utf8mb4_bin",
"SET SESSION character_set_client = gbk",
} {
if _, err := db.Exec(q); err != nil {
log.Fatal(err)
}
}
// The bytes D6 D0 are the GBK encoding of the Chinese character “中”.
query := "SELECT LENGTH('" + string([]byte{0xd6, 0xd0}) + "')"
for i := 0; i < 2; i++ {
stmt, err := db.Prepare(query)
if err != nil {
log.Fatal(err)
}
var result int64
if err := stmt.QueryRow().Scan(&result); err != nil {
log.Fatal(err)
}
stmt.Close()
fmt.Println(result)
}
}
```
The same behavior can be reproduced with any client that repeatedly sends `COM_STMT_PREPARE` on one connection.
### 2. What did you expect to see? (Required)
Both executions should return `3`. The client charset and SQL text are unchanged, so repeated prepares should produce equivalent prepared statements.
With `tidb_enable_cache_prepare_stmt = OFF`, both executions return `3`.
### 3. What did you see instead (Required)
With `tidb_enable_cache_prepare_stmt = ON`, the first execution returns `3` and the second returns `2`:
```text
first=3 second=2
```
The issue was reproduced on a TiDB v8.5.7 cluster. Control cases were normal:
```text
dedup-off-gbk: text=3 prepared=[3 3]
dedup-on-gbk: text=3 prepared=[3 2]
dedup-on-utf8: text=3 prepared=[3 3]
dedup-on-ascii: text=3 prepared=[3 3]
```
The deduplication path introduced by [#68742](https://github.com/pingcap/tidb/pull/68742) re-parses the SQL with `character_set_connection` and `collation_connection`, but omits `character_set_client`. The deduplication key also omits `character_set_client`. Consequently, the second prepare decodes the GBK literal as UTF-8 bytes instead of converting it from GBK, changing the string length from 3 to 2.
### 4. What is your TiDB version? (Required)
```text
Release Version: v8.5.7
Git Commit Hash: 202b7f47286a1109b5c957401d34c9358d130ae
UTC Build Time: 2026-07-15 02:06:00
GoVersion: go1.25.10
Store: tikv
```
Contributor guide
Research direction
Start at the deduplication path introduced by PR #68742 and the binary COM_STMT_PREPARE handling. Check the re-parse settings and deduplication key for character_set_client, then reproduce the repeated GBK prepare case and add coverage showing both executions return 3.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, mysql
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100