Diesel Workshop

Introduction

Comments

  • This workshop was presented during RustWeek 2025
  • You can access speaker notes by pressing s on your keyboard
  • The exercise code is public at GitHub

Who I am

  • I’m Georg (weiznich on Github)
  • Diesel contributor since 8 years and one of the core team members of Diesel since 5 years
  • I’m writing Rust code for 10 years now, including contributions to Diesel, the rust compiler and authoring one RFC
  • Working at GiGa Infosystems GmbH on a database system for geological subsurface models

Content of the workshop

  • Diesel Basics
  • Testing with Diesel
  • Extending Diesel

About you

  • Rust experience?
  • Database experience?
  • Diesel experience?

Diesel Basics

What is Diesel

  • Diesel is a query builder and ORM for Rust
  • Exists since 2015
  • Focus on performance and safety
  • Checks your SQL at compile time
  • Expressive and extensible

What is Diesel

  • Diesel consists of different parts:
    • Core diesel crate
    • A CLI tool diesel-cli
    • Migration crate diesel-migration
    • (Several internal helper crates for derives, etc)
  • Notable extension crates
    • diesel-full-text-search
    • diesel-dynamic-schema
    • diesel-async

What is provided by Diesel

  • Database connection abstractions
  • Database connections for PostgreSQL, MySQL and SQLite
  • A domain specific language (DSL) to describe your database schema
  • A DSL to write your queries
  • Various proc-macros to map data from and to queries
  • Basic ORM like functionality
  • Tools to execute migrations

Diesel Design Goals

  • Check whatever is possible to check at compile time
  • Do not hide database features/differences from the user
  • Be extensible as possible
  • Make type mapping flexible

Diesel Limitations

  • Limited built-in DSL
  • Strongly typed, which restricts certain kinds of dynamic queries
  • Favours explicit query building over implicit model based approach
  • Represent queries at types requires the compiler to check these queries Additional work

Show me some code

diesel::table! {
    users {
        id -> Integer,
        name -> Text,
    }
}

fn load_usernames(conn: &mut PgConnection) 
    -> QueryResult<Vec<String>> 
{
    users::table.select(users::name).load(conn)
}

Creating a Diesel Application

  1. Install diesel_cli
  2. Setup the database using SQL migrations
  3. Create a Rust schema file from your database
  4. Write code that interacts with the database

What is Diesel CLI

  • Helper tool for working with Diesel projects
  • Features:
    • Manage your database
    • Manage your migrations
    • Generate migrations based on your Rust schema and an existing database
    • Generate Rust schema from databases
    • Verify that the generated schema matches your database

The table! Macro

diesel::table! {
    users {
        id -> Integer,
        name -> Text,
    }
}
  • Defines a representation of the database as Rust code
  • Used for type checking + query building

The table! Macro

mod users {
    pub struct table;
    mod columns {
        pub struct id;
        pub struct name;
    }
    pub use columns::*;
    
    pub mod dsl {
        pub use super::table as users;
        pub use super::columns::*;
    }
}
  • Contains zero sized structs that represent your table + columns
  • Prefer qualified uses, e.g users::id
  • You can use cargo doc to see what’s generated

Select Statements

  • Diesel provides a query DSL to construct queries
  • DSL maps literally to the generated SQL, e.g. users::table.select(users::id) maps to SELECT id FROM users
  • Documentation available via the QueryDsl trait
  • Offers support for most select statement constructs

Select Statements

users::table
    .select((users::id, users::name))
    .filter(users::name.like("%John%"))
    .order_by(users::id)
    .limit(42)
    .offset(24)
    .load::<RustTargetType>(&mut conn)?;

Query Execution

  • Different ways to execute a query via RunQueryDsl:
    • load::<U>/get_results::<U>: Returns a list of U as Vec<U>
    • get_result::<U>: Returns the first U ignores the rest
    • first::<U>: Returns the first U, implicitly attaches a LIMIT 1 clause to the executed query
    • execute: Returns the number of affected column

Select Statements (Result Mapping)

  • Diesel maps query results to Rust structs
  • Mapping is performed by field order, not by name
  • Compile time checks ensure that you only use compatible types on the Rust side
  • Default implementation exists for tuples
  • Recommend to use structs that derive Queryable

Result Mapping

diesel::table! {
    users {
        id -> Integer,
        name -> Text,
        age -> Integer,
    }
}

Result Mapping


#[derive(Queryable)]
struct User {
    id: i32,
    name: String,
    age: i32,
}

users::table.load::<User>(&mut conn)?;
users::table.load::<(i32, String, i32)>(&mut conn)?;

Result Mapping

#[derive(Queryable)]
struct UserWithName {
    user_name: String,
    id: i32,
}

users::table.select((users::name, users::id))
    .load::<UserWithName>(&mut conn)?;
users::table.select(users::id).load::<i32>(&mut conn)?;

Result Mapping

the trait bound `(diesel::sql_types::Integer, diesel::sql_types::Text, diesel::sql_types::Integer): load_dsl::private::CompatibleType<User, Sqlite>` is not satisfied
    --> src/main.rs:19:31
     |
19   |     users::table.load::<User>(conn);
     |                  ----         ^^^^ the trait `load_dsl::private::CompatibleType<User, Sqlite>` is not implemented for `(diesel::sql_types::Integer, diesel::sql_types::Text, diesel::sql_types::Integer)`, which is required by `table: LoadQuery<'_, _, User>`
     |                  |
     |                  required by a bound introduced by this call
     |
     = note: this is a mismatch between what your query returns and what your type expects the query to return
     = note: the fields in your struct need to match the fields returned by your query in count, order and type
     = note: consider using `#[derive(Selectable)]` or #[derive(QueryableByName)] + `#[diesel(check_for_backend(Sqlite))]` 
             on your struct `User` and in your query `.select(User::as_select())` to get a better error message

Result Mapping

  • Potentially bad error messages due to field mismatches
  • Need to keep field order in struct and select clause in sync

Diesel provides a #[derive(Selectable)] which allows to generate a matching select clause from your struct

Result Mapping

#[derive(Queryable, Selectable)]
#[diesel(table_name = users)]
#[diesel(check_for_backend(diesel::pg::Pg))]
struct UserWithName {
    #[diesel(column_name = name)]
    user_name: String,
    id: i32,
}

users::table.select(UserWithName::as_select()).load(&mut conn)?;

Result Mapping

error[E0277]: cannot deserialize a value of the database type `diesel::sql_types::Integer` as `*const str`
  --> src/main.rs:16:9
   |
16 |     id: String,
   |         ^^^^^^ the trait `FromSql<diesel::sql_types::Integer, Sqlite>` is not implemented for `*const str`, which is required by `String: FromSqlRow<diesel::sql_types::Integer, Sqlite>`
   |
   = note: double check your type mappings via the documentation of `diesel::sql_types::Integer`
   = help: the trait `FromSql<diesel::sql_types::Text, Sqlite>` is implemented for `*const str`
   = help: for that trait implementation, expected `diesel::sql_types::Text`, found `diesel::sql_types::Integer`
   = note: required for `String` to implement `FromSql<diesel::sql_types::Integer, Sqlite>`
   = note: required for `String` to implement `diesel::Queryable<diesel::sql_types::Integer, Sqlite>`
   = note: required for `String` to implement `FromSqlRow<diesel::sql_types::Integer, Sqlite>`

Select Statements (Select Clause)

users::table.select((
    users::id, // select a column,
    // select clause part based on selectable
    UserWithName::as_select(), 
    users::id + users::id, // arbitary expressions
    diesel::dsl::date(diesel::dsl::now), // SQL function calls
    "abc".into_sql::<Text>((), // constants
))
  • If not provided a default select clause with all columns of the table is generated
  • Calling it twice will replace the previous select clause

Select Statements (Where Clause)

users::table
    .filter(users::id.eq(42))
    .filter(users::name.like("%John%"))
    .or_filter(users::name.is_null())
  • If not provided, no where clause is generated
  • Calling it twice will append the second expression with an AND operator
  • Filter conditions can be arbitrary expressions that evaluate to booleans
  • See various ExpressionMethods for possible expressions

Select Statements (Order Clauses)

users::table.order_by(users::id)
    .then_order_by((users::age.desc(), users::name))
  • If not provided, no order clause is generated
  • Accepts a single or multiple expressions
  • Default order is ASC

Select Statements (Other Methods)

users::table
   .group_by(users::id)
   .limit(1)
   .offset(5)
  • Many other QueryDsl methods for other clauses
  • GROUP BY is special, as it adds additional requirements

Expressions

  • Everything in Diesel is an expression
  • Expressions can be combined in all locations
  • Diesel provides a lot of functions/trait methods to construct expressions
  • Diesel also overloads numeric operators (+-*/) so that they can be used to combine expressions
  • All of them translate quite literally to the underlying SQL

Expressions

users::table
   .filter(users::my_boolean_column) // plain columns
   .filter(users::id.eq(42)) // function call
   .filter(users::id.eq(users::age + users::age)) // operators 
   .filter(42.into_sql::<Integer>().eq(users::id)) // constants
   // chaining function calls
   .filter(users::id.eq(users::id + 5).eq(false)) 
    // sub queries
   .filter(users::id.eq_any(
       posts::table
           .filter(posts::name.eq("My fancy post"))
           .select(posts::user_id)
    ))
  • See various *ExpressionMethod traits for all provided methods

Insert Statements

diesel::insert_into(users::table)
    .values((
        users::name.eq("John"),
        users::age.eq(42),
    )).execute(&mut conn)?;
  • Creates and executes an INSERT INTO table statement

Insert Statements

#[derive(Insertable)]
#[diesel(table_name = users)]
struct NewUser {
    name: String,
    age: i32,
}
let values: Vec<NewUser> = /* … */;

diesel::insert_into(users::table)
    .values(values)
    .returning(User::as_returning())
    .get_results(&mut conn)?;
  • Insert via struct is possible
  • Prefer this variant if the struct already exists

Insert Statements

#[derive(Insertable)]
#[diesel(table_name = users)]
struct NewUser {
    name: String,
    age: i32,
}
let value = NewUser { /*…*/ };

diesel::insert_into(users::table)
    .values((value, users::id.eq(42)))
    .execute(&mut conn);

Update Statements

diesel::update(users::table.find(42))
    .set((
        users::name.eq("Jane"),
        users::age.eq(users::age + 5.into_sql::<Integer>())
    )).execute(&mut conn)?;
  • Creates a UPDATE table statement
  • find is a shorthand for .filter(users::id.eq(42))

Update Statements

#[derive(AsChangeset, Identifiable)]
#[diesel(table_name = users)]
struct UserChangeset {
    id: i32,
    name: String,
    age: i32,
}
let value = UserChangeset { /*…*/ };

diesel::update(&value)
    .set(&value)
    .returning(users::id)
    .get_result::<i32>(&mut conn)?;
  • A struct can be used as input
  • Prefer this variant if the struct already exists
  • Automatic filter construction via Identifiable

Delete Statements

diesel::delete(users::table.find(42)).execute(&mut conn)?;
  • Constructs a DELETE FROM table statement
  • Filter is optional like for updates

Raw SQL Queries

#[derive(QueryableByName)]
#[diesel(table_name = users)]
struct User {
    id: i32,
    name: String,
}

diesel::sql_query("SELECT id, name FROM users WHERE name = $1")
    .bind::<Text, _>("Jane")
    .load::<User>(&mut conn)?;
  • Do never construct the query string via format!()
  • Commonly used to express queries that cannot be expressed with the DSL yet

Complex Queries

table! {
    users (id) {
        id -> Integer,
        name -> Text,
    }
}
table! {
    posts (id) {
        id -> Integer,
        user_id -> Integer,
        title -> Text,
        body -> Nullable<Text>,
    }
}

Complex Queries

table! {
    followers (followed_user, following_user) {
        followed_user -> Integer,
        following_user -> Integer,
    }
}


joinable!(posts -> users (user_id));
joinable!(followers -> users (followed_user));
joinable!(followers -> users (following_user));

Complex Queries (The broken way)

let users = users::table.load::<User>(&mut conn)?;

let users_with_posts = users.into_iter()
    .map(|u| {
        let posts = posts::table.filter(posts::user_id.eq(u.id))
            .load::<Post>(&mtu conn)?;
        Ok((u, posts))
    }).collect()?;
  • What is problematic about this query?

Complex Queries (Joins)

users::table.inner_join(posts::table)
    .load::<(User, Post)>(&mut conn)?;

users::table
    .inner_join(posts::table)
    .group_by(users::id)
    .select((User::as_select(), dsl::count(posts::id)))
    .load::<(User, i64)>(&mut conn)?;
  • Joins are part of the QueryDsl
  • Map to SQL as expected

Complex Queries (Associations)

#[derive(Identifiable, Queryable)]
#[diesel(table_name = users)]
struct User {
    id: i32,
    name: String,
}
#[derive(Identifiable, Queryable, Associations)]
#[diesel(belongs_to(User))]
#[diesel(table_name = users)]
struct Post {
    id: i32,
    user_id: i32,
    //…
}

Complex Queries (Associations)

let users = users::table.load:<User>(&mut conn)?;

let posts = Post::belonging_to(&users)
    .load::<Post>(&mut conn)? // returns `Vec<Post>`
    .grouped_by(&users); // converts this into `Vec<Vec<Post>>` 

let user_with_posts: Vec<(User, Vec<Post>)> = users.into_iter()
    .zip(posts)
    .collect();

Complex Queries (Joins + Associations)

let user = users::table.filter(users::name.eq("Jane"))
    .get_result::<User>(&mut conn)?;

let following_user = Followers::belonging_to(&user)
    .inner_join(users::table)
    .select(User::as_select())
    .load(&mut conn);
  • belonging_to just constructs a query targeting the followers table
  • This query can easily be extended, e.g. by another join

Transactions

let last_inserted_user = my_connection.transaction(|conn| {
    diesel::insert_into(users::table)
        .values(data)
        .execute(conn)?;
    users::table
        .order_by(users::id.desc())
        .first::<User>(conn)
})?;
  • Diesel supports database transactions via Connection::transaction

Exercise I - Basic Diesel

Exercise I - Basic Diesel

Exercise I - Basic Diesel

  • Web Application
  • Registration form for running competitions
  • Allows to:
    • Create and manage different competitions
    • Each competition has different races, each race different starts
    • Participants belong to a certain start and a certain category (E.g. Jane, aged 42 years starts over 10 km)
  • Stripped down version of a real world application

Exercise I - Basic Diesel

  • Steps to get the application up:
    • Clone the repository
    • Run cargo run -- --insert-test-data once to initialize the database
    • Run cargo run again to start the application
    • Open http://0.0.0.0:8000/index.html to get to the entry point of the web application

Exercise I - Basic Diesel

  • Goals:
    • Implement competition list (see competition_overview.rs)
    • Implement registration list (see registration_list.rs)
    • Implement participant registration (see registration.rs)
    • Implement handling of special categories
      :::notes
  • Ask for help if you are stuck
  • I’ve generally let the expected input/output structures in place
  • Removed all Diesel related code at these places, your task is it to add that back
  • Don’t hesitate to just use place holder values if you cannot fill some fields in the first try
  • Start from the beginning, don’t worry if you don’t solve all of that. :::

Exercise I - Basic Diesel

Exercise I - Basic Diesel

  • Short tour through the relevant places in the code

Exercise I - Diesel Extensions

  • Your Solutions
  • (My Solution)
  • Your Questions/Problems

Diesel-Async

What is Diesel-Async

  • An extension crate for diesel
  • Provides async counter parts of any method that interacts with connections
  • Notably it provides drop in replacements for the RunQueryDsl and Connection traits

Using Diesel-Async

Diesel

use diesel::prelude::*;

let mut connection = PgConnection::establish(url)?;

let users = users::table.load::<User>(&mut conn)?;

Diesel-Async

use diesel::prelude::*;
use diesel_async::AsyncPgConnection;
use diesel_async::RunQueryDsl;

let mut connection = AsyncPgConnection::establish(url).await?;

let users = users::table.load::<User>(&mut conn).await?;

Advantages of Diesel-Async

  • Does perform async network IO to your database
  • Fits better into async applications overall
  • Provides pure Rust connection implementations
  • Supports query pipelining for PostgreSQL

Disadvantages of Diesel-Async

  • Cannot guarantee proper handling of transactions due to the lack of async drop
  • Might perform worse than sync diesel connections
  • Increased complexity of async ecosystem

When to use which one?

Use diesel:

  • Working in a sync context
  • Use SQLite as only backend
  • No large number of concurrent requests (with an async connection pool)

Use Diesel-Async:

  • Enjoy using cutting edge async functionality
  • Expected to see traffic spikes or large amounts of requests or significant network latency to your database

Testing with Diesel

Testing Requirements

  • Always test against the same type of database system as you run in production
  • Tests should be fast to run
  • Tests should require a minimal setup
  • Tests should be self contained

Testing Approaches

  • Unit tests
  • Integration tests

Testing Setup

  • Run each test in a transaction
  • Roll back the transaction at the end of the test
  • Advantages:
    • Does not clutter the database with test results
    • Allows to run tests in parallel as the database isolates each run for us
    • Relatively low overhead

Testing Setup

  • Disadvantages:
    • Need to ensure that each test only ever uses the same connection
    • Sequences (Auto incrementing ID’s) change between test runs
    • Does not work with certain schema modifying operations

Testing Setup - Example Application

  • More complicated for our example application
  • Need to provide some setup helpers to make this work
  • Axum provides ways to directly interact with the router and call methods there
  • State can be cloned, so it’s easy to have it in the router and in the test

Testing Setup - HowTo

  1. Refactor code that you have a lib.rs and a main.rs file
  2. In lib.rs have a function that constructs your state and your router
  3. State setup: Have a setting that allows you to configure connection pool to
    1. Have a single connection (pool_size == 1)
    2. Call Connection::begin_test_transaction() on creating a new connection

Testing Setup - HowTo

#[tokio::test]
async fn my_test() {
    let (router, state) = race_timing::setup(test_config(true));
    let resp = router.oneshot(
            Request::get("/index.html")
            .body(Body::empty())
            .unwrap()
        ).await
        .unwrap();
        
    assert_eq!(res.status(), StatusCode::Ok);
    // possibly interact with the database via state here
    // to check that the database contains the relevant details
}

Exercise II - Testing

Exercise II - Testing

  • We have written the implementation different end points in the last exercise
  • We only manually verified that it looks correct
  • We want to have (integration) tests for this functionality
  • As extended goal: There is a login mechanism in admin/users.rs that also needs to be tested
  • Source code at: https://github.com/weiznich/RustWeek2025 (step-1 branch)

Exercise II - Testing

  • Your Solutions
  • (My Solution)
  • Your Questions/Problems

Diesel Internals

How Diesel Works

  • How does Diesel implement these compile time checks internally?
  • Trying to answer the following questions:
    • How does Diesel build SQL from the query DSL?
    • How does Diesel check queries at compile time?
    • How does Diesel compose queries from parts?

Fundamental Traits - Backend

trait Backend {
    type RawValue<'a>;
}
  • Central definition of a supported database backend
  • RawValue<'a> describes how values are represented at the protocol values
  • Implemented for the zero sized types
    • diesel::pg::Pg
    • diesel::sqlite::Sqlite
    • diesel::mysql::Mysql

Fundamental Traits - QueryFragment

pub trait QueryFragment<DB: Backend, SP = NotSpecialized> {
    fn walk_ast<'b>(&'b self, pass: AstPass<'_, 'b, DB>) 
        -> QueryResult<()>;
}
  • Trait indicating how to translate some type into SQL
  • Used to construct queries
  • Helps to perform most of the work at compile time

Fundamental Traits - QueryFragment

struct Eq<L, R>{
    left: L,
    right: R,
}
impl<L, R> QueryFragment<Pg> for Eq<L, R>
where
    L: QueryFragment<Pg>, R: QueryFragment<Pg>,
{
    fn walk_ast(&self, mut pass: AstPass<DB>) -> QueryResult<()> {
        self.left.walk_ast(pass.reborrow())?;
        pass.push_sql(" = ");
        self.right.walk_ast(pass.reborrow())
    }
}

Fundamental Traits - QueryId

trait QueryId {
     type QueryId: Any;
     const HAS_STATIC_QUERY_ID: bool;
     fn query_id() -> Option<TypeId> {/**/}
}

impl<L, R> QueryId for Eq<L, R>
where L: QueryId, R: QueryId
{
    type QueryId = Self;
    const HAS_STATIC_QUERY_ID: bool =
       L::HAS_STATIC_QUERY_ID && R::HAS_STATIC_QUERY_ID;
}
  • Used to optimize the prepared statement cache
  • Calculate TypeId of the composite type, use that as static prepared statement cache key

Fundamental Traits - Expression

pub trait Expression {
    type SqlType;
}

impl<L, R> Expression for Eq<L, R> {
    type SqlType = Bool;
}
  • A marker trait representing a typed SQL fragment
  • Used for type checking the final query

Expanded Example Query

users::table.filter(users::id.eq(42)).select(users::id)
// That's the internal private type
SelectStatement<
    FromClause<users::table>, 
    SelectClause<users::id>, 
    WhereClause<Eq<users::id, Bound<i32, Integer>>>, 
    // Skipped some more parameters for other clauses
>
use diesel::dsl::*;
// that's the public way to write these types
Select<
    Filter<users::table, Eq<users::id, i32>>,
    users::id
>
// or just annotate the function that returns a query/expression
// with `#[diesel::dsl::auto_type]` and use `_` as return type

Dynamic Queries

  • Everything shown so far requires a static query
  • Possible to make certain parts dynamic
  • These query parts always need to be known statically:
    • SELECT clauses
    • FROM clauses
    • GROUP BY clauses

Dynamic Queries - Boxed queries

  • Boxing the whole query allows to dynamically add most query clauses
let mut user_query = users::table.into_boxed();

if !user_is_admin {
    user_query = user_query.filter(users::name.eq(username));
}

Dynamic Queries - Boxed expressions

  • Box expressions to dynamically replace them
let expr = if user_is_admin {
    Box::new(users::name.like(format!("%{name}%")))
        as Box<dyn BoxableExpression<_, _, SqlType = _>>
} else {
    Box::new(users::id.eq(my_user_id)) 
};
let users = users::table.filter(expr).load::<User>(&mut conn)?;

Fundamental Traits - FromSql

pub trait FromSql<A, DB: Backend> {
    fn from_sql(bytes: DB::RawValue<'_>) -> Result<Self>;
}
  • Describes how to translate from the SQL side value representation to a Rust type
  • Generic type A is the SQL side type (i.e. TEXT)
  • Self is the Rust side type
  • Can be backend specific via DB
  • You need to derive FromSqlRow if you implement this trait

Fundamental Traits - ToSql

pub trait ToSql<A, DB: Backend>: Debug {
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) 
        -> Result;
}
  • Describes how to translate a Rust type into the SQL side valuue representation
  • Generic type A is the SQL side type
  • Self is the Rust side type
  • Can be backend specific via DB
  • You need to derive AsExpression if you implement this trait

Extending Diesel

Ways to extend Diesel

  • Most parts of Diesel can be extended:
    • Type mappings by implementing FromSql and ToSql
    • Custom Query DSL parts
    • Custom Connection types by implementing Connection
    • Custom Backends by implementing Backend + Connection

Custom Types

  • Need to handle with two different types
    • SQL side type, e.g. diesel::sql_types::Integer
    • Rust side type, e.g. i32
  • Need to implement FromSql + ToSql
  • Need to derive FromSqlRow and AsExpression

Custom Types - Example

#[derive(SqlType)]
#[diesel(postgres_type(name = "MyEnum"))]
#[diesel(sqlite_type(name = "Integer"))]
struct MyEnum;

#[derive(AsExpression, FromSqlRow)]
#[diesel(sql_type = MyEnum)]
#[diesel(sql_type = Integer)]
enum Test {
    VariantA,
    VariantB,
    VariantC,
}

Custom Types - Example

impl FromSql<MyEnum, Pg> for Test {
    fn from_sql(bytes: PgValue) -> Result<Self> {
        match bytes.as_bytes() {
            b"VariantA" => Ok(Self::VariantA),
            // …
        }
    }
impl FromSql<Integer, Sqlite> for Test {
    fn from_sql(bytes: SqliteValue) -> Result<Self> {
        let value = <
            i32 as FromSql<Integer, Sqlite>
        >::from_sql(bytes)?
        match value {
            1 => Ok(Self::VariantA)
            // …
        }
    }

Custom Types - Example

impl ToSql<MyEnum, Pg> for Test {
  fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) 
      -> Result {
    match self {
      Self::VariantA => out.write(b"VariantA")?,
      // …
    }
    Ok(IsNull::No)
  }
impl ToSql<Integer, Sqlite> for Test {
  fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) 
      -> Result {
    match self {
      Self::VariantA => out.set_value(1),
      // …
    };
    Ok(IsNull::No)
  }

Custom Query DSL

  • Several variants to extent Diesel’s DSL
    • via define_sql_function!() to setup a new “bindings” for a SQL function
    • via infix_operator! / prefix_operator! / postfix_operator! to setup bindings for a SQL operator
    • by implementing QueryFragment, Expression and QueryId for your type

Custom Query DSL - define_sql_function!()

define_sql_function! {
    fn lower(x: Text) -> Text;
}

/// use it like
diesel::select(lower("TEST")).get_result::<String>(&mut conn)?;

/// for sqlite
lower_utils::register_impl(connection, |x: String| {
    x.to_lowercase()
})?;
  • Allows to define “bindings” for SQL function
  • Arguments defined in terms of SQL types

Custom Query DSL - infix_operator!()

diesel::infix_operator!(Concat, " || ");

/// use it like 
diesel::select(
    Concat::new(
        "T".into_sql::<Text>(), 
        "EST".into_sql::<Text>()
    )
).get_result::<Text>(&mut conn)?;

Summary and Outlook

Summary and Outlook

  • Covered Topics:
    • Basic CRUD with Diesel
    • Diesel-Async and when it’s useful
    • Testing with Diesel
    • Diesel Internals
    • Diesel Extensions

Outlook

  • Existing, but unmentioned functionality
    • How to abstract over different database backends (see diesel::MultiConnection)
    • A lot of database specific functionality like upserts or COPY TO statements