# OData V4 Query String Parser for Rust
A Rust library that parses OData V4 query strings into an AST (Abstract Syntax Tree) and renders them into multiple SQL dialects.
## Features
- 🚀 Parse OData V4 query strings with comprehensive support
- 🎯 Type-safe AST representation
- 🔄 Render to multiple SQL dialects:
- **MSSQL/SQL Server** - Uses `TOP`, `OFFSET...ROWS`, `WHERE`, `ORDER BY`, `GROUP BY`
- **SQLite** - Uses `LIMIT`, `OFFSET`, `WHERE`, `ORDER BY`, `GROUP BY`
- **PostgreSQL** - Uses `LIMIT`, `OFFSET`, `WHERE`, `ORDER BY`, `GROUP BY`
- **SurrealQL** - Uses `START`, `LIMIT`, `WHERE`, `FETCH`, `ORDER BY`, `GROUP BY`
- 🔍 Full filter expression support:
- Comparison operators: `eq`, `ne`, `gt`, `ge`, `lt`, `le`
- Logical operators: `and`, `or`, `not`
- Arithmetic operators: `add`, `sub`, `mul`, `div`, `mod`, unary minus
- String functions: `contains`, `startswith`, `endswith`, `length`, `indexof`, `substring`, `tolower`, `toupper`, `trim`, `concat`
- Date/time functions: `year`, `month`, `day`, `hour`, `minute`, `second`, `now`
- Math functions: `round`, `floor`, `ceiling`
- Lambda operators: `any`, `all`
- Special operators: `in`
- 🔗 Navigation property expansion (`FETCH` for SurrealDB, TODO comments for SQL dialects)
- ⚡ Zero-cost abstractions with compile-time safety
- 📝 Informative error messages with position tracking
## Installation
Add this to your `Cargo.toml`:
```toml
[dependencies]
odatav4-parser = "0.1.0"
```
## Usage
### Basic Example
```rust
use odatav4_parser::{parse, renderers::*};
fn main() {
let query = "$select=id,name&$top=10&$skip=20";
let options = parse(query).unwrap();
// Render to different SQL dialects
let mssql = mssql::MssqlRenderer::new();
println!("MSSQL: {}", mssql.render("users", &options));
// Output: SELECT TOP 10 [id], [name] FROM [users] ORDER BY (SELECT NULL) OFFSET 20 ROWS
let sqlite = sqlite::SqliteRenderer::new();
println!("SQLite: {}", sqlite.render("users", &options));
// Output: SELECT "id", "name" FROM "users" LIMIT 10 OFFSET 20
let surrealql = surrealql::SurrealqlRenderer::new();
println!("SurrealQL: {}", surrealql.render("users", &options));
// Output: SELECT id, name FROM users START 20 LIMIT 10
let postgresql = postgresql::PostgresqlRenderer::new();
println!("PostgreSQL: {}", postgresql.render("users", &options));
// Output: SELECT "id", "name" FROM "users" LIMIT 10 OFFSET 20
}
```
### Parsing Individual Options
```rust
use odatav4_parser::parse;
// Parse $select
let options = parse("$select=id,name,email").unwrap();
assert_eq!(options.select, Some(vec!["id".to_string(), "name".to_string(), "email".to_string()]));
// Parse $top
let options = parse("$top=10").unwrap();
assert_eq!(options.top, Some(10));
// Parse $skip
let options = parse("$skip=20").unwrap();
assert_eq!(options.skip, Some(20));
// Parse $filter
let options = parse("$filter=age gt 18 and active eq true").unwrap();
assert!(options.filter.is_some());
// Parse $expand
let options = parse("$expand=orders,profile").unwrap();
assert_eq!(options.expand, Some(vec!["orders".to_string(), "profile".to_string()]));
```
### Filter Examples
```rust
use odatav4_parser::{parse, renderers::*};
// Simple comparison
let query = "$filter=age gt 18";
let options = parse(query).unwrap();
let sqlite = sqlite::SqliteRenderer::new();
println!("{}", sqlite.render("users", &options));
// Output: SELECT * FROM "users" WHERE "age" > 18
// String comparison
let query = "$filter=name eq 'John'";
let options = parse(query).unwrap();
let postgresql = postgresql::PostgresqlRenderer::new();
println!("{}", postgresql.render("users", &options));
// Output: SELECT * FROM "users" WHERE "name" = 'John'
// Logical operators
let query = "$filter=age gt 18 and active eq true";
let options = parse(query).unwrap();
let mssql = mssql::MssqlRenderer::new();
println!("{}", mssql.render("users", &options));
// Output: SELECT * FROM [users] WHERE ([age] > 18 AND [active] = TRUE)
// Complex expression with OR
let query = "$filter=age lt 18 or age gt 65";
let options = parse(query).unwrap();
```
### Expand Examples
```rust
use odatav4_parser::{parse, renderers::*};
// SurrealDB - Full support with FETCH clause
let query = "$expand=orders,profile";
let options = parse(query).unwrap();
let surrealql = surrealql::SurrealqlRenderer::new();
println!("{}", surrealql.render("users", &options));
// Output: SELECT * FROM users FETCH orders, profile
// SQL dialects - Generates TODO comments
let mssql = mssql::MssqlRenderer::new();
println!("{}", mssql.render("users", &options));
// Output: SELECT * FROM [users] /* TODO: JOIN orders, profile */
```
### Advanced Filter Examples
```rust
use odatav4_parser::{parse, renderers::*};
// Arithmetic operators
let query = "$filter=price add tax gt 100";
let options = parse(query).unwrap();
// Renders to: WHERE (price + tax) > 100
// String functions
let query = "$filter=contains(name, 'John')";
let options = parse(query).unwrap();
// Renders to: WHERE name LIKE CONCAT('%', 'John', '%')
// Date/time functions
let query = "$filter=year(birthdate) eq 1990";
let options = parse(query).unwrap();
// Renders to: WHERE YEAR(birthdate) = 1990
// Math functions
let query = "$filter=round(price) lt 50";
let options = parse(query).unwrap();
// Renders to: WHERE ROUND(price, 0) < 50
// Lambda operators
let query = "$filter=orders/any(o: o/total gt 100)";
let options = parse(query).unwrap();
// Note: Lambda operators parsed but SQL generation is dialect-specific
// In operator
let query = "$filter=status in ('Active', 'Pending')";
let options = parse(query).unwrap();
// Renders to: WHERE status IN ('Active', 'Pending')
// Complex expression with multiple operators
let query = "$filter=age gt 18 and (status eq 'Active' or status eq 'Pending')";
let options = parse(query).unwrap();
// Renders to: WHERE (age > 18 AND (status = 'Active' OR status = 'Pending'))
```
### Combined Query
```rust
use odatav4_parser::{parse, renderers::*};
let query = "$select=id,name&$filter=active eq true&$expand=orders&$top=10&$skip=5";
let options = parse(query).unwrap();
let surrealql = surrealql::SurrealqlRenderer::new();
println!("{}", surrealql.render("users", &options));
// Output: SELECT id, name FROM users WHERE active = TRUE START 5 LIMIT 10 FETCH orders
```
### Error Handling
```rust
use odatav4_parser::{parse, ODataError};
match parse("$filter=name eq 'test'") {
Ok(options) => println!("Parsed successfully"),
Err(ODataError::UnsupportedOption(opt)) => {
println!("Unsupported option: {}", opt);
}
Err(e) => println!("Parse error: {}", e),
}
```
## Supported OData V4 Features
### Query Options
- ✅ `$select` - Field selection (including `*` wildcard)
- ✅ `$top` - Limit number of results
- ✅ `$skip` - Skip N results (pagination)
- ✅ `$filter` - Filter expressions (see below)
- ✅ `$expand` - Navigation properties (full support for SurrealDB `FETCH`, placeholder for SQL dialects)
- ✅ `$orderby` - Sorting with `asc`/`desc`
- ✅ `$groupby` - Grouping
- ✅ `$count` - Include total count flag
- ✅ `$format` - Response format
- ✅ `$id` - Entity ID
- ✅ `$skiptoken` - Pagination token
- ✅ `$search` - Full-text search term
### Filter Operators
**Comparison Operators:**
- ✅ `eq` - Equal
- ✅ `ne` - Not equal
- ✅ `gt` - Greater than
- ✅ `ge` - Greater than or equal
- ✅ `lt` - Less than
- ✅ `le` - Less than or equal
**Logical Operators:**
- ✅ `and` - Logical AND
- ✅ `or` - Logical OR
- ✅ `not` - Logical NOT
**Arithmetic Operators:**
- ✅ `add` - Addition
- ✅ `sub` - Subtraction
- ✅ `mul` - Multiplication
- ✅ `div` - Division
- ✅ `mod` - Modulo
- ✅ Unary minus (e.g., `-Price`)
**String Functions:**
- ✅ `contains(field, value)` - Check if string contains value
- ✅ `startswith(field, value)` - Check if string starts with value
- ✅ `endswith(field, value)` - Check if string ends with value
- ✅ `length(field)` - Get string length
- ✅ `indexof(field, value)` - Find substring position
- ✅ `substring(field, start, length)` - Extract substring
- ✅ `tolower(field)` - Convert to lowercase
- ✅ `toupper(field)` - Convert to uppercase
- ✅ `trim(field)` - Remove whitespace
- ✅ `concat(field1, field2, ...)` - Concatenate strings
**Date/Time Functions:**
- ✅ `year(date)` - Extract year
- ✅ `month(date)` - Extract month
- ✅ `day(date)` - Extract day
- ✅ `hour(time)` - Extract hour
- ✅ `minute(time)` - Extract minute
- ✅ `second(time)` - Extract second
- ✅ `now()` - Current date/time
**Math Functions:**
- ✅ `round(number)` - Round to nearest integer
- ✅ `floor(number)` - Round down
- ✅ `ceiling(number)` - Round up
**Lambda Operators:**
- ✅ `any` - Collection has any matching item (e.g., `Orders/any(o: o/Total gt 100)`)
- ✅ `all` - All collection items match (e.g., `Orders/all(o: o/Status eq 'Complete')`)
**Special Operators:**
- ✅ `in` - Value in list (e.g., `Status in ('Active', 'Pending')`)
**Literals:**
- ✅ String literals (e.g., `'John'`)
- ✅ Number literals (e.g., `42`, `3.14`)
- ✅ Boolean literals (`true`, `false`)
- ✅ Null literal (`null`)
- ✅ GUID literals (e.g., `01234567-89ab-cdef-0123-456789abcdef`)
- ✅ Date literals (e.g., `2020-01-01`)
## Architecture
The library follows a classic compiler architecture:
```
Query String → Lexer → Tokens → Parser → AST → Renderer → SQL
```
1. **Lexer** (`lexer.rs`) - Tokenizes the input string
2. **Parser** (`parser.rs`) - Builds an AST from tokens
3. **AST** (`ast.rs`) - Type-safe representation of query options
4. **Renderers** (`renderers/*.rs`) - Generate SQL for specific dialects
## SQL Dialect Differences
| Limit | `TOP N` (before SELECT) | `LIMIT N` | `LIMIT N` | `LIMIT N` |
| Offset | `OFFSET N ROWS` | `OFFSET N` | `OFFSET N` | `START N` |
| Identifier Quote | `[name]` | `"name"` | `"name"` | `name` |
## Development
### Running Tests
```bash
cargo test
```
### Running Tests with Output
```bash
cargo test -- --nocapture
```
### Linting
```bash
cargo clippy -- -D warnings
```
### Formatting
```bash
cargo fmt
```
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## License
MIT
## References
- [OData V4 Specification](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html)
- [MSSQL SELECT Syntax](https://docs.microsoft.com/en-us/sql/t-sql/queries/select-transact-sql)
- [SQLite SELECT Syntax](https://www.sqlite.org/lang_select.html)
- [PostgreSQL SELECT Syntax](https://www.postgresql.org/docs/current/sql-select.html)
- [SurrealDB SELECT Syntax](https://surrealdb.com/docs/surrealql/statements/select)