odatav4-parser 0.0.2

OData V4 query string parser with AST and multi-dialect SQL rendering
Documentation
# 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

| Feature | MSSQL | SQLite | PostgreSQL | SurrealQL |
|---------|-------|--------|------------|-----------|
| 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