Bug #121464 Debug build: assertion lhs.charset()->number == rhs.charset()->number in histograms::Histogram_comparator<String> aborts
Submitted: 9 Oct 1:30
Reporter: Wwwwing Wwwwing Email Updates:
Status: Open Impact on me:
None 
Category:MySQL Server Severity:S3 (Non-critical)
Version:8.4.11 OS:Ubuntu
Assigned to: CPU Architecture:Any

[9 Oct 1:30] Wwwwing Wwwwing
Description:
A **singleton histogram** on a `TINYTEXT`/`TEXT` column plus an equality (`IN`) predicate whose
value expression carries a **different character set** than the column makes the optimizer look the
value up in the histogram's sorted bucket vector:

```
Singleton<String>::get_equal_to_selectivity()      sql/histograms/singleton.cc:394
  -> std::lower_bound(..., Histogram_comparator())
       -> Histogram_comparator::operator()(const String &, const String &)   value_map.cc:55
```

`value_map.cc` asserts the invariant that both strings use the same charset:

```cpp
template <>
bool Histogram_comparator::operator()(const String &lhs, const String &rhs) const {
  // The collation MUST be the same
  assert(lhs.charset()->number == rhs.charset()->number);      // <-- fires
  ...
  return sortcmp(&lhs, &rhs, lhs.charset()) < 0;
}
```

* **Debug build** → assertion fails, `abort()` → server dies (`mysqld got signal 6`), clients see
  `ERROR 2013 (HY000): Lost connection to MySQL server during query`.
* **Release build** → `assert()` is compiled out, so `sortcmp(&lhs, &rhs, lhs.charset())` is executed
  with `rhs` encoded in a **different** charset. The comparison is then not merely undefined but
  silently *wrong*, which can return an incorrect bucket → wrong selectivity → sub-optimal/wrong plan.
  This is the part that matters for production builds.

How to repeat:
Fully self-contained; no data from the original fuzzing run is needed.

```sql
CREATE DATABASE bug;
USE bug;

-- the column type, charset, collation and table options are exactly what the
-- SQLancer CERT oracle created when it found this
CREATE TABLE t0 (
  c0 TINYTEXT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci STATS_PERSISTENT=1;

INSERT INTO t0 VALUES ('&'),('&'),('0.04555069458599481'),('0.4007300132729398'),
                      ('921284471'),('139093205'),('괗s9Nc'),('趫c'),
                      ('0.9176908289757043'),('-580840791');

ANALYZE TABLE t0 UPDATE HISTOGRAM ON c0;      -- creates a singleton histogram (9 buckets)

-- value expression mixes a numeric and a string literal -> charset differs from c0
EXPLAIN FORMAT=TRADITIONAL SELECT t0.c0 AS ref0 FROM t0
  WHERE (COALESCE(1735249373, '0.9973355543562331')) NOT IN (t0.c0);
```

Result with `8.4.11-debug` (reproduced 10/10 on the fuzzing server, and again from scratch here):

```
ERROR 2013 (HY000) at line 11: Lost connection to MySQL server during query
```

with the server error log containing:

```
mysqld: .../sql/histograms/value_map.cc:55:
bool histograms::Histogram_comparator::operator()(const T&, const T&) const [with T = String]:
Assertion `lhs.charset()->number == rhs.charset()->number' failed
2026-10-09T00:00:00Z UTC - mysqld got signal 6 ;
```

Same script against the **official release** tarball
(`mysql-8.4.11-linux-glibc2.28-x86_64.tar.xz`): **no abort**, `EXPLAIN` returns normally
(`type=ALL ... filtered=100.00`). So the abort itself is debug-only, but the underlying
charset mismatch is present in release builds too.

Stack trace (`8.4.11-debug`)

```
#10 Histogram_comparator::operator()<String>(String const&, String const&)   value_map.cc:55
#11 Histogram_comparator::operator()<String>(SingletonBucket<String> const&, ...)
#12 __gnu_cxx::__ops::_Iter_comp_val<Histogram_comparator>::operator()<...>
#13 std::__lower_bound<SingletonBucket<String> const*, ...>
#14 std::lower_bound<SingletonBucket<String> const*, ..., Histogram_comparator>
#15 histograms::Singleton<String>::get_equal_to_selectivity(String const&)   singleton.cc:394
#16 histograms::Histogram::get_equal_to_selectivity_dispatcher<String>(...)
#17 histograms::Histogram::apply_operator<String>(enum_operator, String const&)
#18 histograms::Histogram::get_selectivity_dispatcher(Item*, enum_operator, TYPELIB const*, double*)
#19 histograms::Histogram::get_raw_selectivity(Item**, unsigned long, enum_operator, double*)
#20 histograms::Histogram::get_selectivity(Item**, unsigned long, enum_operator, double*)
#21 Item_equal::get_filtering_effect(THD*, ...)                              item_cmpfunc.cc:7203
#22 calculate_condition_filter(JOIN_TAB const*, Key_used*, ...)              sql_planner.cc:1471
#23 Optimize_table_order::calculate_scan_cost(...)                           sql_planner.cc
#24 Optimize_table_order::best_access_path(...)                              sql_planner.cc:1145
#25 Optimize_table_order::best_extension_by_limited_search(...)              sql_planner.cc:2798
#27 Optimize_table_order::choose_table_order()                               sql_planner.cc:2031
#32 JOIN::make_join_plan()                                                   sql_optimizer.cc:5446
#33 JOIN::optimize(bool)                                                     sql_optimizer.cc:722
#34 Query_block::optimize(THD*, bool)                                        sql_select.cc:2052
#38 mysql_execute_command(THD*, bool)                                        sql_parse.cc:4739
```

`HISTOGRAM_MAX_COMPARE_LENGTH` is `42` and is unrelated — the failing assertion is the **charset**
one, not the length one.