Diesel Workshop

Introduction

Comments

  • This workshop was presented during EuroRust 2024
  • 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 geologic subsurface models

Content of the workshop

  • Diesel Basics
  • Testing with Diesel
  • Extending Diesel

Timeline

Before Lunch: * Diesel Basics

After Lunch: * 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/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 wherever possible
  • Make type mapping flexible

Diesel Limitations

  • Limited built-in DSL
  • Strongly typed, restricts certain kinds of dynamic queries
  • Favours explicit query building over implicit model based approach

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 on Diesel projects
  • Allows:
    • To manage your database
    • To manage your migrations
    • To 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 in your Rust code
  • Generates a number of types
  • 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

Select Statements

  • Diesel provides a query DSL to construct statically known 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
    • get_result::<U>: Returns the first U ignores the rest
    • first::<U>: Returns the first U, 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 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 call with an AND expression
  • Filter conditions can be arbitrary expressions that evaluate to booleans
  • See various ExpressionMethods for 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 essentially literally to the underlying SQL

Expressions

users::table
   .filter(users::my_boolean_column) 
   .filter(users::id.eq(42)) 
   .filter(users::id.(users::age + users::age)) 
   .filter(42.into_sql::<Integer>().eq(users::id)) 
   .filter(users::id.eq(users::id + 5).eq(false)) 
   .filter(users::id.eq_any(
       posts::table
           .filter(posts::name.eq("My fancy post"))
           .select(posts::user_id)
    )) 

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 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)?;
  • Again: A struct can be used as input
  • Prefer this variant if the struct already exists
  • Automatic filter by id 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()?;

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

  • Diesel supports database transactions via Connection::transaction
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)
})?;

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
  • Stripped down version of a real world 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
    • Implement (parts of) an admin area to modify data (see the admin module)

Exercise I - Basic Diesel

  • Source code at: https://github.com/weiznich/eurorust-2024-diesel-workshop (step-0 branch)

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 a connection
  • Notably it provides drop in replacements for the RunQueryDsl and Connection

Using Diesel async

Normal Diesel

use diesel::prelude::*;

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

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

Async Diesel

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 systems 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 be require 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 a bit more of setup 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
    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 of several end points in the last exercise
  • We only manually verified that it looks correct
  • We should 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/eurorust-2024-diesel-workshop (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 and 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, due to how Diesel represents queries at compile time
  • 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 parts
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

  • Boxing certain 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 representation to a Rust type
  • Generic type A is the SQL side type
  • Self is the rust side type
  • Can be backend specific via DB
  • 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 representation
  • Generic type A is the SQL side type
  • Self is the rust side type
  • Can be backend specific via DB
  • 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"))]
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 => Ok(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 {
    let v = match self {
      Self::VariantA => 1,
    };
    out.set_value(v);
    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!()

infix_operator!(Concat, " || ");

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

Custom Query DSL - QueryFragment

  • Low level approach
  • Gives you the greatest amount of control
  • Used by the macros internally

Custom Query DSL - QueryFragment

// also derive `QueryId` and `ValidGrouping` for the Eq struct
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())?;
        Ok(())
    }
}

Custom Query DSL - QueryFragment

impl<L, R> Expression for Eq<L, R>
where
    L: Expression,
    R: Expression,
{
    type SqlType = Bool;
}

Exercise III - Diesel Extensions

Exercise III - Diesel Extensions

  • We want to switch the ID type from integer to UUID
  • Steps:
    1. Custom type mapping for uuid::Uuid
    2. SQL function for generating UUID values
    3. Change the schema.rs file
    4. Switch the ID type by changing the ID typedef in the database module
    5. We need to migrate existing data
  • We want to have unit tests for the first two steps

Exercise III - Diesel Extensions

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

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