diff --git a/mysql-test/main/mdev_39999.result b/mysql-test/main/mdev_39999.result new file mode 100644 index 0000000000000..fb96322e39dc0 --- /dev/null +++ b/mysql-test/main/mdev_39999.result @@ -0,0 +1,45 @@ +# +# MDEV-39999: JSON_SEARCH must quote keys containing special characters +# +SELECT JSON_SEARCH('{"a.b":"x"}', 'one', 'x') AS path; +path +"$.\"a.b\"" +SELECT JSON_SEARCH('{"a[0]":"x"}', 'one', 'x') AS path; +path +"$.\"a[0]\"" +SELECT JSON_SEARCH('{"normal":"x"}', 'one', 'x') AS path; +path +"$.normal" +SELECT JSON_SEARCH('{"hello world":"x"}', 'one', 'x') AS path; +path +"$.\"hello world\"" +SELECT JSON_SEARCH('{"a*b":"x"}', 'one', 'x') AS path; +path +"$.\"a*b\"" +SELECT JSON_SEARCH('{"a\\"b":"x"}', 'one', 'x') AS path; +path +"$.\"a\\\"b\"" +SELECT JSON_SEARCH('{"a\\\\b":"x"}', 'one', 'x') AS path; +path +"$.\"a\\\\b\"" +SELECT JSON_SEARCH('{"1abc":"x"}', 'one', 'x') AS path; +path +"$.1abc" +SELECT JSON_SEARCH('{"":"x"}', 'one', 'x') AS path; +path +"$.\"\"" +SELECT JSON_VALUE('{"":"x"}', JSON_UNQUOTE(JSON_SEARCH('{"":"x"}', 'one', 'x'))) AS val; +val +x +SELECT JSON_SEARCH('{"a.b":"x", "c":"x", "d[0]":"x"}', 'all', 'x') AS paths; +paths +["$.\"a.b\"", "$.c", "$.\"d[0]\""] +SELECT JSON_VALUE('{"a.b":"found"}', JSON_UNQUOTE(JSON_SEARCH('{"a.b":"found"}', 'one', 'found'))) AS val; +val +found +SELECT JSON_VALUE('{"a\\\\b":"found"}', JSON_UNQUOTE(JSON_SEARCH('{"a\\\\b":"found"}', 'one', 'found'))) AS val; +val +found +SELECT JSON_SEARCH('{"a.b":{"c[d]":"x"}}', 'one', 'x') AS path; +path +"$.\"a.b\".\"c[d]\"" diff --git a/mysql-test/main/mdev_39999.test b/mysql-test/main/mdev_39999.test new file mode 100644 index 0000000000000..c534e3c3ec814 --- /dev/null +++ b/mysql-test/main/mdev_39999.test @@ -0,0 +1,48 @@ +# +# MDEV-39999: JSON path quoting for keys with special characters +# + +--echo # +--echo # MDEV-39999: JSON_SEARCH must quote keys containing special characters +--echo # + +# Dot in key +SELECT JSON_SEARCH('{"a.b":"x"}', 'one', 'x') AS path; + +# Bracket in key +SELECT JSON_SEARCH('{"a[0]":"x"}', 'one', 'x') AS path; + +# Normal key (no quoting needed) +SELECT JSON_SEARCH('{"normal":"x"}', 'one', 'x') AS path; + +# Space in key +SELECT JSON_SEARCH('{"hello world":"x"}', 'one', 'x') AS path; + +# Asterisk in key +SELECT JSON_SEARCH('{"a*b":"x"}', 'one', 'x') AS path; + +# Embedded double-quote in key +SELECT JSON_SEARCH('{"a\\"b":"x"}', 'one', 'x') AS path; + +# Backslash in key +SELECT JSON_SEARCH('{"a\\\\b":"x"}', 'one', 'x') AS path; + +# Key starting with digit +SELECT JSON_SEARCH('{"1abc":"x"}', 'one', 'x') AS path; + +# Empty key +SELECT JSON_SEARCH('{"":"x"}', 'one', 'x') AS path; +SELECT JSON_VALUE('{"":"x"}', JSON_UNQUOTE(JSON_SEARCH('{"":"x"}', 'one', 'x'))) AS val; + +# JSON_SEARCH with 'all' mode - multiple matches +SELECT JSON_SEARCH('{"a.b":"x", "c":"x", "d[0]":"x"}', 'all', 'x') AS paths; + +# Roundtrip: path from JSON_SEARCH works with JSON_VALUE +SELECT JSON_VALUE('{"a.b":"found"}', JSON_UNQUOTE(JSON_SEARCH('{"a.b":"found"}', 'one', 'found'))) AS val; + +# Roundtrip with backslash key +SELECT JSON_VALUE('{"a\\\\b":"found"}', JSON_UNQUOTE(JSON_SEARCH('{"a\\\\b":"found"}', 'one', 'found'))) AS val; + +# Nested object with special keys +SELECT JSON_SEARCH('{"a.b":{"c[d]":"x"}}', 'one', 'x') AS path; + diff --git a/mysql-test/suite/json/r/json_no_table.result b/mysql-test/suite/json/r/json_no_table.result index 500c7baaac169..8b084ac4d0e19 100644 --- a/mysql-test/suite/json/r/json_no_table.result +++ b/mysql-test/suite/json/r/json_no_table.result @@ -2740,7 +2740,7 @@ JSON_SEARCH 'food' ) ) -$.one potato +$."one potato" select json_type(case (null is null) when 1 then json_compact('null') else json_compact('[1,2,3]') end); diff --git a/sql/item_jsonfunc.cc b/sql/item_jsonfunc.cc index 74477fd506e0d..584d97d7b8967 100644 --- a/sql/item_jsonfunc.cc +++ b/sql/item_jsonfunc.cc @@ -4378,9 +4378,45 @@ static int append_json_path(String *str, const json_path_t *p) { if (c->type & JSON_PATH_KEY) { - if (str->append(".", 1) || - append_simple(str, c->key, c->key_end-c->key)) + const char *k= (const char *) c->key; + size_t k_len= c->key_end - c->key; + bool needs_quote= (k_len == 0); + for (size_t i= 0; i < k_len; i++) + { + if (k[i] == '.' || k[i] == '[' || k[i] == ']' || + k[i] == '"' || k[i] == ' ' || k[i] == '*' || k[i] == '\\') + { + needs_quote= true; + break; + } + } + if (str->append(".", 1)) return TRUE; + if (needs_quote) + { + if (str->append('\\') || str->append('"')) + return TRUE; + for (size_t i= 0; i < k_len; i++) + { + if (k[i] == '"' || k[i] == '\\') + { + if (str->append('\\') || str->append(k[i])) + return TRUE; + } + else + { + if (str->append(k[i])) + return TRUE; + } + } + if (str->append('\\') || str->append('"')) + return TRUE; + } + else + { + if (append_simple(str, c->key, k_len)) + return TRUE; + } } else /*JSON_PATH_ARRAY*/ {