From 16020a09e81f4f14176d00d4eab75cf3202ce885 Mon Sep 17 00:00:00 2001 From: bsrikanth-mariadb Date: Wed, 8 Jul 2026 09:53:14 +0530 Subject: [PATCH] MDEV-16462: explain format=json produces illegal json text The attached_condition field of the produced json text had "String with illegal unicode symbol" instead of the proper unicode value. The reason is that, the item field when being written to the json writer in write_item(), used a String with charset my_charset_bin, and that prevented the string from being escaped. Solution is to change: - 1. charset to my_charset_utf8mb4_bin in sql_explain.cc. 2. Item::print_value(), handle string type specially if the charset is my_charset_bin by appending hex value. --- mysql-test/main/cset_narrowing.result | 6 +-- mysql-test/main/explain_json.result | 61 +++++++++++++++++++++++++++ mysql-test/main/explain_json.test | 30 +++++++++++++ sql/item.cc | 10 +++++ sql/sql_explain.cc | 32 +++++++------- 5 files changed, 120 insertions(+), 19 deletions(-) diff --git a/mysql-test/main/cset_narrowing.result b/mysql-test/main/cset_narrowing.result index 797401c67d89b..e14d6c8cc256b 100644 --- a/mysql-test/main/cset_narrowing.result +++ b/mysql-test/main/cset_narrowing.result @@ -303,7 +303,7 @@ EXPLAIN "rows": 4, "cost": "COST_REPLACED", "filtered": 100, - "index_condition": "t1.mb3 = '????' or t1.mb3 = 'hello'" + "index_condition": "t1.mb3 = '😊' or t1.mb3 = 'hello'" } } ] @@ -338,7 +338,7 @@ EXPLAIN "rows": 10002, "cost": "COST_REPLACED", "filtered": 100, - "attached_condition": "convert(t1.mb3 using utf8mb4) > '????'" + "attached_condition": "convert(t1.mb3 using utf8mb4) > '😊'" } } ] @@ -384,7 +384,7 @@ EXPLAIN "rows": 3, "cost": "COST_REPLACED", "filtered": 100, - "index_condition": "t2.mb4 > '????'" + "index_condition": "t2.mb4 > '😊'" } } ] diff --git a/mysql-test/main/explain_json.result b/mysql-test/main/explain_json.result index 84cffd18424b9..81409cfbc1590 100644 --- a/mysql-test/main/explain_json.result +++ b/mysql-test/main/explain_json.result @@ -2305,3 +2305,64 @@ SET optimizer_trace = 'enabled=on'; SELECT * FROM t1 WHERE a IN ( SELECT b FROM t2 INNER JOIN t1 ON (a = pk) ); a DROP TABLE t1, t2; +# +# MDEV-16462: EXPLAIN FORMAT=JSON produces illegal json text in the attached_condition field +# +set names utf8mb4; +create table t1 ( +id int(11) not null auto_increment, +oid binary(16) not null, +primary key (id, oid) +); +insert into t1 values +(1,0x02697732663011E39AE220CF3068F21D), +(2,0x02697732663011E39AE220CF3068F21D); +create table t2 ( +id int(11) not null, +oid binary(16) not null, +primary key (id) +); +insert into t2 values +(1,0x02697732663011E39AE220CF3068F21D), +(2,0x02697732663011E39AE220CF3068F21D); +explain format=json +select t1.id from t1, t2 where t1.oid=t2.oid and t2.id=1; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": 0.006067082, + "nested_loop": [ + { + "table": { + "table_name": "t2", + "access_type": "const", + "possible_keys": ["PRIMARY"], + "key": "PRIMARY", + "key_length": "4", + "used_key_parts": ["id"], + "ref": ["const"], + "rows": 1, + "filtered": 100 + } + }, + { + "table": { + "table_name": "t1", + "access_type": "index", + "key": "PRIMARY", + "key_length": "20", + "used_key_parts": ["id", "oid"], + "loops": 1, + "rows": 2, + "cost": 0.006067082, + "filtered": 100, + "attached_condition": "t1.oid = 0x02697732663011E39AE220CF3068F21D", + "using_index": true + } + } + ] + } +} +drop table t1, t2; +set names default; diff --git a/mysql-test/main/explain_json.test b/mysql-test/main/explain_json.test index 0d34b95070426..39d44b08544d4 100644 --- a/mysql-test/main/explain_json.test +++ b/mysql-test/main/explain_json.test @@ -500,3 +500,33 @@ INSERT INTO t2 VALUES SET optimizer_trace = 'enabled=on'; SELECT * FROM t1 WHERE a IN ( SELECT b FROM t2 INNER JOIN t1 ON (a = pk) ); DROP TABLE t1, t2; + +--echo # +--echo # MDEV-16462: EXPLAIN FORMAT=JSON produces illegal json text in the attached_condition field +--echo # +set names utf8mb4; +create table t1 ( + id int(11) not null auto_increment, + oid binary(16) not null, + primary key (id, oid) +); + +insert into t1 values +(1,0x02697732663011E39AE220CF3068F21D), +(2,0x02697732663011E39AE220CF3068F21D); + +create table t2 ( + id int(11) not null, + oid binary(16) not null, + primary key (id) +); + +insert into t2 values +(1,0x02697732663011E39AE220CF3068F21D), +(2,0x02697732663011E39AE220CF3068F21D); + +explain format=json +select t1.id from t1, t2 where t1.oid=t2.oid and t2.id=1; + +drop table t1, t2; +set names default; diff --git a/sql/item.cc b/sql/item.cc index 3bdd7f82da327..e9bf4c61af0c8 100644 --- a/sql/item.cc +++ b/sql/item.cc @@ -555,6 +555,16 @@ void Item::print_value(String *str) { switch (cmp_type()) { case STRING_RESULT: + if (ptr->charset() == &my_charset_bin && ptr->length()) + { + StringBuffer<64> escaped_val; + escaped_val.set_hex(ptr->ptr(), ptr->length()); + str->append(STRING_WITH_LEN("0x")); + str->append(escaped_val); + } + else + append_unescaped(str, ptr->ptr(), ptr->length()); + break; case TIME_RESULT: append_unescaped(str, ptr->ptr(), ptr->length()); break; diff --git a/sql/sql_explain.cc b/sql/sql_explain.cc index 77aab40092f65..288dd0637c786 100644 --- a/sql/sql_explain.cc +++ b/sql/sql_explain.cc @@ -211,15 +211,15 @@ int Explain_query::send_explain(THD *thd, bool extended) if (extended) { char buff[1024]; - String str(buff,(uint32) sizeof(buff), system_charset_info); - str.length(0); - /* - The warnings system requires input in utf8, @see - mysqld_show_warnings(). - */ - lex->unit.print(&str, QT_EXPLAIN_EXTENDED); - push_warning(thd, Sql_condition::WARN_LEVEL_NOTE, - ER_YES, str.c_ptr_safe()); + String str(buff, (uint32) sizeof(buff), &my_charset_utf8mb4_bin); + str.length(0); + /* + The warnings system requires input in utf8, @see + mysqld_show_warnings(). + */ + lex->unit.print(&str, QT_EXPLAIN_EXTENDED); + push_warning(thd, Sql_condition::WARN_LEVEL_NOTE, ER_YES, + str.c_ptr_safe()); } } if (res) @@ -330,7 +330,7 @@ bool Explain_query::print_query_blocks_json(Json_writer *writer, const bool is_a void Explain_query::send_explain_json_to_output(Json_writer *writer, select_result_sink *output) { - CHARSET_INFO *cs= system_charset_info; + CHARSET_INFO *cs= &my_charset_utf8mb4_bin; List item_list; const String *buf= writer->output.get_string(); THD *thd= output->thd; @@ -1287,7 +1287,7 @@ void Explain_aggr_filesort::print_json_members(Json_writer *writer, bool is_analyze) { char item_buf[256]; - String str(item_buf, sizeof(item_buf), &my_charset_bin); + String str(item_buf, sizeof(item_buf), &my_charset_utf8mb4_bin); str.length(0); List_iterator_fast it(sort_items); @@ -1389,7 +1389,7 @@ void Explain_table_access::push_extra(enum explain_extra_tag extra_tag) void Explain_table_access::fill_key_str(String *key_str, bool is_json) const { - CHARSET_INFO *cs= system_charset_info; + CHARSET_INFO *cs= &my_charset_utf8mb4_bin; bool is_hj= (type == JT_HASH || type == JT_HASH_NEXT || type == JT_HASH_RANGE || type == JT_HASH_INDEX_MERGE); LEX_CSTRING hash_key_prefix= { STRING_WITH_LEN("#hash#") }; @@ -1775,7 +1775,7 @@ static void write_item(Json_writer *writer, Item *item) { THD *thd= current_thd; char item_buf[256]; - String str(item_buf, sizeof(item_buf), &my_charset_bin); + String str(item_buf, sizeof(item_buf), &my_charset_utf8mb4_bin); str.length(0); ulonglong save_option_bits= thd->variables.option_bits; @@ -1784,7 +1784,7 @@ static void write_item(Json_writer *writer, Item *item) item->print(&str, QT_EXPLAIN); thd->variables.option_bits= save_option_bits; - writer->add_str(str.c_ptr_safe()); + writer->add_str(str.c_ptr_safe(), str.length()); } static void append_item_to_str(String *out, Item *item) @@ -2739,10 +2739,10 @@ int Explain_update::print_explain(Explain_query *query, else if (key.get_key_name()) { const char *name= key.get_key_name(); - key_buf.set(name, strlen(name), &my_charset_bin); + key_buf.set(name, strlen(name), &my_charset_utf8mb4_bin); char buf[64]; size_t length= longlong10_to_str(key.get_key_len(), buf, 10) - buf; - key_len_buf.copy(buf, length, &my_charset_bin); + key_len_buf.copy(buf, length, &my_charset_utf8mb4_bin); } if (using_where)