Examples of dynamic filtering with diesel-rs
Quite often we want to build filters at runtime (i.e. from Graphql input), this requires a structure of defining dynamic filter shape and a mechanism to convert it to database queries.
Anything dynamic in diesel-rs can be quite daunting at the beginning, thus I wanted to show examples of dynamic filtering with diesel-rs via evolutionary guide (from basic to complex).
This tutorial is meant to be consumed in steps, each step has a branch and each step evolves from the previous step (git diff helps with understanding the progression). These topics are covered:
- Basic condition boxing
- Generic type filters
- And/Or and nesting/grouping condition
- Joins
- Macros to help with extendability
- Inner queries
- Complex joins
I tried to find a balance between the use of generic and complexity, and in some cases opted in for macros or copy paste approach. When working in a team, readability and simplicity trumps most other subjective characteristics of code, and I think complex use of generics undermines that characteristic.
We start with basic-generic-filters branch and this file.
Basically we get to below data structure:
diesel-rs-dynamic-filters/src/dynamic_filters.rs
Lines 15 to 20 in 74283f9
That can be used in the following way:
diesel-rs-dynamic-filters/src/dynamic_filters.rs
Lines 116 to 127 in 74283f9
The next step, in generic-field-filters branch and as per this diff, generic type filters were added, to be able to filter more then one field.
This allowed this structure:
diesel-rs-dynamic-filters/src/dynamic_filters.rs
Lines 17 to 23 in d715883
To be used in the following way:
diesel-rs-dynamic-filters/src/dynamic_filters.rs
Lines 158 to 162 in d715883
This is where it starts to get interesting, in and/or branch as per this diff we are able to use dynamic and and or statement with unlimited number of nesting and fine grained control over the grouping.
The tests have a good example of this, basically we get:
diesel-rs-dynamic-filters/src/dynamic_filters.rs
Lines 217 to 224 in b8591aa
diesel-rs-dynamic-filters/src/dynamic_filters.rs
Lines 240 to 247 in b8591aa
The next step was to show how we can build dynamic conditions for joined tables, in joins branch as per this diff.
You will note in the following statement, BoxedCondition now returns Nullable
bool, and .nullable()
needs to be added to every single field, this doesn't affect the condition, and macros will help us with the syntax.
An example of more complex joins will be shown in later section
It should be easy to extend existing condition and add filtering functionality to new tables, thus helper were added in macros branch and this diff.
Now new filter can be added by one line addition of a field in conditions enum and a case in match:
In addition, is_null was added to boolean filter type to show how we can enforce that filter:
Even with macros there seems to be a bit of bloat in code and to add dynamic filtering functionality to a new table we would need to add Condition
enum, Impl
of that Condition
enum and create_filter
method, it would still be just copy paster operation, I thought that was a good compromise for readability, since a macro to auto generate this method or to make it a method with generics would add a bit of complexity.
Sometimes you may want to re-use existing queries and filters as a condition elsewhere. The inner-query branch as per this diff, makes this possible by also creating a boxed query.
You would have seen the use of diesel type helpers already, here the use is extended to create boxed query with filter, that is later used as a condition in another table.
diesel-rs-dynamic-filters/src/inner_statement/bike.rs
Lines 77 to 84 in 776eca2
diesel-rs-dynamic-filters/src/inner_statement/person.rs
Lines 45 to 53 in 776eca2
That results in being able to:
diesel-rs-dynamic-filters/src/inner_statement/mod.rs
Lines 78 to 80 in 776eca2
This example also shows that macros make extension of field type filters quite easy
diesel-rs-dynamic-filters/src/lib.rs
Line 30 in 776eca2
diesel-rs-dynamic-filters/src/lib.rs
Line 39 in 776eca2
TODO is there a way to use one 'source' (ConditionSource and QuerySource)
One of the harder things I found with diesel-rs types was construction boxed join types, especially when join has ON statements as showcase in complex-joins branch and this diff.
You can extend this example by adding road_on_bike_ride
join table, rather than road_id
on bike_ride
table (this will allow multiple roads to be attached to a bike_ride and a good exercise for diesel-rs query boxing)
I hope you found this tutorial useful, you can create an issue if you need some clarification or found an error, etc..
(For anyone that's interested, the original trigger for writing this tutorial came from a more demanding filtering requirement from omSupply project during a central server synchronization research done in one of our monthly RnD days)