[Rust] Dieselのfilterを条件ありなしで変えたい

ちょっとなんて表現すればいいか迷ってわかりにくいタイトルに、、、
rust-jpのSlackで質問してみたら、速攻回答頂いて解決したので、メモしておきます

# 環境

- rustc 1.30.0
- diesel 1.3.3

[yagince/rust-diesel-sample](https://github.com/yagince/rust-diesel-sample)

# 何に困ったのか？

以下のようなテーブルがあったとします。
(http://diesel.rs/guides/getting-started/ のサンプルから拝借しました)

```sql
CREATE TABLE posts (
  id SERIAL PRIMARY KEY,
  title VARCHAR NOT NULL,
  body TEXT NOT NULL,
  published BOOLEAN NOT NULL DEFAULT 'f'
)
```

このテーブルを検索する以下のような条件の関数を考えていまして

- titleを指定してもいいし、しなくてもいい
- bodyを指定してもいいし、しなくてもいい

つまり

```rust
fn search(
    conn: &PgConnection,
    title: Option<String>,
    body: Option<String>,
) -> Result<Vec<Post>, diesel::result::Error> {
    use diesel_sample::schema::posts;
    let query = posts::table.order(posts::id.desc());

    query = title
        .map(|x| query.filter(posts::title.eq(&x)))
        .unwrap_or(query);
    query = body
        .map(|x| query.filter(posts::body.eq(&x)))
        .unwrap_or(query);

    query.load::<Post>(conn)
}
```

こんな感じの事がやりたかったのですが
これではだめで

```
error[E0308]: mismatched types
  --> src/bin/show_posts.rs:29:13
   |
29 |       query = title
   |  _____________^
30 | |         .map(|x| query.filter(posts::title.eq(&x)))
31 | |         .unwrap_or(query);
   | |_________________________^ expected struct `diesel::query_builder::where_clause::NoWhereClause`, found struct `diesel::query_builder::where_clause::WhereClause`
   |
   = note: expected type `diesel::query_builder::SelectStatement<_, _, _, diesel::query_builder::where_clause::NoWhereClause, _>`
              found type `diesel::query_builder::SelectStatement<_, _, _, diesel::query_builder::where_clause::WhereClause<diesel::expression::operators::Eq<diesel_sample::schema::posts::title, diesel::expression::bound::Bound<diesel::sql_types::Text, &std::string::String>>>, _>`
```

filterの戻り値の型が元の型と違う（filterに渡した条件によって変わってしまう）のでこれではコンパイルエラーになります。

# どうするのがいいか質問してみたらすぐ解決

rust-jpのSlackに入っていたので、早速質問してみました。
30分ぐらいで回答を頂けて、解決！
こういうのは有識者に聞くのが速い

[diesel::query_dsl::QueryDsl - Rust](https://docs.diesel.rs/diesel/query_dsl/trait.QueryDsl.html#method.into_boxed)

`into_boxed`を使ってトレイトオブジェクトにするのが手っ取り早い、と。
なるほどー

さっそく書き直してみる

```rust
fn search(
    conn: &PgConnection,
    title: Option<String>,
    body: Option<String>,
) -> Result<Vec<Post>, diesel::result::Error> {
    use diesel_sample::schema::posts;
    let mut query = posts::table.order(posts::id.desc()).into_boxed();

    if let Some(x) = title {
        query = query.filter(posts::title.eq(x))
    }

    if let Some(x) = body {
        query = query.filter(posts::body.eq(x))
    }

    query.load::<Post>(conn)
}
```

できた！

# paginationする場合に件数も欲しい…

さて、paginationしたい場合、一緒に件数も返したい。
という想定でやってみることにしました

```rust
fn search(
    conn: &PgConnection,
    title: Option<String>,
    body: Option<String>,
) -> Result<(i64, Vec<Post>), diesel::result::Error> {
    use diesel_sample::schema::posts;
    let mut query = posts::table.order(posts::id.desc()).into_boxed();

    if let Some(x) = title {
        query = query.filter(posts::title.eq(x))
    }

    if let Some(x) = body {
        query = query.filter(posts::body.eq(x))
    }

    let posts = query.offset(0).limit(100).load::<Post>(conn)?;
    let total = query.select(count(posts::id)).first(conn)?;
    Ok((total, posts))
}
```

こんな感じのことをやりたいのです。
でも、やっぱりこれはだめで、、、

```
error[E0382]: use of moved value: `query`
  --> src/bin/show_posts.rs:39:17
   |
38 |     let posts = query.offset(0).limit(100).load::<Post>(conn)?;
   |                 ----- value moved here
39 |     let total = query.select(count(posts::id)).first(conn)?;
   |                 ^^^^^ value used here after move
   |
   = note: move occurs because `query` has type `diesel::query_builder::BoxedSelectStatement<'_, (diesel::sql_types::Integer, diesel::sql_types::Text, diesel::sql_types::Text, diesel::sql_types::Bool), diesel_sample::schema::posts::table, diesel::pg::Pg>`, which does not implement the `Copy` trait
```

queryがmoveされてしまって同じqueryは使えない。

んー、ちなみにinto_boxedしてない時は大丈夫でした。
into_boxedしたあとの型を見てみることに。
↑のエラーメッセージから見るに `diesel::query_builder::BoxedSelectStatement` になっている模様。
[diesel::query_builder - Rust](https://docs.diesel.rs/diesel/query_builder/index.html#structs)
ドキュメントには載ってない、、、

じゃあ、ソースコードを見てます
https://github.com/diesel-rs/diesel/blob/1.3.x/diesel/src/query_builder/select_statement/boxed.rs#L24
これっぽい

https://github.com/diesel-rs/diesel/blob/1.3.x/diesel/src/query_builder/select_statement/boxed.rs#L232
このあたりでしょうか
たしかに&selfではない、、、

どうすればいいか。
じゃあ、queryをビルドする部分を関数化して同じqueryをビルドしてみます。
って、思ったら、それっぽいのがドキュメントに書いてありました。
https://docs.diesel.rs/diesel/query_dsl/trait.QueryDsl.html#example-15
`users::BoxedQuery<'a, DB>` これ、何者でしょうか。。。
ソースコードから探してみます。
https://github.com/diesel-rs/diesel/blob/1.3.x/diesel/src/macros/mod.rs#L772
これっぽい。
なるほど、どうやら `table!` マクロで定義される型っぽい。

`DB` ってなんだろう？
https://github.com/diesel-rs/diesel/blob/1.3.x/diesel/src/query_builder/select_statement/boxed.rs#L24-L34
ここを見る限り、QueryFragmentが要求する型っぽい

https://github.com/diesel-rs/diesel/blob/1.3.x/diesel/src/pg/query_builder/query_fragment_impls.rs#L7-L12
このあたり見ると
`pg::Pg`を使っている。
QueryFragmentが定義されているのは
https://github.com/diesel-rs/diesel/blob/1.3.x/diesel/src/query_builder/mod.rs#L134
ここですね。
じゃあ、`diesel::pg::Pg`を使えば良さそうなのでやってみます

```rust
fn search(
    conn: &PgConnection,
    title: Option<String>,
    body: Option<String>,
) -> Result<(i64, Vec<Post>), diesel::result::Error> {
    let posts = query(&title, &body).offset(0).limit(100).load::<Post>(conn)?;
    let total = query(&title, &body).select(count(posts::id)).group_by(posts::id).first(conn)?;
    Ok((total, posts))
}

fn query<'a>(title: &'a Option<String>, body: &'a Option<String>) -> posts::BoxedQuery<'a, diesel::pg::Pg> {
    let mut query = posts::table.order(posts::id.desc()).into_boxed();

    if let Some(x) = title {
        query = query.filter(posts::title.eq(x))
    }

    if let Some(x) = body {
        query = query.filter(posts::body.eq(x))
    }

    query
}
```

こんな感じ
一応できました。

# joinしたテーブルだと大変だった…

検索条件にjoinしたテーブルのカラムを追加してみます。
usersテーブルを追加してみます。

```sql
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  name VARCHAR NOT NULL
);

ALTER TABLE posts ADD COLUMN user_id SERIAL;
ALTER TABLE posts ADD FOREIGN KEY (user_id) references users(id);
```

```schema.rs
table! {
    posts (id) {
        id -> Int4,
        title -> Varchar,
        body -> Text,
        published -> Bool,
        user_id -> Int4,
    }
}

table! {
    users (id) {
        id -> Int4,
        name -> Varchar,
    }
}

joinable!(posts -> users (user_id));

allow_tables_to_appear_in_same_query!(
    posts,
    users,
);
```

postsにusersをジョインして、userのnameも条件に入れてみます。

```rust
fn search(
    conn: &PgConnection,
    title: Option<String>,
    body: Option<String>,
    user_name: Option<String>,
) -> Result<(i64, Vec<(Post, User)>), diesel::result::Error> {
    let posts = query(&title, &body, &user_name).offset(0).limit(100).load::<(Post, User)>(conn)?;
    let total = query(&title, &body, &user_name).select(count(posts::id)).group_by(posts::id).first(conn)?;
    Ok((total, posts))
}

fn query<'a>(title: &'a Option<String>, body: &'a Option<String>, user_name: &'a Option<String>) -> posts::BoxedQuery<'a, diesel::pg::Pg> {
    let mut query = posts::table.inner_join(users::table).order(posts::id.desc()).into_boxed();

    if let Some(x) = title {
        query = query.filter(posts::title.eq(x))
    }

    if let Some(x) = body {
        query = query.filter(posts::body.eq(x))
    }

    if let Some(x) = user_name {
        query = query.filter(users::name.eq(x))
    }

    query
}
```

こんな感じ。
でも、これもコンパイルエラーになります。

```
error[E0308]: mismatched types
  --> src/bin/show_posts.rs:49:5
   |
49 |     query
   |     ^^^^^ expected a tuple with 5 elements, found one with 2 elements
   |
   = note: expected type `diesel::query_builder::BoxedSelectStatement<'a, (diesel::sql_types::Integer, diesel::sql_types::Text, diesel::sql_types::Text, diesel::sql_types::Bool, diesel::sql_types::Integer), diesel_sample::schema::posts::table, diesel::pg::Pg>`
              found type `diesel::query_builder::BoxedSelectStatement<'_, ((diesel::sql_types::Integer, diesel::sql_types::Text, diesel::sql_types::Text, diesel::sql_types::Bool, diesel::sql_types::Integer), (diesel::sql_types::Integer, diesel::sql_types::Text)), diesel::query_source::joins::JoinOn<diesel::query_source::joins::Join<diesel_sample::schema::posts::table, diesel_sample::schema::users::table, diesel::query_source::joins::Inner>, diesel::expression::operators::Eq<diesel::expression::nullable::Nullable<diesel_sample::schema::posts::user_id>, diesel::expression::nullable::Nullable<diesel_sample::schema::users::id>>>, _>`
```

当然ですが、joinしたことによって、postsテーブルのデータが結果ではなくなってしまったのでした。
しかし、found typeを見ると超長い、、、
`posts::BoxedQuery` みたいに便利なエイリアスがどこかに無いだろうか、、、
joinable!あたりで作ってないのか？
どうやら作ってなさそうなので、愚直に書いてみます

```rust
fn query<'a>(
    title: &'a Option<String>,
    body: &'a Option<String>,
    user_name: &'a Option<String>,
) -> diesel::query_builder::BoxedSelectStatement<
    'a,
    (posts::SqlType, users::SqlType),
    diesel::query_source::joins::JoinOn<
        diesel::query_source::joins::Join<
            posts::table,
            users::table,
            diesel::query_source::joins::Inner,
        >,
        diesel::expression::operators::Eq<
            diesel::expression::nullable::Nullable<posts::user_id>,
            diesel::expression::nullable::Nullable<users::id>,
        >,
    >,
    diesel::pg::Pg,
> {
    let mut query = posts::table
        .inner_join(users::table)
        .order(posts::id.desc())
        .into_boxed();

    if let Some(x) = title {
        query = query.filter(posts::title.eq(x))
    }

    if let Some(x) = body {
        query = query.filter(posts::body.eq(x))
    }

    if let Some(x) = user_name {
        query = query.filter(users::name.eq(x))
    }

    query
}
```

パッケージ名はuseすれば省略できますが、、、
[SqlType](https://github.com/diesel-rs/diesel/blob/1.3.x/diesel/src/macros/mod.rs#L769) というエイリアスを見つけたので、そこだけは楽できました。

**こうするしかないのでしょうか？** ※未解決


# まとめ

- filterするかしないかを条件によって切り替えるような場合は `into_boxed` でトレイトオブジェクトにするといい
- 同じqueryを他でも使いたい場合（カウント取るとか）は同じオブジェクトを使えないので、もう一度同じqueryをビルドする？
  - PostgresではWindow関数を使って一回で取るのが普通なのかな？
      - https://github.com/diesel-rs/diesel/blob/1.3.x/examples/postgres/advanced-blog-cli/src/pagination.rs#L58-L66
  - MySQLの場合、8.0以上じゃないとWindow関数使えないかな？
      - [MySQL王国に黒船（Window関数）がやってきた！ - Qiita](https://qiita.com/kakuka4430/items/8c66e743800fcb8bc040)
  - 実は普段MySQLしか使わない、、、
- joinした場合が非常に面倒。。。
  - 良い方法ないだろうか