diff --git a/mysql-test/main/func_json.result b/mysql-test/main/func_json.result index 57520346c4f82..4b7b0aaceeb4b 100644 --- a/mysql-test/main/func_json.result +++ b/mysql-test/main/func_json.result @@ -300,6 +300,46 @@ t1 CREATE TABLE `t1` ( `json_quote('foo')` varchar(38) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci drop table t1; +select json_quote(17); +json_quote(17) +17 +select json_quote(3.14); +json_quote(3.14) +3.14 +select json_quote(1.5e2); +json_quote(1.5e2) +150 +select json_quote(NULL); +json_quote(NULL) +null +select isnull(json_quote(NULL)); +isnull(json_quote(NULL)) +0 +select json_quote('hello'); +json_quote('hello') +"hello" +select json_quote('"already_quoted"'); +json_quote('"already_quoted"') +"\"already_quoted\"" +select json_contains(json_quote(NULL), 'null'); +json_contains(json_quote(NULL), 'null') +1 +create table t1 (a int); +insert into t1 values (NULL), (5); +select json_quote(a) from t1 order by a; +json_quote(a) +null +5 +drop table t1; +select json_quote(NULL) is NULL; +json_quote(NULL) is NULL +0 +select json_quote(NULL) = 'null'; +json_quote(NULL) = 'null' +1 +select coalesce(json_quote(NULL), 'fallback'); +coalesce(json_quote(NULL), 'fallback') +null select json_merge('string'); ERROR 42000: Incorrect parameter count in the call to native function 'json_merge' select json_merge('string', 123); diff --git a/mysql-test/main/func_json.test b/mysql-test/main/func_json.test index cd6ab43412c76..154fd4d365ec7 100644 --- a/mysql-test/main/func_json.test +++ b/mysql-test/main/func_json.test @@ -133,6 +133,37 @@ select * from t1; show create table t1; drop table t1; +# +# MDEV-13645 JSON_QUOTE should handle numeric and NULL arguments +# +# Integer literal: must return the unquoted number, not SQL NULL +select json_quote(17); +# Decimal literal: must return the unquoted decimal, not SQL NULL +select json_quote(3.14); +# Double/float literal: must return the unquoted value text, not SQL NULL +select json_quote(1.5e2); +# NULL input: must return the JSON null literal as a non-NULL string result +select json_quote(NULL); +select isnull(json_quote(NULL)); +# String regression: must still quote and escape exactly as before +select json_quote('hello'); +select json_quote('"already_quoted"'); +# JSON_QUOTE(NULL) must compose correctly inside another JSON function +# as the JSON null literal, not break the outer call with SQL NULL +select json_contains(json_quote(NULL), 'null'); +# Column-sourced NULL: result must match literal NULL case (JSON null literal) +create table t1 (a int); +insert into t1 values (NULL), (5); +select json_quote(a) from t1 order by a; +drop table t1; +# SQL-NULL propagation checks: json_quote(NULL) must NOT propagate SQL NULL +# - IS NULL must return 0 (the string "null" is not SQL NULL) +select json_quote(NULL) is NULL; +# - equality with the string 'null' must return 1 +select json_quote(NULL) = 'null'; +# - COALESCE must return 'null', NOT 'fallback' (result is not SQL NULL) +select coalesce(json_quote(NULL), 'fallback'); + --error ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT select json_merge('string'); select json_merge('string', 123); diff --git a/mysql-test/suite/json/r/json_no_table.result b/mysql-test/suite/json/r/json_no_table.result index 500c7baaac169..af4309834c8bb 100644 --- a/mysql-test/suite/json/r/json_no_table.result +++ b/mysql-test/suite/json/r/json_no_table.result @@ -2801,7 +2801,7 @@ select json_unquote('"abc"', NULL); ERROR 42000: Incorrect parameter count in the call to native function 'json_unquote' select json_quote(NULL); json_quote(NULL) -NULL +null select json_unquote(NULL); json_unquote(NULL) NULL @@ -2846,11 +2846,9 @@ json_unquote('"') " Warnings: Warning 4037 Unexpected end of JSON text in argument 1 to function 'json_unquote' -error ER_INCORRECT_TYPE select json_quote(123); json_quote(123) -NULL -error ER_INCORRECT_TYPE +123 select json_unquote(123); json_unquote(123) 123 @@ -2931,7 +2929,6 @@ JSON_UNQUOTE( '"abc' ) "abc Warnings: Warning 4037 Unexpected end of JSON text in argument 1 to function 'json_unquote' -error ER_INCORRECT_TYPE SELECT JSON_UNQUOTE( 123 ); JSON_UNQUOTE( 123 ) 123 @@ -2968,10 +2965,9 @@ AS CHAR SELECT JSON_QUOTE( 'abc' ); JSON_QUOTE( 'abc' ) "abc" -error ER_INCORRECT_TYPE SELECT JSON_QUOTE( 123 ); JSON_QUOTE( 123 ) -NULL +123 SELECT json_compact( JSON_QUOTE( '123' )); json_compact( JSON_QUOTE( '123' )) "123" diff --git a/mysql-test/suite/json/r/member_of.result b/mysql-test/suite/json/r/member_of.result new file mode 100644 index 0000000000000..00e2e238ddb8c --- /dev/null +++ b/mysql-test/suite/json/r/member_of.result @@ -0,0 +1,184 @@ +# +# MDEV-38591: MEMBER OF operator +# +# 1. Simple member checks (should succeed) +SELECT 1 MEMBER OF ('[1,2,3]'); +1 MEMBER OF ('[1,2,3]') +1 +SELECT 2 MEMBER OF ('[1,2,3]'); +2 MEMBER OF ('[1,2,3]') +1 +# 2. Non-member checks (should return 0) +SELECT 4 MEMBER OF ('[1,2,3]'); +4 MEMBER OF ('[1,2,3]') +0 +# 3. Type-strict checks (string "2" must NOT match number 2, should return 0) +SELECT '2' MEMBER OF ('[1,2,3]'); +'2' MEMBER OF ('[1,2,3]') +0 +SELECT 2 MEMBER OF ('["1","2","3"]'); +2 MEMBER OF ('["1","2","3"]') +0 +# 4. SQL NULL propagation checks +SELECT NULL MEMBER OF ('[1,2,3]'); +NULL MEMBER OF ('[1,2,3]') +NULL +SELECT NULL MEMBER OF ('[1,2,3]') IS NULL; +NULL MEMBER OF ('[1,2,3]') IS NULL +1 +SELECT 1 MEMBER OF (NULL); +1 MEMBER OF (NULL) +NULL +SELECT 1 MEMBER OF (NULL) IS NULL; +1 MEMBER OF (NULL) IS NULL +1 +# 5. Error/Warning handling for malformed JSON +SELECT 1 MEMBER OF ('[1,2'); +1 MEMBER OF ('[1,2') +NULL +Warnings: +Warning 4037 Unexpected end of JSON text in argument 2 to function 'member of' +SHOW WARNINGS; +Level Code Message +Warning 4037 Unexpected end of JSON text in argument 2 to function 'member of' +# 6. Nested array and object checks using JSON_COMPACT() vs uncast strings +SELECT JSON_COMPACT('[1,2]') MEMBER OF ('[[1,2],[3,4]]'); +JSON_COMPACT('[1,2]') MEMBER OF ('[[1,2],[3,4]]') +1 +SELECT '[1,2]' MEMBER OF ('[[1,2],[3,4]]'); +'[1,2]' MEMBER OF ('[[1,2],[3,4]]') +0 +SELECT JSON_COMPACT('{"name":"John"}') MEMBER OF ('[{"name":"John"},{"name":"Joe"}]'); +JSON_COMPACT('{"name":"John"}') MEMBER OF ('[{"name":"John"},{"name":"Joe"}]') +1 +SELECT '{"name":"John"}' MEMBER OF ('[{"name":"John"},{"name":"Joe"}]'); +'{"name":"John"}' MEMBER OF ('[{"name":"John"},{"name":"Joe"}]') +0 +# 7. MEMBER OF with scalar/object as container (acting as 1-element array) +SELECT 2 MEMBER OF ('2'); +2 MEMBER OF ('2') +1 +SELECT 2 MEMBER OF ('3'); +2 MEMBER OF ('3') +0 +SELECT 2 MEMBER OF ('"2"'); +2 MEMBER OF ('"2"') +0 +# 8. Test using table columns and row evaluations +CREATE TABLE t1 (id INT, val JSON); +INSERT INTO t1 VALUES (1, '[1, 2, 3]'), (2, '[4, 5, 6]'), (3, NULL); +SELECT id, val, 2 MEMBER OF (val) FROM t1; +id val 2 MEMBER OF (val) +1 [1, 2, 3] 1 +2 [4, 5, 6] 0 +3 NULL NULL +SELECT id, val, NULL MEMBER OF (val) FROM t1; +id val NULL MEMBER OF (val) +1 [1, 2, 3] NULL +2 [4, 5, 6] NULL +3 NULL NULL +CREATE TABLE t2 (candidate INT); +INSERT INTO t2 VALUES (2), (4), (NULL); +SELECT candidate, val, candidate MEMBER OF (val) FROM t1, t2 ORDER BY id, candidate; +candidate val candidate MEMBER OF (val) +NULL [1, 2, 3] NULL +2 [1, 2, 3] 1 +4 [1, 2, 3] 0 +NULL [4, 5, 6] NULL +2 [4, 5, 6] 0 +4 [4, 5, 6] 1 +NULL NULL NULL +2 NULL NULL +4 NULL NULL +DROP TABLE t1, t2; +# 9. Prepared Statements twice execution test +PREPARE stmt1 FROM 'SELECT ? MEMBER OF (?)'; +SET @candidate = 2; +SET @container = '[1,2,3]'; +EXECUTE stmt1 USING @candidate, @container; +? MEMBER OF (?) +1 +SET @candidate = 4; +SET @container = '[1,2,3]'; +EXECUTE stmt1 USING @candidate, @container; +? MEMBER OF (?) +0 +DEALLOCATE PREPARE stmt1; +# 10. Prepared Statements with JSON_COMPACT +PREPARE stmt2 FROM 'SELECT JSON_COMPACT(?) MEMBER OF (?)'; +SET @candidate_json = '[1,2]'; +SET @container_nested = '[[1,2],[3,4]]'; +EXECUTE stmt2 USING @candidate_json, @container_nested; +JSON_COMPACT(?) MEMBER OF (?) +1 +EXECUTE stmt2 USING @candidate_json, @container_nested; +JSON_COMPACT(?) MEMBER OF (?) +1 +DEALLOCATE PREPARE stmt2; +# 11. Malformed JSON-typed candidate test (using CHECK constraints bypass) +CREATE TABLE t3 (val JSON); +SET check_constraint_checks=0; +INSERT INTO t3 VALUES ('[1,2'); +SELECT val MEMBER OF ('[1,2,3]') FROM t3; +val MEMBER OF ('[1,2,3]') +NULL +Warnings: +Warning 4037 Unexpected end of JSON text in argument 1 to function 'member of' +SHOW WARNINGS; +Level Code Message +Warning 4037 Unexpected end of JSON text in argument 1 to function 'member of' +DROP TABLE t3; +SET check_constraint_checks=1; +# 12. JSON scalar candidate test (type-strictness in passthrough branch) +CREATE TABLE t4 (val JSON); +INSERT INTO t4 VALUES ('2'), ('"2"'); +SELECT val, val MEMBER OF ('[1,2,3]') FROM t4; +val val MEMBER OF ('[1,2,3]') +2 1 +"2" 0 +DROP TABLE t4; +# 13. NOT MEMBER OF tests +# 13.1 1 NOT MEMBER OF ('[1,2,3]') -> 0 (1 IS a member) +SELECT 1 NOT MEMBER OF ('[1,2,3]'); +1 NOT MEMBER OF ('[1,2,3]') +0 +# 13.2 4 NOT MEMBER OF ('[1,2,3]') -> 1 +SELECT 4 NOT MEMBER OF ('[1,2,3]'); +4 NOT MEMBER OF ('[1,2,3]') +1 +# 13.3 '2' NOT MEMBER OF ('[1,2,3]') -> 1 (type-strict comparison) +SELECT '2' NOT MEMBER OF ('[1,2,3]'); +'2' NOT MEMBER OF ('[1,2,3]') +1 +# 13.4 NULL NOT MEMBER OF ('[1,2,3]') -> NULL (and IS NULL -> 1) +SELECT NULL NOT MEMBER OF ('[1,2,3]'); +NULL NOT MEMBER OF ('[1,2,3]') +NULL +SELECT NULL NOT MEMBER OF ('[1,2,3]') IS NULL; +NULL NOT MEMBER OF ('[1,2,3]') IS NULL +1 +# 13.5 1 NOT MEMBER OF (NULL) -> NULL (and IS NULL -> 1) +SELECT 1 NOT MEMBER OF (NULL); +1 NOT MEMBER OF (NULL) +NULL +SELECT 1 NOT MEMBER OF (NULL) IS NULL; +1 NOT MEMBER OF (NULL) IS NULL +1 +# 13.6 Malformed JSON with NOT MEMBER OF (returns NULL with warning) +SELECT 1 NOT MEMBER OF ('not_json'); +1 NOT MEMBER OF ('not_json') +NULL +Warnings: +Warning 4038 Syntax error in JSON text in argument 2 to function 'member of' at position 1 +SHOW WARNINGS; +Level Code Message +Warning 4038 Syntax error in JSON text in argument 2 to function 'member of' at position 1 +# 13.7 EXPLAIN EXTENDED round-trip print check +EXPLAIN EXTENDED SELECT 1 NOT MEMBER OF ('[1,2,3]'); +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE NULL NULL NULL NULL NULL NULL NULL NULL No tables used +Warnings: +Note 1003 select 1 not member of ('[1,2,3]') AS `1 NOT MEMBER OF ('[1,2,3]')` +SHOW WARNINGS; +Level Code Message +Note 1003 select 1 not member of ('[1,2,3]') AS `1 NOT MEMBER OF ('[1,2,3]')` diff --git a/mysql-test/suite/json/t/json_no_table.test b/mysql-test/suite/json/t/json_no_table.test index 114c16da7edc6..00e0ad7996abe 100644 --- a/mysql-test/suite/json/t/json_no_table.test +++ b/mysql-test/suite/json/t/json_no_table.test @@ -1842,12 +1842,10 @@ select json_unquote(convert('"abc"' using utf8mb4)); select json_quote('"'); select json_unquote('"'); # should do nothing ---echo error ER_INCORRECT_TYPE -select json_quote(123); # integer not allowed +select json_quote(123); # integer is now allowed, returns "123" # Enable after fix MDEV-31554 --disable_cursor_protocol ---echo error ER_INCORRECT_TYPE -select json_unquote(123); # integer not allowed +select json_unquote(123); # integer is now allowed, returns "123" --enable_cursor_protocol select json_unquote('""'); # empty string @@ -1903,7 +1901,6 @@ SELECT JSON_UNQUOTE( '"abc"' ); # returns the SQL string literal "abc SELECT JSON_UNQUOTE( '"abc' ); ---echo error ER_INCORRECT_TYPE SELECT JSON_UNQUOTE( 123 ); --enable_cursor_protocol @@ -1930,7 +1927,6 @@ SELECT # returns "abc" SELECT JSON_QUOTE( 'abc' ); ---echo error ER_INCORRECT_TYPE SELECT JSON_QUOTE( 123 ); # returns the JSON document consisting of the string scalar "123" diff --git a/mysql-test/suite/json/t/member_of.test b/mysql-test/suite/json/t/member_of.test new file mode 100644 index 0000000000000..b92ca4733e9ae --- /dev/null +++ b/mysql-test/suite/json/t/member_of.test @@ -0,0 +1,110 @@ +--echo # +--echo # MDEV-38591: MEMBER OF operator +--echo # + +--echo # 1. Simple member checks (should succeed) +SELECT 1 MEMBER OF ('[1,2,3]'); +SELECT 2 MEMBER OF ('[1,2,3]'); + +--echo # 2. Non-member checks (should return 0) +SELECT 4 MEMBER OF ('[1,2,3]'); + +--echo # 3. Type-strict checks (string "2" must NOT match number 2, should return 0) +SELECT '2' MEMBER OF ('[1,2,3]'); +SELECT 2 MEMBER OF ('["1","2","3"]'); + +--echo # 4. SQL NULL propagation checks +SELECT NULL MEMBER OF ('[1,2,3]'); +SELECT NULL MEMBER OF ('[1,2,3]') IS NULL; +SELECT 1 MEMBER OF (NULL); +SELECT 1 MEMBER OF (NULL) IS NULL; + +--echo # 5. Error/Warning handling for malformed JSON +SELECT 1 MEMBER OF ('[1,2'); +SHOW WARNINGS; + +--echo # 6. Nested array and object checks using JSON_COMPACT() vs uncast strings +SELECT JSON_COMPACT('[1,2]') MEMBER OF ('[[1,2],[3,4]]'); +SELECT '[1,2]' MEMBER OF ('[[1,2],[3,4]]'); +SELECT JSON_COMPACT('{"name":"John"}') MEMBER OF ('[{"name":"John"},{"name":"Joe"}]'); +SELECT '{"name":"John"}' MEMBER OF ('[{"name":"John"},{"name":"Joe"}]'); + +--echo # 7. MEMBER OF with scalar/object as container (acting as 1-element array) +SELECT 2 MEMBER OF ('2'); +SELECT 2 MEMBER OF ('3'); +SELECT 2 MEMBER OF ('"2"'); + +--echo # 8. Test using table columns and row evaluations +CREATE TABLE t1 (id INT, val JSON); +INSERT INTO t1 VALUES (1, '[1, 2, 3]'), (2, '[4, 5, 6]'), (3, NULL); + +SELECT id, val, 2 MEMBER OF (val) FROM t1; +SELECT id, val, NULL MEMBER OF (val) FROM t1; + +CREATE TABLE t2 (candidate INT); +INSERT INTO t2 VALUES (2), (4), (NULL); + +SELECT candidate, val, candidate MEMBER OF (val) FROM t1, t2 ORDER BY id, candidate; + +DROP TABLE t1, t2; + +--echo # 9. Prepared Statements twice execution test +PREPARE stmt1 FROM 'SELECT ? MEMBER OF (?)'; +SET @candidate = 2; +SET @container = '[1,2,3]'; +EXECUTE stmt1 USING @candidate, @container; +SET @candidate = 4; +SET @container = '[1,2,3]'; +EXECUTE stmt1 USING @candidate, @container; +DEALLOCATE PREPARE stmt1; + +--echo # 10. Prepared Statements with JSON_COMPACT +PREPARE stmt2 FROM 'SELECT JSON_COMPACT(?) MEMBER OF (?)'; +SET @candidate_json = '[1,2]'; +SET @container_nested = '[[1,2],[3,4]]'; +EXECUTE stmt2 USING @candidate_json, @container_nested; +EXECUTE stmt2 USING @candidate_json, @container_nested; +DEALLOCATE PREPARE stmt2; + +--echo # 11. Malformed JSON-typed candidate test (using CHECK constraints bypass) +CREATE TABLE t3 (val JSON); +SET check_constraint_checks=0; +INSERT INTO t3 VALUES ('[1,2'); +SELECT val MEMBER OF ('[1,2,3]') FROM t3; +SHOW WARNINGS; +DROP TABLE t3; +SET check_constraint_checks=1; + +--echo # 12. JSON scalar candidate test (type-strictness in passthrough branch) +CREATE TABLE t4 (val JSON); +INSERT INTO t4 VALUES ('2'), ('"2"'); +SELECT val, val MEMBER OF ('[1,2,3]') FROM t4; +DROP TABLE t4; + +--echo # 13. NOT MEMBER OF tests +--echo # 13.1 1 NOT MEMBER OF ('[1,2,3]') -> 0 (1 IS a member) +SELECT 1 NOT MEMBER OF ('[1,2,3]'); + +--echo # 13.2 4 NOT MEMBER OF ('[1,2,3]') -> 1 +SELECT 4 NOT MEMBER OF ('[1,2,3]'); + +--echo # 13.3 '2' NOT MEMBER OF ('[1,2,3]') -> 1 (type-strict comparison) +SELECT '2' NOT MEMBER OF ('[1,2,3]'); + +--echo # 13.4 NULL NOT MEMBER OF ('[1,2,3]') -> NULL (and IS NULL -> 1) +SELECT NULL NOT MEMBER OF ('[1,2,3]'); +SELECT NULL NOT MEMBER OF ('[1,2,3]') IS NULL; + +--echo # 13.5 1 NOT MEMBER OF (NULL) -> NULL (and IS NULL -> 1) +SELECT 1 NOT MEMBER OF (NULL); +SELECT 1 NOT MEMBER OF (NULL) IS NULL; + +--echo # 13.6 Malformed JSON with NOT MEMBER OF (returns NULL with warning) +SELECT 1 NOT MEMBER OF ('not_json'); +SHOW WARNINGS; + +--echo # 13.7 EXPLAIN EXTENDED round-trip print check +EXPLAIN EXTENDED SELECT 1 NOT MEMBER OF ('[1,2,3]'); +SHOW WARNINGS; + + diff --git a/sql/item_cmpfunc.h b/sql/item_cmpfunc.h index 4fa07ccc8c971..2827eaf6529ed 100644 --- a/sql/item_cmpfunc.h +++ b/sql/item_cmpfunc.h @@ -1075,6 +1075,8 @@ class Item_func_opt_neg :public Item_bool_func public: Item_func_opt_neg(THD *thd, Item *a, Item *b, Item *c): Item_bool_func(thd, a, b, c), negated(0) {} + Item_func_opt_neg(THD *thd, Item *a, Item *b): + Item_bool_func(thd, a, b), negated(0) {} Item_func_opt_neg(THD *thd, List &list): Item_bool_func(thd, list), negated(0) {} public: diff --git a/sql/item_jsonfunc.cc b/sql/item_jsonfunc.cc index 68bdb7cb080ae..cb71de81076e8 100644 --- a/sql/item_jsonfunc.cc +++ b/sql/item_jsonfunc.cc @@ -953,6 +953,9 @@ bool Item_func_json_quote::fix_length_and_dec(THD *thd) /* Odd but realistic worst case is when all characters of the argument turn into '\uXXXX\uXXXX', which is 12. + For NULL input we return the 4-character literal "null", + for numeric input we return the unquoted text, so the + function never returns SQL NULL — do not set maybe_null. */ fix_char_length_ulonglong((ulonglong) args[0]->max_char_length() * 12 + 2); return FALSE; @@ -961,25 +964,65 @@ bool Item_func_json_quote::fix_length_and_dec(THD *thd) String *Item_func_json_quote::val_str(String *str) { + /* + Evaluate the argument. For STRING_RESULT we need the returned pointer + (which may differ from &tmp_s for const-item optimisations). + For numeric types the text lives in tmp_s; val_str() still fills it. + */ String *s= args[0]->val_str(&tmp_s); - if ((null_value= (args[0]->null_value || - args[0]->result_type() != STRING_RESULT))) - return NULL; - str->length(0); str->set_charset(&my_charset_utf8mb4_bin); - if (str->append('"') || - st_append_escaped(str, s) || - str->append('"')) + if (args[0]->null_value) { - /* Report an error. */ + /* + SQL NULL input maps to the JSON null literal — return the + 4-character string "null" as a non-NULL result. + */ + null_value= 0; + if (str->append(STRING_WITH_LEN("null"))) + { + null_value= 1; + return 0; + } + return str; + } + + switch (args[0]->result_type()) + { + case STRING_RESULT: + /* String input: quote and escape exactly as before. */ + if (str->append('"') || + st_append_escaped(str, s) || + str->append('"')) + { + null_value= 1; + return 0; + } + null_value= 0; + return str; + + case INT_RESULT: + case REAL_RESULT: + case DECIMAL_RESULT: + /* + Numeric input: the text representation is already valid JSON — + copy it unquoted and unescaped. + */ + if (!s || str->append(s->ptr(), s->length(), &my_charset_utf8mb4_bin)) + { + null_value= 1; + return 0; + } + null_value= 0; + return str; + + default: + /* Unknown result type (e.g. ROW_RESULT): preserve original NULL behaviour. */ null_value= 1; return 0; } - - return str; } @@ -6493,3 +6536,217 @@ void Item_func_is_json::print(String *str, enum_query_type query_type) if (with_unique_keys) str->append(STRING_WITH_LEN(" WITH UNIQUE KEYS")); } + + +bool Item_func_member_of::val_bool() +{ + DBUG_ASSERT(fixed()); + + // 1. Evaluate args[1] (the JSON document/array) to check if it is SQL NULL + String *js_doc= args[1]->val_json(&tmp_js_doc); + if (args[1]->null_value || !js_doc) + { + null_value= 1; + return false; + } + + // 2. Validate args[1] (the JSON document/array) is well-formed JSON. + // This prevents the composed JSON_CONTAINS item from raising its own warning. + // je_val.stack is pre-wired via mem_root_dynamic_array_init() in fix_length_and_dec(); + // json_scan_start() resets all other engine state itself, so je_val is safe to reuse. + json_scan_start(&je_val, js_doc->charset(), (const uchar *) js_doc->ptr(), + (const uchar *) js_doc->ptr() + js_doc->length()); + while (json_scan_next(&je_val) == 0) /* no-op */ ; + if (je_val.s.error) + { + report_json_error_ex(js_doc->ptr(), &je_val, "member of", 1, Sql_condition::WARN_LEVEL_WARN); + null_value= 1; + return false; + } + + // 3. Evaluate args[0] (the candidate value) to check if it is SQL NULL. + if (is_json_type(args[0])) + { + String *js_cand= args[0]->val_json(&tmp_candidate); + if (args[0]->null_value || !js_cand) + { + null_value= 1; + return false; + } + json_scan_start(&je_val, js_cand->charset(), (const uchar *) js_cand->ptr(), + (const uchar *) js_cand->ptr() + js_cand->length()); + while (json_scan_next(&je_val) == 0) /* no-op */ ; + if (je_val.s.error) + { + report_json_error_ex(js_cand->ptr(), &je_val, "member of", 0, Sql_condition::WARN_LEVEL_WARN); + null_value= 1; + return false; + } + } + else + { + (void) args[0]->val_str(&tmp_candidate); + if (args[0]->null_value) + { + null_value= 1; + return false; + } + } + + // 4. Delegate the containment check to the composed JSON_CONTAINS item + bool res= json_contains_item->val_bool(); + null_value= json_contains_item->null_value; + if (null_value) + return false; + return negated ? !res : res; +} + +bool Item_func_member_of::fix_length_and_dec(THD *thd) +{ + max_length= 1; + set_maybe_null(); + + /* + Wire je_val.stack once per statement lifetime so that json_scan_start() + in val_bool() has a properly backed DYNAMIC_ARRAY. Mirrors exactly the + je.stack init done by Item_func_json_contains::fix_length_and_dec(). + */ + mem_root_dynamic_array_init(thd->mem_root, PSI_INSTRUMENT_MEM, + &je_val.stack, sizeof(int), NULL, + JSON_DEPTH_DEFAULT, JSON_DEPTH_INC, MYF(0)); + + List contains_args; + if (contains_args.push_back(args[1], thd->mem_root)) + return true; + + if (is_json_type(args[0])) + { + json_quote_item= NULL; + if (contains_args.push_back(args[0], thd->mem_root)) + return true; + } + else + { + json_quote_item= new (thd->mem_root) Item_func_json_quote(thd, args[0]); + if (!json_quote_item) + return true; + if (json_quote_item->fix_fields_if_needed(thd, (Item **)&json_quote_item)) + return true; + if (contains_args.push_back(json_quote_item, thd->mem_root)) + return true; + } + + json_contains_item= new (thd->mem_root) Item_func_json_contains(thd, contains_args); + if (!json_contains_item) + return true; + if (json_contains_item->fix_fields_if_needed(thd, (Item **)&json_contains_item)) + return true; + + return false; +} + + +bool Item_func_member_of::walk(Item_processor processor, void *arg, item_walk_flags flags) +{ + if (json_quote_item && json_quote_item->walk(processor, arg, flags)) + return true; + if (json_contains_item && json_contains_item->walk(processor, arg, flags)) + return true; + return Item_bool_func::walk(processor, arg, flags); +} + +Item *Item_func_member_of::transform(THD *thd, Item_transformer transformer, uchar *arg) +{ + DBUG_ASSERT(!thd->stmt_arena->is_stmt_prepare()); + if (transform_args(thd, transformer, arg)) + return 0; + + if (json_quote_item) + { + Item *new_item= json_quote_item->transform(thd, transformer, arg); + if (!new_item) + return 0; + if (json_quote_item != new_item) + thd->change_item_tree((Item**)&json_quote_item, new_item); + } + + if (json_contains_item) + { + Item *new_item= json_contains_item->transform(thd, transformer, arg); + if (!new_item) + return 0; + if (json_contains_item != new_item) + thd->change_item_tree((Item**)&json_contains_item, new_item); + } + + return (this->*transformer)(thd, arg); +} + +Item *Item_func_member_of::compile(THD *thd, Item_analyzer analyzer, uchar **arg_p, + Item_transformer transformer, uchar *arg_t) +{ + if (!(this->*analyzer)(arg_p)) + return 0; + if (*arg_p && arg_count) + { + Item **arg,**arg_end; + for (arg= args, arg_end= args+arg_count; arg != arg_end; arg++) + { + uchar *arg_v= *arg_p; + Item *new_item= (*arg)->compile(thd, analyzer, &arg_v, transformer, + arg_t); + if (new_item && *arg != new_item) + thd->change_item_tree(arg, new_item); + } + + if (json_quote_item) + { + uchar *arg_v= *arg_p; + Item *new_item= json_quote_item->compile(thd, analyzer, &arg_v, transformer, arg_t); + if (new_item && json_quote_item != new_item) + thd->change_item_tree((Item**)&json_quote_item, new_item); + } + + if (json_contains_item) + { + uchar *arg_v= *arg_p; + Item *new_item= json_contains_item->compile(thd, analyzer, &arg_v, transformer, arg_t); + if (new_item && json_contains_item != new_item) + thd->change_item_tree((Item**)&json_contains_item, new_item); + } + } + return (this->*transformer)(thd, arg_t); +} + +Item *Item_func_member_of::propagate_equal_fields(THD *thd, const Item::Context &ctx, + COND_EQUAL *cond) +{ + Item_bool_func::propagate_equal_fields(thd, ctx, cond); + if (json_quote_item) + json_quote_item->propagate_equal_fields(thd, ctx, cond); + if (json_contains_item) + json_contains_item->propagate_equal_fields(thd, ctx, cond); + return this; +} + +void Item_func_member_of::update_used_tables() +{ + Item_bool_func::update_used_tables(); + if (json_quote_item) + json_quote_item->update_used_tables(); + if (json_contains_item) + json_contains_item->update_used_tables(); +} + + +void Item_func_member_of::print(String *str, enum_query_type query_type) +{ + args[0]->print_parenthesised(str, query_type, higher_precedence()); + if (negated) + str->append(STRING_WITH_LEN(" not")); + str->append(STRING_WITH_LEN(" member of (")); + args[1]->print(str, query_type); + str->append(')'); +} + + diff --git a/sql/item_jsonfunc.h b/sql/item_jsonfunc.h index 5cf9ae92a0fa2..275542838db8a 100644 --- a/sql/item_jsonfunc.h +++ b/sql/item_jsonfunc.h @@ -1126,4 +1126,73 @@ class Item_func_is_json: public Item_bool_func }; +/* + Implements the SQL standard "value MEMBER OF (json_doc)" operator, and its + negation "value NOT MEMBER OF (json_doc)" via the inherited negated flag from + Item_func_opt_neg. + + This follows the same pattern used by Item_func_between and Item_func_in: + a single item class represents both the positive and negated form, with + neg_transformer() toggling the negated flag and val_bool() honouring it. + + Design: composition, not reimplementation. fix_length_and_dec() builds + an internal JSON_QUOTE(args[0]) item (when args[0] is not already JSON- + typed) and an internal JSON_CONTAINS(args[1], ...) item. val_bool() + pre-validates both operands and then delegates the actual containment + test to json_contains_item, then negates only when negated==true and + null_value==false, matching the Item_func_between pattern exactly. + + je_val (a persistent json_engine_t) exists solely for that pre-validation + pass. Its purpose is twofold: (1) errors are attributed to "member of" + with the correct argument index, not to "json_contains"; (2) the full- + document scan catches malformed trailing bytes that json_contains would + miss if it found an early match and returned before scanning to EOF. + je_val.stack is wired once in fix_length_and_dec() via + mem_root_dynamic_array_init(), mirroring the pattern used by + Item_func_json_contains::fix_length_and_dec(). + + walk / transform / compile / propagate_equal_fields / update_used_tables + are overridden to expose json_quote_item and json_contains_item to the + optimizer. Those two helper items live outside the normal args[] array + that Item_func_opt_neg would otherwise walk automatically, so without these + overrides the optimizer would never see them. This follows the precedent + set by Item_in_optimizer, which similarly maintains hidden child items and + explicitly threads them through every tree-traversal method. +*/ +class Item_func_member_of : public Item_func_opt_neg +{ + Item_func_json_quote *json_quote_item; + Item_func_json_contains *json_contains_item; + /* Persistent engine for manual validation of args[1] in val_bool(). + je_val.stack must be wired via mem_root_dynamic_array_init() in + fix_length_and_dec() before any call to json_scan_start(&je_val,...). */ + json_engine_t je_val; + String tmp_js_doc; + String tmp_candidate; +public: + Item_func_member_of(THD *thd, Item *a, Item *b): + Item_func_opt_neg(thd, a, b), json_quote_item(NULL), json_contains_item(NULL) + {} + + bool val_bool() override; + bool fix_length_and_dec(THD *thd) override; + void print(String *str, enum_query_type query_type) override; + enum precedence precedence() const override { return CMP_PRECEDENCE; } + LEX_CSTRING func_name_cstring() const override + { + static LEX_CSTRING name= {STRING_WITH_LEN("member_of") }; + return name; + } + Item *shallow_copy(THD *thd) const override + { return get_item_copy(thd, this); } + + bool walk(Item_processor processor, void *arg, item_walk_flags flags) override; + Item *transform(THD *thd, Item_transformer transformer, uchar *arg) override; + Item *compile(THD *thd, Item_analyzer analyzer, uchar **arg_p, + Item_transformer transformer, uchar *arg_t) override; + Item *propagate_equal_fields(THD *thd, const Item::Context &ctx, COND_EQUAL *cond) override; + void update_used_tables() override; +}; + + #endif /* ITEM_JSONFUNC_INCLUDED */ diff --git a/sql/lex.h b/sql/lex.h index 9754c9a6a712b..260c16dad203a 100644 --- a/sql/lex.h +++ b/sql/lex.h @@ -398,6 +398,7 @@ SYMBOL symbols[] = { { "MEDIUMBLOB", SYM(MEDIUMBLOB)}, { "MEDIUMINT", SYM(MEDIUMINT)}, { "MEDIUMTEXT", SYM(MEDIUMTEXT)}, + { "MEMBER", SYM(MEMBER_SYM)}, { "MEMORY", SYM(MEMORY_SYM)}, { "MERGE", SYM(MERGE_SYM)}, { "MESSAGE_TEXT", SYM(MESSAGE_TEXT_SYM)}, diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index c7072acefc7ff..ee0a26aefef67 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -385,10 +385,20 @@ bool my_yyoverflow(short **a, YYSTYPE **b, size_t *yystacksize); We should not introduce any further shift/reduce conflicts. */ +/* + %expect bumped by 1 for MEMBER_SYM: a new shift/reduce conflict + identical in category to the existing SOUNDS_SYM conflict in this + same state (both are non-reserved keywords that begin a binary + predicate operator: SOUNDS LIKE / MEMBER OF). Resolved by Bison's + default shift, which is correct here. MEMBER is kept non-reserved + to match MySQL 8.0.19+ semantics (MDEV-38591) — reserving it would + itself be a compatibility regression for users migrating schemas + with existing `member` columns/tables. +*/ %ifdef MARIADB -%expect 70 -%else %expect 71 +%else +%expect 72 %endif /* @@ -990,6 +1000,7 @@ bool my_yyoverflow(short **a, YYSTYPE **b, size_t *yystacksize); %token MAX_STATEMENT_TIME_SYM %token MAX_USER_CONNECTIONS_SYM %token MEDIUM_SYM +%token MEMBER_SYM %token MEMORY_SYM %token MERGE_SYM /* SQL-2003-R */ %token MESSAGE_TEXT_SYM /* SQL-2003-N */ @@ -1226,7 +1237,7 @@ bool my_yyoverflow(short **a, YYSTYPE **b, size_t *yystacksize); %left '=' EQUAL_SYM GE '>' LE '<' NE %nonassoc IS %right BETWEEN_SYM -%left LIKE SOUNDS_SYM REGEXP IN_SYM +%left LIKE SOUNDS_SYM REGEXP IN_SYM MEMBER_SYM %left '|' %left '&' %left SHIFT_LEFT SHIFT_RIGHT @@ -10119,6 +10130,20 @@ predicate: if (unlikely($$ == NULL)) MYSQL_YYABORT; } + | predicate MEMBER_SYM OF_SYM '(' expr ')' + { + $$= new (thd->mem_root) Item_func_member_of(thd, $1, $5); + if (unlikely($$ == NULL)) + MYSQL_YYABORT; + } + | predicate not MEMBER_SYM OF_SYM '(' expr ')' %prec MEMBER_SYM + { + Item_func_member_of *item= + new (thd->mem_root) Item_func_member_of(thd, $1, $6); + if (unlikely(item == NULL)) + MYSQL_YYABORT; + $$= item->neg_transformer(thd); + } | predicate LIKE predicate { $$= new (thd->mem_root) Item_func_like(thd, $1, $3, escape(thd), false); @@ -17165,6 +17190,7 @@ keyword_sp_var_and_label: | ID_SYM | LAST_VALUE | LASTVAL_SYM + | MEMBER_SYM | MINUTE_SYM | MONTH_SYM | NEXTVAL_SYM