1
0
mirror of https://github.com/MariaDB/server.git synced 2026-10-23 18:49:29 +03:00

MDEV-41360 OPTIMIZE TABLE crashes with ib_vector_size(optim->words) > 0 on FULLTEXT index with collation-equal words

Problem:
========
  OPTIMIZE TABLE on a FULLTEXT index crashes when the aux tables hold
two spellings of a word that are equal under the index collation.
The index lookup in fts_index_fetch_nodes() and the DELETE in
fts_optimize_write_word() both compare words by collation,
but fts_optimize_index_fetch_node() compares the bytes,
so each spelling becomes a separate fts_word_t.

fts_optimize_compact() then optimizes and writes back each of them
separately. Writing one deletes the rows of the other, including the
rows that were just written, so index entries can be lost. If all the
documents were deleted, the first write removes both words. The next
word that fts_optimize_words() reads from the zip buffer is the other
spelling, the fetch finds nothing, and the server aborts

Solution:
========
fts_optimize_collation_fetch_node(): New callback used only
by OPTIMIZE. It adds the fetched records whose words are
equal under the index collation to the same fts_word_t,
so the spellings are optimized and written back as one word.
The records arrive ordered by first_doc_id,
so the nodes stay in order.

fts_optimize_fetch_node(): The helper of the fetch callbacks,
which compares the words either by bytes or by the given collation.
fts_optimize_index_fetch_node() still compares by bytes, which is
what the information_schema code expects.

fts_optimize_words(): Use fts_optimize_collation_fetch_node().
The word read from the zip buffer can already
have been optimized together with an earlier spelling, so an empty
fetch is not an error. Skip the word instead of asserting.
This commit is contained in:
Thirunarayanan Balathandayuthapani
2026-10-13 21:19:34 +05:30
parent a466cdb832
commit 96aee50cfb
3 changed files with 126 additions and 16 deletions

View File

@@ -0,0 +1,35 @@
#
# MDEV-41360 OPTIMIZE TABLE crashes with
# ib_vector_size(optim->words) > 0 on FULLTEXT index
# with collation-equal words
#
SET GLOBAL innodb_optimize_fulltext_only = ON;
CREATE TABLE t1 (
id INT PRIMARY KEY,
a TEXT CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci,
FULLTEXT KEY (a)
) ENGINE=InnoDB;
INSERT INTO t1 VALUES (1, 'resume');
OPTIMIZE TABLE t1;
Table Op Msg_type Msg_text
test.t1 optimize status OK
INSERT INTO t1 VALUES (2, 'résumé');
OPTIMIZE TABLE t1;
Table Op Msg_type Msg_text
test.t1 optimize status OK
DELETE FROM t1 WHERE id = 2;
OPTIMIZE TABLE t1;
Table Op Msg_type Msg_text
test.t1 optimize status OK
SELECT * FROM t1 WHERE MATCH(a) against ('resume');
id a
1 resume
INSERT INTO t1 VALUES(3, 'résumé');
DELETE FROM t1;
OPTIMIZE TABLE t1;
Table Op Msg_type Msg_text
test.t1 optimize status OK
SELECT * FROM t1 WHERE MATCH(a) against ('resume');
id a
DROP TABLE t1;
SET GLOBAL innodb_optimize_fulltext_only = OFF;

View File

@@ -0,0 +1,26 @@
--source include/have_innodb.inc
--echo #
--echo # MDEV-41360 OPTIMIZE TABLE crashes with
--echo # ib_vector_size(optim->words) > 0 on FULLTEXT index
--echo # with collation-equal words
--echo #
SET GLOBAL innodb_optimize_fulltext_only = ON;
CREATE TABLE t1 (
id INT PRIMARY KEY,
a TEXT CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci,
FULLTEXT KEY (a)
) ENGINE=InnoDB;
INSERT INTO t1 VALUES (1, 'resume');
OPTIMIZE TABLE t1;
INSERT INTO t1 VALUES (2, 'résumé');
OPTIMIZE TABLE t1;
DELETE FROM t1 WHERE id = 2;
OPTIMIZE TABLE t1;
SELECT * FROM t1 WHERE MATCH(a) against ('resume');
INSERT INTO t1 VALUES(3, 'résumé');
DELETE FROM t1;
OPTIMIZE TABLE t1;
SELECT * FROM t1 WHERE MATCH(a) against ('resume');
DROP TABLE t1;
SET GLOBAL innodb_optimize_fulltext_only = OFF;

View File

@@ -408,19 +408,19 @@ fts_optimize_read_node(
return(node);
}
/**********************************************************************//**
Callback function to fetch the rows in an FTS INDEX record.
@return always returns non-NULL */
ibool
fts_optimize_index_fetch_node(
/*==========================*/
void* row, /*!< in: sel_node_t* */
void* user_arg) /*!< in: pointer to ib_vector_t */
/** Add an FTS INDEX record to the fetched words.
@param row sel_node_t*
@param fetch fetch context
@param words the fetched words
@param charset if not NULL, the consecutive records whose words
compare equal under this collation are added to the same word
@return TRUE to continue fetching */
static ibool
fts_optimize_fetch_node(void *row, fts_fetch_t *fetch, ib_vector_t *words,
const CHARSET_INFO *charset= nullptr)
{
fts_word_t* word;
sel_node_t* sel_node = static_cast<sel_node_t*>(row);
fts_fetch_t* fetch = static_cast<fts_fetch_t*>(user_arg);
ib_vector_t* words = static_cast<ib_vector_t*>(fetch->read_arg);
que_node_t* exp = sel_node->select_list;
dfield_t* dfield = que_node_get_val(exp);
void* data = dfield_get_data(dfield);
@@ -439,8 +439,18 @@ fts_optimize_index_fetch_node(
word = static_cast<fts_word_t*>(ib_vector_last(words));
if (dfield_len != word->text.f_len
|| memcmp(word->text.f_str, data, dfield_len)) {
bool same_word;
if (charset) {
fts_string_t str;
str.f_str = static_cast<byte*>(data);
str.f_len = dfield_len;
same_word = !innobase_fts_text_cmp(charset, &word->text, &str);
} else {
same_word = dfield_len == word->text.f_len
&& !memcmp(word->text.f_str, data, dfield_len);
}
if (!same_word) {
word = static_cast<fts_word_t*>(ib_vector_push(words, NULL));
fts_word_init(word, (byte*) data, dfield_len);
@@ -465,6 +475,37 @@ fts_optimize_index_fetch_node(
return(TRUE);
}
/** Callback function to fetch the rows in an FTS INDEX record.
@param row sel_node_t for the row fetched from the table
@param user_arg pointer to ib_vector_t
@return always returns non-NULL */
ibool
fts_optimize_index_fetch_node(void *row, void *user_arg)
{
fts_fetch_t *fetch= static_cast<fts_fetch_t*>(user_arg);
return fts_optimize_fetch_node(
row, fetch, static_cast<ib_vector_t*>(fetch->read_arg));
}
/** Callback function to fetch the rows in an FTS INDEX record
for OPTIMIZE. The index lookup and the DELETE in
fts_optimize_write_word() match words by collation, so the
records of words that are equal under the index collation
(such as 'resume' and 'résumé' in utf8_general_ci) are
collected into a single word. Otherwise writing back one of
them would delete the rows that were written for the other.
@param row sel_node_t*
@param user_arg fts_fetch_t whose read_arg is the fts_optimize_t
@return always returns non-NULL */
static ibool
fts_optimize_collation_fetch_node(void *row, void *user_arg)
{
fts_fetch_t *fetch= static_cast<fts_fetch_t*>(user_arg);
fts_optimize_t *optim= static_cast<fts_optimize_t*>(fetch->read_arg);
return fts_optimize_fetch_node(row, fetch, optim->words,
optim->fts_index_table.charset);
}
/**********************************************************************//**
Read the rows from the FTS inde.
@return DB_SUCCESS or error code */
@@ -1786,8 +1827,8 @@ fts_optimize_words(
const time_t start_time = time(NULL);
/* Setup the callback to use for fetching the word ilist etc. */
fetch.read_arg = optim->words;
fetch.read_record = fts_optimize_index_fetch_node;
fetch.read_arg = optim;
fetch.read_record = fts_optimize_collation_fetch_node;
while (!optim->done) {
dberr_t error;
@@ -1806,8 +1847,15 @@ fts_optimize_words(
ut_ad(fetch.total_memory < fts_result_cache_limit);
if (error == DB_SUCCESS) {
/* There must be some nodes to read. */
ut_a(ib_vector_size(optim->words) > 0);
/* The word may already have been optimized and
rewritten together with an earlier word that compares
equal under the index collation, see
fts_optimize_collation_fetch_node(). The zip buffer
still holds the other spelling, so there is nothing
to read. */
if (ib_vector_size(optim->words) == 0) {
goto next_word;
}
/* Optimize the nodes that were read and write
back to DB. */
@@ -1820,6 +1868,7 @@ fts_optimize_words(
}
}
next_word:
ib_vector_reset(optim->words);
if (error == DB_SUCCESS) {