🐈

[Rust] Diesel使用してMySQLでUnsigned扱おうとしたらエラー続出

2022/12/24に公開

バージョン情報

Diesel: v2.0.2

% rustc --version
rustc 1.61.0 (fe5b13d68 2022-05-18)

DieselでMySQLを使用しているがユーザーID(Primary Key)を現実世界に合わせるため、Rustのi32 -> u32に切り替えたらエラーに遭遇した。

とりあえず、詰まったので記録。(というかこれ書き始めているが、まだ解決していない笑。イケそうなので記録し始めた。)

結論:多分、Dieselをv2.0.2にしたあと、タイプ変換の対応入れて、cargo buildを入れるとすんなり解決しそうな気がする。(が、私の場合そこに行き着くまでに色々やりすぎてわからなくなってしまったので真実はわからない。)

問題・エラー編

まず、コンパイルエラーが発生していたschema(before)などを公開。

今回のschema

table! {
    users (id) {
        id -> Integer,
        first_name -> Varchar,
        last_name -> Nullable<Varchar>,
        email -> Varchar,
    }
}

今回のモデル

use diesel::{Insertable, Queryable, QueryableByName};
use rust_api::schema::users;

#[derive(Queryable, Debug)]
#[diesel(table_name = users)]
pub struct User {
    pub id: u32, // ここを変換 i32 -> u32
    pub first_name: String,
    pub last_name: Option<String>,
    pub email: String,
}

// 以下、略

エラーが発生しているのはMySQLからSELECT文でデータを取得する時。

let user_vec: Result<Vec<User>, Error> = sql_query(
            "
            SELECT
                id,
                first_name,
                last_name,
                email
            FROM
                users
            WHERE
                id = ?
            ",
        )
        .bind::<Integer, _>(input.id)
        .load::<User>(&mut connection); // loadでエラー発生!!

最初のコンパイルエラーがこちら。

error[E0277]: the trait bound `diesel::query_builder::sql_query::UncheckedBind<diesel::query_builder::SqlQuery, u32, diesel::sql_types::Integer>: diesel::query_dsl::LoadQuery<_, infrastructures::models::user::User>` is not satisfied
    --> src/use_cases/users/find_user.rs:55:10
     |
55   |         .load::<User>(&mut connection);
     |          ^^^^ the trait `diesel::query_dsl::LoadQuery<_, infrastructures::models::user::User>` is not implemented for `diesel::query_builder::sql_query::UncheckedBind<diesel::query_builder::SqlQuery, u32, diesel::sql_types::Integer>`
     |
     = help: the following implementations were found:
               <diesel::query_builder::sql_query::UncheckedBind<Query, Value, ST> as diesel::query_dsl::LoadQuery<Conn, T>>
note: required by a bound in `diesel::RunQueryDsl::load`
    --> /Users/takahiro/.cargo/registry/src/github.com-1ecc6299db9ec823/diesel-1.4.8/src/query_dsl/mod.rs:1238:15
     |
1238 |         Self: LoadQuery<Conn, U>,
     |               ^^^^^^^^^^^^^^^^^^ required by this bound in `diesel::RunQueryDsl::load`

Rustのことまだわかっていないので正確な説明にならないかもしれないですが、エラーの内容はtraitが実装されていないよ的なやつ。Dieselのパッケージ追っていくもどう解決するのかはよくわからず、諦めかけていた。

全く解決できず困り果てていたところ、使用しているDieselのバージョン(v1.4.4)が古いことに気づき、バージョンを上記(v2.0.2)に更新したら別のコンパイルエラーになった。

それがこちら。

error[E0277]: the trait bound `u32: diesel::serialize::ToSql<diesel::sql_types::Integer, diesel::mysql::Mysql>` is not satisfied
    --> src/use_cases/users/find_user.rs:55:10
     |
55   |         .load::<User>(&mut connection);
     |          ^^^^ the trait `diesel::serialize::ToSql<diesel::sql_types::Integer, diesel::mysql::Mysql>` is not implemented for `u32`
     |
     = help: the following implementations were found:
               <u32 as diesel::serialize::ToSql<diesel::sql_types::Nullable<diesel::sql_types::Unsigned<diesel::sql_types::Integer>>, __DB>>
               <u32 as diesel::serialize::ToSql<diesel::sql_types::Unsigned<diesel::sql_types::Integer>, diesel::mysql::Mysql>>
               <f32 as diesel::serialize::ToSql<diesel::sql_types::Float, diesel::mysql::Mysql>>
               <f32 as diesel::serialize::ToSql<diesel::sql_types::Nullable<diesel::sql_types::Float>, __DB>>
             and 16 others
     = note: required because of the requirements on the impl of `diesel::query_builder::QueryFragment<diesel::mysql::Mysql>` for `diesel::query_builder::sql_query::UncheckedBind<diesel::query_builder::SqlQuery, u32, diesel::sql_types::Integer>`
     = note: required because of the requirements on the impl of `diesel::query_dsl::LoadQuery<'_, _, infrastructures::models::user::User>` for `diesel::query_builder::sql_query::UncheckedBind<diesel::query_builder::SqlQuery, u32, diesel::sql_types::Integer>`
note: required by a bound in `diesel::RunQueryDsl::load`
    --> /Users/takahiro/.cargo/registry/src/github.com-1ecc6299db9ec823/diesel-2.0.2/src/query_dsl/mod.rs:1499:15
     |
1499 |         Self: LoadQuery<'query, Conn, U>,
     |               ^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `diesel::RunQueryDsl::load`

ようやく意味のわかるエラーになったので解決できそう?

さて、まず、INTに対するdieselのsql_typesは以下のように定義されていました。

/// The integer SQL type.
///
/// ### [`ToSql`](crate::serialize::ToSql) impls
///
/// - [`i32`][i32]
///
/// ### [`FromSql`](crate::deserialize::FromSql) impls
///
/// - [`i32`][i32]
///
/// [i32]: https://doc.rust-lang.org/nightly/std/primitive.i32.html
#[derive(Debug, Clone, Copy, Default, QueryId, SqlType)]
#[diesel(postgres_type(oid = 23, array_oid = 1007))]
#[diesel(sqlite_type(name = "Integer"))]
#[diesel(mysql_type(name = "Long"))]
pub struct Integer;
#[doc(hidden)]
pub type Int4 = Integer;

u32についての記述はIntegerには一切ありません。

#[diesel(mysql_type(name = "Long"))]

ちょっとこのマクロのLongが何言っているかよくわからない...

探してみると、mysql/backend.rsに存在していました。32ビットsignedですね。unsignedもありました。UnsignedLongです。

#[allow(missing_debug_implementations)]
/// Represents possible types, that can be transmitted as via the
/// Mysql wire protocol
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
#[non_exhaustive]
pub enum MysqlType {
    /// A 8 bit signed integer
    Tiny,
    /// A 8 bit unsigned integer
    UnsignedTiny,
    /// A 16 bit signed integer
    Short,
    /// A 16 bit unsigned integer
    UnsignedShort,
    /// A 32 bit signed integer
    Long,
    /// A 32 bit unsigned integer
    UnsignedLong,
    /// A 64 bit signed integer
    LongLong,
    /// A 64 bit unsigned integer
    UnsignedLongLong,
    /// A 32 bit floating point number
    以下略
}

つまり、DieselでMysSQL使って、32ビットunsignedは使える可能性が見えてきました!
そこで初心に帰って先ほどのエラーを見直してみると、

u32 as diesel::serialize::ToSql<diesel::sql_types::Nullable<diesel::sql_types::Unsigned<diesel::sql_types::Integer>>, __DB>

u32はUnsigned<diesel::sql_types::Integer>みたいにキャストされているっぽい?

ここでようやくUnsignedというStructが存在することに気づき(遅い)、Githubで検索。

Github PRでのMySQL Unsignedについてはこの辺で扱ってあります。ここを読むと解決しそうです。
Add Support for Unsigned Types in Mysql

存在した!

mysql/types/mod.rs

/// Represents the MySQL unsigned type.
#[derive(Debug, Clone, Copy, Default, SqlType, QueryId)]
#[cfg(feature = "mysql_backend")]
pub struct Unsigned<ST: 'static>(ST);

解決編

そもそもschemaのIntergerがRustのi32にしか対応していないのでダメみたいなんですね。今回はユーザーIDということでUnsignedで扱いたいため見直しを図ります。

schemaの定義をUnsignedにしたいので変更する。

migrationのup.sql, down.sqlを追加。

schema(after)

table! {
    users (id) {
        id -> Unsigned<Integer>, // 修正
        first_name -> Varchar,
        last_name -> Nullable<Varchar>,
        email -> Varchar,
    }
}

加えて、QueryBuilderのところも修正

let user_vec = sql_query(
            "
            SELECT
                id,
                first_name,
                last_name,
                email
            FROM
                users
            WHERE
                id = ?
            ",
        )
        .bind::<Unsigned<Integer>, _>(input.id)
        .load::<User>(&mut connection); // loadでエラー発生!!

うまくいくかと思いきや、、、
Unsigned<Integer>のu32変換がうまくいかない。(※おそらくここでcargo build挟めばよかった説、がある。私はそのまま彷徨った。)

error[E0277]: the trait bound `diesel::sql_types::Untyped: diesel::query_dsl::load_dsl::private::CompatibleType<infrastructures::models::user::User, diesel::mysql::Mysql>` is not satisfied
    --> src/use_cases/users/find_user.rs:55:10
     |
55   |         .load::<User>(&mut connection);
     |          ^^^^ the trait `diesel::query_dsl::load_dsl::private::CompatibleType<infrastructures::models::user::User, diesel::mysql::Mysql>` is not implemented for `diesel::sql_types::Untyped`
     |
     = help: the following implementations were found:
               <diesel::sql_types::Untyped as diesel::query_dsl::load_dsl::private::CompatibleType<U, DB>>
     = note: required because of the requirements on the impl of `diesel::query_dsl::LoadQuery<'_, _, infrastructures::models::user::User>` for `diesel::query_builder::sql_query::UncheckedBind<diesel::query_builder::SqlQuery, u32, diesel::sql_types::Unsigned<diesel::sql_types::Integer>>`
note: required by a bound in `diesel::RunQueryDsl::load`
    --> /Users/takahiro/.cargo/registry/src/github.com-1ecc6299db9ec823/diesel-2.0.2/src/query_dsl/mod.rs:1499:15
     |
1499 |         Self: LoadQuery<'query, Conn, U>,
     |               ^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `diesel::RunQueryDsl::load`

Rustを理解していなさすぎてさっぱりわからないorz...
とにかく、(Diesel内での)トレイト境界の実装が不十分っぽいのでUnsigned<Integer>u32をむすびつけるのって不可能なんじゃ...と思い始める。(Integerとi32はいけてそう。)

この後も実はいろいろと工夫したが何をやっても無理だった。

しかし、そんな中、途中でcargo buildを挟んだら上の実装で一瞬にしてエラーが消えた。
cargo build何やっているのか全く理解していないことがわかったので、次回の宿題にしたい。なぜコンパイルに影響しているのか。(ほんとは凡ミスとかしているんだろうけど、もう追えなくなってしまった。)

ちなみに、ORMapperっぽくなら、以下のようにやってもOK。(今回はIDなので、findでget_resultsは実はおかしいですが動作確認のためなのでご了承ください。)

use xxx::schema::users;

略

let user_vec = users.find(input.id).get_results::<User>(&mut connection);

[おまけ]いろいろと工夫したけど無理だった時にやっていたこと(これは結局意味なかったと思う。)

そもそもstruct SqlQueryに以下の注意文が書いてあったのを思い出す。

/// This function should be used with care, as Diesel cannot validate that
/// the value is of the right type nor can it validate that you have passed
/// the correct number of parameters.

あー、これかな。

なので、query builder使っていると、パラメータや返り値の型の保証はできない(それはそうだ)のでそこまでコンパイル時も面倒見ていないよ、ということか?

今のパッケージではこれが解決できなさそうなので、sql_queryの実装を変更。
loadからUserを除いて、取得後にvalidateやoutputデータ作成を個別でやればいいってことかな。

let user_vec = sql_query(
            "
            SELECT
                id,
                first_name,
                last_name,
                email
            FROM
                users
            WHERE
                id = ?
            ",
        )
        .bind::<Unsigned<Integer>, _>(input.id)
        .load(&mut connection); // 返り値の型(User)を消す。

無事にコンパイルエラーが解消。

と思っていたが、次は、user_vecを以降で扱おうとするとtype annotationでコンパイルエラーが発生する。つまり、先ほどの返り値の型(User)を消すのは解決できない。(レスポンス結果の型推論がResult<Vec<{unkown}>, Error>になっちゃったのがよろしくないのはわかるけど、こっちの方が上の注意書きに忠実に沿っている気はする。)

余談

Rust理解していないよね、が浮き彫りになりました。それはそれでよかった。

RustではDB関連のcrateとかはまだまだこれからですね。成長に期待します!

※use_casesにDBアクセスの実装が入っているのは無視してください。まだ動作確認レベルでレイヤーは無視してやっています。

Discussion