Skip to content

Commit fe5b5d6

Browse files
committed
Implement SQL parser using sqlparser-rs and custom Nom for INSERT
1 parent 3193b0a commit fe5b5d6

1 file changed

Lines changed: 336 additions & 0 deletions

File tree

src/parser.rs

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
use crate::query::{Expression, BinaryOperator, LogicalOperator, LogicalPlan, Statement};
2+
use serde_json::Value;
3+
use sqlparser::dialect::GenericDialect;
4+
use sqlparser::parser::Parser;
5+
use sqlparser::ast::{self, SetExpr, TableFactor, BinaryOperator as SqlBinaryOperator, Expr, LimitClause};
6+
use nom::{
7+
bytes::complete::{tag_no_case, take_while1},
8+
character::complete::{multispace0, multispace1, char},
9+
sequence::tuple,
10+
multi::separated_list1,
11+
IResult,
12+
};
13+
14+
pub fn parse(sql: &str) -> Result<Statement, String> {
15+
let trimmed = sql.trim();
16+
if trimmed.to_uppercase().starts_with("INSERT") {
17+
parse_insert(trimmed).map_err(|e| format!("Insert parse error: {}", e))
18+
} else {
19+
parse_select(trimmed)
20+
}
21+
}
22+
23+
// --- INSERT Parsing (Custom using Nom) ---
24+
25+
fn parse_insert(input: &str) -> Result<Statement, String> {
26+
match insert_statement(input) {
27+
Ok((_, stmt)) => Ok(stmt),
28+
Err(e) => Err(format!("{}", e)),
29+
}
30+
}
31+
32+
fn insert_statement(input: &str) -> IResult<&str, Statement> {
33+
// INSERT INTO <collection> RECORDS <json>...
34+
let (input, _) = tag_no_case("INSERT")(input)?;
35+
let (input, _) = multispace1(input)?;
36+
let (input, _) = tag_no_case("INTO")(input)?;
37+
let (input, _) = multispace1(input)?;
38+
39+
let (input, collection) = identifier(input)?;
40+
41+
let (input, _) = multispace1(input)?;
42+
let (input, _) = tag_no_case("RECORDS")(input)?;
43+
let (input, _) = multispace0(input)?;
44+
45+
let (input, documents) = separated_list1(
46+
tuple((multispace0, char(','), multispace0)),
47+
json_object
48+
)(input)?;
49+
50+
Ok((input, Statement::Insert {
51+
collection: collection.to_string(),
52+
documents,
53+
}))
54+
}
55+
56+
fn identifier(input: &str) -> IResult<&str, &str> {
57+
take_while1(|c: char| c.is_alphanumeric() || c == '_')(input)
58+
}
59+
60+
fn json_object(input: &str) -> IResult<&str, Value> {
61+
let mut depth = 0;
62+
let mut len = 0;
63+
let mut found_start = false;
64+
65+
// Skip leading whitespace
66+
let leading_ws = input.chars().take_while(|c| c.is_whitespace()).count();
67+
let trimmed_input = &input[leading_ws..];
68+
69+
if !trimmed_input.starts_with('{') {
70+
return Err(nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Tag)));
71+
}
72+
73+
// Iterate to find the matching brace
74+
for (i, c) in trimmed_input.char_indices() {
75+
if c == '{' {
76+
depth += 1;
77+
found_start = true;
78+
} else if c == '}' {
79+
depth -= 1;
80+
}
81+
82+
if found_start && depth == 0 {
83+
len = i + 1;
84+
break;
85+
}
86+
}
87+
88+
if depth != 0 {
89+
return Err(nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Complete)));
90+
}
91+
92+
let json_str = &trimmed_input[0..len];
93+
let value: Value = serde_json::from_str(json_str).map_err(|_| {
94+
nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::MapRes))
95+
})?;
96+
97+
Ok((&trimmed_input[len..], value))
98+
}
99+
100+
101+
// --- SELECT Parsing (sqlparser) ---
102+
103+
fn parse_select(sql: &str) -> Result<Statement, String> {
104+
let dialect = GenericDialect {};
105+
let ast = Parser::parse_sql(&dialect, sql).map_err(|e| e.to_string())?;
106+
107+
if ast.len() != 1 {
108+
return Err("Expected exactly one statement".to_string());
109+
}
110+
111+
match &ast[0] {
112+
ast::Statement::Query(query) => {
113+
let logical_plan = convert_query(query)?;
114+
Ok(Statement::Select(logical_plan))
115+
}
116+
_ => Err("Only SELECT statements are supported".to_string()),
117+
}
118+
}
119+
120+
fn convert_query(query: &ast::Query) -> Result<LogicalPlan, String> {
121+
let mut limit_val = None;
122+
let mut offset_val = None;
123+
124+
if let Some(limit_clause) = &query.limit_clause {
125+
match limit_clause {
126+
LimitClause::Limit { limit, offset } => {
127+
limit_val = Some(parse_limit_expr(limit)?);
128+
if let Some(off) = offset {
129+
offset_val = Some(parse_limit_expr(off)?);
130+
}
131+
}
132+
LimitClause::LimitOffset { limit, offset } => {
133+
limit_val = Some(parse_limit_expr(limit)?);
134+
offset_val = Some(parse_limit_expr(offset)?);
135+
}
136+
}
137+
}
138+
139+
// Body (SetExpr)
140+
let plan = match &*query.body {
141+
SetExpr::Select(select) => convert_select(select)?,
142+
_ => return Err("Only SELECT queries are supported (no UNION, etc.)".to_string()),
143+
};
144+
145+
// Wrap with Limit/Offset
146+
let plan = if let Some(o) = offset_val {
147+
LogicalPlan::Offset { input: Box::new(plan), offset: o }
148+
} else {
149+
plan
150+
};
151+
152+
let plan = if let Some(l) = limit_val {
153+
LogicalPlan::Limit { input: Box::new(plan), limit: l }
154+
} else {
155+
plan
156+
};
157+
158+
Ok(plan)
159+
}
160+
161+
fn parse_limit_expr(expr: &Expr) -> Result<usize, String> {
162+
match expr {
163+
Expr::Value(val_span) => match &val_span.value {
164+
ast::Value::Number(n, _) => n.parse::<usize>().map_err(|_| "Invalid number".to_string()),
165+
_ => Err("Expected number".to_string()),
166+
},
167+
_ => Err("Expected value for limit/offset".to_string()),
168+
}
169+
}
170+
171+
fn convert_select(select: &ast::Select) -> Result<LogicalPlan, String> {
172+
// 1. FROM (Scan)
173+
if select.from.len() != 1 {
174+
return Err("FROM clause must have exactly one table".to_string());
175+
}
176+
let table = &select.from[0];
177+
let collection = match &table.relation {
178+
TableFactor::Table { name, .. } => name.to_string(),
179+
_ => return Err("Unsupported FROM clause".to_string()),
180+
};
181+
182+
let mut plan = LogicalPlan::Scan { collection };
183+
184+
// 2. WHERE (Filter)
185+
if let Some(selection) = &select.selection {
186+
let predicate = convert_expr(selection)?;
187+
plan = LogicalPlan::Filter {
188+
input: Box::new(plan),
189+
predicate,
190+
};
191+
}
192+
193+
// 3. SELECT (Project)
194+
let mut projections = Vec::new();
195+
for item in &select.projection {
196+
match item {
197+
ast::SelectItem::UnnamedExpr(expr) => {
198+
projections.push(convert_expr(expr)?);
199+
}
200+
ast::SelectItem::ExprWithAlias { expr, alias: _ } => {
201+
// Ignore alias for now as LogicalPlan doesn't support renaming explicitly yet
202+
projections.push(convert_expr(expr)?);
203+
}
204+
ast::SelectItem::Wildcard(_) => {
205+
return Err("Wildcard * not supported yet".to_string());
206+
}
207+
_ => return Err("Unsupported projection item".to_string()),
208+
}
209+
}
210+
211+
plan = LogicalPlan::Project {
212+
input: Box::new(plan),
213+
projections,
214+
};
215+
216+
Ok(plan)
217+
}
218+
219+
fn convert_expr(expr: &Expr) -> Result<Expression, String> {
220+
match expr {
221+
Expr::Identifier(ident) => Ok(Expression::FieldReference(ident.value.clone())),
222+
Expr::CompoundIdentifier(idents) => {
223+
let path = idents.iter().map(|i| i.value.clone()).collect::<Vec<_>>().join(".");
224+
Ok(Expression::FieldReference(path))
225+
}
226+
Expr::Value(val_span) => match &val_span.value {
227+
ast::Value::Number(n, _) => {
228+
// Try parse as i64 or f64
229+
if let Ok(i) = n.parse::<i64>() {
230+
Ok(Expression::Literal(serde_json::Value::Number(i.into())))
231+
} else if let Ok(f) = n.parse::<f64>() {
232+
Ok(Expression::Literal(serde_json::Value::Number(serde_json::Number::from_f64(f).ok_or("Invalid float")?)))
233+
} else {
234+
Err("Invalid number".to_string())
235+
}
236+
}
237+
ast::Value::SingleQuotedString(s) => Ok(Expression::Literal(Value::String(s.clone()))),
238+
ast::Value::Boolean(b) => Ok(Expression::Literal(Value::Bool(*b))),
239+
ast::Value::Null => Ok(Expression::Literal(Value::Null)),
240+
_ => Err(format!("Unsupported literal: {:?}", val_span.value)),
241+
},
242+
Expr::BinaryOp { left, op, right } => {
243+
let left_expr = Box::new(convert_expr(left)?);
244+
let right_expr = Box::new(convert_expr(right)?);
245+
246+
let (is_logical, b_op, l_op) = match op {
247+
SqlBinaryOperator::Eq => (false, Some(BinaryOperator::Eq), None),
248+
SqlBinaryOperator::NotEq => (false, Some(BinaryOperator::Neq), None),
249+
SqlBinaryOperator::Lt => (false, Some(BinaryOperator::Lt), None),
250+
SqlBinaryOperator::LtEq => (false, Some(BinaryOperator::Lte), None),
251+
SqlBinaryOperator::Gt => (false, Some(BinaryOperator::Gt), None),
252+
SqlBinaryOperator::GtEq => (false, Some(BinaryOperator::Gte), None),
253+
SqlBinaryOperator::And => (true, None, Some(LogicalOperator::And)),
254+
SqlBinaryOperator::Or => (true, None, Some(LogicalOperator::Or)),
255+
_ => return Err(format!("Unsupported binary operator: {:?}", op)),
256+
};
257+
258+
if is_logical {
259+
Ok(Expression::Logical {
260+
left: left_expr,
261+
op: l_op.unwrap(),
262+
right: right_expr,
263+
})
264+
} else {
265+
Ok(Expression::Binary {
266+
left: left_expr,
267+
op: b_op.unwrap(),
268+
right: right_expr,
269+
})
270+
}
271+
},
272+
Expr::JsonAccess { .. } => Err("JsonAccess not implemented".to_string()),
273+
_ => Err(format!("Unsupported expression: {:?}", expr)),
274+
}
275+
}
276+
277+
#[cfg(test)]
278+
mod tests {
279+
use super::*;
280+
281+
#[test]
282+
fn test_parse_insert() {
283+
let sql = r#"INSERT INTO users RECORDS {"name": "Alice", "age": 30}, {"name": "Bob"}"#;
284+
let stmt = parse(sql).unwrap();
285+
match stmt {
286+
Statement::Insert { collection, documents } => {
287+
assert_eq!(collection, "users");
288+
assert_eq!(documents.len(), 2);
289+
assert_eq!(documents[0]["name"], "Alice");
290+
assert_eq!(documents[1]["name"], "Bob");
291+
}
292+
_ => panic!("Expected Insert"),
293+
}
294+
}
295+
296+
#[test]
297+
fn test_parse_select() {
298+
let sql = "SELECT name, age FROM users WHERE age > 18 AND active = true LIMIT 10 OFFSET 5";
299+
let stmt = parse(sql).unwrap();
300+
match stmt {
301+
Statement::Select(plan) => {
302+
// Verify structure: Limit(Offset(Project(Filter(Scan))))
303+
match plan {
304+
LogicalPlan::Limit { input, limit } => {
305+
assert_eq!(limit, 10);
306+
match *input {
307+
LogicalPlan::Offset { input, offset } => {
308+
assert_eq!(offset, 5);
309+
match *input {
310+
LogicalPlan::Project { input, projections } => {
311+
assert_eq!(projections.len(), 2);
312+
match *input {
313+
LogicalPlan::Filter { input, predicate: _ } => {
314+
match *input {
315+
LogicalPlan::Scan { collection } => {
316+
assert_eq!(collection, "users");
317+
}
318+
_ => panic!("Expected Scan"),
319+
}
320+
}
321+
_ => panic!("Expected Filter"),
322+
}
323+
}
324+
_ => panic!("Expected Project"),
325+
}
326+
}
327+
_ => panic!("Expected Offset"),
328+
}
329+
}
330+
_ => panic!("Expected Limit"),
331+
}
332+
}
333+
_ => panic!("Expected Select"),
334+
}
335+
}
336+
}

0 commit comments

Comments
 (0)