Skip to content

Commit 7c9a1fe

Browse files
committed
Add UpdateParser<D> header-only template for UPDATE deep parsing
Supports MySQL (LOW_PRIORITY, IGNORE, multi-table JOINs, ORDER BY, LIMIT) and PostgreSQL (ONLY, alias, FROM clause, RETURNING). Uses ExpressionParser for expressions and TableRefParser for table refs.
1 parent ec435cc commit 7c9a1fe

1 file changed

Lines changed: 296 additions & 0 deletions

File tree

include/sql_parser/update_parser.h

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
#ifndef SQL_PARSER_UPDATE_PARSER_H
2+
#define SQL_PARSER_UPDATE_PARSER_H
3+
4+
#include "sql_parser/common.h"
5+
#include "sql_parser/token.h"
6+
#include "sql_parser/tokenizer.h"
7+
#include "sql_parser/ast.h"
8+
#include "sql_parser/arena.h"
9+
#include "sql_parser/expression_parser.h"
10+
#include "sql_parser/table_ref_parser.h"
11+
12+
namespace sql_parser {
13+
14+
template <Dialect D>
15+
class UpdateParser {
16+
public:
17+
UpdateParser(Tokenizer<D>& tokenizer, Arena& arena)
18+
: tok_(tokenizer), arena_(arena), expr_parser_(tokenizer, arena),
19+
table_ref_parser_(tokenizer, arena, expr_parser_) {}
20+
21+
// Parse UPDATE statement (UPDATE keyword already consumed).
22+
AstNode* parse() {
23+
AstNode* root = make_node(arena_, NodeType::NODE_UPDATE_STMT);
24+
if (!root) return nullptr;
25+
26+
if constexpr (D == Dialect::MySQL) {
27+
return parse_mysql(root);
28+
} else {
29+
return parse_pgsql(root);
30+
}
31+
}
32+
33+
private:
34+
Tokenizer<D>& tok_;
35+
Arena& arena_;
36+
ExpressionParser<D> expr_parser_;
37+
TableRefParser<D> table_ref_parser_;
38+
39+
// ---- MySQL UPDATE ----
40+
// UPDATE [LOW_PRIORITY] [IGNORE] table_references SET col=expr [,...] [WHERE] [ORDER BY] [LIMIT]
41+
42+
AstNode* parse_mysql(AstNode* root) {
43+
// Options: LOW_PRIORITY, IGNORE
44+
AstNode* opts = parse_stmt_options();
45+
if (opts) root->add_child(opts);
46+
47+
// Table references (supports JOINs for multi-table UPDATE)
48+
// Use parse_from_clause which handles comma-joins and explicit JOINs
49+
AstNode* from = table_ref_parser_.parse_from_clause();
50+
if (from) {
51+
// For single-table UPDATE, hoist the single TABLE_REF as direct child
52+
// For multi-table, keep the FROM_CLAUSE
53+
int ref_count = 0;
54+
bool has_join = false;
55+
for (const AstNode* c = from->first_child; c; c = c->next_sibling) {
56+
if (c->type == NodeType::NODE_JOIN_CLAUSE) has_join = true;
57+
++ref_count;
58+
}
59+
if (ref_count == 1 && !has_join) {
60+
// Single table -- add TABLE_REF directly
61+
root->add_child(from->first_child);
62+
} else {
63+
// Multi-table -- keep FROM_CLAUSE
64+
root->add_child(from);
65+
}
66+
}
67+
68+
// SET keyword
69+
if (tok_.peek().type == TokenType::TK_SET) {
70+
tok_.skip();
71+
AstNode* set_clause = parse_update_set_clause();
72+
if (set_clause) root->add_child(set_clause);
73+
}
74+
75+
// WHERE
76+
if (tok_.peek().type == TokenType::TK_WHERE) {
77+
tok_.skip();
78+
AstNode* where = parse_where_clause();
79+
if (where) root->add_child(where);
80+
}
81+
82+
// ORDER BY (single-table only)
83+
if (tok_.peek().type == TokenType::TK_ORDER) {
84+
tok_.skip();
85+
if (tok_.peek().type == TokenType::TK_BY) tok_.skip();
86+
AstNode* order_by = parse_order_by();
87+
if (order_by) root->add_child(order_by);
88+
}
89+
90+
// LIMIT (single-table only)
91+
if (tok_.peek().type == TokenType::TK_LIMIT) {
92+
tok_.skip();
93+
AstNode* limit = parse_limit();
94+
if (limit) root->add_child(limit);
95+
}
96+
97+
return root;
98+
}
99+
100+
// ---- PostgreSQL UPDATE ----
101+
// UPDATE [ONLY] table [[AS] alias] SET col=expr [,...] [FROM from_list] [WHERE] [RETURNING]
102+
103+
AstNode* parse_pgsql(AstNode* root) {
104+
// Optional ONLY keyword
105+
if (tok_.peek().type == TokenType::TK_ONLY) {
106+
AstNode* opts = make_node(arena_, NodeType::NODE_STMT_OPTIONS);
107+
Token only_tok = tok_.next_token();
108+
opts->add_child(make_node(arena_, NodeType::NODE_IDENTIFIER, only_tok.text));
109+
root->add_child(opts);
110+
}
111+
112+
// Single table reference with optional alias
113+
AstNode* table_ref = table_ref_parser_.parse_table_reference();
114+
if (table_ref) root->add_child(table_ref);
115+
116+
// SET keyword
117+
if (tok_.peek().type == TokenType::TK_SET) {
118+
tok_.skip();
119+
AstNode* set_clause = parse_update_set_clause();
120+
if (set_clause) root->add_child(set_clause);
121+
}
122+
123+
// FROM clause (PostgreSQL: additional table sources)
124+
if (tok_.peek().type == TokenType::TK_FROM) {
125+
tok_.skip();
126+
AstNode* from = table_ref_parser_.parse_from_clause();
127+
if (from) root->add_child(from);
128+
}
129+
130+
// WHERE
131+
if (tok_.peek().type == TokenType::TK_WHERE) {
132+
tok_.skip();
133+
AstNode* where = parse_where_clause();
134+
if (where) root->add_child(where);
135+
}
136+
137+
// RETURNING
138+
if (tok_.peek().type == TokenType::TK_RETURNING) {
139+
AstNode* ret = parse_returning();
140+
if (ret) root->add_child(ret);
141+
}
142+
143+
return root;
144+
}
145+
146+
// ---- Shared helpers ----
147+
148+
// Parse MySQL options: LOW_PRIORITY, IGNORE
149+
AstNode* parse_stmt_options() {
150+
AstNode* opts = nullptr;
151+
while (true) {
152+
Token t = tok_.peek();
153+
if (t.type == TokenType::TK_LOW_PRIORITY ||
154+
t.type == TokenType::TK_IGNORE) {
155+
if (!opts) opts = make_node(arena_, NodeType::NODE_STMT_OPTIONS);
156+
tok_.skip();
157+
opts->add_child(make_node(arena_, NodeType::NODE_IDENTIFIER, t.text));
158+
} else {
159+
break;
160+
}
161+
}
162+
return opts;
163+
}
164+
165+
// Parse SET clause: col=expr [, col=expr ...]
166+
AstNode* parse_update_set_clause() {
167+
AstNode* set_clause = make_node(arena_, NodeType::NODE_UPDATE_SET_CLAUSE);
168+
if (!set_clause) return nullptr;
169+
170+
while (true) {
171+
AstNode* item = parse_set_item();
172+
if (item) set_clause->add_child(item);
173+
if (tok_.peek().type == TokenType::TK_COMMA) {
174+
tok_.skip();
175+
} else {
176+
break;
177+
}
178+
}
179+
return set_clause;
180+
}
181+
182+
// Parse a single col=expr pair
183+
AstNode* parse_set_item() {
184+
AstNode* item = make_node(arena_, NodeType::NODE_UPDATE_SET_ITEM);
185+
if (!item) return nullptr;
186+
187+
// Column name (may be qualified: table.col)
188+
Token col = tok_.next_token();
189+
if (tok_.peek().type == TokenType::TK_DOT) {
190+
tok_.skip();
191+
Token actual_col = tok_.next_token();
192+
AstNode* qname = make_node(arena_, NodeType::NODE_QUALIFIED_NAME);
193+
qname->add_child(make_node(arena_, NodeType::NODE_IDENTIFIER, col.text));
194+
qname->add_child(make_node(arena_, NodeType::NODE_IDENTIFIER, actual_col.text));
195+
item->add_child(qname);
196+
} else {
197+
item->add_child(make_node(arena_, NodeType::NODE_COLUMN_REF, col.text));
198+
}
199+
200+
// = sign
201+
if (tok_.peek().type == TokenType::TK_EQUAL) {
202+
tok_.skip();
203+
}
204+
205+
// Expression value
206+
AstNode* val = expr_parser_.parse();
207+
if (val) item->add_child(val);
208+
209+
return item;
210+
}
211+
212+
// Parse WHERE clause
213+
AstNode* parse_where_clause() {
214+
AstNode* where = make_node(arena_, NodeType::NODE_WHERE_CLAUSE);
215+
if (!where) return nullptr;
216+
AstNode* expr = expr_parser_.parse();
217+
if (expr) where->add_child(expr);
218+
return where;
219+
}
220+
221+
// Parse ORDER BY clause
222+
AstNode* parse_order_by() {
223+
AstNode* order_by = make_node(arena_, NodeType::NODE_ORDER_BY_CLAUSE);
224+
if (!order_by) return nullptr;
225+
226+
while (true) {
227+
AstNode* expr = expr_parser_.parse();
228+
if (!expr) break;
229+
230+
AstNode* item = make_node(arena_, NodeType::NODE_ORDER_BY_ITEM);
231+
item->add_child(expr);
232+
233+
// Optional ASC/DESC
234+
Token dir = tok_.peek();
235+
if (dir.type == TokenType::TK_ASC || dir.type == TokenType::TK_DESC) {
236+
tok_.skip();
237+
item->add_child(make_node(arena_, NodeType::NODE_IDENTIFIER, dir.text));
238+
}
239+
240+
order_by->add_child(item);
241+
242+
if (tok_.peek().type == TokenType::TK_COMMA) {
243+
tok_.skip();
244+
} else {
245+
break;
246+
}
247+
}
248+
return order_by;
249+
}
250+
251+
// Parse LIMIT clause
252+
AstNode* parse_limit() {
253+
AstNode* limit = make_node(arena_, NodeType::NODE_LIMIT_CLAUSE);
254+
if (!limit) return nullptr;
255+
256+
AstNode* count = expr_parser_.parse();
257+
if (count) limit->add_child(count);
258+
259+
return limit;
260+
}
261+
262+
// Parse PostgreSQL RETURNING clause
263+
AstNode* parse_returning() {
264+
if (tok_.peek().type != TokenType::TK_RETURNING) return nullptr;
265+
tok_.skip(); // RETURNING
266+
267+
AstNode* ret = make_node(arena_, NodeType::NODE_RETURNING_CLAUSE);
268+
if (!ret) return nullptr;
269+
270+
while (true) {
271+
AstNode* expr = expr_parser_.parse();
272+
if (!expr) break;
273+
ret->add_child(expr);
274+
275+
// Check for optional alias
276+
Token next = tok_.peek();
277+
if (next.type == TokenType::TK_AS) {
278+
tok_.skip();
279+
Token alias_name = tok_.next_token();
280+
ret->add_child(make_node(arena_, NodeType::NODE_ALIAS, alias_name.text));
281+
}
282+
283+
if (tok_.peek().type == TokenType::TK_COMMA) {
284+
tok_.skip();
285+
} else {
286+
break;
287+
}
288+
}
289+
290+
return ret;
291+
}
292+
};
293+
294+
} // namespace sql_parser
295+
296+
#endif // SQL_PARSER_UPDATE_PARSER_H

0 commit comments

Comments
 (0)