Closed11

RustでSQLからコードを生成するcornucopiaについて

nazo6nazo6

cornucopiaとは

SQLからRustのコードを生成して安全にデータベース操作ができる。恐らくGoのsqlcと同じ感じなんだと思う。

というかcornucopiaって何よ

1
[the cornucopia] 【ギリシャ神話】 豊饒(ほうじよう)の角 《幼時の Zeus 神に授乳したと伝えられるやぎの角》.
2
可算名詞 豊饒の角の装飾 《角の中に花・果物・穀類を盛った形で,物の豊かな象徴》.
3
[a cornucopia] 豊富 〔of〕.
a cornucopia of good things to eat たくさんのおいしい食物.
4
可算名詞 円錐形の容器.

(weblio - https://ejje.weblio.jp/content/cornucopia)

resources

nazo6nazo6

準備

nazo6nazo6

PostgreSQLのサーバーを適当に建てる

docker-compose.yml
version: "3"
services:
  db:
    image: postgres:13.3
    environment:
      POSTGRES_USER: test
      POSTGRES_PASSWORD: password
    ports:
      - 5432:5432
    volumes:
      - postgres:/var/lib/postgresql
volumes:
  postgres:

nazo6nazo6

cornucopiaにはmigration機能などはついていない。
まず最初にデータベースのスキーマを作る必要がある。
ここではatlasを使って適当にスキーマを決める

schema.hcl
schema "public" {}
table "users" {
  schema = schema.public
  column "id" {
    null = false
    type = int
    identity {
        generated = ALWAYS
        start = 10
        increment = 10
    }
  }
  primary_key  {
    columns = [column.id]
  }
  column "name" {
    null = false
    type = text
  }
}

そして適用

atlas schema apply --url "postgresql://test:password@localhost:5432/test?sslmode=disable" --to "file://schema.hcl"
nazo6nazo6

使い方

ここから実際にcornucopiaを使う

nazo6nazo6

クエリの作成

Rustプロジェクトのルートにqueriesフォルダを作り、そこにSQLを置く
例:

queries/user.sql
--! insert_user
INSERT INTO users(name)
VALUES (:name);
nazo6nazo6

インストール

cliをインストール

cargo install cornucopia

生成

cornucopiaコマンドでRustのファイルを生成できる。ちなみにcornucopiaがpostgresのdockerコンテナを勝手に作るようにもできるみたい。

cornucopia live "postgresql://test:password@localhost:5432/test"
rustfmt --edition 2021 ./src/cornucopia.rs

このコマンドでは実際のデータベースに接続することで存在しないテーブルにアクセスしようとした際などにエラーを出してくれる。とても便利。

src/cornucopia.rsが生成される。先のSQLではこのようなファイルが得られる。

src/cornucopia.rs
// This file was generated with `cornucopia`. Do not modify.

#[allow(clippy::all, clippy::pedantic)]
#[allow(unused_variables)]
#[allow(unused_imports)]
#[allow(dead_code)]
pub mod types {}
#[allow(clippy::all, clippy::pedantic)]
#[allow(unused_variables)]
#[allow(unused_imports)]
#[allow(dead_code)]
pub mod queries {
    pub mod user {
        use cornucopia_async::GenericClient;
        use futures;
        use futures::{StreamExt, TryStreamExt};
        pub fn insert_user() -> InsertUserStmt {
            InsertUserStmt(cornucopia_async::private::Stmt::new(
                "INSERT INTO users(name)
VALUES ($1)",
            ))
        }
        pub struct InsertUserStmt(cornucopia_async::private::Stmt);
        impl InsertUserStmt {
            pub async fn bind<'a, C: GenericClient, T1: cornucopia_async::StringSql>(
                &'a mut self,
                client: &'a C,
                name: &'a T1,
            ) -> Result<u64, tokio_postgres::Error> {
                let stmt = self.0.prepare(client).await?;
                client.execute(stmt, &[name]).await
            }
        }
    }
}

とてもわかりやすい

nazo6nazo6

使用方法

この生成されたファイルの依存をいろいろ追加する。

cargo add tokio cornucopia_async futures tokio_postgres

なおデフォルトではtokio_postgresを用いた非同期コードが生成されるが同期コードも生成できるみたい。

あとは生成された関数を呼び出すだけ。

src/main.rs
use cornucopia::queries::user::insert_user;
use tokio_postgres::{Error, NoTls};

mod cornucopia;

#[tokio::main]
async fn main() -> Result<(), Error> {
    let (client, connection) = tokio_postgres::connect(
        "host=localhost port=5432 user=test password=password",
        NoTls,
    )
    .await?;

    tokio::spawn(async move {
        if let Err(e) = connection.await {
            eprintln!("connection error: {}", e);
        }
    });

    insert_user().bind(&client, &"me").await.unwrap();

    Ok(())
}

これで無事insertを行うことができた。

nazo6nazo6

良かったところ

今までRustでRDBを扱う方法を色々さがしてきて

  • ORM: まだあまり成熟してない感じだった(個人的にはprisma-client-rustが一番よかったと感じたが依存が重すぎなのと色々不安定だった)
  • sqlx: 確かにSQLを書けばマクロで型が補完されるのはすごいが開発中もデータベースの状態を気にしないといけないし何よりマクロは辛い

などの問題を感じていたがcornucopiaはデータベースに接続するのはコードを生成するときだけだし生成されたコードも普通のRustファイルで見やすいのがとても良い。

あとbind()以外にもparams()関数が用意されていてパラメータを構造体で作成できるのも嬉しい。

nazo6nazo6

改善されてほしいところ

  • まだ色々機能が足りてない感じがする
    • 例えばBatch insertionなど
      • イテレータを挿入できるようになれば結構便利になりそう
    • あとはこれとか
  • PostgreSQLにしか対応してない
    • まあこれは仕方ないのかなと思いつつもsqliteとかで使えたらとてもいいなと
このスクラップは2023/05/27にクローズされました