read-only user variable is corrupted after SELECT/EXPLAIN when getvar() stays in root plan
- 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)
Reproduced locally on **2026-04-20** against a local `tiup playground v8.5.6` cluster (`127.0.0.1:4000`).
```sql
DROP TABLE IF EXISTS statement_bug;
DROP TABLE IF EXISTS folio_bug;
DROP TABLE IF EXISTS customer_bug;
CREATE TABLE `customer_bug` (
`Id` char(36) COLLATE utf8mb4_0900_ai_ci NOT NULL,
`PortfolioCode` varchar(255) COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
PRIMARY KEY (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE `folio_bug` (
`Id` char(36) COLLATE utf8mb4_0900_ai_ci NOT NULL,
`CustomerId` char(36) COLLATE utf8mb4_0900_ai_ci NOT NULL,
`Type` varchar(50) COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`Number` int DEFAULT NULL,
PRIMARY KEY (`Id`),
KEY `IX_CustomerId` (`CustomerId`),
CONSTRAINT `FK_Folio_Customer` FOREIGN KEY (`CustomerId`) REFERENCES `customer_bug` (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_0900_ai_ci;
CREATE TABLE `statement_bug` (
`Id` char(36) COLLATE utf8mb4_0900_ai_ci NOT NULL,
`CustomerId` char(36) COLLATE utf8mb4_0900_ai_ci NOT NULL,
`FolioId` char(36) COLLATE utf8mb4_0900_ai_ci NOT NULL,
`Number` int NOT NULL,
`CreatedOn` datetime NOT NULL,
`JournalNumberFrom` int NOT NULL,
`JournalNumberTo` int NOT NULL,
`FolioCode` varchar(255) COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`Status` text COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`AccountDetails` text COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`StatementDate` datetime DEFAULT NULL,
`DocumentStorageId` char(36) COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`RentAndTenancyText` text COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`OpeningBalance` decimal(12,2) DEFAULT NULL,
`ClosingBalance` decimal(12,2) DEFAULT NULL,
`TrustAccountType` int DEFAULT NULL,
PRIMARY KEY (`Id`),
KEY `FK_Statement_Customer` (`CustomerId`),
KEY `FK_Statement_Folio` (`FolioId`),
CONSTRAINT `FK_Statement_Customer` FOREIGN KEY (`CustomerId`) REFERENCES `customer_bug` (`Id`) ON DELETE CASCADE,
CONSTRAINT `FK_Statement_Folio` FOREIGN KEY (`FolioId`) REFERENCES `folio_bug` (`Id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_0900_ai_ci;
INSERT INTO customer_bug VALUES ('11111111-1111-1111-1111-111111111111', 'SUPER6DO');
INSERT INTO folio_bug VALUES (
'22222222-2222-2222-2222-222222222222',
'11111111-1111-1111-1111-111111111111',
'supplier', 1
);
INSERT INTO statement_bug VALUES (
UUID(), '11111111-1111-1111-1111-111111111111',
'22222222-2222-2222-2222-222222222222',
1, NOW(), 1, 10, 'FOLIO1', 'Active', 'Account details here',
NOW(), UUID(), 'Rent text here', 100.00, 200.00, 1
);
INSERT INTO statement_bug VALUES (
UUID(), '11111111-1111-1111-1111-111111111111',
'22222222-2222-2222-2222-222222222222',
2, NOW(), 11, 20, 'FOLIO1', 'Closed', 'More account details',
NOW(), UUID(), 'More rent text', 300.00, 400.00, 1
);
INSERT INTO customer_bug VALUES ('33333333-3333-3333-3333-333333333333', 'OTHER');
INSERT INTO folio_bug VALUES (
'44444444-4444-4444-4444-444444444444',
'33333333-3333-3333-3333-333333333333',
'other', 1
);
INSERT INTO statement_bug
SELECT
UUID(), '33333333-3333-3333-3333-333333333333',
'44444444-4444-4444-4444-444444444444',
seq, NOW(), seq, seq + 10, CONCAT('CODE', seq),
CONCAT('BulkStatus', seq), CONCAT('BulkAccount', seq),
NOW(), UUID(), CONCAT('BulkRent', seq),
ROUND(RAND() * 1000, 2), ROUND(RAND() * 1000, 2), 1
FROM (
WITH RECURSIVE nums AS (
SELECT 3 AS seq
UNION ALL
SELECT seq + 1 FROM nums WHERE seq < 1001
)
SELECT seq FROM nums
) AS gen;
SET NAMES utf8mb4 COLLATE utf8mb4_0900_ai_ci;
SET @CustomerId = (SELECT Id FROM customer_bug WHERE PortfolioCode = 'SUPER6DO');
SET @FolioId = (SELECT Id FROM folio_bug WHERE CustomerId = @CustomerId AND Type = 'supplier' AND Number = 1);
SELECT @FolioId;
-- expected: 22222222-2222-2222-2222-222222222222
EXPLAIN SELECT * FROM statement_bug WHERE FolioId = @FolioId ORDER BY Number;
SELECT @FolioId;
-- on my v8.5.6 playground this is already corrupted to:
-- test.statement_bug.numbereq(test.sta
SET @FolioId = (SELECT Id FROM folio_bug WHERE CustomerId = @CustomerId AND Type = 'supplier' AND Number = 1);
SELECT * FROM statement_bug WHERE FolioId = @FolioId ORDER BY Number;
SELECT @FolioId;
-- expected: 22222222-2222-2222-2222-222222222222
-- actual on my v8.5.6 playground:
-- ActiveClosed2-2222-2222-222222222222
```
The `EXPLAIN` plan on my playground is:
```sql
+---------------------+----------+-----------+---------------------+------------------------------------------------+
| id | estRows | task | access object | operator info |
+---------------------+----------+-----------+---------------------+------------------------------------------------+
| Sort_5 | 8000.00 | root | | statement_bug.Number |
| └─Selection_8 | 8000.00 | root | | eq(statement_bug.FolioId, getvar("folioid")) |
| └─TableReader_10 | 10000.00 | root | | data:TableFullScan_9 |
| └─TableFullScan_9 | 10000.00 | cop[tikv] | table:statement_bug | keep order:false, stats:pseudo |
+---------------------+----------+-----------+---------------------+------------------------------------------------+
```
### 2. What did you expect to see? (Required)
`@FolioId` is only read here, never assigned inside the `SELECT` or `EXPLAIN`, so its value should stay unchanged:
```sql
22222222-2222-2222-2222-222222222222
```
This should remain true both after:
```sql
EXPLAIN SELECT * FROM statement_bug WHERE FolioId = @FolioId ORDER BY Number;
```
and after:
```sql
SELECT * FROM statement_bug WHERE FolioId = @FolioId ORDER BY Number;
```
### 3. What did you see instead (Required)
I can reproduce two forms of corruption on the same v8.5.6 playground:
1. Running `EXPLAIN SELECT * FROM statement_bug WHERE FolioId = @FolioId ORDER BY Number;` mutates `@FolioId` from the UUID to:
```sql
test.statement_bug.numbereq(test.sta
```
After that, the following `SELECT ... WHERE FolioId = @FolioId` returns `0` rows because the variable is already corrupted.
2. If I reset `@FolioId` and run the `SELECT` directly without the preceding `EXPLAIN`, the query returns the expected 2 matching rows, but `@FolioId` is still corrupted afterwards:
```sql
ActiveClosed2-2222-2222-222222222222
```
This looks like the variable buffer is being overwritten by data from the result / plan text, even though the query only reads `@FolioId`.
I searched for existing issues and found related-but-different reports such as:
- #36944 `wrong results when the query contains setting user var`
- #66339 `planner: read-only variables with uppercase name cannot be converted to constant and lead to sub-optimal plans`
- #52742 `Optimizer should use index when comparing with user-defined variables`
but I could not find one describing the user variable value itself being overwritten by `SELECT` / `EXPLAIN` output like this.
### 4. What is your TiDB version? (Required)
```sql
SELECT tidb_version();
Release Version: v8.5.6
Edition: Community
Git Commit Hash: ae18096e023780bb56bfce33698abec0d4640d0a
Git Branch: HEAD
UTC Build Time: 2026-04-14 07:11:54
GoVersion: go1.25.8
Race Enabled: false
Check Table Before Drop: false
Store: tikv
```
Contributor guide
Assessment
This issue has not been assessed yet.