Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/sql_engine/local_txn.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <unordered_map>
#include <cstring>
#include <memory>
#include <algorithm>

namespace sql_engine {

Expand Down
93 changes: 90 additions & 3 deletions include/sql_parser/emitter.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ class Emitter {
case NodeType::NODE_ARRAY_CONSTRUCTOR: emit_array_constructor(node); break;
case NodeType::NODE_ARRAY_SUBSCRIPT: emit_array_subscript(node); break;
case NodeType::NODE_FIELD_ACCESS: emit_field_access(node); break;
case NodeType::NODE_SUBQUERY: emit_value(node); break;
case NodeType::NODE_SUBQUERY: emit_subquery(node); break;

// ---- Leaf nodes (emit value directly) ----
case NodeType::NODE_PLACEHOLDER:
Expand Down Expand Up @@ -1061,10 +1061,28 @@ class Emitter {
}

void emit_unary_op(const AstNode* node) {
const AstNode* child = node->first_child;
if (is_not_operator(node) && child) {
if (child->type == NodeType::NODE_IN_LIST) {
emit_not_in_list(child);
return;
}
if (child->type == NodeType::NODE_BETWEEN) {
emit_not_between(child);
return;
}
if (child->type == NodeType::NODE_BINARY_OP &&
(child->value().equals_ci("LIKE", 4) ||
child->value().equals_ci("REGEXP", 6))) {
emit_not_binary_op(child);
return;
}
}

emit_value(node);
// Add space for keyword operators like NOT, no space for - or +
if (node->value_len > 1) sb_.append_char(' ');
if (node->first_child) emit_node(node->first_child);
if (child) emit_node(child);
}

void emit_function_call(const AstNode* node) {
Expand Down Expand Up @@ -1102,13 +1120,61 @@ class Emitter {

void emit_in_list(const AstNode* node) {
const AstNode* expr = node->first_child;
const AstNode* first_val = expr ? expr->next_sibling : nullptr;
if (expr) emit_node(expr);
if (first_val && first_val->type == NodeType::NODE_SUBQUERY &&
!first_val->next_sibling) {
sb_.append(" IN ");
emit_node(first_val);
return;
}
sb_.append(" IN (");
if (mode_ == EmitMode::DIGEST) {
sb_.append_char('?');
} else {
bool first = true;
for (const AstNode* val = expr ? expr->next_sibling : nullptr; val; val = val->next_sibling) {
for (const AstNode* val = first_val; val; val = val->next_sibling) {
if (!first) sb_.append(", ");
first = false;
emit_node(val);
}
}
sb_.append_char(')');
}

void emit_subquery(const AstNode* node) {
if (node->flags == 1) {
sb_.append("EXISTS ");
}
sb_.append_char('(');
if (node->first_child) {
emit_node(node->first_child);
} else {
emit_value(node);
}
sb_.append_char(')');
}

bool is_not_operator(const AstNode* node) const {
return node && node->value().equals_ci("NOT", 3);
}

void emit_not_in_list(const AstNode* node) {
const AstNode* expr = node->first_child;
const AstNode* first_val = expr ? expr->next_sibling : nullptr;
if (expr) emit_node(expr);
if (first_val && first_val->type == NodeType::NODE_SUBQUERY &&
!first_val->next_sibling) {
sb_.append(" NOT IN ");
emit_node(first_val);
return;
}
sb_.append(" NOT IN (");
if (mode_ == EmitMode::DIGEST) {
sb_.append_char('?');
} else {
bool first = true;
for (const AstNode* val = first_val; val; val = val->next_sibling) {
if (!first) sb_.append(", ");
first = false;
emit_node(val);
Expand All @@ -1117,6 +1183,27 @@ class Emitter {
sb_.append_char(')');
}

void emit_not_between(const AstNode* node) {
const AstNode* expr = node->first_child;
const AstNode* low = expr ? expr->next_sibling : nullptr;
const AstNode* high = low ? low->next_sibling : nullptr;
if (expr) emit_node(expr);
sb_.append(" NOT BETWEEN ");
if (low) emit_node(low);
sb_.append(" AND ");
if (high) emit_node(high);
}

void emit_not_binary_op(const AstNode* node) {
const AstNode* left = node->first_child;
const AstNode* right = left ? left->next_sibling : nullptr;
if (left) emit_node(left);
sb_.append(" NOT ");
emit_value(node);
sb_.append_char(' ');
if (right) emit_node(right);
}

void emit_case_when(const AstNode* node) {
sb_.append("CASE ");
for (const AstNode* child = node->first_child; child; child = child->next_sibling) {
Expand Down
130 changes: 124 additions & 6 deletions include/sql_parser/expression_parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ namespace sql_parser {
enum class Precedence : uint8_t {
NONE = 0,
OR, // OR
XOR, // XOR
AND, // AND
NOT, // NOT (prefix)
COMPARISON, // =, <, >, <=, >=, !=, <>, IS, LIKE, IN, BETWEEN
BIT_OR, // |
BIT_XOR, // ^
BIT_AND, // &
SHIFT, // <<, >>
Comment on lines +16 to +23

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).

ADDITION, // +, -
MULTIPLICATION,// *, /, %
UNARY, // - (prefix), NOT
Expand Down Expand Up @@ -77,7 +82,12 @@ class ExpressionParser {
if (tok_.peek().type == TokenType::TK_RPAREN) tok_.skip();
} else {
// Legacy: skip to matching paren
skip_to_matching_paren();
const char* start = tok_.peek().text.ptr;
const char* end = skip_to_matching_paren();
if (start && end && end >= start) {
node->set_value(StringRef{start,
static_cast<uint32_t>(end - start)});
}
}
return node;
}
Expand Down Expand Up @@ -112,6 +122,12 @@ class ExpressionParser {
tok_.skip();
return make_node(arena_, NodeType::NODE_IDENTIFIER, t.text);
}
case TokenType::TK_INTERVAL: {
return parse_interval_literal(t);
}
case TokenType::TK_ALL: {
return parse_quantified_subquery(t);
}
case TokenType::TK_ASTERISK: {
tok_.skip();
return make_node(arena_, NodeType::NODE_ASTERISK, t.text);
Expand Down Expand Up @@ -336,6 +352,7 @@ class ExpressionParser {
static Precedence infix_precedence(TokenType type) {
switch (type) {
case TokenType::TK_OR: return Precedence::OR;
case TokenType::TK_XOR: return Precedence::XOR;
case TokenType::TK_AND: return Precedence::AND;
case TokenType::TK_NOT: return Precedence::COMPARISON; // NOT IN/BETWEEN/LIKE
case TokenType::TK_EQUAL:
Expand All @@ -344,15 +361,25 @@ class ExpressionParser {
case TokenType::TK_GREATER:
case TokenType::TK_LESS_EQUAL:
case TokenType::TK_GREATER_EQUAL:
case TokenType::TK_REGEXP:
case TokenType::TK_SOUNDS:
case TokenType::TK_MEMBER:
case TokenType::TK_LIKE: return Precedence::COMPARISON;
case TokenType::TK_IS: return Precedence::COMPARISON;
case TokenType::TK_IN: return Precedence::COMPARISON;
case TokenType::TK_BETWEEN: return Precedence::COMPARISON;
case TokenType::TK_PIPE: return Precedence::BIT_OR;
case TokenType::TK_CARET: return Precedence::BIT_XOR;
case TokenType::TK_AMPERSAND: return Precedence::BIT_AND;
case TokenType::TK_SHIFT_LEFT:
case TokenType::TK_SHIFT_RIGHT: return Precedence::SHIFT;
case TokenType::TK_PLUS:
case TokenType::TK_MINUS: return Precedence::ADDITION;
case TokenType::TK_ASTERISK:
case TokenType::TK_SLASH:
case TokenType::TK_PERCENT: return Precedence::MULTIPLICATION;
case TokenType::TK_PERCENT:
case TokenType::TK_DIV:
case TokenType::TK_MOD: return Precedence::MULTIPLICATION;
case TokenType::TK_DOUBLE_PIPE: return Precedence::ADDITION; // string concat
default: return Precedence::NONE;
}
Expand All @@ -363,7 +390,7 @@ class ExpressionParser {

switch (op.type) {
case TokenType::TK_NOT: {
// NOT IN / NOT BETWEEN / NOT LIKE — compound negated infix
// NOT IN / NOT BETWEEN / NOT LIKE / NOT REGEXP — compound negated infix
Token actual_op = tok_.peek();
if (actual_op.type == TokenType::TK_IN) {
tok_.skip();
Expand All @@ -380,7 +407,8 @@ class ExpressionParser {
not_node->add_child(between_node);
return not_node;
}
if (actual_op.type == TokenType::TK_LIKE) {
if (actual_op.type == TokenType::TK_LIKE ||
actual_op.type == TokenType::TK_REGEXP) {
tok_.skip();
AstNode* right = parse(prec);
AstNode* like_node = make_node(arena_, NodeType::NODE_BINARY_OP, actual_op.text);
Expand Down Expand Up @@ -422,6 +450,10 @@ class ExpressionParser {
return parse_in(left);
case TokenType::TK_BETWEEN:
return parse_between(left);
case TokenType::TK_SOUNDS:
return parse_sounds_like(left, op, prec);
case TokenType::TK_MEMBER:
return parse_member_of(left, op, prec);
default: {
// Standard binary operator
AstNode* right = parse(prec);
Expand Down Expand Up @@ -459,6 +491,54 @@ class ExpressionParser {
return node;
}

AstNode* parse_sounds_like(AstNode* left, const Token& sounds, Precedence prec) {
Token like = tok_.peek();
StringRef op_text = sounds.text;
if (like.type == TokenType::TK_LIKE) {
tok_.skip();
op_text = StringRef{sounds.text.ptr,
static_cast<uint32_t>((like.text.ptr + like.text.len) - sounds.text.ptr)};
}
AstNode* right = parse(prec);
AstNode* node = make_node(arena_, NodeType::NODE_BINARY_OP, op_text);
node->add_child(left);
if (right) node->add_child(right);
return node;
}

AstNode* parse_member_of(AstNode* left, const Token& member, Precedence prec) {
Token of = tok_.peek();
StringRef op_text = member.text;
if (of.type == TokenType::TK_OF) {
tok_.skip();
op_text = StringRef{member.text.ptr,
static_cast<uint32_t>((of.text.ptr + of.text.len) - member.text.ptr)};
}

AstNode* right = nullptr;
if (tok_.peek().type == TokenType::TK_LPAREN) {
tok_.skip();
AstNode* tuple = make_node(arena_, NodeType::NODE_TUPLE);
if (tok_.peek().type != TokenType::TK_RPAREN) {
while (true) {
AstNode* elem = parse();
if (elem) tuple->add_child(elem);
if (tok_.peek().type == TokenType::TK_COMMA) tok_.skip();
else break;
}
}
if (tok_.peek().type == TokenType::TK_RPAREN) tok_.skip();
right = tuple;
} else {
right = parse(prec);
}

AstNode* node = make_node(arena_, NodeType::NODE_BINARY_OP, op_text);
node->add_child(left);
if (right) node->add_child(right);
return node;
}

// BETWEEN low AND high
AstNode* parse_between(AstNode* left) {
AstNode* node = make_node(arena_, NodeType::NODE_BETWEEN);
Expand All @@ -473,6 +553,38 @@ class ExpressionParser {
return node;
}

AstNode* parse_interval_literal(const Token& interval) {
tok_.skip();
Token amount = tok_.next_token();
if (amount.type == TokenType::TK_EOF || amount.type == TokenType::TK_ERROR) {
return make_node(arena_, NodeType::NODE_IDENTIFIER, interval.text);
}
Token unit = tok_.next_token();
if (unit.type == TokenType::TK_EOF || unit.type == TokenType::TK_ERROR) {
StringRef span{interval.text.ptr,
static_cast<uint32_t>((amount.text.ptr + amount.text.len) - interval.text.ptr)};
return make_node(arena_, NodeType::NODE_IDENTIFIER, span);
}
StringRef span{interval.text.ptr,
static_cast<uint32_t>((unit.text.ptr + unit.text.len) - interval.text.ptr)};
return make_node(arena_, NodeType::NODE_IDENTIFIER, span);
}

AstNode* parse_quantified_subquery(const Token& quantifier) {
tok_.skip();
if (tok_.peek().type == TokenType::TK_LPAREN) {
tok_.skip();
if (tok_.peek().type == TokenType::TK_SELECT) {
const char* close = skip_to_matching_paren();
const char* end = close ? close + 1 : tok_.input_end();
return make_node(arena_, NodeType::NODE_IDENTIFIER,
StringRef{quantifier.text.ptr,
static_cast<uint32_t>(end - quantifier.text.ptr)});
}
}
return make_node(arena_, NodeType::NODE_IDENTIFIER, quantifier.text);
}

// CASE [expr] WHEN ... THEN ... [ELSE ...] END
AstNode* parse_case() {
AstNode* node = make_node(arena_, NodeType::NODE_CASE_WHEN);
Expand Down Expand Up @@ -607,14 +719,18 @@ class ExpressionParser {
}

// Skip tokens until matching closing paren (handles nesting)
void skip_to_matching_paren() {
const char* skip_to_matching_paren() {
int depth = 1;
while (depth > 0) {
Token t = tok_.next_token();
if (t.type == TokenType::TK_LPAREN) ++depth;
else if (t.type == TokenType::TK_RPAREN) --depth;
else if (t.type == TokenType::TK_RPAREN) {
--depth;
if (depth == 0) return t.text.ptr;
}
else if (t.type == TokenType::TK_EOF) break;
}
return tok_.input_end();
}

// Some keywords can appear as identifiers in expression context
Expand Down Expand Up @@ -698,6 +814,8 @@ class ExpressionParser {
case TokenType::TK_PARTITION:
case TokenType::TK_RECURSIVE:
case TokenType::TK_REPLACE:
case TokenType::TK_DIV:
case TokenType::TK_MOD:
return true;
default:
return false;
Expand Down
Loading
Loading