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.
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.