Skip to content

Commit 96145db

Browse files
committed
Add comprehensive UPDATE parser tests with round-trip verification
23 tests covering MySQL (simple, multi-column, options, ORDER BY, LIMIT, multi-table JOIN, comma join, LEFT JOIN) and PostgreSQL (simple, FROM, multi-table FROM, RETURNING, alias). Includes bulk data-driven tests (17 MySQL + 9 PostgreSQL variants) and round-trip emitter tests for both dialects.
1 parent b2ed564 commit 96145db

2 files changed

Lines changed: 332 additions & 1 deletion

File tree

Makefile.new

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ TEST_SRCS = $(TEST_DIR)/test_main.cpp \
2828
$(TEST_DIR)/test_select.cpp \
2929
$(TEST_DIR)/test_emitter.cpp \
3030
$(TEST_DIR)/test_stmt_cache.cpp \
31-
$(TEST_DIR)/test_insert.cpp
31+
$(TEST_DIR)/test_insert.cpp \
32+
$(TEST_DIR)/test_update.cpp
3233
TEST_OBJS = $(TEST_SRCS:.cpp=.o)
3334
TEST_TARGET = $(PROJECT_ROOT)/run_tests
3435

tests/test_update.cpp

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
#include <gtest/gtest.h>
2+
#include "sql_parser/parser.h"
3+
#include "sql_parser/emitter.h"
4+
5+
using namespace sql_parser;
6+
7+
class MySQLUpdateTest : public ::testing::Test {
8+
protected:
9+
Parser<Dialect::MySQL> parser;
10+
11+
int child_count(const AstNode* node) {
12+
int n = 0;
13+
for (const AstNode* c = node->first_child; c; c = c->next_sibling) ++n;
14+
return n;
15+
}
16+
17+
const AstNode* find_child(const AstNode* node, NodeType type) {
18+
for (const AstNode* c = node->first_child; c; c = c->next_sibling) {
19+
if (c->type == type) return c;
20+
}
21+
return nullptr;
22+
}
23+
24+
std::string round_trip(const char* sql) {
25+
auto r = parser.parse(sql, strlen(sql));
26+
if (!r.ast) return "[PARSE_FAILED]";
27+
Emitter<Dialect::MySQL> emitter(parser.arena());
28+
emitter.emit(r.ast);
29+
StringRef result = emitter.result();
30+
return std::string(result.ptr, result.len);
31+
}
32+
};
33+
34+
// ========== Basic UPDATE ==========
35+
36+
TEST_F(MySQLUpdateTest, SimpleUpdate) {
37+
auto r = parser.parse("UPDATE users SET name = 'Alice' WHERE id = 1", 45);
38+
EXPECT_EQ(r.status, ParseResult::OK);
39+
EXPECT_EQ(r.stmt_type, StmtType::UPDATE);
40+
ASSERT_NE(r.ast, nullptr);
41+
EXPECT_EQ(r.ast->type, NodeType::NODE_UPDATE_STMT);
42+
}
43+
44+
TEST_F(MySQLUpdateTest, UpdateMultipleColumns) {
45+
const char* sql = "UPDATE users SET name = 'Alice', email = 'a@b.com' WHERE id = 1";
46+
auto r = parser.parse(sql, strlen(sql));
47+
EXPECT_EQ(r.status, ParseResult::OK);
48+
ASSERT_NE(r.ast, nullptr);
49+
auto* set_clause = find_child(r.ast, NodeType::NODE_UPDATE_SET_CLAUSE);
50+
ASSERT_NE(set_clause, nullptr);
51+
EXPECT_EQ(child_count(set_clause), 2);
52+
}
53+
54+
TEST_F(MySQLUpdateTest, UpdateNoWhere) {
55+
auto r = parser.parse("UPDATE users SET active = 0", 27);
56+
EXPECT_EQ(r.status, ParseResult::OK);
57+
ASSERT_NE(r.ast, nullptr);
58+
auto* where = find_child(r.ast, NodeType::NODE_WHERE_CLAUSE);
59+
EXPECT_EQ(where, nullptr);
60+
}
61+
62+
TEST_F(MySQLUpdateTest, UpdateQualifiedTable) {
63+
auto r = parser.parse("UPDATE mydb.users SET name = 'x' WHERE id = 1", 46);
64+
EXPECT_EQ(r.status, ParseResult::OK);
65+
ASSERT_NE(r.ast, nullptr);
66+
}
67+
68+
// ========== MySQL Options ==========
69+
70+
TEST_F(MySQLUpdateTest, UpdateLowPriority) {
71+
auto r = parser.parse("UPDATE LOW_PRIORITY users SET name = 'x' WHERE id = 1", 54);
72+
EXPECT_EQ(r.status, ParseResult::OK);
73+
ASSERT_NE(r.ast, nullptr);
74+
auto* opts = find_child(r.ast, NodeType::NODE_STMT_OPTIONS);
75+
ASSERT_NE(opts, nullptr);
76+
}
77+
78+
TEST_F(MySQLUpdateTest, UpdateIgnore) {
79+
auto r = parser.parse("UPDATE IGNORE users SET name = 'x' WHERE id = 1", 48);
80+
EXPECT_EQ(r.status, ParseResult::OK);
81+
ASSERT_NE(r.ast, nullptr);
82+
}
83+
84+
TEST_F(MySQLUpdateTest, UpdateLowPriorityIgnore) {
85+
auto r = parser.parse("UPDATE LOW_PRIORITY IGNORE users SET name = 'x' WHERE id = 1", 61);
86+
EXPECT_EQ(r.status, ParseResult::OK);
87+
ASSERT_NE(r.ast, nullptr);
88+
}
89+
90+
// ========== MySQL ORDER BY + LIMIT ==========
91+
92+
TEST_F(MySQLUpdateTest, UpdateOrderByLimit) {
93+
const char* sql = "UPDATE users SET rank = rank + 1 WHERE active = 1 ORDER BY score DESC LIMIT 10";
94+
auto r = parser.parse(sql, strlen(sql));
95+
EXPECT_EQ(r.status, ParseResult::OK);
96+
ASSERT_NE(r.ast, nullptr);
97+
EXPECT_NE(find_child(r.ast, NodeType::NODE_ORDER_BY_CLAUSE), nullptr);
98+
EXPECT_NE(find_child(r.ast, NodeType::NODE_LIMIT_CLAUSE), nullptr);
99+
}
100+
101+
TEST_F(MySQLUpdateTest, UpdateLimit) {
102+
auto r = parser.parse("UPDATE users SET active = 0 LIMIT 100", 37);
103+
EXPECT_EQ(r.status, ParseResult::OK);
104+
ASSERT_NE(r.ast, nullptr);
105+
EXPECT_NE(find_child(r.ast, NodeType::NODE_LIMIT_CLAUSE), nullptr);
106+
}
107+
108+
// ========== MySQL Multi-Table UPDATE ==========
109+
110+
TEST_F(MySQLUpdateTest, MultiTableJoin) {
111+
const char* sql = "UPDATE users u JOIN orders o ON u.id = o.user_id "
112+
"SET u.total = u.total + o.amount WHERE o.status = 'shipped'";
113+
auto r = parser.parse(sql, strlen(sql));
114+
EXPECT_EQ(r.status, ParseResult::OK);
115+
ASSERT_NE(r.ast, nullptr);
116+
}
117+
118+
TEST_F(MySQLUpdateTest, MultiTableCommaJoin) {
119+
const char* sql = "UPDATE users, orders SET users.total = orders.amount "
120+
"WHERE users.id = orders.user_id";
121+
auto r = parser.parse(sql, strlen(sql));
122+
EXPECT_EQ(r.status, ParseResult::OK);
123+
ASSERT_NE(r.ast, nullptr);
124+
}
125+
126+
TEST_F(MySQLUpdateTest, MultiTableLeftJoin) {
127+
const char* sql = "UPDATE users u LEFT JOIN orders o ON u.id = o.user_id "
128+
"SET u.has_orders = 0 WHERE o.id IS NULL";
129+
auto r = parser.parse(sql, strlen(sql));
130+
EXPECT_EQ(r.status, ParseResult::OK);
131+
ASSERT_NE(r.ast, nullptr);
132+
}
133+
134+
// ========== PostgreSQL UPDATE ==========
135+
136+
class PgSQLUpdateTest : public ::testing::Test {
137+
protected:
138+
Parser<Dialect::PostgreSQL> parser;
139+
140+
int child_count(const AstNode* node) {
141+
int n = 0;
142+
for (const AstNode* c = node->first_child; c; c = c->next_sibling) ++n;
143+
return n;
144+
}
145+
146+
const AstNode* find_child(const AstNode* node, NodeType type) {
147+
for (const AstNode* c = node->first_child; c; c = c->next_sibling) {
148+
if (c->type == type) return c;
149+
}
150+
return nullptr;
151+
}
152+
153+
std::string round_trip(const char* sql) {
154+
auto r = parser.parse(sql, strlen(sql));
155+
if (!r.ast) return "[PARSE_FAILED]";
156+
Emitter<Dialect::PostgreSQL> emitter(parser.arena());
157+
emitter.emit(r.ast);
158+
StringRef result = emitter.result();
159+
return std::string(result.ptr, result.len);
160+
}
161+
};
162+
163+
TEST_F(PgSQLUpdateTest, SimpleUpdate) {
164+
auto r = parser.parse("UPDATE users SET name = 'Alice' WHERE id = 1", 45);
165+
EXPECT_EQ(r.status, ParseResult::OK);
166+
EXPECT_EQ(r.stmt_type, StmtType::UPDATE);
167+
ASSERT_NE(r.ast, nullptr);
168+
}
169+
170+
TEST_F(PgSQLUpdateTest, UpdateFrom) {
171+
const char* sql = "UPDATE users SET total = orders.amount FROM orders WHERE users.id = orders.user_id";
172+
auto r = parser.parse(sql, strlen(sql));
173+
EXPECT_EQ(r.status, ParseResult::OK);
174+
ASSERT_NE(r.ast, nullptr);
175+
auto* from = find_child(r.ast, NodeType::NODE_FROM_CLAUSE);
176+
ASSERT_NE(from, nullptr);
177+
}
178+
179+
TEST_F(PgSQLUpdateTest, UpdateFromMultipleTables) {
180+
const char* sql = "UPDATE users SET total = o.amount "
181+
"FROM orders o, payments p "
182+
"WHERE users.id = o.user_id AND o.id = p.order_id";
183+
auto r = parser.parse(sql, strlen(sql));
184+
EXPECT_EQ(r.status, ParseResult::OK);
185+
ASSERT_NE(r.ast, nullptr);
186+
}
187+
188+
TEST_F(PgSQLUpdateTest, UpdateReturning) {
189+
const char* sql = "UPDATE users SET name = 'Alice' WHERE id = 1 RETURNING id, name";
190+
auto r = parser.parse(sql, strlen(sql));
191+
EXPECT_EQ(r.status, ParseResult::OK);
192+
ASSERT_NE(r.ast, nullptr);
193+
auto* ret = find_child(r.ast, NodeType::NODE_RETURNING_CLAUSE);
194+
ASSERT_NE(ret, nullptr);
195+
EXPECT_EQ(child_count(ret), 2);
196+
}
197+
198+
TEST_F(PgSQLUpdateTest, UpdateReturningStar) {
199+
const char* sql = "UPDATE users SET name = 'Alice' WHERE id = 1 RETURNING *";
200+
auto r = parser.parse(sql, strlen(sql));
201+
EXPECT_EQ(r.status, ParseResult::OK);
202+
ASSERT_NE(r.ast, nullptr);
203+
}
204+
205+
TEST_F(PgSQLUpdateTest, UpdateFromReturning) {
206+
const char* sql = "UPDATE users SET total = o.amount FROM orders o "
207+
"WHERE users.id = o.user_id RETURNING users.id, users.total";
208+
auto r = parser.parse(sql, strlen(sql));
209+
EXPECT_EQ(r.status, ParseResult::OK);
210+
ASSERT_NE(r.ast, nullptr);
211+
EXPECT_NE(find_child(r.ast, NodeType::NODE_FROM_CLAUSE), nullptr);
212+
EXPECT_NE(find_child(r.ast, NodeType::NODE_RETURNING_CLAUSE), nullptr);
213+
}
214+
215+
TEST_F(PgSQLUpdateTest, UpdateWithAlias) {
216+
const char* sql = "UPDATE users AS u SET name = 'Alice' WHERE u.id = 1";
217+
auto r = parser.parse(sql, strlen(sql));
218+
EXPECT_EQ(r.status, ParseResult::OK);
219+
ASSERT_NE(r.ast, nullptr);
220+
}
221+
222+
// ========== Bulk data-driven tests ==========
223+
224+
struct UpdateTestCase {
225+
const char* sql;
226+
const char* description;
227+
};
228+
229+
static const UpdateTestCase mysql_update_bulk_cases[] = {
230+
{"UPDATE t SET a = 1", "simple no where"},
231+
{"UPDATE t SET a = 1 WHERE b = 2", "simple with where"},
232+
{"UPDATE t SET a = 1, b = 2 WHERE c = 3", "multi column"},
233+
{"UPDATE t SET a = a + 1 WHERE b > 0", "expression value"},
234+
{"UPDATE t SET a = 'hello' WHERE b = 1", "string value"},
235+
{"UPDATE db.t SET a = 1", "qualified table"},
236+
{"UPDATE LOW_PRIORITY t SET a = 1", "low priority"},
237+
{"UPDATE IGNORE t SET a = 1", "ignore"},
238+
{"UPDATE LOW_PRIORITY IGNORE t SET a = 1", "low priority ignore"},
239+
{"UPDATE t SET a = 1 ORDER BY b LIMIT 10", "order by limit"},
240+
{"UPDATE t SET a = 1 LIMIT 100", "limit only"},
241+
{"UPDATE t1 JOIN t2 ON t1.id = t2.fk SET t1.a = t2.b", "join update"},
242+
{"UPDATE t1, t2 SET t1.a = t2.b WHERE t1.id = t2.fk", "comma join update"},
243+
{"UPDATE t1 LEFT JOIN t2 ON t1.id = t2.fk SET t1.a = 0 WHERE t2.id IS NULL", "left join"},
244+
{"UPDATE t SET a = NOW()", "function in value"},
245+
{"UPDATE t SET a = NULL WHERE b = 1", "set null"},
246+
{"UPDATE t SET a = CASE WHEN b > 0 THEN 1 ELSE 0 END", "case expression"},
247+
};
248+
249+
TEST(MySQLUpdateBulk, AllCasesParseSuccessfully) {
250+
Parser<Dialect::MySQL> parser;
251+
for (const auto& tc : mysql_update_bulk_cases) {
252+
auto r = parser.parse(tc.sql, strlen(tc.sql));
253+
EXPECT_EQ(r.status, ParseResult::OK)
254+
<< "Failed: " << tc.description << "\n SQL: " << tc.sql;
255+
EXPECT_EQ(r.stmt_type, StmtType::UPDATE)
256+
<< "Failed: " << tc.description;
257+
EXPECT_NE(r.ast, nullptr)
258+
<< "Failed: " << tc.description << "\n SQL: " << tc.sql;
259+
}
260+
}
261+
262+
static const UpdateTestCase pgsql_update_bulk_cases[] = {
263+
{"UPDATE t SET a = 1", "simple no where"},
264+
{"UPDATE t SET a = 1 WHERE b = 2", "simple with where"},
265+
{"UPDATE t SET a = 1, b = 2 WHERE c = 3", "multi column"},
266+
{"UPDATE t AS x SET a = 1 WHERE x.b = 2", "alias"},
267+
{"UPDATE t SET a = 1 FROM t2 WHERE t.id = t2.fk", "from clause"},
268+
{"UPDATE t SET a = t2.b FROM t2, t3 WHERE t.id = t2.fk AND t2.id = t3.fk", "from multi"},
269+
{"UPDATE t SET a = 1 WHERE b = 2 RETURNING *", "returning star"},
270+
{"UPDATE t SET a = 1 WHERE b = 2 RETURNING a, b", "returning cols"},
271+
{"UPDATE t SET a = 1 FROM t2 WHERE t.id = t2.fk RETURNING t.a", "from + returning"},
272+
};
273+
274+
TEST(PgSQLUpdateBulk, AllCasesParseSuccessfully) {
275+
Parser<Dialect::PostgreSQL> parser;
276+
for (const auto& tc : pgsql_update_bulk_cases) {
277+
auto r = parser.parse(tc.sql, strlen(tc.sql));
278+
EXPECT_EQ(r.status, ParseResult::OK)
279+
<< "Failed: " << tc.description << "\n SQL: " << tc.sql;
280+
EXPECT_EQ(r.stmt_type, StmtType::UPDATE)
281+
<< "Failed: " << tc.description;
282+
EXPECT_NE(r.ast, nullptr)
283+
<< "Failed: " << tc.description << "\n SQL: " << tc.sql;
284+
}
285+
}
286+
287+
// ========== Round-trip tests ==========
288+
289+
static const UpdateTestCase mysql_update_roundtrip_cases[] = {
290+
{"UPDATE t SET a = 1 WHERE b = 2", "simple"},
291+
{"UPDATE t SET a = 1, b = 'x' WHERE c = 3", "multi col"},
292+
{"UPDATE LOW_PRIORITY IGNORE t SET a = 1", "options"},
293+
{"UPDATE t SET a = 1 ORDER BY b DESC LIMIT 10", "order by limit"},
294+
};
295+
296+
TEST(MySQLUpdateRoundTrip, AllCasesRoundTrip) {
297+
Parser<Dialect::MySQL> parser;
298+
for (const auto& tc : mysql_update_roundtrip_cases) {
299+
auto r = parser.parse(tc.sql, strlen(tc.sql));
300+
ASSERT_NE(r.ast, nullptr)
301+
<< "Parse failed: " << tc.description << "\n SQL: " << tc.sql;
302+
Emitter<Dialect::MySQL> emitter(parser.arena());
303+
emitter.emit(r.ast);
304+
StringRef result = emitter.result();
305+
std::string out(result.ptr, result.len);
306+
EXPECT_EQ(out, std::string(tc.sql))
307+
<< "Round-trip mismatch: " << tc.description;
308+
}
309+
}
310+
311+
static const UpdateTestCase pgsql_update_roundtrip_cases[] = {
312+
{"UPDATE t SET a = 1 WHERE b = 2", "simple"},
313+
{"UPDATE t SET a = 1 FROM t2 WHERE t.id = t2.fk", "from clause"},
314+
{"UPDATE t SET a = 1 WHERE b = 2 RETURNING *", "returning"},
315+
};
316+
317+
TEST(PgSQLUpdateRoundTrip, AllCasesRoundTrip) {
318+
Parser<Dialect::PostgreSQL> parser;
319+
for (const auto& tc : pgsql_update_roundtrip_cases) {
320+
auto r = parser.parse(tc.sql, strlen(tc.sql));
321+
ASSERT_NE(r.ast, nullptr)
322+
<< "Parse failed: " << tc.description << "\n SQL: " << tc.sql;
323+
Emitter<Dialect::PostgreSQL> emitter(parser.arena());
324+
emitter.emit(r.ast);
325+
StringRef result = emitter.result();
326+
std::string out(result.ptr, result.len);
327+
EXPECT_EQ(out, std::string(tc.sql))
328+
<< "Round-trip mismatch: " << tc.description;
329+
}
330+
}

0 commit comments

Comments
 (0)