Skip to content

dohrm/rest-sql

Repository files navigation

rest-sql

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)"

Table of contents


Syntax reference

Operators

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

Logical connectors

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.

Value types

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=

Extensions beyond the RSQL/FIQL spec

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.)

Quick start

[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, &params.iter().map(|p| p.as_ref()).collect::<Vec<_>>()).await?;

Field allowlisting

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")?;

Field mapping

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"

Drivers

tokio-postgres

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")

mongodb

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.


surrealdb

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.


WASM compatibility

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 flags

rest-sql

Feature Default Description
serde off Enables RestSql::new_for::<T>() — field allowlist from #[derive(Deserialize)] structs

rest-sql-drivers

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.

License

MIT — see LICENSE.

About

A Rust library that parses [RSQL / FIQL](https://github.com/jirutka/rsql-parser) filter queries and compiles them into native backend representations

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages