From da52af649ccd6356f5017b4756d31c72abd47f21 Mon Sep 17 00:00:00 2001 From: kjarir Date: Wed, 4 Mar 2026 09:51:17 +0530 Subject: [PATCH] MDEV-18530: Implement -> and ->> JSON path operators --- mysql-test/main/json_operators.result | 315 ++++++++++++++++++ mysql-test/main/json_operators.test | 193 +++++++++++ .../sys_vars/r/ft_boolean_syntax_basic.result | 2 +- sql/lex.h | 2 + sql/sql_lex.cc | 11 + sql/sql_yacc.yy | 56 +++- 6 files changed, 573 insertions(+), 6 deletions(-) create mode 100644 mysql-test/main/json_operators.result create mode 100644 mysql-test/main/json_operators.test diff --git a/mysql-test/main/json_operators.result b/mysql-test/main/json_operators.result new file mode 100644 index 0000000000000..f500c3c4a4ff7 --- /dev/null +++ b/mysql-test/main/json_operators.result @@ -0,0 +1,315 @@ +# +# MDEV-13594: Support for -> and ->> JSON operators +# +# 1. Basic Extraction with -> +SELECT '{"a": 1, "b": "text"}'->'$.a' AS int_val, +'{"a": 1, "b": "text"}'->'$.b' AS str_val; +int_val str_val +1 "text" +# 2. Unquoted Extraction with ->> +SELECT '{"a": 1, "b": "text"}'->> '$.a' AS int_val, +'{"a": 1, "b": "text"}'->> '$.b' AS str_val; +int_val str_val +1 text +# 3. Chaining Operators +# Should support nested extraction +SELECT '{"a": {"b": {"c": 42}}}'->'$.a'->'$.b'->'$.c' AS chained_arrow, +'{"a": {"b": {"c": 42}}}'->'$.a'->'$.b'->>'$.c' AS chained_unquoted; +chained_arrow chained_unquoted +42 42 +# 4. Operator Precedence +# -> and ->> bind tighter than arithmetic operators +SELECT '{"a": 5}'->'$.a' + 1 AS arrow_plus_one; +arrow_plus_one +6 +SELECT '{"a": 5}'->> '$.a' + 1 AS unquoted_arrow_plus_one; +unquoted_arrow_plus_one +6 +SELECT '{"a": 5}'->'$.a' * 2 AS arrow_times_two; +arrow_times_two +10 +# 5. Table Integration +CREATE TABLE t1 ( +id INT PRIMARY KEY, +data JSON +); +INSERT INTO t1 VALUES +(1, '{"name": "Alice", "meta": {"age": 25, "city": "London"}}'), +(2, '{"name": "Bob", "meta": {"age": 30, "city": "Paris"}}'), +(3, '{"name": "Charlie", "meta": null}'); +# Usage in SELECT list +SELECT id, data->'$.name', data->>'$.meta.city' FROM t1; +id data->'$.name' data->>'$.meta.city' +1 "Alice" London +2 "Bob" Paris +3 "Charlie" NULL +# Usage in WHERE clause +SELECT data->>'$.name' FROM t1 WHERE data->'$.meta.age' > 25; +data->>'$.name' +Bob +# Usage in ORDER BY +SELECT data->>'$.name' FROM t1 ORDER BY data->'$.meta.age' DESC; +data->>'$.name' +Bob +Alice +Charlie +DROP TABLE t1; +# 6. Edge Cases: Invalid JSON and Paths +# Invalid JSON text returns a warning; result is NULL +SELECT '{"a": 1'->'$.a'; +'{"a": 1'->'$.a' +NULL +Warnings: +Warning 4037 Unexpected end of JSON text in argument 1 to function 'json_extract' +# Missing key returns NULL (no error) +SELECT '{"a": 1}'->'$.no_key'; +'{"a": 1}'->'$.no_key' +NULL +# Invalid path returns a warning; result is NULL +SELECT '{"a": 1}'->'invalid_path'; +'{"a": 1}'->'invalid_path' +NULL +Warnings: +Warning 4042 Syntax error in JSON path in argument 2 to function 'json_extract' at position 1 +# 7. Integration with Generated Columns +CREATE TABLE t2 ( +j JSON, +name VARCHAR(50) AS (j->>'$.name') VIRTUAL, +INDEX (name) +); +INSERT INTO t2 (j) VALUES ('{"name": "Zoe", "score": 100}'); +SELECT name FROM t2; +name +Zoe +EXPLAIN SELECT name FROM t2 WHERE name = 'Zoe'; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 system name NULL NULL NULL 1 +DROP TABLE t2; +# 8. Chaining with non-JSON results +# ->> returns a string. If the string is valid JSON, another -> can follow. +SELECT '{"outer": "{\\"inner\\": 1}"}'->> '$.outer'->'$.inner' AS complex_chain; +complex_chain +1 +# +# 9. Ported tests from MySQL (Oracle). Authors at Oracle Corporation. +# Path: mysql-test/suite/json/inc/json_functions.inc +# +CREATE TABLE t1_mysql(autopk int primary key auto_increment, f1 JSON); +INSERT INTO t1_mysql(f1) VALUES +('{"a":1}'), +('{"a":3}'), +('{"a":2}'), +('{"a":11, "b":3}'), +('{"a":33, "b":1}'), +('{"a":22,"b":2}'); +ANALYZE TABLE t1_mysql; +Table Op Msg_type Msg_text +test.t1_mysql analyze status Engine-independent statistics collected +test.t1_mysql analyze Warning Engine-independent statistics are not collected for column 'f1' +test.t1_mysql analyze status OK +SELECT f1->"$.a" FROM t1_mysql order by autopk; +f1->"$.a" +1 +3 +2 +11 +33 +22 +EXPLAIN SELECT f1->"$.a" FROM t1_mysql order by autopk; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1_mysql ALL NULL NULL NULL NULL 6 Using filesort +SELECT f1->"$.a" FROM t1_mysql WHERE f1->"$.b" > 1 order by autopk; +f1->"$.a" +11 +22 +EXPLAIN SELECT f1->"$.a" FROM t1_mysql WHERE f1->"$.b" > 1 order by autopk; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1_mysql ALL NULL NULL NULL NULL 6 Using where; Using filesort +SELECT f1->"$.a", f1->"$.b" FROM t1_mysql ORDER BY autopk; +f1->"$.a" f1->"$.b" +1 NULL +3 NULL +2 NULL +11 3 +33 1 +22 2 +SELECT MAX(f1->"$.a"), f1->"$.b" FROM t1_mysql GROUP BY f1->"$.b"; +MAX(f1->"$.a") f1->"$.b" +11 3 +22 2 +3 NULL +33 1 +SELECT JSON_OBJECT("c",f1->"$.b") AS f2 +FROM t1_mysql HAVING JSON_TYPE(f2->"$.c") <> 'NULL' ORDER BY autopk; +f2 +{"c": 3} +{"c": 1} +{"c": 2} +Test unquoting operator +INSERT INTO t1_mysql(f1) VALUES +('{"t":"a"}'), +('{"t":"b"}'), +('{"t":"c"}'); +Returned values should be quoted +SELECT f1->"$.t" FROM t1_mysql WHERE f1->"$.t" <> 'NULL' order by autopk; +f1->"$.t" +"a" +"b" +"c" +Returned values should be unquoted +SELECT f1->>"$.t" FROM t1_mysql WHERE f1->>"$.t" <> 'NULL' order by autopk; +f1->>"$.t" +a +b +c +# Error cases (using MariaDB error codes/behaviors) +# MariaDB allows NULL on the RHS (returns NULL) +SELECT f1->>NULL FROM t1_mysql; +f1->>NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +# Invalid path returns a warning +SELECT f1 ->> "NULL" FROM t1_mysql; +f1 ->> "NULL" +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +Warnings: +Warning 4042 Syntax error in JSON path in argument 2 to function 'json_extract' at position 1 +Warning 4042 Syntax error in JSON path in argument 2 to function 'json_extract' at position 1 +Warning 4042 Syntax error in JSON path in argument 2 to function 'json_extract' at position 1 +Warning 4042 Syntax error in JSON path in argument 2 to function 'json_extract' at position 1 +Warning 4042 Syntax error in JSON path in argument 2 to function 'json_extract' at position 1 +Warning 4042 Syntax error in JSON path in argument 2 to function 'json_extract' at position 1 +Warning 4042 Syntax error in JSON path in argument 2 to function 'json_extract' at position 1 +Warning 4042 Syntax error in JSON path in argument 2 to function 'json_extract' at position 1 +Warning 4042 Syntax error in JSON path in argument 2 to function 'json_extract' at position 1 +INSERT INTO t1_mysql(f1) VALUES +('[ { "a": 1 }, { "a": 2 } ]'), +('{ "a" : "foo", "b" : [ true, { "c" : 123, "c" : 456 } ] }'), +('{ "a" : "foo", "b" : [ true, { "c" : "123" } ] }'), +('{ "a" : "foo", "b" : [ true, { "c" : 123 } ] }'); +SELECT +f1->>"$**.b", +JSON_UNQUOTE(JSON_EXTRACT(f1,"$**.b")) +FROM t1_mysql order by autopk; +f1->>"$**.b" JSON_UNQUOTE(JSON_EXTRACT(f1,"$**.b")) +NULL NULL +NULL NULL +NULL NULL +[3] [3] +[1] [1] +[2] [2] +NULL NULL +NULL NULL +NULL NULL +NULL NULL +[[true, {"c": 123, "c": 456}]] [[true, {"c": 123, "c": 456}]] +[[true, {"c": "123"}]] [[true, {"c": "123"}]] +[[true, {"c": 123}]] [[true, {"c": 123}]] +SELECT +f1->>"$.c", +JSON_UNQUOTE(JSON_EXTRACT(f1,"$.c")) +FROM t1_mysql order by autopk; +f1->>"$.c" JSON_UNQUOTE(JSON_EXTRACT(f1,"$.c")) +NULL NULL +NULL NULL +NULL NULL +NULL NULL +NULL NULL +NULL NULL +NULL NULL +NULL NULL +NULL NULL +NULL NULL +NULL NULL +NULL NULL +NULL NULL +DROP TABLE t1_mysql; +# +# 10. Ported tests from MySQL (Oracle). Authors at Oracle Corporation. +# Path: mysql-test/suite/json/t/json_prep_stmts.test +# +CREATE TABLE t1_prep(j JSON); +set @int=123; +set @intstr='123'; +set @dec=3.14; +set @decstr='3.14'; +set @flt=3.14E1; +set @fltstr='3.14E1'; +set @strstr='xyz'; +set @quotstr='"xyz"'; +set @json='{"int" : 123, "dec" : 3.14, "flt" : 3.14E1, "str" : "xyz", "array" : [1, 2, 4]}'; +insert into t1_prep values (@json); +prepare ps_get_int from "select * from t1_prep where j->'$.int' = ?"; +prepare ps_get_dec from "select * from t1_prep where j->'$.dec' = ?"; +prepare ps_get_flt from "select * from t1_prep where j->'$.flt' = ?"; +prepare ps_get_str from "select * from t1_prep where j->'$.str' = ?"; +execute ps_get_int using @int; +j +{"int" : 123, "dec" : 3.14, "flt" : 3.14E1, "str" : "xyz", "array" : [1, 2, 4]} +execute ps_get_int using @intstr; +j +{"int" : 123, "dec" : 3.14, "flt" : 3.14E1, "str" : "xyz", "array" : [1, 2, 4]} +execute ps_get_dec using @dec; +j +{"int" : 123, "dec" : 3.14, "flt" : 3.14E1, "str" : "xyz", "array" : [1, 2, 4]} +execute ps_get_dec using @decstr; +j +{"int" : 123, "dec" : 3.14, "flt" : 3.14E1, "str" : "xyz", "array" : [1, 2, 4]} +execute ps_get_flt using @flt; +j +{"int" : 123, "dec" : 3.14, "flt" : 3.14E1, "str" : "xyz", "array" : [1, 2, 4]} +execute ps_get_flt using @fltstr; +j +{"int" : 123, "dec" : 3.14, "flt" : 3.14E1, "str" : "xyz", "array" : [1, 2, 4]} +execute ps_get_str using @quotstr; +j +deallocate prepare ps_get_int; +deallocate prepare ps_get_dec; +deallocate prepare ps_get_flt; +deallocate prepare ps_get_str; +prepare ps_get_int from "select * from t1_prep where j->>'$.int' = ?"; +prepare ps_get_dec from "select * from t1_prep where j->>'$.dec' = ?"; +prepare ps_get_flt from "select * from t1_prep where j->>'$.flt' = ?"; +prepare ps_get_str from "select * from t1_prep where j->>'$.str' = ?"; +execute ps_get_int using @int; +j +{"int" : 123, "dec" : 3.14, "flt" : 3.14E1, "str" : "xyz", "array" : [1, 2, 4]} +execute ps_get_int using @intstr; +j +{"int" : 123, "dec" : 3.14, "flt" : 3.14E1, "str" : "xyz", "array" : [1, 2, 4]} +execute ps_get_dec using @dec; +j +{"int" : 123, "dec" : 3.14, "flt" : 3.14E1, "str" : "xyz", "array" : [1, 2, 4]} +execute ps_get_dec using @decstr; +j +{"int" : 123, "dec" : 3.14, "flt" : 3.14E1, "str" : "xyz", "array" : [1, 2, 4]} +execute ps_get_flt using @flt; +j +{"int" : 123, "dec" : 3.14, "flt" : 3.14E1, "str" : "xyz", "array" : [1, 2, 4]} +execute ps_get_flt using @fltstr; +j +{"int" : 123, "dec" : 3.14, "flt" : 3.14E1, "str" : "xyz", "array" : [1, 2, 4]} +execute ps_get_str using @strstr; +j +{"int" : 123, "dec" : 3.14, "flt" : 3.14E1, "str" : "xyz", "array" : [1, 2, 4]} +deallocate prepare ps_get_int; +deallocate prepare ps_get_dec; +deallocate prepare ps_get_flt; +deallocate prepare ps_get_str; +DROP TABLE t1_prep; +End of 13.0 tests diff --git a/mysql-test/main/json_operators.test b/mysql-test/main/json_operators.test new file mode 100644 index 0000000000000..e2606c96a09b5 --- /dev/null +++ b/mysql-test/main/json_operators.test @@ -0,0 +1,193 @@ +--echo # +--echo # MDEV-13594: Support for -> and ->> JSON operators +--echo # + +--echo # 1. Basic Extraction with -> +SELECT '{"a": 1, "b": "text"}'->'$.a' AS int_val, + '{"a": 1, "b": "text"}'->'$.b' AS str_val; + +--echo # 2. Unquoted Extraction with ->> +SELECT '{"a": 1, "b": "text"}'->> '$.a' AS int_val, + '{"a": 1, "b": "text"}'->> '$.b' AS str_val; + +--echo # 3. Chaining Operators +--echo # Should support nested extraction +SELECT '{"a": {"b": {"c": 42}}}'->'$.a'->'$.b'->'$.c' AS chained_arrow, + '{"a": {"b": {"c": 42}}}'->'$.a'->'$.b'->>'$.c' AS chained_unquoted; + +--echo # 4. Operator Precedence +--echo # -> and ->> bind tighter than arithmetic operators +SELECT '{"a": 5}'->'$.a' + 1 AS arrow_plus_one; +SELECT '{"a": 5}'->> '$.a' + 1 AS unquoted_arrow_plus_one; +SELECT '{"a": 5}'->'$.a' * 2 AS arrow_times_two; + +--echo # 5. Table Integration +CREATE TABLE t1 ( + id INT PRIMARY KEY, + data JSON +); + +INSERT INTO t1 VALUES +(1, '{"name": "Alice", "meta": {"age": 25, "city": "London"}}'), +(2, '{"name": "Bob", "meta": {"age": 30, "city": "Paris"}}'), +(3, '{"name": "Charlie", "meta": null}'); + +--echo # Usage in SELECT list +SELECT id, data->'$.name', data->>'$.meta.city' FROM t1; + +--echo # Usage in WHERE clause +SELECT data->>'$.name' FROM t1 WHERE data->'$.meta.age' > 25; + +--echo # Usage in ORDER BY +SELECT data->>'$.name' FROM t1 ORDER BY data->'$.meta.age' DESC; + +DROP TABLE t1; + +--echo # 6. Edge Cases: Invalid JSON and Paths +--echo # Invalid JSON text returns a warning; result is NULL +SELECT '{"a": 1'->'$.a'; + +--echo # Missing key returns NULL (no error) +SELECT '{"a": 1}'->'$.no_key'; + +--echo # Invalid path returns a warning; result is NULL +SELECT '{"a": 1}'->'invalid_path'; + +--echo # 7. Integration with Generated Columns +CREATE TABLE t2 ( + j JSON, + name VARCHAR(50) AS (j->>'$.name') VIRTUAL, + INDEX (name) +); + +INSERT INTO t2 (j) VALUES ('{"name": "Zoe", "score": 100}'); +SELECT name FROM t2; +EXPLAIN SELECT name FROM t2 WHERE name = 'Zoe'; + +DROP TABLE t2; + +--echo # 8. Chaining with non-JSON results +--echo # ->> returns a string. If the string is valid JSON, another -> can follow. +SELECT '{"outer": "{\\"inner\\": 1}"}'->> '$.outer'->'$.inner' AS complex_chain; + +--echo # +--echo # 9. Ported tests from MySQL (Oracle). Authors at Oracle Corporation. +--echo # Path: mysql-test/suite/json/inc/json_functions.inc +--echo # + +CREATE TABLE t1_mysql(autopk int primary key auto_increment, f1 JSON); +INSERT INTO t1_mysql(f1) VALUES + ('{"a":1}'), + ('{"a":3}'), + ('{"a":2}'), + ('{"a":11, "b":3}'), + ('{"a":33, "b":1}'), + ('{"a":22,"b":2}'); +ANALYZE TABLE t1_mysql; + +SELECT f1->"$.a" FROM t1_mysql order by autopk; +EXPLAIN SELECT f1->"$.a" FROM t1_mysql order by autopk; + +SELECT f1->"$.a" FROM t1_mysql WHERE f1->"$.b" > 1 order by autopk; +EXPLAIN SELECT f1->"$.a" FROM t1_mysql WHERE f1->"$.b" > 1 order by autopk; + +SELECT f1->"$.a", f1->"$.b" FROM t1_mysql ORDER BY autopk; + +--sorted_result +SELECT MAX(f1->"$.a"), f1->"$.b" FROM t1_mysql GROUP BY f1->"$.b"; + +SELECT JSON_OBJECT("c",f1->"$.b") AS f2 + FROM t1_mysql HAVING JSON_TYPE(f2->"$.c") <> 'NULL' ORDER BY autopk; + +--echo Test unquoting operator +INSERT INTO t1_mysql(f1) VALUES + ('{"t":"a"}'), + ('{"t":"b"}'), + ('{"t":"c"}'); +--echo Returned values should be quoted +SELECT f1->"$.t" FROM t1_mysql WHERE f1->"$.t" <> 'NULL' order by autopk; +--echo Returned values should be unquoted +SELECT f1->>"$.t" FROM t1_mysql WHERE f1->>"$.t" <> 'NULL' order by autopk; + +--echo # Error cases (using MariaDB error codes/behaviors) +--echo # MariaDB allows NULL on the RHS (returns NULL) +SELECT f1->>NULL FROM t1_mysql; + +--echo # Invalid path returns a warning +SELECT f1 ->> "NULL" FROM t1_mysql; + +INSERT INTO t1_mysql(f1) VALUES + ('[ { "a": 1 }, { "a": 2 } ]'), + ('{ "a" : "foo", "b" : [ true, { "c" : 123, "c" : 456 } ] }'), + ('{ "a" : "foo", "b" : [ true, { "c" : "123" } ] }'), + ('{ "a" : "foo", "b" : [ true, { "c" : 123 } ] }'); + +SELECT + f1->>"$**.b", + JSON_UNQUOTE(JSON_EXTRACT(f1,"$**.b")) + FROM t1_mysql order by autopk; + +SELECT + f1->>"$.c", + JSON_UNQUOTE(JSON_EXTRACT(f1,"$.c")) + FROM t1_mysql order by autopk; + +DROP TABLE t1_mysql; + +--echo # +--echo # 10. Ported tests from MySQL (Oracle). Authors at Oracle Corporation. +--echo # Path: mysql-test/suite/json/t/json_prep_stmts.test +--echo # + +CREATE TABLE t1_prep(j JSON); +set @int=123; +set @intstr='123'; +set @dec=3.14; +set @decstr='3.14'; +set @flt=3.14E1; +set @fltstr='3.14E1'; +set @strstr='xyz'; +set @quotstr='"xyz"'; +set @json='{"int" : 123, "dec" : 3.14, "flt" : 3.14E1, "str" : "xyz", "array" : [1, 2, 4]}'; + +insert into t1_prep values (@json); + +prepare ps_get_int from "select * from t1_prep where j->'$.int' = ?"; +prepare ps_get_dec from "select * from t1_prep where j->'$.dec' = ?"; +prepare ps_get_flt from "select * from t1_prep where j->'$.flt' = ?"; +prepare ps_get_str from "select * from t1_prep where j->'$.str' = ?"; + +execute ps_get_int using @int; +execute ps_get_int using @intstr; +execute ps_get_dec using @dec; +execute ps_get_dec using @decstr; +execute ps_get_flt using @flt; +execute ps_get_flt using @fltstr; +execute ps_get_str using @quotstr; + +deallocate prepare ps_get_int; +deallocate prepare ps_get_dec; +deallocate prepare ps_get_flt; +deallocate prepare ps_get_str; + +prepare ps_get_int from "select * from t1_prep where j->>'$.int' = ?"; +prepare ps_get_dec from "select * from t1_prep where j->>'$.dec' = ?"; +prepare ps_get_flt from "select * from t1_prep where j->>'$.flt' = ?"; +prepare ps_get_str from "select * from t1_prep where j->>'$.str' = ?"; + +execute ps_get_int using @int; +execute ps_get_int using @intstr; +execute ps_get_dec using @dec; +execute ps_get_dec using @decstr; +execute ps_get_flt using @flt; +execute ps_get_flt using @fltstr; +execute ps_get_str using @strstr; + +deallocate prepare ps_get_int; +deallocate prepare ps_get_dec; +deallocate prepare ps_get_flt; +deallocate prepare ps_get_str; + +DROP TABLE t1_prep; + +--echo End of 13.0 tests diff --git a/mysql-test/suite/sys_vars/r/ft_boolean_syntax_basic.result b/mysql-test/suite/sys_vars/r/ft_boolean_syntax_basic.result index e514638eda985..d55adedda60f8 100644 --- a/mysql-test/suite/sys_vars/r/ft_boolean_syntax_basic.result +++ b/mysql-test/suite/sys_vars/r/ft_boolean_syntax_basic.result @@ -77,7 +77,7 @@ ERROR 42000: Variable 'ft_boolean_syntax' can't be set to the value of 'ON' SET @@global.ft_boolean_syntax = true; ERROR 42000: Incorrect argument type to variable 'ft_boolean_syntax' SET @@global.ft_boolean_syntax = + -><()~*:""&|; -ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '><()~*:""&|' at line 1 +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '-><()~*:""&|' at line 1 SET @@global.ft_boolean_syntax = ENABLE; ERROR 42000: Variable 'ft_boolean_syntax' can't be set to the value of 'ENABLE' SET @@global.ft_boolean_syntax = 'IGNORE'; diff --git a/sql/lex.h b/sql/lex.h index 9754c9a6a712b..3f09392523598 100644 --- a/sql/lex.h +++ b/sql/lex.h @@ -56,6 +56,8 @@ SYMBOL symbols[] = { { ">>", SYM(SHIFT_RIGHT)}, { "<=>", SYM(EQUAL_SYM)}, { "=>", SYM(ARROW_SYM)}, + { "->", SYM(JSON_ARROW_SYM)}, + { "->>", SYM(JSON_UNQUOTED_ARROW_SYM)}, { "ACCOUNT", SYM(ACCOUNT_SYM)}, { "ACTION", SYM(ACTION)}, { "ADD", SYM(ADD)}, diff --git a/sql/sql_lex.cc b/sql/sql_lex.cc index b492dbe8411a2..4b420bba0f671 100644 --- a/sql/sql_lex.cc +++ b/sql/sql_lex.cc @@ -2091,6 +2091,17 @@ int Lex_input_stream::lex_one_token(YYSTYPE *yylval, THD *thd) return((int) c); case MY_LEX_MINUS_OR_COMMENT: + /* MDEV-18530: Efficiently detect MySQL-compatible JSON path operators -> and ->> */ + if (yyPeek() == '>') + { + yySkip(); + if (yyPeek() == '>') + { + yySkip(); + return JSON_UNQUOTED_ARROW_SYM; + } + return JSON_ARROW_SYM; + } if (yyPeek() == '-' && (my_isspace(cs,yyPeekn(1)) || my_iscntrl(cs,yyPeekn(1)))) diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index 21525c7990ccf..d3bb6474e469b 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -386,9 +386,9 @@ bool my_yyoverflow(short **a, YYSTYPE **b, size_t *yystacksize); */ %ifdef MARIADB -%expect 70 -%else -%expect 71 +%expect 72 +%ifdef ORACLE +%expect 73 %endif /* @@ -474,6 +474,9 @@ bool my_yyoverflow(short **a, YYSTYPE **b, size_t *yystacksize); %token SHIFT_LEFT /* OPERATOR */ %token SHIFT_RIGHT /* OPERATOR */ %token ARROW_SYM /* OPERATOR */ +/* MDEV-13594: MySQL-compatible JSON path operators -> and ->> */ +%token JSON_ARROW_SYM /* OPERATOR */ +%token JSON_UNQUOTED_ARROW_SYM /* OPERATOR */ /* @@ -1254,6 +1257,7 @@ bool my_yyoverflow(short **a, YYSTYPE **b, size_t *yystacksize); %nonassoc NOT_SYM %nonassoc NEG '~' NOT2_SYM BINARY %nonassoc COLLATE_SYM +%left JSON_ARROW_SYM JSON_UNQUOTED_ARROW_SYM %nonassoc SUBQUERY_AS_EXPR /* @@ -1592,7 +1596,7 @@ bool my_yyoverflow(short **a, YYSTYPE **b, size_t *yystacksize); boolean_test predicate bit_expr parenthesized_expr table_wild simple_expr column_default_non_parenthesized_expr udf_expr - primary_expr string_factor_expr mysql_concatenation_expr + primary_expr primary_base_expr string_factor_expr mysql_concatenation_expr select_sublist_qualified_asterisk expr_or_ignore expr_or_ignore_or_default signed_literal expr_or_literal @@ -10612,7 +10616,7 @@ column_default_non_parenthesized_expr: } ; -primary_expr: +primary_base_expr: column_default_non_parenthesized_expr | explicit_cursor_attr | '(' parenthesized_expr ')' { $$= $2; } @@ -10623,6 +10627,48 @@ primary_expr: } ; +/* + MDEV-13594: JSON operators -> and ->> + These rules map the operators to Item_func_json_extract and Item_func_json_unquote. + Splitting into primary_base_expr and primary_expr allows proper operator chaining + (e.g., col->'$.a'->'$.b') by keeping the grammar left-associative. +*/ +primary_expr: + primary_base_expr + | primary_expr JSON_ARROW_SYM primary_base_expr + { + /* + Build the argument list on thd->mem_root so it is tied to the + parser arena. Assign $$ first so the allocation is not orphaned + if push_back or object construction fails and YYABORT is called. + */ + List *list= new (thd->mem_root) List; + if (unlikely(list == NULL)) + MYSQL_YYABORT; + if (unlikely(list->push_back($1, thd->mem_root)) || + unlikely(list->push_back($3, thd->mem_root))) + MYSQL_YYABORT; + $$= new (thd->mem_root) Item_func_json_extract(thd, *list); + if (unlikely($$ == NULL)) + MYSQL_YYABORT; + } + | primary_expr JSON_UNQUOTED_ARROW_SYM primary_base_expr + { + List *list= new (thd->mem_root) List; + if (unlikely(list == NULL)) + MYSQL_YYABORT; + if (unlikely(list->push_back($1, thd->mem_root)) || + unlikely(list->push_back($3, thd->mem_root))) + MYSQL_YYABORT; + Item *extract= new (thd->mem_root) Item_func_json_extract(thd, *list); + if (unlikely(extract == NULL)) + MYSQL_YYABORT; + $$= new (thd->mem_root) Item_func_json_unquote(thd, extract); + if (unlikely($$ == NULL)) + MYSQL_YYABORT; + } + ; + string_factor_expr: primary_expr | string_factor_expr COLLATE_SYM collation_name