From d81720625d8f456f4b8b0f418fe146fd821583ef Mon Sep 17 00:00:00 2001 From: bsrikanth-mariadb Date: Thu, 23 Jul 2026 14:30:42 +0530 Subject: [PATCH] MDEV-40383:innodb_gis.point_basic fails on replay There are 2 problems: - 1. The REPLACE statement that is recorded doesn't store the value of geometry type field correctly. 2. The table definition that got recorded has fields with non-null constraint, and no default value is specified. Also, the "REPLACE INTO" statement that gets stored in the context, doesn't have any value specified for these non-null fields. Solution is to: - 1. When using REPLACE INTO statement, store all the non-numeric values in HEX, whenever conversion from field's charset to output's charset is lossy. 2. Instead of storing only the column values that were projected in the query, store all the non-virtual column values into the recorded REPLACE INTO statement. Implementation details: - 1. Introduce a new method is_target_cs_superset() in filesort.cc, to check if the output charset to which field's data is being written to, a superset of it. If so, non-numeric values being witten using REPLACE INTO statement are stored in string representation, else they are converted to HEX. 2. From join_read_const(), and join_read_system() methods in sql_select.cc, re-read the const row for all the non-virtual fields in the table. After the row is re-read and recorded, restore the table->read_set, table->status, and the const row, to the value that was before. --- .../main/opt_context_replay_basic.result | 39 +++++++++++ mysql-test/main/opt_context_replay_basic.test | 49 +++++++++++++ sql/filesort.cc | 43 ++++++++++-- sql/opt_context_store_replay.h | 5 -- sql/sql_select.cc | 70 +++++++++++++++++-- 5 files changed, 188 insertions(+), 18 deletions(-) diff --git a/mysql-test/main/opt_context_replay_basic.result b/mysql-test/main/opt_context_replay_basic.result index e8a091036c596..03317ac8409d5 100644 --- a/mysql-test/main/opt_context_replay_basic.result +++ b/mysql-test/main/opt_context_replay_basic.result @@ -549,4 +549,43 @@ select context like '%bar%' from information_schema.optimizer_context; context like '%bar%' 1 drop table t1; +# +# MDEV-40383: innodb_gis.point_basic fails on replay +# +CREATE TABLE t1 ( +a INT NOT NULL, +p POINT NOT NULL, +l LINESTRING NOT NULL, +g GEOMETRY NOT NULL, +PRIMARY KEY(p), +SPATIAL KEY `idx2` (p), +SPATIAL KEY `idx3` (l), +SPATIAL KEY `idx4` (g) +); +INSERT INTO t1 VALUES( +1, ST_GeomFromText('POINT(10 10)'), +ST_GeomFromText('LINESTRING(1 1, 5 5, 10 10)'), +ST_GeomFromText('POLYGON((30 30, 40 40, 50 50, 30 50, 30 40, 30 30))')); +INSERT INTO t1 VALUES( +2, ST_GeomFromText('POINT(20 20)'), +ST_GeomFromText('LINESTRING(2 3, 7 8, 9 10, 15 16)'), +ST_GeomFromText('POLYGON((10 30, 30 40, 40 50, 40 30, 30 20, 10 30))')); +set optimizer_record_context=1; +EXPLAIN SELECT a, ST_AsText(p) FROM t1 WHERE a = 2 AND p = ST_GeomFromText('POINT(20 20)'); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 const PRIMARY,idx2 PRIMARY 27 const 1 +select context into dumpfile "../../tmp/dump1.sql" +from information_schema.optimizer_context; +set optimizer_record_context=0; +drop table t1; +set optimizer_replay_context='opt_context'; +# Same query as above, must have same explain: +EXPLAIN SELECT a, ST_AsText(p) FROM t1 WHERE a = 2 AND p = ST_GeomFromText('POINT(20 20)'); +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 const PRIMARY,idx2 PRIMARY 27 const 1 +set optimizer_replay_context=''; +drop table t1; +# +# End of 13.1 tests +# drop database db1; diff --git a/mysql-test/main/opt_context_replay_basic.test b/mysql-test/main/opt_context_replay_basic.test index b8a17c0bf5758..17a6354e27655 100644 --- a/mysql-test/main/opt_context_replay_basic.test +++ b/mysql-test/main/opt_context_replay_basic.test @@ -351,4 +351,53 @@ explain select * from t1 where a >'foo' or a < 'bar'; select context like '%bar%' from information_schema.optimizer_context; drop table t1; + +--echo # +--echo # MDEV-40383: innodb_gis.point_basic fails on replay +--echo # + +CREATE TABLE t1 ( + a INT NOT NULL, + p POINT NOT NULL, + l LINESTRING NOT NULL, + g GEOMETRY NOT NULL, + PRIMARY KEY(p), + SPATIAL KEY `idx2` (p), + SPATIAL KEY `idx3` (l), + SPATIAL KEY `idx4` (g) +); + +INSERT INTO t1 VALUES( +1, ST_GeomFromText('POINT(10 10)'), +ST_GeomFromText('LINESTRING(1 1, 5 5, 10 10)'), +ST_GeomFromText('POLYGON((30 30, 40 40, 50 50, 30 50, 30 40, 30 30))')); + +INSERT INTO t1 VALUES( +2, ST_GeomFromText('POINT(20 20)'), +ST_GeomFromText('LINESTRING(2 3, 7 8, 9 10, 15 16)'), +ST_GeomFromText('POLYGON((10 30, 30 40, 40 50, 40 30, 30 20, 10 30))')); + +set optimizer_record_context=1; +EXPLAIN SELECT a, ST_AsText(p) FROM t1 WHERE a = 2 AND p = ST_GeomFromText('POINT(20 20)'); +select context into dumpfile "../../tmp/dump1.sql" +from information_schema.optimizer_context; +set optimizer_record_context=0; +drop table t1; +--disable_query_log +--disable_result_log +--source "$MYSQLTEST_VARDIR/tmp/dump1.sql" +--enable_query_log +--enable_result_log +set optimizer_replay_context='opt_context'; +--echo # Same query as above, must have same explain: +EXPLAIN SELECT a, ST_AsText(p) FROM t1 WHERE a = 2 AND p = ST_GeomFromText('POINT(20 20)'); + +set optimizer_replay_context=''; +--remove_file "$MYSQLTEST_VARDIR/tmp/dump1.sql" +drop table t1; + +--echo # +--echo # End of 13.1 tests +--echo # + drop database db1; diff --git a/sql/filesort.cc b/sql/filesort.cc index 0b8fbfb83feeb..52b42c941bf1f 100644 --- a/sql/filesort.cc +++ b/sql/filesort.cc @@ -3024,6 +3024,23 @@ static uint make_packed_sortkey(Sort_param *param, uchar *to) return length; } +static bool is_target_cs_superset(CHARSET_INFO *from_cs, CHARSET_INFO *to_cs) +{ + if (to_cs == &my_charset_bin) + return true; // binary swallows any bytes + if (from_cs == &my_charset_bin) + return false; // arbitrary bytes ⊄ text charset + if (my_charset_same(from_cs, to_cs)) + return true; // same repertoire family + if (from_cs->state & MY_CS_PUREASCII) // ASCII-only source... + return my_charset_is_ascii_based(to_cs); // ...into any ASCII-based target + if ((to_cs->state & MY_CS_UNICODE) && // Unicode target covers everything, + (to_cs->state & + MY_CS_UNICODE_SUPPLEMENT)) // incl. non-BMP (so utf8mb4/utf16/utf32, + return true; // but NOT plain utf8mb3/ucs2) + return false; // unknown → treat as possibly lossy +} + /* @brief Format the row record and store it in the output @@ -3148,12 +3165,26 @@ void format_and_store_row(TABLE *table, const uchar *rec, bool print_names, } field->val_str(&tmp); } - if (require_quote) - output.append('\''); - output.append_for_single_quote_opt_convert(tmp.ptr(), tmp.length(), - field->charset()); - if (require_quote) - output.append('\''); + /* + Emit non-empty values as a hex literal whenever converting field's + charset to the output charset conversion is lossy; otherwise emit the + charset-converted value, quoted only when the type requires it. + */ + if (require_quote && tmp.length() && + !is_target_cs_superset(field->charset(), output.charset())) + { + output.append(STRING_WITH_LEN("0x")); + output.append_hex(tmp.ptr(), tmp.length()); + } + else + { + if (require_quote) + output.append('\''); + output.append_for_single_quote_opt_convert(tmp.ptr(), tmp.length(), + field->charset()); + if (require_quote) + output.append('\''); + } } } output.append(')'); diff --git a/sql/opt_context_store_replay.h b/sql/opt_context_store_replay.h index 55b587dc2a166..719ff8525ef8e 100644 --- a/sql/opt_context_store_replay.h +++ b/sql/opt_context_store_replay.h @@ -66,11 +66,6 @@ class Optimizer_context_recorder const KEY_PART_INFO *key_part, uint keynr, const key_range *min_range, const key_range *max_range, ha_rows records); - void record_const_table_row(TABLE *tbl) - { - /* use table->record[1] */ - record_table_row(tbl, 1); - } void record_current_table_row(TABLE *tbl) { /* use table->record[0] */ diff --git a/sql/sql_select.cc b/sql/sql_select.cc index a82fb09ab0832..6594202f63176 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -25066,6 +25066,63 @@ join_read_const_table(THD *thd, JOIN_TAB *tab, POSITION *pos) DBUG_RETURN(0); } +/* + Re-read a const/system table row with ALL columns and record it for the + optimizer context. + + join_read_system()/join_read_const() only fetch the columns present in + table->read_set. That's sufficient for execution, but when we record the + const row for replay it yields an incomplete REPLACE INTO -- columns that + are NOT NULL and have no default then depend on a relaxed sql_mode (see + Optimizer_context_recorder::record_table_row()). Widen read_set to all + columns, re-read the single row, record it, then restore read_set so the + chosen plan is not disturbed. + + The caller must have cached the row the optimizer actually used in + record[1] before calling this. We re-read the full row into record[0] only + to record it, then unconditionally restore record[0] from record[1] so that + recording leaves execution's record[0] byte-for-byte identical to the + non-recording case -- regardless of whether the re-read found a row. +*/ +static void record_const_row_full(JOIN_TAB *tab, bool is_system) +{ + TABLE *table= tab->table; + Optimizer_context_recorder *rec= tab->join->thd->opt_ctx_recorder; + + MY_BITMAP *saved_read_set= table->read_set; + uint saved_status= table->status; + table->column_bitmaps_set(&table->s->all_set, table->write_set); + int error; + for (Field **pfield= table->field; *pfield; pfield++) + { + Field *field= *pfield; + /* virtual columns need not be stored. */ + if (field->vcol_info) + bitmap_fast_test_and_clear(table->read_set, field->field_index); + } + /* + Re-read the single const row with the widened read_set, mirroring how the + optimizer originally fetched it: a system table (join_read_system) has at + most one row, read via the primary key; a const eq_ref table + (join_read_const) is fetched by an exact lookup on tab->ref. + */ + if (is_system) + error= table->file->ha_read_first_row(table->record[0], + table->s->primary_key); + else + error= table->file->ha_index_read_idx_map( + table->record[0], tab->ref.key, (uchar *) tab->ref.key_buff, + make_prev_keypart_map(tab->ref.key_parts), HA_READ_KEY_EXACT); + + if (likely(!error)) + rec->record_current_table_row(table); // records the full record[0] + + /* Recording must not perturb the row execution uses: restore it. */ + restore_record(table, record[1]); + + table->column_bitmaps_set(saved_read_set, table->write_set); + table->status= saved_status; +} /** Read a constant table when there is at most one matching row, using a table @@ -25095,9 +25152,9 @@ join_read_system(JOIN_TAB *tab) empty_record(table); // Make empty record return -1; } - store_record(table,record[1]); - if (Optimizer_context_recorder *rec= tab->join->thd->opt_ctx_recorder) - rec->record_const_table_row(table); + store_record(table, record[1]); // cache the row the optimizer used + if (tab->join->thd->opt_ctx_recorder) + record_const_row_full(tab, true); // re-read full row, record, restore } else if (!table->status) // Only happens with left join restore_record(table,record[1]); // restore old record @@ -25152,10 +25209,9 @@ join_read_const(JOIN_TAB *tab) return report_error(table, error); return -1; } - store_record(table,record[1]); - - if (Optimizer_context_recorder *rec= tab->join->thd->opt_ctx_recorder) - rec->record_const_table_row(table); + store_record(table, record[1]); // cache the row the optimizer used + if (tab->join->thd->opt_ctx_recorder) + record_const_row_full(tab, false); // re-read full row, record, restore } else if (!(table->status & ~STATUS_NULL_ROW)) // Only happens with left join {