`cast(repeat(...) as char)` unexpectedly returns an empty string
- 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)
```sql
create table t1(c1 text);
insert into t1(c1) values ('abc');
alter table t1 set tiflash replica 1;
SET SESSION tidb_enforce_mpp = ON;
select 1 where false union
( select cast(repeat(c1, 1) as char) from t1 );
```
### 2. What did you expect to see? (Required)
```
+------+
| 1 |
+------+
| abc |
+------+
1 row in set (0.00 sec)
```
### 3. What did you see instead (Required)
```
+------+
| 1 |
+------+
| |
+------+
1 row in set (0.01 sec)
```
### 4. What is your TiDB version? (Required)
```
Release Version: v8.5.3
Edition: Community
Git Commit Hash: dc2548aac79a712265e831cff2a3a896bc0a5a38
Git Branch: HEAD
UTC Build Time: 2025-07-31 13:54:43
GoVersion: go1.23.8
Race Enabled: false
Check Table Before Drop: false
Store: tikv
```
### analysis
#### plan
```
+--------------------------------+----------+--------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------+
| id | estRows | task | access object | operator info |
+--------------------------------+----------+--------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------+
| HashAgg_13 | 8001.00 | root | | group by:Column#5, funcs:firstrow(Column#5)->Column#5 |
| └─Union_17 | 10000.00 | root | | |
| ├─Projection_19 | 0.00 | root | | 1->Column#5 |
| │ └─TableDual_20 | 0.00 | root | | rows:0 |
| └─TableReader_30 | 10000.00 | root | | MppVersion: 2, data:ExchangeSender_29 |
| └─ExchangeSender_29 | 10000.00 | mpp[tiflash] | | ExchangeType: PassThrough |
| └─Projection_22 | 10000.00 | mpp[tiflash] | | cast(cast(repeat(d3.t1.c1, 1), var_string(4294967295)), varchar(4294967296) BINARY CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci)->Column#5 |
| └─TableFullScan_28 | 10000.00 | mpp[tiflash] | table:t1 | keep order:false, stats:pseudo |
+--------------------------------+----------+--------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------+
```
The plan shows varchar(4294967296), which later causes incorrect behavior.
#### Abstract
The combination of `repeat(...)` setting `flen = mysql.MaxBlobWidth`, type promotion to `LongBlob`, and cast adjustment causes `flen` to reach `4294967295`. In union planning, `flen` is increased to `4294967296`. When converting to protobuf (`int32`), `flen` overflows and becomes `0`. This leads to `cast(repeat(...) as char)` unexpectedly producing an empty string.
#### How does 4294967296 come from?
In `planner/core/logical_plan_builder.go`, within `unionJoinFieldType`:
```go
if a.GetFlen() == -1 || b.GetFlen() == -1 {
resultTp.SetFlenUnderLimit(-1)
} else {
resultTp.SetFlenUnderLimit(max(a.GetFlen()-a.GetDecimal(), b.GetFlen()-b.GetDecimal()) + resultTp.GetDecimal())
}
```
The `else` branch is taken. On a 64-bit system this leads to:
* `flen = 4294967295` (`mysql.MaxBlobWidth`)
* `decimal = -1`
* final `flen = 4294967296`
Later, in `expression/expr_to_pb.go`:
```go
func ToPBFieldType(ft *types.FieldType) *tipb.FieldType {
return &tipb.FieldType{
Tp: int32(ft.GetType()),
Flag: uint32(ft.GetFlag()),
Flen: int32(ft.GetFlen()),
Decimal: int32(ft.GetDecimal()),
Charset: ft.GetCharset(),
Collate: collate.CollationToProto(ft.GetCollate()),
Elems: ft.GetElems(),
}
}
```
Here, `flen` is truncated to `0`.
#### Detailed Flow Leading to `flen = 4294967295`
1. **`repeat` function** (`expression/builtin_string.go`):
```go
bf, err := newBaseBuiltinFuncWithTp(ctx, c.funcName, args, types.ETString, types.ETString, types.ETInt)
...
bf.tp.SetFlen(mysql.MaxBlobWidth)
```
`flen` is set to `mysql.MaxBlobWidth`.
2. **`newFunctionImpl`** (`expression/scalar_function.go`):
```go
if builtinRetTp := f.getRetTp(); builtinRetTp.GetType() != mysql.TypeUnspecified || retType.GetType() == mysql.TypeUnspecified {
retType = builtinRetTp
}
```
3. **`getRetTp`** (`baseBuiltinFunc`):
```go
if b.tp.EvalType() == types.ETString {
if b.tp.GetFlen() >= mysql.MaxBlobWidth {
b.tp.SetType(mysql.TypeLongBlob)
} else if b.tp.GetFlen() >= 65536 {
b.tp.SetType(mysql.TypeMediumBlob)
}
...
}
```
At this point, type is set to `mysql.TypeLongBlob`.
4. **Cast logic** (`expression/builtin_cast.go`, `adjustRetFtForCastString()`):
```go
case types.ETString:
if originalFlen == types.UnspecifiedLength {
switch argFt.GetType() {
...
case mysql.TypeLongBlob:
retFt.SetFlen(maxLongBlobSize)
```
Here, `retFt.SetFlen(maxLongBlobSize)` sets `flen = 4294967295`.
Contributor guide
Assessment
This issue has not been assessed yet.