Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions mysql-test/main/mdev_39999.result
Original file line number Diff line number Diff line change
@@ -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]\""
48 changes: 48 additions & 0 deletions mysql-test/main/mdev_39999.test
Original file line number Diff line number Diff line change
@@ -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;

2 changes: 1 addition & 1 deletion mysql-test/suite/json/r/json_no_table.result
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
40 changes: 38 additions & 2 deletions sql/item_jsonfunc.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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] == ']' ||

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's more complex than this I'm afraid. https://www.rfc-editor.org/info/rfc9535/#segments-details defines what is a valid "shorthand" notation as follows:

child-segment       = bracketed-selection /
                      ("."
                       (wildcard-selector /
                        member-name-shorthand))

bracketed-selection = "[" S selector *(S "," S selector) S "]"

member-name-shorthand = name-first *name-char
name-first          = ALPHA /
                      "_"   /
                      %x80-D7FF /
                         ; skip surrogate code points
                      %xE000-10FFFF
name-char           = name-first / DIGIT

DIGIT               = %x30-39              ; 0-9
ALPHA               = %x41-5A / %x61-7A    ; A-Z / a-z

This is implemented by json_escape() in the MariaDB code I believe.

So here's what I would suggest: either always produce bracketed-selection (see above) or call st_append_escaped (or json_escape directly), see if the result is different from the source string and if it is, go with the bracketed expression that you have, otherwise do the unescaped source string.

This will have the disadvantage that you will always produce a bracketed escaped string. But I believe this is not such a big deal performance wise.

You could of course consider implementing a non-outputting version of json_escape() and call that json_needs_escaping() for example. But I believe it needs to be done along the same lines at least.

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('"'))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please consider using st_append_escaped() instead. See append_json_value_from_field(). And produce a "bracketed-selection" (see above).

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*/
{
Expand Down
Loading