Skip to content

MDEV-38591: Implement MEMBER OF and NOT MEMBER OF operators - #5278

Open
kjarir wants to merge 2 commits into
MariaDB:mainfrom
kjarir:bb-38591-member-of-grammar
Open

MDEV-38591: Implement MEMBER OF and NOT MEMBER OF operators#5278
kjarir wants to merge 2 commits into
MariaDB:mainfrom
kjarir:bb-38591-member-of-grammar

Conversation

@kjarir

@kjarir kjarir commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

This PR adds native support for the SQL-standard value MEMBER OF (json_doc) and value NOT MEMBER OF (json_doc) containment predicates. It builds on MDEV-13645 (JSON_QUOTE numeric/NULL fixes) which is included in this branch.

Rather than reimplementing a JSON search engine, Item_func_member_of composes existing items internally. It holds Item_func_json_quote and Item_func_json_contains as child items. In fix_length_and_dec(), if the candidate operand is not already JSON-typed, it gets wrapped in json_quote_item. The actual containment check is then delegated to an internal json_contains_item(json_doc, candidate). AST traversal methods (walk(), transform(), compile(), propagate_equal_fields(), update_used_tables()) explicitly expose these hidden child items to the optimizer, following the Item_in_optimizer precedent.

negation:
Item_func_member_of derives from Item_func_opt_neg, the same pattern used by Item_func_between and Item_func_in, rather than wrapping in a generic Item_func_not. NULL propagation is handled directly in val_bool() via negated ? !res : res on non-NULL outcomes. print() emits value not member of (json_doc) to preserve surface syntax in optimizer traces and EXPLAIN EXTENDED.

json validation and error attribution:
val_bool() performs explicit JSON validation before delegating to json_contains. A persistent json_engine_t je_val member with a stack buffer initialized at prepare-time runs a full-document scan to catch trailing garbage that a simple json_read_value() would miss. Syntax errors are intercepted and reported via report_json_error_ex() attributed to "member of" with correct 0-based argument indices. This prevents warning misattribution to "json_contains" or wrong argument numbers leaking through to the user.

Parser:
MEMBER_SYM is registered alphabetically in lex.h and declared via %token. It is added to keyword_sp_var_and_label to keep it non-reserved, resolving prior reserved keyword conflicts with MySQL. Grammar rules cover both forms:

predicate MEMBER_SYM OF_SYM '(' expr ')'
predicate not MEMBER_SYM OF_SYM '(' expr ')' %prec MEMBER_SYM

A state-by-state conflict analysis confirms zero new conflicts introduced. Parser conflict count stays at 71, no %expect bump needed.

Test coverage:
New tests in mysql-test/suite/json/t/member_of.test cover scalar member checks, type-strict matches, correct NULL propagation under negation, malformed JSON warning attribution, prepared statement re-execution, nested container checks, table column cross-joins, and EXPLAIN EXTENDED round-trip print verification.

Out of scope: Multi-valued index support (MySQL WL#8955/WL#8763) is not part of this implementation.

Please Note:

This branch contains a merge commit (9f17b9a1af9 merging bb-13645-json_quote-numeric into bb-38591-member-of-grammar). I am happy to rebase and squash this branch chain into a clean linear history if preferred by reviewers.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request implements the SQL standard MEMBER OF and NOT MEMBER OF operators (MDEV-38591) and updates JSON_QUOTE to support numeric and NULL arguments (MDEV-13645). The review feedback highlights several critical and high-severity issues: a potential double-free or use-after-free vulnerability in shallow_copy due to raw pointer copying, type safety issues from using specific subclass pointers for helper items instead of Item*, double evaluation of non-deterministic arguments in val_bool(), and the need to sync helper arguments during optimization in update_used_tables(). Additionally, minor improvements are suggested to value-initialize the je_val struct and clean up C-style casts.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread sql/item_jsonfunc.h Outdated
Comment thread sql/item_jsonfunc.h
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread sql/item_jsonfunc.cc
Comment thread sql/item_jsonfunc.h
Comment thread sql/item_jsonfunc.cc Outdated
@grooverdan grooverdan added GSoC External Contribution All PRs from entities outside of MariaDB Foundation, Corporation, Codership agreements. labels Jun 25, 2026
@grooverdan grooverdan self-assigned this Jun 25, 2026
Comment thread mysql-test/suite/json/t/member_of.test
Comment thread mysql-test/suite/json/t/member_of.test Outdated
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread sql/item_jsonfunc.h Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR adds SQL-standard JSON containment predicates value MEMBER OF (json_doc) and value NOT MEMBER OF (json_doc) by introducing a new Item_func_member_of that composes existing JSON items (JSON_QUOTE + JSON_CONTAINS), plus parser/lexer support and dedicated MTR coverage. It also updates JSON_QUOTE behavior for numeric and NULL inputs (and adjusts tests/results accordingly).

Changes:

  • Extend the SQL grammar/lexer to parse MEMBER OF / NOT MEMBER OF and print it back in EXPLAIN EXTENDED.
  • Add Item_func_member_of implementation with explicit JSON validation/error attribution and optimizer traversal hooks.
  • Update JSON_QUOTE to handle numeric and NULL inputs, and add/adjust MTR test coverage and expected results.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
sql/sql_yacc.yy Adds MEMBER token, precedence, and grammar rules for MEMBER OF / NOT MEMBER OF; adjusts %expect.
sql/lex.h Registers MEMBER keyword in the lexer symbol table.
sql/item_jsonfunc.h Declares Item_func_member_of (new composed JSON predicate item).
sql/item_jsonfunc.cc Implements Item_func_member_of; changes JSON_QUOTE to accept numeric/NULL inputs.
sql/item_cmpfunc.h Adds a 2-argument Item_func_opt_neg constructor used by the new predicate item.
mysql-test/suite/json/t/member_of.test New JSON suite test coverage for MEMBER OF / NOT MEMBER OF semantics.
mysql-test/suite/json/r/member_of.result Expected results for the new MEMBER OF test.
mysql-test/suite/json/t/json_no_table.test Updates expectations for JSON_QUOTE/JSON_UNQUOTE numeric handling.
mysql-test/suite/json/r/json_no_table.result Updates expected results for JSON_QUOTE/JSON_UNQUOTE numeric handling.
mysql-test/main/func_json.test Adds coverage for JSON_QUOTE numeric/NULL behavior and composition.
mysql-test/main/func_json.result Expected results for the new JSON_QUOTE coverage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread sql/item_jsonfunc.h
Comment thread sql/item_jsonfunc.cc
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread sql/item_jsonfunc.cc
Comment thread sql/item_jsonfunc.cc
Comment thread sql/sql_yacc.yy Outdated
@kjarir
kjarir force-pushed the bb-38591-member-of-grammar branch 2 times, most recently from 53d1e25 to 731ac67 Compare July 10, 2026 06:31
Comment thread sql/item_jsonfunc.h
Comment thread sql/item_jsonfunc.cc
Comment thread sql/item_jsonfunc.cc
Comment thread mysql-test/suite/json/t/member_of.test
kjarir added a commit to kjarir/server that referenced this pull request Jul 15, 2026
Resolve review feedback from Gemini, Copilot, grooverdan, and Rucha on PR MariaDB#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.
kjarir added a commit to kjarir/server that referenced this pull request Jul 17, 2026
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 MariaDB#5278: 'Note this function is a good place to preprocess if args
are constant to save expensive processing on each row.'

@grooverdan grooverdan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I need to get transformations looked at closer.

Comment thread mysql-test/main/func_json.test
Comment thread mysql-test/main/func_json.test
Comment thread mysql-test/main/func_json.test
Comment thread mysql-test/suite/json/r/member_of.result Outdated
Comment thread mysql-test/suite/json/t/member_of.test
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread sql/item_jsonfunc.cc
Comment thread sql/item_jsonfunc.h
Comment thread mysql-test/suite/json/t/member_of.test
Comment thread mysql-test/suite/json/t/member_of.test Outdated
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread sql/item_jsonfunc.h Outdated
Comment thread sql/item_jsonfunc.h Outdated
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread sql/item_jsonfunc.cc Outdated
kjarir added a commit to kjarir/server that referenced this pull request Aug 19, 2026
Per review on PR MariaDB#5278:
- Restore specific subclass pointer types for json_quote_item and
  json_contains_item (Item_func_json_quote* / Item_func_json_contains*)
- Use fix_length_and_dec() instead of fix_fields_if_needed() for helper
  items in fix_length_and_dec(); use Item_bool_func::fix_length_and_dec()
  to replace manual max_length/set_maybe_null
- Remove pre-validation JSON scan loops from val_bool() — json_contains
  already validates internally; remove je_val, a1_constant, a1_parsed,
  js_doc_cached and related state entirely
- Remove compile() override; simplify walk() and transform() to delegate
  through json_contains_item (which naturally covers json_quote_item as
  its argument); inline propagate_equal_fields() in header
- shallow_copy(): revert to one-liner — fix_length_and_dec() always
  re-initializes state before any evaluation
- Remove SHOW WARNINGS from member_of.test (MTR includes warnings
  in output by default); error messages for malformed JSON now correctly
  attribute to internal json_contains delegation
- Update class comment to reflect current architecture
Comment thread mysql-test/suite/json/r/member_of.result Outdated
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread sql/item_jsonfunc.cc
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread mysql-test/suite/json/r/member_of.result Outdated
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread sql/item_jsonfunc.h
Comment thread sql/item_jsonfunc.cc Outdated
kjarir added a commit to kjarir/server that referenced this pull request Aug 21, 2026
Fixes all 12 review items from grooverdan's review of PR MariaDB#5278:

- Fix stale a2_constant/a2_parsed cache in Item_func_json_contains that
  caused wrong MEMBER OF results for non-constant candidates (root cause:
  json_quote_item's const_item_cache was never updated after
  fix_length_and_dec, so it was read as constant-true by
  Item_func_json_contains::fix_length_and_dec). Fixed by calling
  update_used_tables() on both helper items before they're consumed.
- Fix null_value check ordering in val_bool() (json_contains_item's
  null_value must be checked before args[0]'s, since args[0] is only
  evaluated as a side effect of json_contains_item->val_bool()).
- Restructure Item_func_json_quote::val_str() switch to handle ROW_RESULT
  (single-column unwrap) and TIME_RESULT, replace silent NULL return on
  unknown types with ER_WRONG_ARGUMENTS, and set maybe_null accordingly.
- Simplify null check and numeric-append logic in val_str().
- Name immediate parent class explicitly in fix_length_and_dec()/walk()
  (Item_func_opt_neg); transform() intentionally kept as direct pointer
  call to avoid double transform_args() (see PR reply).
- Remove redundant is_json_type()/val_json() pre-check in val_bool() and
  unused tmp_candidate member.
- Found and fixed a latent bug in propagate_equal_fields() (was returning
  json_contains_item instead of this, violating the build_equal_items
  cond==this invariant) while regression-testing equality propagation
  for update_used_tables() (see PR reply) - kept update_used_tables()
  override, it is not redundant.
- Added mentor's four null-literal-vs-JSON-null test cases, a full
  cross-join regression test matching the mentor's original wrong-result
  table, a ROW_RESULT error-path test, and an equality-propagation
  regression test.

All changes verified against real source (not assumed) and the full
suite/json mtr suite (26/26 pass).
@kjarir

kjarir commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Re: return Item_func_opt_neg::transform(thd, transformer, arg); suggestion on Item_func_member_of::transform():

Item_func_opt_neg does not override transform(), so calling Item_func_opt_neg::transform() resolves to Item_func::transform(), which unconditionally calls transform_args() first. Item_func_member_of::transform() already calls transform_args() earlier in the function (to also transform json_contains_item). Calling Item_func_opt_neg::transform() at the end would run transform_args() a second time, double-transforming args[]. Kept return (this->*transformer)(thd, arg); with an inline comment explaining this — matches the pattern in Item_row::transform().

@kjarir

kjarir commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Re: removing update_used_tables() override on Item_func_member_of:

This turned out not to be redundant. It re-syncs json_quote_item's and json_contains_item's internal argument pointers to args[0]/args[1] whenever the optimizer substitutes an equal item during equality propagation. Found this while adding a regression test for a join condition (t1.a = t2.b AND t1.a MEMBER OF (t2.k)) — removing the override, combined with a separate pre-existing bug in propagate_equal_fields() (it was returning json_contains_item instead of this, violating the cond==this invariant in build_equal_items), triggered a DBUG_ASSERT crash in sql_select.cc. Fixed propagate_equal_fields() to correctly return this after delegating to json_contains_item, kept update_used_tables() as originally written, and added the join case as a regression test in member_of.test.

@kjarir

kjarir commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the exact repro! Our earlier test used RETURNS ROW(a INT), which is rejected at bind time by check_cols(1) and never reaches val_str() — that led us to incorrectly conclude the ROW_RESULT unwrap branch was unreachable dead code.

Corrected using your actual example: an ordinary scalar-returning stored function (RETURNS VARCHAR(...)) passes check_cols(1) (since Item_func_sp::check_cols() checks cmp_type(), not result_type()), but reports ROW_RESULT with cols() == 1 when val_str() inspects args[0]->result_type(). Reproduced your exact example and it matches — f_row_result() MEMBER OF (...) returns 0 with no error.

Updated member_of.test (section 19) to use this construct, kept the original RETURNS ROW(a INT) test as a separate section (still a valid test of literal ROW-type rejection at bind time), and documented the double-quoting behavior you noted — unwrapped VARCHAR return values get treated as STRING_RESULT and quoted/escaped even when their content is already valid JSON.

Comment thread mysql-test/suite/json/t/member_of.test Outdated
Comment thread mysql-test/suite/json/t/member_of.test Outdated
Comment thread mysql-test/suite/json/t/member_of.test Outdated
Comment thread mysql-test/suite/json/t/member_of.test Outdated
Comment thread mysql-test/suite/json/t/member_of.test
Comment thread mysql-test/suite/json/t/member_of.test Outdated
Comment thread mysql-test/suite/json/t/member_of.test Outdated
Comment thread mysql-test/suite/json/t/member_of.test Outdated
Comment thread sql/item_jsonfunc.cc Outdated
Comment thread sql/item_jsonfunc.cc
@grooverdan
grooverdan marked this pull request as ready for review August 25, 2026 06:41

@grooverdan grooverdan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Before final merge:

  • rebase/squash
  • mtr OPTIMIZER_TRACE to notembedded.test file.

Otherwise 🥳 - well done @kjarir !

@gkodinov gkodinov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a preliminary review. Please squash the commits down to 2:

  • the MDEV-13645 one. I would actually prefer that this fix is done in a separate PR, since it's needed down to 10.11.
  • the current one.

Also, please make sure the latest main is merged and conflicts are resolved.

I'd also appreciate an explanation on why Item_func_json_contains() cannot be used, but that's optional since it beyond the scope of the preliminary review.

Comment thread sql/sql_yacc.yy
JSON_QUOTE() previously only accepted STRING_RESULT arguments.
Extend it to handle all argument types correctly:

- Numeric types (INT_RESULT, REAL_RESULT, DECIMAL_RESULT): call val_str()
  which fills tmp_s with the text representation, then wrap it in JSON
  double-quotes.
- TIME_RESULT: treat the value as a string literal (quoted, not escaped).
- ROW_RESULT with a single column: unwrap and re-evaluate as string.
- SQL NULL input: return the 4-character JSON literal "null" (not SQL NULL),
  as required by the MEMBER OF operator (MDEV-38591) which needs json_quote_item
  to produce a well-formed JSON value to pass to json_contains_item.
- Fix fix_length_and_dec() to declare max_char_length as at least 4 characters
  via MY_MAX(args[0]->max_char_length() * 12 + 2, 4ULL): when the argument is
  a literal NULL its max_char_length() is 0, which would yield 2, causing the
  "null" output to be truncated to "nu" under the --view-protocol field-length
  check. The minimum of 4 ensures "null" is never truncated.

Tests in mysql-test/main/func_json.test and mysql-test/suite/json/r/json_no_table
are updated to reflect the new behavior.
@kjarir
kjarir force-pushed the bb-38591-member-of-grammar branch from 91d35c3 to 66a9345 Compare August 28, 2026 05:12
Add support for the SQL/JSON MEMBER OF predicate and its negation
NOT MEMBER OF, which test whether a scalar value is an element of a
JSON array:

  SELECT 2 MEMBER OF ('[1, 2, 3]');       -- 1
  SELECT 5 NOT MEMBER OF ('[1, 2, 3]');   -- 1

1. Grammar (sql/sql_yacc.yy, sql/lex.h):
   - Added MEMBER_SYM keyword to the lexer keyword table.
   - Added 'expr MEMBER_SYM OF_SYM ( expr )' production at CMP_PRECEDENCE,
     constructing Item_func_member_of(thd, $1, $5).
   - Added 'expr NOT_SYM MEMBER_SYM OF_SYM ( expr )' for the negated form,
     setting Item_func_opt_neg::negated = true.

2. Item class (sql/item_jsonfunc.h, sql/item_jsonfunc.cc):
   - Item_func_member_of inherits from Item_func_opt_neg (which provides the
     negated flag and NOT MEMBER OF printing for free).
   - Composition model: fix_length_and_dec() constructs two private helper items
     outside the normal args[] array:
       * json_quote_item  (Item_func_json_quote*): wraps the candidate if it is
         not already a JSON-typed column, so any scalar SQL value becomes a
         well-formed JSON string literal suitable for containment testing.
       * json_contains_item (Item_func_json_contains*): does the actual lookup
         of the quoted candidate inside the JSON array container.
     Both helpers have update_used_tables() called after fix_length_and_dec() to
     ensure const_item_cache is correct before json_contains_item reads it;
     incorrect const_item_cache caused the stale-cache bug (wrong results on
     repeated rows with non-constant candidate).
   - val_bool() calls json_contains_item->val_bool() and propagates
     json_contains_item->null_value (container NULL gives SQL NULL); negates
     the result if negated is set.
   - walk(), transform(), update_used_tables(), and propagate_equal_fields() are
     overridden to explicitly visit the private helper items, which are invisible
     to the default Item_func traversal of args[].
   - set_maybe_null() is called in fix_length_and_dec() for cursor protocol
     compatibility.

3. Dependency on MDEV-13645 and intentional NULL-candidate semantics:
   MDEV-13645 made JSON_QUOTE(NULL) return the string "null" rather than SQL
   NULL. MEMBER OF exploits this: when the candidate is SQL NULL, json_quote_item
   produces the four-character string "null", which json_contains_item correctly
   matches against a JSON null literal inside the array. This means:
     - NULL MEMBER OF ('[null]')   -> 1   (SQL NULL equals JSON null)
     - NULL MEMBER OF ('[1,2,3]')  -> 0   (no JSON null in array)
     - NULL MEMBER OF (NULL)       -> NULL (container IS NULL propagates)
   This intentionally diverges from MySQL's documented "If value is NULL,
   returns NULL" rule, per reviewer grooverdan's explicit request.

4. Kill-query / interruption support:
   Item_func_member_of::val_bool() is interruptible because it delegates
   entirely to json_contains_item->val_bool(), which respects THD::killed
   checks through the normal JSON traversal path.

5. Test coverage:
   - mysql-test/suite/json/t/member_of.test: 21 sections covering simple
     membership, type-strict comparison, SQL NULL propagation, malformed JSON,
     nested arrays/objects, scalar-as-container, table-column evaluation,
     prepared statements, constant-caching regression, EXPLAIN EXTENDED,
     equality-propagation regression, ROW_RESULT rejection, stored function
     VARCHAR and JSON return types, TIME_RESULT, and RETURNS ROW rejection.
   - mysql-test/suite/json/t/member_of_notembedded.test: optimizer trace test
     confirming MEMBER OF participates correctly in condition_processing, with
     equality_propagation folding 'x MEMBER OF (j) AND x=10' into
     '10 MEMBER OF (j)'.
@kjarir
kjarir force-pushed the bb-38591-member-of-grammar branch from 1357fe0 to 7dbc4be Compare August 28, 2026 05:42

@gkodinov gkodinov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Now the following tests are failing, please re-record: perfschema.start_server_low_digest_sql_length perfschema.digest_view

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

External Contribution All PRs from entities outside of MariaDB Foundation, Corporation, Codership agreements. GSoC

Development

Successfully merging this pull request may close these issues.

5 participants