diff --git a/mysql-test/main/opt_context_replay_basic.result b/mysql-test/main/opt_context_replay_basic.result index 6ffa4f5a55e37..e4d6e12e1bb7a 100644 --- a/mysql-test/main/opt_context_replay_basic.result +++ b/mysql-test/main/opt_context_replay_basic.result @@ -409,4 +409,33 @@ id select_type table type possible_keys key key_len ref rows Extra 1 SIMPLE t1 ALL btn NULL NULL NULL 4 Using where set optimizer_replay_context=''; drop table t1; +# +# MDEV-39412: Failed to parse saved optimizer context: error reading ranges value +# +set optimizer_record_context=0; +CREATE TABLE t1( +a VARCHAR(8), +b VARCHAR(8), +KEY(A), +KEY(B) +); +INSERT INTO t1 SELECT REPEAT('a',8), REPEAT('b',8) FROM seq_1_to_10; +set optimizer_record_context=1; +EXPLAIN +SELECT * FROM t1 FORCE INDEX(a,b) WHERE a LIKE 'a%' OR b LIKE 'b%' +ORDER BY a,b; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 index_merge a,b a,b 35,35 NULL 10 Using sort_union(a,b); Using where; Using filesort +select context into dumpfile "../../tmp/dump1.sql" +from information_schema.optimizer_context; +drop table t1; +set optimizer_replay_context='opt_context'; +# Same query as above, must have same explain: +EXPLAIN +SELECT * FROM t1 FORCE INDEX(a,b) WHERE a LIKE 'a%' OR b LIKE 'b%' +ORDER BY a,b; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 index_merge a,b a,b 35,35 NULL 10 Using sort_union(a,b); Using where; Using filesort +set optimizer_replay_context=''; +drop table t1; drop database db1; diff --git a/mysql-test/main/opt_context_replay_basic.test b/mysql-test/main/opt_context_replay_basic.test index 70b712ac825df..101309c760cf9 100644 --- a/mysql-test/main/opt_context_replay_basic.test +++ b/mysql-test/main/opt_context_replay_basic.test @@ -200,4 +200,41 @@ set optimizer_replay_context=''; --remove_file "$MYSQLTEST_VARDIR/tmp/dump1.sql" drop table t1; +--echo # +--echo # MDEV-39412: Failed to parse saved optimizer context: error reading ranges value +--echo # +set optimizer_record_context=0; + +CREATE TABLE t1( + a VARCHAR(8), + b VARCHAR(8), + KEY(A), + KEY(B) +); +INSERT INTO t1 SELECT REPEAT('a',8), REPEAT('b',8) FROM seq_1_to_10; + +set optimizer_record_context=1; +EXPLAIN +SELECT * FROM t1 FORCE INDEX(a,b) WHERE a LIKE 'a%' OR b LIKE 'b%' +ORDER BY a,b; + +select context into dumpfile "../../tmp/dump1.sql" +from information_schema.optimizer_context; +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 * FROM t1 FORCE INDEX(a,b) WHERE a LIKE 'a%' OR b LIKE 'b%' +ORDER BY a,b; + +set optimizer_replay_context=''; +--remove_file "$MYSQLTEST_VARDIR/tmp/dump1.sql" +drop table t1; + drop database db1; diff --git a/sql/opt_context_store_replay.cc b/sql/opt_context_store_replay.cc index 7692b34d9a1e5..7accd4f0a0d3e 100644 --- a/sql/opt_context_store_replay.cc +++ b/sql/opt_context_store_replay.cc @@ -233,12 +233,18 @@ void dump_mrr_info_calls(List *mrr_list, Json_writer_object irc_wrapper(ctx_writer); irc_wrapper.add("index_name", irc->idx_name); + List_iterator rc_li(irc->range_list); + Json_writer_array ranges_wrapper(ctx_writer, "ranges"); + StringBuffer<128> escaped_range_info; + while (const char *range_str= rc_li++) { - Json_writer_array ranges_wrapper(ctx_writer, "ranges"); - List_iterator rc_li(irc->range_list); - while (const char *range_str= rc_li++) - ranges_wrapper.add(range_str, strlen(range_str)); + const String range_info(range_str, strlen(range_str), + system_charset_info); + json_escape_to_string(&range_info, &escaped_range_info); + ranges_wrapper.add(escaped_range_info.c_ptr_safe(), + escaped_range_info.length()); } + ranges_wrapper.end(); irc_wrapper.add("num_rows", irc->rows); { @@ -518,6 +524,31 @@ static bool store_db_ddl(THD *thd, HASH *db_name_hash, String &script, return false; } +/* + @brief + Append the json "str" to sql_script, by escaping only backslash, + and single quote +*/ +static void escape_json_for_sql_literal(String &sql_script, const char *str, + size_t len) +{ + const char *end= str + len; + for (; str < end; str++) + { + switch (*str) + { + case '\\': + sql_script.append(STRING_WITH_LEN("\\\\")); + break; + case '\'': + sql_script.append(STRING_WITH_LEN("\\'")); + break; + default: + sql_script.append(*str); + } + } +} + /* @brief Dump definitions, basic stats of all tables and views used by the @@ -719,9 +750,12 @@ bool store_optimizer_context(THD *thd) const char *SET_OPT_CONTEXT_VAR= "set @opt_context=\'\n"; const char *SET_REPLAY_CONTEXT_VAR= "set optimizer_replay_context=\'opt_context\'"; - String *s= const_cast(ctx_writer.output.get_string()); sql_script.append(SET_OPT_CONTEXT_VAR, strlen(SET_OPT_CONTEXT_VAR)); - sql_script.append(*s); + String *opt_context= const_cast(ctx_writer.output.get_string()); + // require extra escaping of the opt_ctx so as to counter the + // unescaping done by sql parse + escape_json_for_sql_literal(sql_script, opt_context->c_ptr_safe(), + opt_context->length()); sql_script.append(STRING_WITH_LEN("\n\';#opt_context_ends\n\n")); sql_script.append(SET_REPLAY_CONTEXT_VAR, strlen(SET_REPLAY_CONTEXT_VAR)); sql_script.append(STRING_WITH_LEN(";\n\n")); diff --git a/unittest/sql/json_reader-t.cc b/unittest/sql/json_reader-t.cc index 72e6d1670957b..e53ed1854ba94 100644 --- a/unittest/sql/json_reader-t.cc +++ b/unittest/sql/json_reader-t.cc @@ -39,25 +39,29 @@ int main(int args, char **argv) MEM_ROOT alloc; json_engine_t je; int rc; + const char *esc_str_val= "a\bc"; init_alloc_root(0, &alloc, 32768, 0, 0); mem_root_dynamic_array_init(&alloc, 0, &je.stack, sizeof(int), NULL, JSON_DEPTH_DEFAULT, JSON_DEPTH_INC, MYF(0)); system_charset_info= &my_charset_utf8mb3_bin; - const char *js_doc="{ \"str_val\": \"abc\", \"double_val\": 1234.5 }"; + const char *js_doc= "{ \"str_val\": \"abc\", \"double_val\": 1234.5, " + "\"esc_str_val\": \"a\\bc\" }"; json_scan_start(&je, &my_charset_utf8mb3_bin, (const uchar *) js_doc, (const uchar *) js_doc + strlen(js_doc)); char *parsed_name; double parsed_dbl; + char *parsed_esc_str; Read_named_member array[]= { - {"str_val", Read_string(&alloc, &parsed_name), false}, + {"str_val", Read_string(&alloc, &parsed_name), false}, {"double_val", Read_double(&parsed_dbl), false}, - {NULL, Read_double(NULL), false } - }; + {"esc_str_val", Read_string(&alloc, &parsed_esc_str), false}, + {NULL, Read_double(NULL), false}}; String err_buf; - rc= json_read_object(&je, array, &err_buf); + rc= json_read_object(&je, array, &err_buf) || + strcmp(parsed_esc_str, esc_str_val); ok(!rc, "Basic object read"); free_root(&alloc, 0); #if 0