nazo6 notememo

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

作成:2023/05/26

更新:2023/05/26

cornucopiaとは (2023-05-26)

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

というかcornucopiaって何よ

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

resources

準備 (2023-05-26)

(2023-05-26)

Rustのプロジェクトを作成
cargo new --bin cornucopia-example

(2023-05-26)

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:

(2023-05-26)

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"

使い方 (2023-05-26)

ここから実際にcornucopiaを使う

クエリの作成 (2023-05-26)

Rustプロジェクトのルートにqueriesフォルダを作り、そこにSQLを置く
例:
queries/user.sql
--! insert_user
INSERT INTO users(name)
VALUES (:name);

インストール (2023-05-26)

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
}
}
}
}
とてもわかりやすい

使用方法 (2023-05-26)

この生成されたファイルの依存をいろいろ追加する。
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を行うことができた。
https://storage.googleapis.com/zenn-user-upload/b5e0309001e1-20230526.png

良かったところ (2023-05-26)

今までRustでRDBを扱う方法を色々さがしてきて
  • ORM: まだあまり成熟してない感じだった(個人的にはprisma-client-rustが一番よかったと感じたが依存が重すぎなのと色々不安定だった)
  • sqlx: 確かにSQLを書けばマクロで型が補完されるのはすごいが開発中もデータベースの状態を気にしないといけないし何よりマクロは辛い
などの問題を感じていたがcornucopiaはデータベースに接続するのはコードを生成するときだけだし生成されたコードも普通のRustファイルで見やすいのがとても良い。
あとbind()以外にもparams()関数が用意されていてパラメータを構造体で作成できるのも嬉しい。

改善されてほしいところ (2023-05-26)

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