Skip to content

Repository files navigation

modql

modql is a set of types and utilities designed to structurally express model query filters and list options (e.g., offset, limit, order_bys). These can be easily represented in JSON.

In essence, it offers a MongoDB-like filter syntax that is storage-agnostic, has built-in support for sea-query, and can be expressed either in JSON or Rust types.

For example:

/// This is the model entity, annotated with Fields.#[derive(Debug,Clone, modql::field::Fields,FromRow,Serialize)]pubstructTask{pubid:i64,pubproject_id:i64,pubtitle:String,pubdone:bool,}/// This is a Filter, with the modql::filter::OpVals... properties#[derive(modql::filter::FilterNodes,Deserialize,Default,Debug)]pubstructTaskFilter{project_id:Option<OpValsInt64>,title:Option<OpValsString>,done:Option<OpValsBool>,}// -- Parsing JSON representation to TaskFilter// This condition requires all of these rules to match (AND).let list_filter:TaskFilter = serde_json::from_value(json!({"project_id":123,"title":{"$startsWith":"Hello","$contains":"World"},}))?;// -- modql ListOptionslet list_options: modql::filter::ListOptions =
serde_json::from_value(json!({"offset":0,"limit":2,"order_bys":"!title"// ! for descending}))?;// -- Building a sea-query select query with those condition// Convert the TaskFilter into sea-query conditionlet cond: sea_query::Condition = filter.try_into()?;letmut query = sea_query::Query::select();// Select only the columns corresponding to the task type.// This is determined by the modql::field::Fields annotation.
query.from(task_table).columns(Task::field_column_refs());// Add the condition from the filter
query.cond_where(cond);// Apply the list options
list_options.apply_to_sea_query(&mut query);// and execute querylet(sql, values) = query.build_sqlx(PostgresQueryBuilder);let entities = sqlx::query_as_with::<_,E,_>(&sql, values).fetch_all(db).await?;

This crate is instrumental for JSON-RPC or other types of model APIs (e.g., the joql pattern).

IMPORTANT v0.3.x represents the new version of modql, featuring the with-sea-query feature set. It is utilized in the rust10x web-app production code blueprint Episode 02. This version is somewhat incompatible with v0.2.x, mainly due to module reorganization. If you are using the rust10x/awesomeapp desktop app, please stick with v0.2.x for the time being. I plan to upgrade the codebase to v0.3.x soon.

changelog

OpVal[Type] Conditional Operators

OpVal[Type] is a filter unit that allows the expression of an operator on a given value for a specified type.

The corresponding OpVals[Type], with an "s", is typically used in filter properties, as it permits multiple operators for the same field.

The basic JSON representation of an OpVal[Type] follows the {field_name: {$operator1: value1, $operator2: value2}} format. For example:

{"title": {"$startsWith": "Hello","$contains": "World"}}

This expresses the conditions that both "startsWith" and "contains" must be met.

The following tables show the list of possible operators for each type.

OpValString Operators

OperatorMeaningExample
$eqExact match with one value{name: {"$eq": "Jon Doe"}} same as {name: "Jon Doe"}
$inExact match with within a list of values (or){name: {"$in": ["Alice", "Jon Doe"]}}
$notExclude any exact match{name: {"$not": "Jon Doe"}}
$notInExclude any exact withing a list{name: {"$notIn": ["Jon Doe"]}}
$containsFor string, does a contains{name: {"$contains": "Doe"}}
$containsAnyFor string, match if contained in any of items{name: {"$containsAny": ["Doe", "Ali"]}}
$containsAllFor string, match if all items are in the src{name: {"$containsAll": ["Hello", "World"]}}
$notContainsDoes not contain{name: {"$notContains": "Doe"}}
$notContainsAnyDoes not call any of (none is contained){name: {"$notContainsAny": ["Doe", "Ali"]}}
$startsWithFor string, does a startsWith{name: {"$startsWith": "Jon"}}
$startsWithAnyFor string, match if startsWith in any of items{name: {"$startsWithAny": ["Jon", "Al"]}}
$notStartsWithDoes not start with{name: {"$notStartsWith": "Jon"}}
$notStartsWithAnyDoes not start with any of the items{name: {"$notStartsWithAny": ["Jon", "Al"]}}
$endsWithFor string, does and end with{name: {"$endsWithAny": "Doe"}}
$endsWithAnyFor string, does a contains (or){name: {"$endsWithAny": ["Doe", "ice"]}}
$notEndsWithDoes not end with{name: {"$notEndsWithAny": "Doe"}}
$notEndsWithAnyDoes not end with any of the items{name: {"$notEndsWithAny": ["Doe", "ice"]}}
$ltLesser Than{name: {"$lt": "C"}}
$lteLesser Than or ={name: {"$lte": "C"}}
$gtGreater Than{name: {"$gt": "J"}}
$gteGreater Than or ={name: {"$gte": "J"}}
$nullIf the value is null{name: {"$null": true}}

OpValInt32, OpValInt64, OpValFloat64 Operators

OperatorMeaningExample
$eqExact match with one value{age: {"$eq": 24}} same as {age: 24}
$inExact match with within a list of values (or){age: {"$in": [23, 24]}}
$notExclude any exact match{age: {"$not": 24}}
$notInExclude any exact withing a list{age: {"$notIn": [24]}}
$ltLesser Than{age: {"$lt": 30}}
$lteLesser Than or ={age: {"$lte": 30}}
$gtGreater Than{age: {"$gt": 30}}
$gteGreater Than or ={age: {"$gte": 30}}
$nullIf the value is null{name: {"$null": true}}

OpValBool Operators

OperatorMeaningExample
$eqExact match with one value{dev: {"$eq": true}} same as {dev: true}
$notExclude any exact match{dev: {"$not": false}}
$nullIf the value is null{name: {"$null": true}}

More Info

  • modql::filter - Delivers a declarative structure that can be deserialized from JSON.
  • modql::field - Provides a method to derive a sea-query compatible data structure from standard structs.

This introduces the following:

  • Task::field_column_refs() -> Vec<ColumnRef>: Constructs sea-query select queries.
  • Task::field_idens() -> Vec<ColumnRef>: Constructs sea-query select queries, suited for simpler cases.
  • task.all_fields().for_sea_insert() -> (Vec<DynIden>, Vec<SimpleExpr>): Used for sea-query inserts.
  • task.all_fields().for_sea_update() -> impl Iterator<Item = (DynIden, SimpleExpr)>: Used for sea-query updates.

Additionally, it offers:

  • task.not_none_fields(): Operates similarly to the above, but only for fields where their Option is not None.

Rust types

On the Rust side, this can be expressed like this:

use modql::filter::{FilterGroups,FilterNode,OpValtring};fnmain() -> anyhow::Result<()>{let filter_nodes:Vec<FilterNode> = vec![("title",OpValtring::ContainsAny(vec!["Hello".to_string(),"welcome".to_string()]),).into(),("done",true).into(),];let filter_groups:FilterGroups = filter_nodes.into();println!("filter_groups:\n{filter_groups:#?}");Ok(())}

A Model or Store layer can take the filter_groups and serialize them into their DSL (e.g., SQL for databases).

The Filter structure is as follows:

  • FilterGroups is the top level and consists of multiple FilterGroup elements. FilterGroup elements are intended to be executed with an OR operation between them.
  • Each FilterGroup contains a vector of FilterNode elements, which are intended to be executed with an AND operation.
  • FilterNode contains a context_path (not used yet), name which represents the property name from where the value originates, and a Vec<OpVal>, representing the Operator Value.
  • OpVal is an enum for type-specific OpVal[Type] entities, such as OpValString that holds the specific operation for that type along with the associated pattern value.

GitHub Repo

About

Rust implementation for Model Query Language support

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages