Skip to content

Commit d656036

Browse files
committed
Implement SQL parser using sqlparser-rs and custom dialect
1 parent fe5b5d6 commit d656036

3 files changed

Lines changed: 64 additions & 104 deletions

File tree

specs/query-language.md

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,19 @@ The `INSERT` statement is used to add new documents to a collection.
1616

1717
```sql
1818
INSERT INTO <collection_name>
19-
RECORDS <json_object> [, <json_object> ...]
19+
RECORDS `json_object` [, `json_object` ...]
2020
```
2121

2222
**Parameters:**
2323

2424
* `collection_name`: The name of the collection to insert into.
25-
* `json_object`: A standard JSON object literal representing the document. Keys in the JSON object usually do not require quotes if they are simple identifiers, but standard JSON requires quotes. The parser should support standard JSON syntax.
25+
* `json_object`: A standard JSON object literal enclosed in backticks (`). This allows for unescaped JSON content to be embedded directly in the query.
2626

2727
**Example:**
2828

2929
```sql
3030
INSERT INTO people
31-
RECORDS {
32-
"name": "Alice",
33-
"age": 30,
34-
"address": {"city": "Paris", "zip": "75001"}
35-
}
31+
RECORDS `{"name": "Alice", "age": 30, "address": {"city": "Paris", "zip": "75001"}}`
3632
```
3733

3834
### SELECT
@@ -87,4 +83,4 @@ FROM people
8783
WHERE age >= 21 AND active = TRUE
8884
LIMIT 10
8985
OFFSET 5
90-
```
86+
```

src/jstable.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ impl Iterator for JSTableIterator {
115115
}
116116

117117
pub fn read_jstable(path: &str) -> io::Result<JSTable> {
118-
let mut iterator = JSTableIterator::new(path)?;
118+
let iterator = JSTableIterator::new(path)?;
119119
let timestamp = iterator.timestamp;
120120
let schema = iterator.schema.clone();
121121

src/parser.rs

Lines changed: 59 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,84 @@
11
use crate::query::{Expression, BinaryOperator, LogicalOperator, LogicalPlan, Statement};
22
use serde_json::Value;
3-
use sqlparser::dialect::GenericDialect;
3+
use sqlparser::dialect::Dialect;
44
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-
};
5+
use sqlparser::ast::{self, SetExpr, TableFactor, BinaryOperator as SqlBinaryOperator, Expr, LimitClause, Values};
136

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) ---
7+
#[derive(Debug)]
8+
struct ArgusDialect;
249

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)),
10+
impl Dialect for ArgusDialect {
11+
fn is_identifier_start(&self, ch: char) -> bool {
12+
(ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_'
2913
}
30-
}
3114

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)));
15+
fn is_identifier_part(&self, ch: char) -> bool {
16+
(ch >= 'a' && ch <= 'z')
17+
|| (ch >= 'A' && ch <= 'Z')
18+
|| (ch >= '0' && ch <= '9')
19+
|| ch == '_'
7120
}
7221

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-
}
22+
fn is_delimited_identifier_start(&self, ch: char) -> bool {
23+
ch == '`'
8624
}
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))
9825
}
9926

27+
pub fn parse(sql: &str) -> Result<Statement, String> {
28+
let dialect = ArgusDialect {};
29+
30+
// Hack: Replace RECORDS with VALUES to satisfy sqlparser
31+
// We strictly assume "RECORDS" is used for INSERT.
32+
let sql_to_parse = if sql.trim().to_uppercase().starts_with("INSERT") {
33+
sql.replacen("RECORDS", "VALUES", 1).replacen("records", "VALUES", 1)
34+
} else {
35+
sql.to_string()
36+
};
10037

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())?;
38+
let ast = Parser::parse_sql(&dialect, &sql_to_parse).map_err(|e| e.to_string())?;
10639

10740
if ast.len() != 1 {
10841
return Err("Expected exactly one statement".to_string());
10942
}
11043

11144
match &ast[0] {
45+
ast::Statement::Insert { table_name, source, .. } => {
46+
let collection = table_name.to_string();
47+
let documents = convert_insert_source(source)?;
48+
Ok(Statement::Insert { collection, documents })
49+
}
11250
ast::Statement::Query(query) => {
11351
let logical_plan = convert_query(query)?;
11452
Ok(Statement::Select(logical_plan))
11553
}
116-
_ => Err("Only SELECT statements are supported".to_string()),
54+
_ => Err("Only SELECT and INSERT statements are supported".to_string()),
55+
}
56+
}
57+
58+
fn convert_insert_source(source: &Option<Box<ast::Query>>) -> Result<Vec<Value>, String> {
59+
let query = source.as_ref().ok_or("Insert must have a source")?;
60+
61+
match &*query.body {
62+
SetExpr::Values(Values { rows, .. }) => {
63+
let mut docs = Vec::new();
64+
for row in rows {
65+
if row.len() != 1 {
66+
return Err("Each record must contain exactly one JSON object".to_string());
67+
}
68+
let expr = &row[0];
69+
match expr {
70+
Expr::Identifier(ident) => {
71+
// We expect a backtick-quoted identifier which contains the JSON
72+
let json_str = &ident.value;
73+
let value: Value = serde_json::from_str(json_str).map_err(|e| format!("Invalid JSON in INSERT: {}", e))?;
74+
docs.push(value);
75+
}
76+
_ => return Err("Expected a JSON object enclosed in backticks".to_string()),
77+
}
78+
}
79+
Ok(docs)
80+
}
81+
_ => Err("INSERT expects VALUES (RECORDS) clause".to_string()),
11782
}
11883
}
11984

@@ -198,7 +163,6 @@ fn convert_select(select: &ast::Select) -> Result<LogicalPlan, String> {
198163
projections.push(convert_expr(expr)?);
199164
}
200165
ast::SelectItem::ExprWithAlias { expr, alias: _ } => {
201-
// Ignore alias for now as LogicalPlan doesn't support renaming explicitly yet
202166
projections.push(convert_expr(expr)?);
203167
}
204168
ast::SelectItem::Wildcard(_) => {
@@ -280,7 +244,7 @@ mod tests {
280244

281245
#[test]
282246
fn test_parse_insert() {
283-
let sql = r#"INSERT INTO users RECORDS {"name": "Alice", "age": 30}, {"name": "Bob"}"#;
247+
let sql = r#"INSERT INTO users RECORDS `{"name": "Alice", "age": 30}`, `{"name": "Bob"}`"#;
284248
let stmt = parse(sql).unwrap();
285249
match stmt {
286250
Statement::Insert { collection, documents } => {

0 commit comments

Comments
 (0)