Skip to content

[codex] Fix MySQL SET expression parser gaps#54

Merged
renecannao merged 2 commits into
mainfrom
fix/mysql-set-expression-gaps
Jul 8, 2026
Merged

[codex] Fix MySQL SET expression parser gaps#54
renecannao merged 2 commits into
mainfrom
fix/mysql-set-expression-gaps

Conversation

@renecannao

@renecannao renecannao commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes MySQL SET parsing gaps found while porting ProxySQL PR #5088 TAP coverage to ParserSQL.

  • support MySQL SET LOCAL, SET PERSIST, SET PERSIST_ONLY, scoped comma items, and dotted single-at user variables
  • add MySQL expression support for XOR, bit operators, shifts, DIV, MOD, REGEXP, NOT REGEXP, MEMBER OF, SOUNDS LIKE, and INTERVAL n unit literals
  • parse and emit SET RHS subqueries, including IN (SELECT ...), NOT IN (SELECT ...), scalar subqueries, and quantified ALL (SELECT ...)
  • add focused AST and emitter round-trip tests for the ProxySQL-derived cases

Closes #52.
Closes #53.

Root Cause

SetParser only recognized GLOBAL and SESSION as MySQL assignment scopes, comma-separated assignments did not re-check scoped forms, and single-at user variable targets stopped before dotted names. The expression parser also lacked tokenization and Pratt precedence entries for several MySQL operators, and SET RHS parsing was not wired to the existing subquery callback/emitter path.

Validation

  • make test
  • Result: 1311 tests run, 1274 passed, 37 integration tests skipped

Summary by CodeRabbit

  • New Features

    • Expanded SQL support for additional operators and expressions, including shifts, bitwise ops, DIV/MOD, REGEXP, SOUNDS LIKE, MEMBER OF, INTERVAL, and quantified subqueries.
    • Added support for more SET statement scopes, including LOCAL, PERSIST, and PERSIST_ONLY, plus dotted user-variable assignments.
  • Bug Fixes

    • Improved handling of negated expressions such as NOT IN, NOT BETWEEN, NOT LIKE, and NOT REGEXP.
    • Fixed subquery rendering so standalone subqueries and SET-related subqueries round-trip more consistently.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@renecannao, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 973141fc-a74f-460f-9e43-5461b85dc838

📥 Commits

Reviewing files that changed from the base of the PR and between c5085c7 and 4c74bee.

📒 Files selected for processing (1)
  • include/sql_engine/local_txn.h
📝 Walkthrough

Walkthrough

This PR extends MySQL SQL parsing: SET statements now recognize LOCAL/PERSIST/PERSIST_ONLY scopes and dotted user variables, with subquery callback wiring. Expression parsing gains new tokens/operators (XOR, bitwise, shifts, DIV, MOD, REGEXP, SOUNDS LIKE, MEMBER OF, INTERVAL) and subquery-aware NOT/IN emission, plus corresponding tests.

Changes

SET Statement Scope and Variable Handling

Layer / File(s) Summary
SET assignment scope detection
include/sql_parser/set_parser.h
New is_set_assignment_scope_ helper centralizes recognition of GLOBAL/SESSION/LOCAL/PERSIST/PERSIST_ONLY, applied in the main parse() loop and comma-item parsing.
Dotted user variable parsing
include/sql_parser/set_parser.h
parse_variable_assignment now builds a canonical identifier for @name.actual in addition to plain @name.
Subquery callback wiring for SET
include/sql_parser/set_parser.h, src/sql_parser/parser.cpp
New set_subquery_callback forwards a subquery parse callback into ExpressionParser; Parser::parse_set installs parse_subquery_select.
SET scope and dotted variable tests
tests/test_emitter.cpp, tests/test_set.cpp
New round-trip and AST-structural tests cover PERSIST/PERSIST_ONLY/LOCAL scopes and dotted/mixed user variable assignments, plus a value() test helper.

Estimated code review effort: 4 (Complex) | ~60 minutes

Expression Parsing and Emission Extensions

Layer / File(s) Summary
New tokens and keywords
include/sql_parser/token.h, include/sql_parser/tokenizer.h, include/sql_parser/keywords_mysql.h
Adds token types for shift operators, DIV, MOD, REGEXP, SOUNDS, MEMBER, INTERVAL; tokenizer recognizes <</>>; keyword table gets matching entries.
Operator precedence and infix parsing
include/sql_parser/expression_parser.h
Precedence enum and infix_precedence() gain XOR/bitwise/shift/DIV/MOD levels; NOT-compound infix handling covers LIKE/REGEXP; DIV/MOD allowed as identifiers.
New expression constructs
include/sql_parser/expression_parser.h
Adds parse_sounds_like, parse_member_of, parse_interval_literal, parse_quantified_subquery, and an updated skip_to_matching_paren returning the matched position.
Subquery span capture and NOT/subquery emission
include/sql_parser/expression_parser.h, include/sql_parser/emitter.h
Legacy subquery parsing captures skipped text span; emitter adds NODE_SUBQUERY dispatch and dedicated NOT IN/NOT BETWEEN/negated binary op emitters, including single-subquery IN/NOT IN forms.
RHS expression and subquery round-trip tests
tests/test_emitter.cpp
New parameterized tests assert round-trip emission for bitwise/shift/XOR/DIV/MOD/REGEXP/MEMBER OF/SOUNDS LIKE/INTERVAL expressions and IN/ALL/NOT IN/scalar subquery forms.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Parser as Parser::parse_set
  participant SetParser
  participant ExpressionParser
  participant Emitter

  Parser->>SetParser: construct and set_subquery_callback(parse_subquery_select)
  SetParser->>SetParser: is_set_assignment_scope_(token)
  SetParser->>SetParser: parse_variable_assignment(scope_token)
  SetParser->>ExpressionParser: parse RHS expression
  ExpressionParser->>ExpressionParser: parse_infix (SOUNDS/MEMBER/NOT REGEXP/quantified subquery)
  ExpressionParser->>ExpressionParser: parse_quantified_subquery / parse_subquery_inner (capture span)
  ExpressionParser-->>SetParser: expression AST node
  SetParser-->>Parser: SET AST
  Parser->>Emitter: emit_node(SET AST)
  Emitter->>Emitter: emit_unary_op / emit_in_list (NOT IN, NOT BETWEEN, subquery)
  Emitter-->>Parser: emitted SQL text
Loading

Possibly related PRs

  • ProxySQL/ParserSQL#39: Both PRs modify the SET parsing flow in include/sql_parser/set_parser.h, specifically parse_variable_assignment()/parse().

Poem

A rabbit hops through SET and scope,
LOCAL, PERSIST — new paths of hope. 🐇
XOR and SOUNDS and MEMBER OF too,
INTERVAL hops with NOT REGEXP too!
Subqueries nest without a fret,
This burrow's build is my best set. 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change: fixing MySQL SET parser gaps.
Linked Issues check ✅ Passed The changes cover the requested SET scopes, dotted user vars, operator support, subqueries, and INTERVAL parsing/emission for [#52, #53].
Out of Scope Changes check ✅ Passed The modified parser, emitter, tokenizer, keywords, and tests all align with the linked SET parsing objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mysql-set-expression-gaps

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@renecannao renecannao marked this pull request as ready for review July 8, 2026 13:37

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
include/sql_parser/set_parser.h (1)

563-571: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider validating actual_name token type in dotted @ variable branch.

The new dotted @name.actual branch consumes actual_name without checking its token type, consistent with the pre-existing @@ branch (lines 579-583). However, the PostgreSQL schema-qualified branch (lines 604-613) does validate rhs.type == TK_IDENTIFIER. Adding the same guard here would catch malformed input like @user.= 1 earlier and produce a clearer error instead of building a bogus identifier like @user.=.

♻️ Optional: add token type validation
             if (tok_.peek().type == TokenType::TK_DOT) {
                 tok_.skip();
                 Token actual_name = tok_.next_token();
+                if (actual_name.type != TokenType::TK_IDENTIFIER) {
+                    tok_.flag_error();
+                    return nullptr;
+                }
                 target->add_child(make_node(arena_, NodeType::NODE_IDENTIFIER,
                     build_scoped_dotted_identifier_("@", name, actual_name)));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/sql_parser/set_parser.h` around lines 563 - 571, Validate the token
consumed in the dotted `@name.actual` branch before building the scoped
identifier. In set_parser.h, within the logic that handles tok_.peek().type ==
TokenType::TK_DOT, check that actual_name from tok_.next_token() is a valid
identifier (same style as the PostgreSQL schema-qualified branch and consistent
with build_scoped_dotted_identifier_ usage), and raise a parser error for
malformed input instead of creating an invalid NODE_IDENTIFIER.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@include/sql_parser/expression_parser.h`:
- Around line 16-23: The operator precedence list in the expression parser
places BIT_XOR too low, causing XOR to bind after multiplication instead of
before it. Update the precedence ordering in the expression parser
enum/precedence table so BIT_XOR is moved above MULTIPLICATION, and verify the
parser path that uses this ordering now parses expressions like a * b ^ c as a *
(b ^ c).

---

Nitpick comments:
In `@include/sql_parser/set_parser.h`:
- Around line 563-571: Validate the token consumed in the dotted `@name.actual`
branch before building the scoped identifier. In set_parser.h, within the logic
that handles tok_.peek().type == TokenType::TK_DOT, check that actual_name from
tok_.next_token() is a valid identifier (same style as the PostgreSQL
schema-qualified branch and consistent with build_scoped_dotted_identifier_
usage), and raise a parser error for malformed input instead of creating an
invalid NODE_IDENTIFIER.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 05da4a72-7632-4717-a456-537a95f24f00

📥 Commits

Reviewing files that changed from the base of the PR and between 3cfc581 and c5085c7.

📒 Files selected for processing (9)
  • include/sql_parser/emitter.h
  • include/sql_parser/expression_parser.h
  • include/sql_parser/keywords_mysql.h
  • include/sql_parser/set_parser.h
  • include/sql_parser/token.h
  • include/sql_parser/tokenizer.h
  • src/sql_parser/parser.cpp
  • tests/test_emitter.cpp
  • tests/test_set.cpp

Comment on lines +16 to +23
XOR, // XOR
AND, // AND
NOT, // NOT (prefix)
COMPARISON, // =, <, >, <=, >=, !=, <>, IS, LIKE, IN, BETWEEN
BIT_OR, // |
BIT_XOR, // ^
BIT_AND, // &
SHIFT, // <<, >>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

MySQL operator precedence caret bitwise XOR vs multiplication

💡 Result:

In MySQL, the bitwise XOR operator (^) has a higher precedence than the multiplication operator (). According to the official MySQL operator precedence documentation, the caret (^) operator appears earlier in the precedence list—meaning it is evaluated first—than the multiplication () operator [1][2]. The relevant portion of the MySQL operator precedence hierarchy (from highest to lowest) is [1][2]: 1. ^ (Bitwise XOR) 2. *, /, DIV, %, MOD 3. -, + (Addition and Subtraction) Because the bitwise XOR operator has higher precedence, an expression such as 2 ^ 3 * 4 would be evaluated as (2 ^ 3) * 4. To ensure a different order of operations, you must use parentheses explicitly (e.g., 2 ^ (3 * 4)) [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== File map =="
git ls-files include/sql_parser/expression_parser.h src/sql_parser | sed -n '1,120p'

echo
echo "== Outline of include/sql_parser/expression_parser.h =="
ast-grep outline include/sql_parser/expression_parser.h --view expanded || true

echo
echo "== Relevant lines in include/sql_parser/expression_parser.h =="
nl -ba include/sql_parser/expression_parser.h | sed -n '1,220p'

echo
echo "== Search for precedence mappings and CARET token handling =="
rg -n "CARET|BIT_XOR|MULTIPLICATION|SHIFT|precedence|operator precedence|infix_precedence|parse_atom|parse_sounds_like|parse_member_of|skip_to_matching_paren|is_keyword_as_identifier" include/sql_parser src/sql_parser

echo
echo "== Relevant parser implementation excerpts =="
for f in $(rg -l "BIT_XOR|infix_precedence|parse_atom|skip_to_matching_paren" include/sql_parser src/sql_parser); do
  echo "--- $f ---"
  nl -ba "$f" | sed -n '1,260p'
done

Repository: ProxySQL/ParserSQL

Length of output: 1959


BIT_XOR (^) needs to move above MULTIPLICATION. MySQL evaluates ^ before * / DIV % MOD, so this order makes a * b ^ c parse as (a * b) ^ c instead of a * (b ^ c).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/sql_parser/expression_parser.h` around lines 16 - 23, The operator
precedence list in the expression parser places BIT_XOR too low, causing XOR to
bind after multiplication instead of before it. Update the precedence ordering
in the expression parser enum/precedence table so BIT_XOR is moved above
MULTIPLICATION, and verify the parser path that uses this ordering now parses
expressions like a * b ^ c as a * (b ^ c).

@renecannao renecannao merged commit a5cd096 into main Jul 8, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant