From 15ab7147c79196212d75ed09f2c582f3612fabf8 Mon Sep 17 00:00:00 2001 From: Raghunandan Bhat Date: Thu, 16 Jul 2026 15:41:21 +0530 Subject: [PATCH] MDEV-39843: Prefix index causes incorrect range query result Problem: When a prefix index is built on ignorable characters (under NON1TO1 collation), it has an empty weight, which sorts at the bottom of the index, even though full value's real weight is large. The range scan for col >= c therefore positions past that empty weight entry and never reads it, even though full value actually satisfies col >= c. That's why the row is missing from the output. Fix: In `Field::stored_field_make_mm_leaf` do not derive an inequality range bound (<, <=, >, >=) from a prefix key segment(HA_PART_KEY_SEG) when the field's collation is MY_CS_NON1TO1. The prefix cannot be trusted as a range bound in that case and let the optimizer fall back to a scan with the full condition re-checked. Equality lookups and full-length (non-prefix) keys are unaffected. --- mysql-test/main/ctype_utf8.result | 2 +- mysql-test/main/ctype_utf8mb4.result | 2 +- mysql-test/main/range.result | 23 +++++++++++++++++++++++ mysql-test/main/range.test | 21 +++++++++++++++++++++ sql/opt_range.cc | 27 +++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 2 deletions(-) diff --git a/mysql-test/main/ctype_utf8.result b/mysql-test/main/ctype_utf8.result index 670cd9242b94b..dd1c12e4a383f 100644 --- a/mysql-test/main/ctype_utf8.result +++ b/mysql-test/main/ctype_utf8.result @@ -1521,9 +1521,9 @@ id a b 11 bbbbbb 60 SELECT id, a, b FROM t1 WHERE a BETWEEN 'aaaa' AND 'bbbbbb'; id a b +6 bbbbbb 40 8 aaaa 10 9 aaaa 50 -6 bbbbbb 40 11 bbbbbb 60 SELECT id, a FROM t1 WHERE a='bbbbbb'; id a diff --git a/mysql-test/main/ctype_utf8mb4.result b/mysql-test/main/ctype_utf8mb4.result index c97ba3b6d5369..26a273906d2ba 100644 --- a/mysql-test/main/ctype_utf8mb4.result +++ b/mysql-test/main/ctype_utf8mb4.result @@ -1546,9 +1546,9 @@ id a b 11 bbbbbb 60 SELECT id, a, b FROM t1 WHERE a BETWEEN 'aaaa' AND 'bbbbbb'; id a b +6 bbbbbb 40 8 aaaa 10 9 aaaa 50 -6 bbbbbb 40 11 bbbbbb 60 SELECT id, a FROM t1 WHERE a='bbbbbb'; id a diff --git a/mysql-test/main/range.result b/mysql-test/main/range.result index 188bae82f1a82..e304836dc4edf 100644 --- a/mysql-test/main/range.result +++ b/mysql-test/main/range.result @@ -3873,3 +3873,26 @@ DROP TABLE lineitem; # # End of 11.0 tests # +# +# MDEV-39843: Prefix index causes incorrect range query result +# +SET NAMES utf8mb4; +CREATE TABLE t0 (c0 LONGTEXT, KEY i0 (c0(1))); +INSERT INTO t0(c0) VALUES ('꙰dcF'), ('kER'); +SELECT c0 FROM t0 WHERE 'C0' <= t0.c0; +c0 +꙰dcF +kER +explain SELECT c0 FROM t0 WHERE 'C0' <= t0.c0; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t0 ALL i0 NULL NULL NULL 2 Using where +DROP INDEX i0 ON t0; +SELECT c0 FROM t0 WHERE 'C0' <= t0.c0; +c0 +꙰dcF +kER +explain SELECT c0 FROM t0 WHERE 'C0' <= t0.c0; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t0 ALL NULL NULL NULL NULL 2 Using where +DROP TABLE t0; +# End of 11.4 tests diff --git a/mysql-test/main/range.test b/mysql-test/main/range.test index dbd9d2b9cb5e2..0f9fa5549791e 100644 --- a/mysql-test/main/range.test +++ b/mysql-test/main/range.test @@ -2641,3 +2641,24 @@ DROP TABLE lineitem; --echo # --echo # End of 11.0 tests --echo # + +--echo # +--echo # MDEV-39843: Prefix index causes incorrect range query result +--echo # + +SET NAMES utf8mb4; + +CREATE TABLE t0 (c0 LONGTEXT, KEY i0 (c0(1))); +INSERT INTO t0(c0) VALUES ('꙰dcF'), ('kER'); + +SELECT c0 FROM t0 WHERE 'C0' <= t0.c0; +explain SELECT c0 FROM t0 WHERE 'C0' <= t0.c0; + +DROP INDEX i0 ON t0; + +SELECT c0 FROM t0 WHERE 'C0' <= t0.c0; +explain SELECT c0 FROM t0 WHERE 'C0' <= t0.c0; + +DROP TABLE t0; + +--echo # End of 11.4 tests diff --git a/sql/opt_range.cc b/sql/opt_range.cc index 0d2949adcabdc..e92ffa7adfdcb 100644 --- a/sql/opt_range.cc +++ b/sql/opt_range.cc @@ -9701,6 +9701,25 @@ SEL_ARG *Field::stored_field_make_mm_leaf_bounded_int(RANGE_OPT_PARAM *param, } +/* + Check whether a partial (prefix) key segment cannot be used as a range + bound because of the collation. + + For a collation with a complex, non one-to-one character-to-weight mapping + (contractions, expansions or ignorable characters: MY_CS_NON1TO1) the sort + key of a character prefix does not preserve the ordering of the full value, + so neither a lower nor an upper prefix bound is sound. + Equality is unaffected and keeps using the prefix (the full condition is + always rechecked). +*/ +static bool prefix_key_part_unsafe_for_range(const KEY_PART *key_part, + const Field *field) +{ + return (key_part->flag & HA_PART_KEY_SEG) && + (field->charset()->state & MY_CS_NON1TO1); +} + + SEL_ARG *Field::stored_field_make_mm_leaf(RANGE_OPT_PARAM *param, KEY_PART *key_part, scalar_comparison_op op, @@ -9715,12 +9734,20 @@ SEL_ARG *Field::stored_field_make_mm_leaf(RANGE_OPT_PARAM *param, switch (op) { case SCALAR_CMP_LE: + if (prefix_key_part_unsafe_for_range(key_part, this)) + DBUG_RETURN(0); DBUG_RETURN(new (mem_root) SEL_ARG_LE(str, this)); case SCALAR_CMP_LT: + if (prefix_key_part_unsafe_for_range(key_part, this)) + DBUG_RETURN(0); DBUG_RETURN(new (mem_root) SEL_ARG_LT(thd, str, key_part, this, value)); case SCALAR_CMP_GT: + if (prefix_key_part_unsafe_for_range(key_part, this)) + DBUG_RETURN(0); DBUG_RETURN(new (mem_root) SEL_ARG_GT(thd, str, key_part, this, value)); case SCALAR_CMP_GE: + if (prefix_key_part_unsafe_for_range(key_part, this)) + DBUG_RETURN(0); DBUG_RETURN(new (mem_root) SEL_ARG_GE(thd, str, key_part, this, value)); case SCALAR_CMP_EQ: case SCALAR_CMP_EQUAL: