Understanding Rust and Diesel: Handling nullable fields in models

While learning a new programming language, it can be easy to miss a little detail you'd never overlook in a familiar environment. The Rust's compiler is rather good at telling the user what they're doing wrong, but for a beginner it can be difficult to understand more advanced errors. I got one while going through the Diesel ORM Getting Started guide. Let's explore it!

 --> src/models.rs:7:13n  |
 7 |     pub id: i32,
   |             ^^^ the trait `FromSql<diesel::sql_types::Nullable<diesel::sql_types::Integer>, Sqlite>` is not implemented for `i32`
   |
     = help: the trait `FromSql<diesel::sql_types::Integer, Sqlite>` is implemented for `i32`
     = note: required for `i32` to implement `diesel::Queryable<diesel::sql_types::Nullable<diesel::sql_types::Integer>, Sqlite>`
     = note: required for `i32` to implement `FromSqlRow<diesel::sql_types::Nullable<diesel::sql_types::Integer>, Sqlite>`
     = help: see issue #48214

My model:

use diesel::prelude::*;

#[derive(Queryable, Selectable)]
#[diesel(table_name = crate::schema::posts)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Post {
    pub id: i32,
    pub title: String,
    pub body: String,
    pub published: bool,
}

My table:

CREATE TABLE posts (
    id INTEGER PRIMARY KEY AUTOINCREMENT, -- NOT NULL is missing
    title TEXT(64) NOT NULL,
    body TEXT NOT NULL,
    published BOOLEAN NOT NULL DEFAULT 0
)

Naturally, since the id field is not marked as NOT NULL, the value is optional. However, the struct states that the expected type for the id field is i32. Professionally I've been working with Java, where null values are part of the game. That's why I didn't even think about this discrepancy. In Rust, on the other hand, there are no null values.

Therefore, I should either make this field optional in the struct:

use diesel::prelude::*;

#[derive(Queryable, Selectable)]
#[diesel(table_name = crate::schema::posts)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Post {
    pub id: Option<i32>, // allow null values
    pub title: String,
    pub body: String,
    pub published: bool,
}

Or modify the table:

CREATE TABLE posts (
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, -- value must be specified
    title TEXT(64) NOT NULL,
    body TEXT NOT NULL,
    published BOOLEAN NOT NULL DEFAULT 0
)

If you change the migration script, don't forget to recreate the schema with diesel migration redo. Never modify the schema.rs file manually.

When the issue is localized and solved, it's easier to understand the error:

 --> src/models.rs:7:13
  |
7 |     pub id: i32,
  |             ^^^ the trait `FromSql<diesel::sql_types::Nullable<diesel::sql_types::Integer>, Sqlite>` is not implemented for `i32`
  |
    = help: the trait `FromSql<diesel::sql_types::Integer, Sqlite>` is implemented for `i32`

The schema.rs file describes the table based on the actual sql request in the migrations/ directory. In my case, the id field wasn't marked as NOT NULL, so the schema specified it as nullable.

diesel::table! {
    posts (id) {
        id -> Nullable<Integer>,
        title -> Text,
        body -> Text,
        published -> Bool,
    }
}

The compiler detected the mistmatch between the schema and the struct:

 --> src/models.rs:7:13n  |
7 |     pub id: i32,
  |             ^^^ the trait `FromSql<diesel::sql_types::Nullable<diesel::sql_types::Integer>, Sqlite>` is not implemented for `i32`

And even suggested that the i32 type has the following trait implemented:

  = help: the trait `FromSql<diesel::sql_types::Integer, Sqlite>` is implemented for `i32`

Instead of FromSql<diesel::sql_types::Nullable<diesel::sql_types::Integer>, Sqlite>.

Now I know what this means. Learning new stuff is fun! I can also recommend these two courses:

To my taste, the crash course was a bit shallow, because core topics like the errors handling are part of the intermediate course. But together these courses helped me in gaining confidence, and I enjoyed Nathan's style. The official book is deeper, of course, since it's a book. At the same time, it can be hard to consume and store all details in your memory. However, after taking these courses I can use the book to expand my knowledge.