Skip to content

Commit 85fd9c0

Browse files
committed
Extend SQL dialect to support JSONPath via $ prefix
1 parent e5a2609 commit 85fd9c0

2 files changed

Lines changed: 51 additions & 7 deletions

File tree

specs/query-language.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ The language supports various expressions to interact with JSON data:
5959

6060
* **Field Access**:
6161
* Dot notation: `info.contact` accesses the `contact` field within the `info` object.
62-
* Array Indexing: `contact[0]` accesses the first element of the `contact` array.
62+
* **JSONPath**:
63+
* Identifiers starting with `$` are treated as JSONPath expressions.
64+
* Example: `$.store.book[0].title`
65+
* Complex paths containing special characters (like brackets `[]`) should be enclosed in backticks: `` `$.store.book[0].title` ``.
6366
* **Literals**:
6467
* Strings: `'value'` (single quotes) or `"value"` (double quotes)
6568
* Numbers: `123`, `45.67`
@@ -78,9 +81,9 @@ The language supports various expressions to interact with JSON data:
7881
**Example:**
7982

8083
```sql
81-
SELECT name, info.contact[0].tel
84+
SELECT name, `$.info.contact[0].tel`
8285
FROM people
83-
WHERE age >= 21 AND active = TRUE
86+
WHERE `$.age` >= 21 AND active = TRUE
8487
LIMIT 10
8588
OFFSET 5
86-
```
89+
```

src/parser.rs

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ struct ArgusDialect;
99

1010
impl Dialect for ArgusDialect {
1111
fn is_identifier_start(&self, ch: char) -> bool {
12-
(ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_'
12+
(ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' || ch == '$'
1313
}
1414

1515
fn is_identifier_part(&self, ch: char) -> bool {
1616
(ch >= 'a' && ch <= 'z')
1717
|| (ch >= 'A' && ch <= 'Z')
1818
|| (ch >= '0' && ch <= '9')
1919
|| ch == '_'
20+
|| ch == '$'
2021
}
2122

2223
fn is_delimited_identifier_start(&self, ch: char) -> bool {
@@ -174,10 +175,21 @@ fn convert_select(select: &ast::Select) -> Result<LogicalPlan, String> {
174175

175176
fn convert_expr(expr: &Expr) -> Result<Expression, String> {
176177
match expr {
177-
Expr::Identifier(ident) => Ok(Expression::FieldReference(ident.value.clone())),
178+
Expr::Identifier(ident) => {
179+
let value = ident.value.clone();
180+
if value.starts_with('$') {
181+
Ok(Expression::JsonPath(value))
182+
} else {
183+
Ok(Expression::FieldReference(value))
184+
}
185+
},
178186
Expr::CompoundIdentifier(idents) => {
179187
let path = idents.iter().map(|i| i.value.clone()).collect::<Vec<_>>().join(".");
180-
Ok(Expression::FieldReference(path))
188+
if path.starts_with('$') {
189+
Ok(Expression::JsonPath(path))
190+
} else {
191+
Ok(Expression::FieldReference(path))
192+
}
181193
}
182194
Expr::Value(val_span) => match &val_span.value {
183195
ast::Value::Number(n, _) => {
@@ -289,4 +301,33 @@ mod tests {
289301
_ => panic!("Expected Select"),
290302
}
291303
}
304+
305+
#[test]
306+
fn test_parse_jsonpath() {
307+
// Test standard dot notation with $
308+
let sql = "SELECT $.a.b FROM t";
309+
let stmt = parse(sql).unwrap();
310+
match stmt {
311+
Statement::Select(LogicalPlan::Project { projections, .. }) => {
312+
match &projections[0] {
313+
Expression::JsonPath(p) => assert_eq!(p, "$.a.b"),
314+
_ => panic!("Expected JsonPath"),
315+
}
316+
}
317+
_ => panic!("Expected Select Project"),
318+
}
319+
320+
// Test backtick quoted jsonpath with brackets
321+
let sql = "SELECT `$.a[0]` FROM t";
322+
let stmt = parse(sql).unwrap();
323+
match stmt {
324+
Statement::Select(LogicalPlan::Project { projections, .. }) => {
325+
match &projections[0] {
326+
Expression::JsonPath(p) => assert_eq!(p, "$.a[0]"),
327+
_ => panic!("Expected JsonPath"),
328+
}
329+
}
330+
_ => panic!("Expected Select Project"),
331+
}
332+
}
292333
}

0 commit comments

Comments
 (0)