TQL (compile-time Rust ORM) を使ってみたメモ

RustなORMとして有名なのは[Diesel](http://diesel.rs/)ですが、もう少し手軽に使えるものがないかなと思っていました。

生のクエリは書きたくないけど、手軽には使い始めたい。
そんなあなたに[tql](https://github.com/antoyo/tql)です。

READMEに記載されている例になりますが、以下のような形でデータベースにアクセスできます。

```Rust
fn main() {
    let _ = sql!(Model.create());

    let text = String::new();

    let id = sql!(Model.insert(text = &text, date_added = Utc::now())).unwrap();

    let result = sql!(Model.get(id).update(text = "new-text"));

    let result = sql!(Model.get(id).delete());

    let items = sql!(Model.sort(-date_added)[..10]);
}
```

なかなか良さげです。

ただし現時点ではPostgreSQLしかサポートしていないので、SQLiteやMySQLでは使えません。
[SQLiteサポートについてはGitHub Issueにあがっている](https://github.com/antoyo/tql/issues/14)ので、近いうちに使えるようになるのではないかと淡い期待を寄せています。今の所は、SQLiteであれば[rusqlite](https://github.com/jgallagher/rusqlite)、MySQLであれば[rust-mysql-simple](https://github.com/blackbeam/rust-mysql-simple)あたりをつかうのが良い気がします。


## TQLを使ってみる

さっそく使ってみます。
特に記載がなければnightlyチャネル前提です。

まずは`createdb`コマンドでデータベース作っておきます。
tql_exampleという名前にします。（特に制約はありません）

```shell-session
$ createdb tql_example
```

```shell-session
$ psql -l
                                        List of databases
      Name       |   Owner   | Encoding |   Collate   |    Ctype    |      Access privileges
-----------------+-----------+----------+-------------+-------------+-----------------------------
 tql_example     | hogera    | UTF8     | ja_JP.UTF-8 | ja_JP.UTF-8 |
```

ここからRustでアクセスしていきます。

TQLは[crates.ioに登録されているバージョンが古い](https://crates.io/crates/tql)ので、GitHubリポジトリの最新版を使います。

```TOML:Cargo.toml
[dependencies]
chrono = "^0.4.0"
tql = { git = "https://github.com/antoyo/tql" }
tql_macros = { git = "https://github.com/antoyo/tql" }

[dependencies.postgres]
features = ["with-chrono"]
version = "^0.15.1"
```

[chrono](https://docs.rs/chrono/0.4.0/chrono/)は時刻データ型をいい感じに扱えるので、一緒に`Cargo.toml`に追加しておくと良いです。

あとは必要なクレイトを読み込んでおきます。

```Rust:src/main.rs
#![feature(proc_macro)]

extern crate chrono;
extern crate postgres;
extern crate tql;
#[macro_use]
extern crate tql_macros;

use postgres::{Connection, TlsMode};
use tql::PrimaryKey;
use tql_macros::{sql, to_sql};
use chrono::DateTime;
use chrono::offset::Utc;
```

## スキーマ定義とテーブル作成

以下のようなPersonテーブルを定義することにします。

| カラム名 | 型        | その他         |
|----------|-----------|----------------|
| id       | integer   | プライマリキー |
| name     | varchar   | not null       |
| age      | integer   | nullable       |
| created  | timestamp | not null       |

Personテーブルにアクセスするために、`Person`構造体をつくります。

```Rust:src/main.rs
#[derive(SqlTable)]
struct Person {
    id: PrimaryKey,
    name: String,
    age: Option<i32>,
    created: DateTime<Utc>,
}
```

データベース接続する関数。

```Rust:src/main.rs
fn get_connection() -> Connection {
    Connection::connect("postgres://user:password@hostname/tql_example", TlsMode::None).unwrap()
}
```

テーブルを作ってみます。
ここはイマイチなところなのですが、`get_connection()`で取得するデータベースコネクションオブジェクトは、今の所必ず`connection`変数にする必要があります。
クエリ実行する`sql!()`マクロに接続オブジェクトを渡すことができず固定で処理しているようです。
`TODO`になってるのでそのうち変わりそうな気がします。

```Rust:src/main.rs
fn main() {
    let connection = get_connection();

    let ret = sql!(Person.create());
    println!("ret={:?}", ret);
}
```

実行

```shell-session
$ cargo +nightly run --release
ret=Ok(0)
```

```shell-session
$ psql tql_example -c "\d person"
                                  Table "public.person"
 Column  |           Type           |                      Modifiers
---------+--------------------------+-----------------------------------------------------
 age     | integer                  |
 created | timestamp with time zone | not null
 id      | integer                  | not null default nextval('person_id_seq'::regclass)
 name    | character varying        | not null
Indexes:
    "person_pkey" PRIMARY KEY, btree (id)
```

`person`テーブルが作成できました。

### INSERT

```Rust:src/main.rs
fn main() {
    let connection = get_connection();
    for (name, age) in vec!(("たろう", Some(20)), ("はなこ", Some(30)), ("さとし", None),
                            ("せつこ", Some(50)), ("むが", Some(60))) {
        let name = String::from(name);
        let id = match age {
            Some(v) => sql!(Person.insert(name=&name, age=v, created=Utc::now())).expect("fail insert(Person)"),
            None =>  sql!(Person.insert(name=&name, created=Utc::now())).expect("fail insert(Person)"),
        };
        println!("id={:?}", id);
    }
}
```

実行してみます。

```shell-session
$ cargo +nightly run --release
id=1
id=2
id=3
id=4
id=5
```

データ挿入した時のidを返します。`psql`コマンドでデータ挿入されたかどうか確認します。

```shell-session
$ psql tql_example -c "select * from person"
 age |            created            | id |  name
-----+-------------------------------+----+--------
  20 | 2017-12-21 19:35:32.29468+09  |  1 | たろう
  30 | 2017-12-21 19:35:32.297931+09 |  2 | はなこ
     | 2017-12-21 19:35:32.299607+09 |  3 | さとし
  50 | 2017-12-21 19:35:32.301274+09 |  4 | せつこ
  60 | 2017-12-21 19:35:32.302847+09 |  5 | むが
```

おっけーそうです。


### SELECT

おもむろにデータ取得してみます。

```Rust:src/main.rs
fn main() {
    let connection = get_connection();
    for row in sql!(Person[2..5]) {
        println!("row={:?}", row);
    }
```

```shell-session
$ cargo +nightly run --release
row=Person { id: 3, name: "さとし", age: None, created: 2017-12-21T10:35:32.299607Z }
row=Person { id: 4, name: "せつこ", age: Some(50), created: 2017-12-21T10:35:32.301274Z }
row=Person { id: 5, name: "むが", age: Some(60), created: 2017-12-21T10:35:32.302847Z }
```

どんなクエリ発行してるか見たい場合は、`to_sql!()`マクロで構築されたSQLクエリを返してくれるので、それを出力することで確認することができます。

```Rust:src/main.rs
fn main() {
    let connection = get_connection();
    let query = to_sql!(Person[2..5]);
    println!("query='{}'", query);
}
```

```shell-session
$ cargo +nightly run --release
query='SELECT Person.age, Person.created, Person.id, Person.name FROM Person OFFSET 2 LIMIT 3'
```

## その他`sql!()`マクロで使える構文
[このあたり](https://github.com/antoyo/tql#syntax-table)に一通りまとまっています。


## stableチャネルで使う

どうしてもstableチャネルで使いたい場合は、以下の変更で使うことができます。

`extern crate tql`に`#[macro_use]`を追加。

```Rust:src/main.rs
#[macro_use]
extern crate tql;
```

以下の行を削除。

```Rust:src/main.rs
#![feature(proc_macro)]

use tql_macros::sql;
```

`Cargo.toml`の依存関係の`tqlの箇所に`default-features=false`を追加

```TOML:Cargo.toml
tql = { git = "https://github.com/antoyo/tql", default-features = false }
```

stableとnightlyでエラーメッセージが以下のように異なります。

```text:stable
error: mismatched types:
 expected `Option<i32>`,
    found `floating-point variable`
note: in this expansion of sql! (defined in tql)
  --> src/main.rs:32:14
   |
32 |     let id = sql!(Person.insert(name=&name, age=30.1, created=Utc::now())).expect("fail insert(Person)");
   |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: this error originates in a macro outside of the current crate
```

```text:nightly
error: mismatched types:
 expected `Option<i32>`,
    found `floating-point variable`
  --> src/main.rs:35:49
   |
35 |     let id = sql!(Person.insert(name=&name, age=30.1, created=Utc::now())).expect("fail insert(Person)");
   |                                                 ^^^^
   |
   = note: in this expansion of sql! (defined in tql)
```

nightlyチャネルの場合、ピンポイントで誤りのある箇所を指摘してくれます。よいですね。

## おわりに

わりとREADMEなぞっただけの内容になってしまいましたが、できることは一通り紹介してみました。
まだまだTODO等残っておりAPI変わりそうな感じもしますが、一度試してみてもよいのではないでしょうか。