A Rust library that parses RSQL / FIQL filter queries and compiles them into native backend representations — PostgreSQL clauses, MongoDB documents, or SurrealQL strings.
name==Christopher*;year=gt=1990
↓
tokio-postgres: ("name LIKE $1 AND year > $2", ["Christopher%", 1990])
mongodb: { "$and": [{ "name": { "$regex": "^Christopher.*$" } }, { "year": { "$gt": 1990 } }] }
surrealdb: "(string::matches(name, '^Christopher.*$') AND year > 1990)"
- Syntax reference
- Extensions beyond the RSQL/FIQL spec
- Quick start
- Field allowlisting
- Field mapping
- Drivers
- WASM compatibility
- Feature flags
| Syntax (short) | Syntax (long) | Meaning |
|---|---|---|
== |
=eq= |
Equal |
!= |
=neq= |
Not equal |
< |
=lt= |
Less than |
<= |
=le= |
Less than or equal |
> |
=gt= |
Greater than |
>= |
=ge= |
Greater than or equal |
=in= |
In list | |
=out= |
Not in list | |
=between= |
Between two values (inclusive) | |
=null= |
Is null / absent | |
=notnull= |
Is not null / present | |
=like= |
Pattern match (* = any chars, _ = one char) |
|
=ilike= |
Case-insensitive pattern match |
| Character | Meaning | Example |
|---|---|---|
; |
AND | year=gt=1990;rating=ge=8 |
, |
OR | genre==Drama,genre==Thriller |
(...) |
Grouping | (genre==Drama,genre==Horror);rating=gt=7 |
Standard operator precedence: AND binds tighter than OR. Use parentheses to override.
Values are typed at parse time — no quotes needed for numbers, booleans, or dates.
| Type | Example | Notes |
|---|---|---|
| String | Alice, "O'Brien", 'hello world' |
Bare words, or single/double-quoted |
| Integer | 42, -7 |
|
| Float | 8.5, -0.1 |
Must contain . |
| Boolean | true, false |
Case-sensitive keywords |
| Null | null |
|
| Date | 2024-03-15 |
YYYY-MM-DD |
| DateTime | 2024-03-15T20:30:00Z |
YYYY-MM-DDTHH:MM:SSZ |
| List | (Drama,Horror,Thriller) |
Only with =in=, =out=, =between= |
The original FIQL spec and the jirutka/rsql-parser reference implementation define a minimal set of comparison operators. This library extends that baseline:
| Extension | Details |
|---|---|
| Typed literals | Date (2024-03-15), DateTime (2024-03-15T20:30:00Z), Bool, Int, Float, Null recognized at lex time — no quotes required |
=like= / =ilike= |
Pattern matching with RSQL wildcard * (multi-char) and _ (single char). Case-insensitive variant =ilike= included |
=between= |
Range filter equivalent to field >= a AND field <= b |
=null= / =notnull= |
Presence check (maps to IS NULL / IS NOT NULL in SQL, NONE in SurrealQL, null match in MongoDB) |
=out= |
Negated list membership (complement of =in=) |
| Field allowlisting | RestSql::new_for_fields() and RestSql::new_for::<T>() reject unknown fields at parse time |
FieldMapper trait |
Translates logical field names to backend column names (JSONB paths, aliases, etc.) |
[dependencies]
rest-sql = "0.1"
rest-sql-drivers = { version = "0.1", features = ["tokio-postgres"] }use rest_sql::RestSql;
use rest_sql_drivers::tokio_postgres::{PgCompiler, PgParams};
use rest_sql_drivers::Driver;
use rest_sql::IdentityMapper;
let rsql = RestSql::new("title=like=Godfather*;year=gt=1970")?;
let (clause, params) = PgCompiler::new(IdentityMapper).compile(&rsql)?;
// clause: "(title LIKE $1 AND year > $2)"
// params: ["Godfather%", 1970]
let query = format!("SELECT * FROM movies WHERE {clause}");
let rows = client.query(&query, ¶ms.iter().map(|p| p.as_ref()).collect::<Vec<_>>()).await?;Reject queries that reference undeclared fields. Useful when filters are built from user input.
// Explicit allowlist
let rsql = RestSql::new_for_fields("title==Inception;secret==x", &["title", "year"])?;
// → Err: field 'secret' is not allowed
// Derive from a serde struct (requires feature `serde`)
#[derive(serde::Deserialize)]
struct Movie {
title: String,
year: i32,
rating: f64,
}
let rsql = RestSql::new_for::<Movie>("title==Inception;year=gt=2000")?;FieldMapper translates logical names to storage columns before compilation.
use rest_sql::FieldMapper;
use std::borrow::Cow;
// Map logical names to a JSONB column
struct MetaMapper;
impl FieldMapper for MetaMapper {
fn map<'a>(&self, field: &'a str) -> Cow<'a, str> {
Cow::Owned(format!("metadata->'{field}'"))
}
}The tokio-postgres driver ships a ready-made JsonbTextMapper (uses ->>):
use rest_sql_drivers::tokio_postgres::{PgCompiler, JsonbTextMapper};
use rest_sql_drivers::Driver;
let rsql = RestSql::new("genre==Drama")?;
let (clause, params) = PgCompiler::new(JsonbTextMapper::new("attrs")).compile(&rsql)?;
// clause: "attrs->>'genre' = $1"Outputs (String, PgParams) — a parameterized WHERE clause with $1/$2/... placeholders.
use rest_sql_drivers::tokio_postgres::{PgCompiler, PgParams};
use rest_sql_drivers::Driver;
use rest_sql::{IdentityMapper, RestSql};
// Movies directed by someone born after 1950 with rating >= 8
let rsql = RestSql::new("director_birth=gt=1950-01-01;rating=ge=8")?;
let (clause, params) = PgCompiler::new(IdentityMapper).compile(&rsql)?;
// "(director_birth > $1 AND rating >= $2)"
// Actors from specific nationalities
let rsql = RestSql::new("nationality=in=(French,Italian,Spanish)")?;
let (clause, params) = PgCompiler::new(IdentityMapper).compile(&rsql)?;
// "nationality IN ($1, $2, $3)"
// Films released between two dates
let rsql = RestSql::new("released_at=between=(1990-01-01,1999-12-31)")?;
let (clause, params) = PgCompiler::new(IdentityMapper).compile(&rsql)?;
// "released_at BETWEEN $1 AND $2"
// All films without a poster
let rsql = RestSql::new("poster_url=null=true")?;
let (clause, _) = PgCompiler::new(IdentityMapper).compile(&rsql)?;
// "poster_url IS NULL"LIKE wildcard: * in the RSQL pattern is translated to % for PostgreSQL. _ remains _.
name=like=Chris* → name LIKE $1 (param: "Chris%")
name=like=Chr_s → name LIKE $1 (param: "Chr_s")
Outputs bson::Document — a MongoDB filter document.
use rest_sql_drivers::mongodb::MongoCompiler;
use rest_sql_drivers::Driver;
use rest_sql::{IdentityMapper, RestSql};
// Action films rated above 7.5
let rsql = RestSql::new("genre==Action;rating=gt=7.5")?;
let filter = MongoCompiler::new(IdentityMapper).compile(&rsql)?;
// { "$and": [{ "genre": "Action" }, { "rating": { "$gt": 7.5 } }] }
// Actors with unknown nationality
let rsql = RestSql::new("nationality=null=true")?;
let filter = MongoCompiler::new(IdentityMapper).compile(&rsql)?;
// { "nationality": null } — matches null or absent field
// Films whose title starts with "The"
let rsql = RestSql::new("title=like=The*")?;
let filter = MongoCompiler::new(IdentityMapper).compile(&rsql)?;
// { "title": { "$regex": "^The.*$", "$options": "" } }
// Case-insensitive director name search
let rsql = RestSql::new("director=ilike=kubrick*")?;
let filter = MongoCompiler::new(IdentityMapper).compile(&rsql)?;
// { "director": { "$regex": "^kubrick.*$", "$options": "i" } }
let results = collection.find(filter, None).await?;LIKE wildcard: * → .* (regex), _ → . (regex). Regex metacharacters are escaped.
Null semantics: field=null=true produces { field: null }, which matches documents
where the field is null or absent. To distinguish the two cases, use the MongoDB driver directly.
Outputs a String — an inline SurrealQL WHERE clause with values embedded as literals.
use rest_sql_drivers::surrealdb::SurrealCompiler;
use rest_sql_drivers::Driver;
use rest_sql::{IdentityMapper, RestSql};
// Sci-Fi or Fantasy films after 2000
let rsql = RestSql::new("(genre==SciFi,genre==Fantasy);year=gt=2000")?;
let clause = SurrealCompiler::new(IdentityMapper).compile(&rsql)?;
// "((genre = 'SciFi' OR genre = 'Fantasy') AND year > 2000)"
let query = format!("SELECT * FROM movie WHERE {clause}");
db.query(query).await?;
// Actors still active, born after a specific date
let rsql = RestSql::new("active==true;birth_date=gt=1970-01-01")?;
let clause = SurrealCompiler::new(IdentityMapper).compile(&rsql)?;
// "(active = true AND birth_date > d'1970-01-01')"
// Records with no awards
let rsql = RestSql::new("awards=null=true")?;
let clause = SurrealCompiler::new(IdentityMapper).compile(&rsql)?;
// "awards = NONE"LIKE wildcard: * → .* (regex), _ → .. Compiled to string::matches(field, 'pattern').
Null semantics: maps to NONE (SurrealQL equivalent of SQL NULL / absent value).
Date/DateTime: emitted as d'YYYY-MM-DD' and d'YYYY-MM-DDTHH:MM:SSZ' SurrealQL literals.
| Crate / feature | WASM-safe | Notes |
|---|---|---|
rest-sql |
Yes | Pure parsing, no I/O |
rest-sql-drivers + mongodb |
Yes | bson transformation is pure |
rest-sql-drivers + surrealdb |
Yes | String transformation is pure |
rest-sql-drivers + tokio-postgres |
No | Requires tokio TCP networking |
For WASM targets, enable only the mongodb feature (and/or surrealdb). Do not enable tokio-postgres.
| Feature | Default | Description |
|---|---|---|
serde |
off | Enables RestSql::new_for::<T>() — field allowlist from #[derive(Deserialize)] structs |
| Feature | Default | Description |
|---|---|---|
tokio-postgres |
off | PostgreSQL driver via tokio-postgres. Not WASM-compatible. |
mongodb |
off | MongoDB driver via bson. WASM-compatible. |
surrealdb |
off | SurrealDB driver (inline SurrealQL). WASM-compatible. |
MIT — see LICENSE.