| Crates.io | odatav4-parser |
| lib.rs | odatav4-parser |
| version | 0.0.3 |
| created_at | 2025-12-20 23:07:27.526686+00 |
| updated_at | 2025-12-21 16:36:51.159116+00 |
| description | OData V4 query string parser with AST and multi-dialect SQL rendering |
| homepage | |
| repository | https://github.com/knackstedt/rust-odatav4 |
| max_upload_size | |
| id | 1997079 |
| size | 358,467 |
A Rust library that parses OData V4 query strings into an AST (Abstract Syntax Tree) and renders them into multiple SQL dialects.
TOP, OFFSET...ROWS, WHERE, ORDER BY, GROUP BYLIMIT, OFFSET, WHERE, ORDER BY, GROUP BYLIMIT, OFFSET, WHERE, ORDER BY, GROUP BYSTART, LIMIT, WHERE, FETCH, ORDER BY, GROUP BYeq, ne, gt, ge, lt, leand, or, notadd, sub, mul, div, mod, unary minuscontains, startswith, endswith, length, indexof, substring, tolower, toupper, trim, concatyear, month, day, hour, minute, second, nowround, floor, ceilingany, allinFETCH for SurrealDB, TODO comments for SQL dialects)Add this to your Cargo.toml:
[dependencies]
odatav4-parser = "0.1.0"
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
}
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()]));
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();
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 */
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'))
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
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),
}
$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 termComparison Operators:
eq - Equalne - Not equalgt - Greater thange - Greater than or equallt - Less thanle - Less than or equalLogical Operators:
and - Logical ANDor - Logical ORnot - Logical NOTArithmetic Operators:
add - Additionsub - Subtractionmul - Multiplicationdiv - Divisionmod - Modulo-Price)String Functions:
contains(field, value) - Check if string contains valuestartswith(field, value) - Check if string starts with valueendswith(field, value) - Check if string ends with valuelength(field) - Get string lengthindexof(field, value) - Find substring positionsubstring(field, start, length) - Extract substringtolower(field) - Convert to lowercasetoupper(field) - Convert to uppercasetrim(field) - Remove whitespaceconcat(field1, field2, ...) - Concatenate stringsDate/Time Functions:
year(date) - Extract yearmonth(date) - Extract monthday(date) - Extract dayhour(time) - Extract hourminute(time) - Extract minutesecond(time) - Extract secondnow() - Current date/timeMath Functions:
round(number) - Round to nearest integerfloor(number) - Round downceiling(number) - Round upLambda 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:
'John')42, 3.14)true, false)null)01234567-89ab-cdef-0123-456789abcdef)2020-01-01)The library follows a classic compiler architecture:
Query String → Lexer → Tokens → Parser → AST → Renderer → SQL
lexer.rs) - Tokenizes the input stringparser.rs) - Builds an AST from tokensast.rs) - Type-safe representation of query optionsrenderers/*.rs) - Generate SQL for specific dialects| 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 |
cargo test
cargo test -- --nocapture
cargo clippy -- -D warnings
cargo fmt
Contributions are welcome! Please feel free to submit a Pull Request.
MIT