pingcap / pingcap/tidb

session: repeated binary PREPARE mishandles non-UTF8 character_set_client

Open
#69,909 4 comments 0 reactions 0 assignees View on GitHub
affects-8.5 severity/critical sig/sql-infra type/bug
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.