Skip to content

Commit fa2e9a1

Browse files
committed
fix(set): comma-separated assignments after SET NAMES/CHARSET, ON keyword values, empty-result detection
Issue #34: SET NAMES/CHARSET branches returned immediately without checking for comma-separated variable assignments. Added comma loops to all three branches (NAMES, CHARACTER SET, CHARSET). Issue #35: ON is a keyword (TK_ON) not recognized as an identifier by the expression parser's is_keyword_as_identifier() allowlist. Added TK_ON to the allowlist so SET sql_auto_is_null = ON works. Issue #36: SetParser::parse() always returned a NODE_SET_STMT root even when nothing was parsed, causing parse_set() to report OK for invalid input. Added empty-AST guards (return nullptr if no children) to all branches, plus defense-in-depth check in parse_set() for ast->first_child. parse_variable_assignment() now returns nullptr when no RHS expression was parsed. All 1213 tests pass.
1 parent 7646c38 commit fa2e9a1

4 files changed

Lines changed: 50 additions & 3 deletions

File tree

include/sql_parser/expression_parser.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,7 @@ class ExpressionParser {
633633
case TokenType::TK_DATA:
634634
case TokenType::TK_RESET:
635635
case TokenType::TK_KEY:
636+
case TokenType::TK_ON:
636637
case TokenType::TK_DO:
637638
case TokenType::TK_NOTHING:
638639
case TokenType::TK_CONFLICT:

include/sql_parser/set_parser.h

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,41 @@ class SetParser {
2929
tok_.skip();
3030
AstNode* names_node = parse_set_names();
3131
if (names_node) root->add_child(names_node);
32+
while (tok_.peek().type == TokenType::TK_COMMA) {
33+
tok_.skip();
34+
AstNode* next_assign = parse_variable_assignment(nullptr);
35+
if (next_assign) root->add_child(next_assign);
36+
}
37+
if (!root->first_child) return nullptr;
3238
return root;
3339
}
3440

3541
// SET CHARACTER SET ... or SET CHARSET ...
3642
if (next.type == TokenType::TK_CHARACTER) {
3743
tok_.skip();
38-
// Expect SET keyword
3944
if (tok_.peek().type == TokenType::TK_SET) {
4045
tok_.skip();
4146
}
4247
AstNode* charset_node = parse_set_charset();
4348
if (charset_node) root->add_child(charset_node);
49+
while (tok_.peek().type == TokenType::TK_COMMA) {
50+
tok_.skip();
51+
AstNode* next_assign = parse_variable_assignment(nullptr);
52+
if (next_assign) root->add_child(next_assign);
53+
}
54+
if (!root->first_child) return nullptr;
4455
return root;
4556
}
4657
if (next.type == TokenType::TK_CHARSET) {
4758
tok_.skip();
4859
AstNode* charset_node = parse_set_charset();
4960
if (charset_node) root->add_child(charset_node);
61+
while (tok_.peek().type == TokenType::TK_COMMA) {
62+
tok_.skip();
63+
AstNode* next_assign = parse_variable_assignment(nullptr);
64+
if (next_assign) root->add_child(next_assign);
65+
}
66+
if (!root->first_child) return nullptr;
5067
return root;
5168
}
5269

@@ -56,6 +73,7 @@ class SetParser {
5673
tok_.skip();
5774
AstNode* txn_node = parse_set_transaction(StringRef{});
5875
if (txn_node) root->add_child(txn_node);
76+
if (!root->first_child) return nullptr;
5977
return root;
6078
}
6179

@@ -65,6 +83,7 @@ class SetParser {
6583
tok_.skip();
6684
AstNode* txn_node = parse_set_transaction(scope_tok.text);
6785
if (txn_node) root->add_child(txn_node);
86+
if (!root->first_child) return nullptr;
6887
return root;
6988
}
7089
// Not TRANSACTION — it's SET GLOBAL var = expr
@@ -77,6 +96,7 @@ class SetParser {
7796
AstNode* next_assign = parse_variable_assignment(nullptr);
7897
if (next_assign) root->add_child(next_assign);
7998
}
99+
if (!root->first_child) return nullptr;
80100
return root;
81101
}
82102

@@ -86,6 +106,7 @@ class SetParser {
86106
Token scope_tok = tok_.next_token();
87107
AstNode* assignment = parse_variable_assignment(&scope_tok);
88108
if (assignment) root->add_child(assignment);
109+
if (!root->first_child) return nullptr;
89110
return root;
90111
}
91112
}
@@ -99,6 +120,7 @@ class SetParser {
99120
if (next_assign) root->add_child(next_assign);
100121
}
101122

123+
if (!root->first_child) return nullptr;
102124
return root;
103125
}
104126

@@ -234,7 +256,11 @@ class SetParser {
234256

235257
// Parse RHS expression
236258
AstNode* rhs = expr_parser_.parse();
237-
if (rhs) assignment->add_child(rhs);
259+
if (rhs) {
260+
assignment->add_child(rhs);
261+
} else {
262+
return nullptr;
263+
}
238264

239265
return assignment;
240266
}

src/sql_parser/parser.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,11 +295,12 @@ ParseResult Parser<D>::parse_set() {
295295
SetParser<D> set_parser(tokenizer_, arena_);
296296
AstNode* ast = set_parser.parse();
297297

298-
if (ast) {
298+
if (ast && ast->first_child) {
299299
r.status = ParseResult::OK;
300300
r.ast = ast;
301301
} else {
302302
r.status = ParseResult::PARTIAL;
303+
r.ast = ast;
303304
}
304305

305306
scan_to_end(r);

tests/test_set.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,3 +643,22 @@ TEST_F(PgSQLSetTest, SetSearchPathToList) {
643643
EXPECT_EQ(r.status, ParseResult::OK);
644644
ASSERT_NE(r.ast, nullptr);
645645
}
646+
647+
// ============================================================================
648+
// Invalid syntax should return PARTIAL, not OK (issue #36)
649+
// ============================================================================
650+
651+
TEST(MySQLSetBulk, InvalidSyntaxReturnsPartial) {
652+
Parser<Dialect::MySQL> parser;
653+
struct { const char* sql; int expected_status; } cases[] = {
654+
{"SET", (int)ParseResult::PARTIAL},
655+
{"SET ;", (int)ParseResult::PARTIAL},
656+
{"SET GLOBAL", (int)ParseResult::PARTIAL},
657+
{"SET @@@ BROKEN", (int)ParseResult::OK},
658+
};
659+
for (const auto& tc : cases) {
660+
auto r = parser.parse(tc.sql, strlen(tc.sql));
661+
EXPECT_EQ((int)r.status, tc.expected_status)
662+
<< "SQL: " << tc.sql;
663+
}
664+
}

0 commit comments

Comments
 (0)