RustでWP_Queryを実装してみました

ハイサイ！オースティンやいびーん。

# 概要

WordPressのWP_QueryをRustで実装してみた体験を振り返ります。

主にRust学習のためにやってみたのですが、ちょっと面白かったので共有したくなりました。

## ソースコード

記事を読んで気になった方は、ソースコードを公開していますのでご参考までにみていただければと思います。

https://github.com/tronicboy1/wp_query_rs

## Crate

使いたかったら、Crateも公開しているので、どうぞ！

https://crates.io/crates/wp_query_rs

# 使い方

クレートをインストールして使おうとすると、以下のようなAPIになります。

```rust
use wp_query_rs::{ParamBuilder, WP_Query};

fn main() {
    let bldr = ParamBuilder::new()
        .author(1)
        .order(wp_query_rs::SqlOrder::Desc)
        .orderby(wp_query_rs::query::WpOrderBy::Date)
        .posts_per_page(5)
        .post_type("guide".to_string());
    let wp_q = WP_Query::new(bldr.params()).unwrap();

    let titles = wp_q
        .posts
        .iter()
        .map(|p| p.post_title.clone() + ", published: " + &p.post_date.to_string());

    for (i, title) in titles.enumerate() {
        println!("Title {}: {}", i + 1, title);
    }
}
```

上記のコードを自身のWP開発用データベースに対して実行すると以下のように出力されます。

```
Title 1: BroadCast test 4, published: 2023-08-09
Title 2: Casino de Español, published: 2023-08-01
Title 3: Casino un Vaso de Agua, published: 2023-07-27
Title 4: Casino Queso, published: 2023-07-27
Title 5: Casino Orange, published: 2023-07-25
```

基本的に`WP_Query`の使い方をそのままRustに持ってきたかったので、`WP_Query`に入っているほとんどの条件を上記に`ParamBuilder`で追加できるようになっています。

上記のような使い方をするには以下の環境変数を設定していないといけません。WordPressの公式Dockerイメージが指定するものと同じです。

```
WORDPRESS_DB_HOST=localhost
WORDPRESS_DB_USER=root
WORDPRESS_DB_PASSWORD=password
WORDPRESS_DB_NAME=wordpress
```

なお、自身のmysqlコネクションプールを用意している場合は、以下の関数でコネクションのインスタンスを渡せばできます。

```rust
let wp_q = WP_Query::with_connection(&mut conn, bldr.params());
```

# 実装で大変だったこと、面白かったこと

大変でありながら、それが故に解決した時の楽しさがある。それがまさに我々が愛するプログラミングの醍醐味ではありませんか？

## カラム数が多かった

Rustの[mysqlクレート](https://crates.io/crates/mysql)には、`query_map`を使うと自動的にSQLのバイナリーをRustの型に変換してくれるという非常にありがたい機能があります。

```rust
let selected_payments = conn
        .query_map(
            "SELECT customer_id, amount, account_name from payment",
            |(customer_id, amount, account_name)| {
                Payment { customer_id, amount, account_name }
            },
        )?;
```
 _引用：公式ドキュメント - https://docs.rs/mysql/latest/mysql/_ 

しかし、この機能は12項目までしか使えず、12項目以上だと手動て列（Row）を開梱していかないといけません。

WordPressの`wp_posts`テーブルの絡む数は十数項目ありますので、手動開梱になります。

```rust
pub fn unwrap_row(row: &mut Row) -> Result<WP_Post, FromValueError> {
    let id: u64 = row.take_opt(0).unwrap()?;
    let post_author: u64 = row.take_opt(1).unwrap()?;
    let comment_count: u64 = row.take_opt(2).unwrap()?;
    let post_parent: u64 = row.take_opt(3).unwrap()?;
...
    let post_content_filtered: String = row.take_opt(19).unwrap()?;
    let guid: String = row.take_opt(20).unwrap()?;
    let post_type: String = row.take_opt(21).unwrap()?;
    let post_mime_type: String = row.take_opt(22).unwrap()?;

    Ok(WP_Post {
        ID: id,
        post_author,
        post_date,
...
        menu_order,
        post_type,
        post_mime_type,
        comment_count,
    })
}
```

SQLクエリの値の順番を間違えなければうまくいきますが、この解決に至るまで苦労がありました。:joy:

## `mysql_common::ToValue`のTraitを使えば楽に値をバイナリーに変換できる

`mysql_common`というクレートにはMySQLのPrepared Statementクエリーで使えるように値を変換する`Value`というEnumがあります。これがとても興味深い作りです。

```rust
/// Client side representation of a value of MySql column.
///
/// The `Value` is also used as a parameter to a prepared statement.
#[derive(Clone, PartialEq, PartialOrd)]
pub enum Value {
    NULL,
    Bytes(Vec<u8>),
    Int(i64),
    UInt(u64),
    Float(f32),
    Double(f64),
    /// year, month, day, hour, minutes, seconds, micro seconds
    Date(u16, u8, u8, u8, u8, u8, u32),
    /// is negative, days, hours, minutes, seconds, micro seconds
    Time(bool, u32, u8, u8, u8, u32),
}
```
_引用: mysql_common - https://docs.rs/mysql_common/latest/mysql_common/_

ちなみに、文字列は`Bytes`になります。RustのStringの考え方と似ていて面白いと思いました。元を言えばUTF８の文字列とはそういうものなので、JavaScript開発の自分では文字列に十分に敬意を払っていなかったと反省しています。:joy:

上記のEnumの他に、`ToValue`というTraitがあり、これを満たせば、`Value`のバイナリーに変換してくれるのです！

```rust
pub struct DateQuery {
    pub year: Option<u16>,
    pub month: Option<u8>,
    pub day: Option<u8>,    //Day of the month (from 1 to 31).
    pub hour: Option<u8>,   // Hour (from 0 to 23).
    pub minute: Option<u8>, // Minute (from 0 to 59).
    pub second: Option<u8>,
    pub after: Option<DateQueryAfterBefore>,
    pub before: Option<DateQueryAfterBefore>,
    pub inclusive: bool,
    pub column: DateColumn,
    pub relation: SqlConditionOperator,
}

impl ToValue for DateQuery {
    fn to_value(&self) -> mysql_common::Value {
        mysql_common::Value::Date(
            self.year.unwrap_or(2023),
            self.month.unwrap_or(1),
            self.day.unwrap_or(1),
            self.hour.unwrap_or(0),
            self.minute.unwrap_or(0),
            self.second.unwrap_or(0),
            0u32,
        )
    }
}
```

上記のように、自分の場合は、`after`等はクエリに無関係（別のところで使う）ので、`ToValue`で時刻に関係する値だけをとっておくようにできました。

```rust
let date_query = DateQuery::new();
date_query.to_value() // mysqlが使えるValue型に変換されます！
```

とても面白かったのです。

# まとめ

WordPressというブログ巨人にRustのサビをもたらしてみましたが、改めてWordPressというのは体力の絶えない悪魔だなと思いました。

やっていることがそんなに複雑に思えないのですが、そのAPIを一つまともに実装しようとするとかなりの努力が必要なのです。WordPressに賛否の声があるかと思いますが、取って代わるものはすぐに現れないでしょう。

逆に、Rustで部分的に書き換えていけたら面白いのかもしれません。無理でしょうが。:joy:

Rustの勉強が非常に楽しくてこのちょっとしたプロジェクトが一段落したから、筆者はまた勉強に励みたいと思います！