[Rust]  Toasty を Amazon Aurora DSQL で動かす [DSQL]

[Rust] Toasty を Amazon Aurora DSQL で動かす [DSQL]

2026.05.26

Introduction

Toasty は Tokio チームが開発している Rust 製の ORM です。
現在SQLite / PostgreSQL / MySQL / DynamoDB をサポートしています。
先日 0.6系がリリースされました。

また、Amazon Aurora DSQL は PostgreSQL互換ですが、
外部キーや sequence が使えない・インデックス作成が非同期など多少の制約があるサーバレス分散DBです。

本記事では PostgreSQL対応してるからDSQLで動くかな?と思ったので試してみました。

DSQL?

Amazon Aurora DSQLはサーバレスの分散SQL データベースです。
特徴は以下。

  • PostgreSQL 16 互換。psql や既存ドライバ(tokio-postgres 等)がそのまま使える
  • マルチリージョン active-active / 自動スケール / 楽観的並行制御 (OCC)
  • 接続は IAM。短命トークンをパスワードとして渡し、TLS 必須
  • PostgreSQL の一部機能が非対応。
    • 外部キー (FK)・sequence/SERIAL・トリガーなし
    • CREATE INDEX は同期版が不可で、CREATE INDEX ASYNC が必須
    • 1 トランザクションに DDL は 1 文まで(DDL と DML の混在も不可)

toastyとは

安心のTokio製async ORM。
derive マクロでモデルを定義すると、CRUD メソッドやスキーマ生成ができます。

#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]                 // 主キーを自動採番
    id: uuid::Uuid,
    name: String,
    #[unique]               // 一意制約 → get_by_email が使える
    email: String,
}

使い方は以下。
push_schema() がモデル定義からテーブルを自動生成してくれます。

let mut db = toasty::Db::builder()
    .models(toasty::models!(crate::*))
    .connect("sqlite::memory:")   // or postgresql://...
    .await?;

// CREATE TABLE / INDEX を自動発行
db.push_schema().await?;

let user = toasty::create!(User { name: "shuta", email: "nakamura.shuta@classmethod.jp" })
    .exec(&mut db).await?;
let got = User::get_by_id(&mut db, &user.id).await?;

接続文字列のschemeで使うドライバが決まります。

Environment

  • Toasty: v0.6.1(features = postgresql
  • Rust: 1.95.0
  • cargo-lambda: 1.9.1(arm64 ビルドは zig 0.16)

Setup

1. DSQL クラスター作成

まずはDSQLの使用準備。

% aws dsql create-cluster --region us-east-1 --no-deletion-protection-enabled
# → identifier と endpoint (<CLUSTER_ID>.dsql.us-east-1.on.aws) を取得
% aws dsql wait cluster-active --identifier "$CLUSTER_ID" --region us-east-1

2. 接続(IAM トークン + TLS)

DSQL はパスワードに IAM トークンを使います。
※トークンには & = / が含まれるため、URL に埋めるときは percent-encode 必須

sslmode=require も付けましょう。

% TOKEN=$(aws dsql generate-db-connect-admin-auth-token \
    --hostname "$ENDPOINT" --region us-east-1 --expires-in 3600)
# postgresql://admin:<percent-encode した $TOKEN>@$ENDPOINT:5432/postgres?sslmode=require

3. Toasty プロジェクト作成

cargo newでプロジェクト作成します。
依存ライブラリは以下のようにします。

[dependencies]
toasty = { version = "0.6.1", features = ["postgresql"] }
tokio  = { version = "1", features = ["macros", "rt-multi-thread"] }
uuid   = { version = "1", features = ["v4"] }

接続先は環境変数 TOASTY_CONNECTION_URL
切り替えられるようにしておくと楽です。

Try

シンプルなモデルの基本動作

id + name だけの単一モデルが、push_schema()
テーブル作成 → INSERT → 主キー取得まで動くか確認。

SQLite・ローカル PostgreSQL 16・DSQL のいずれも問題なく動作。

==> connected OK
==> push_schema OK (table created from model)
==> insert OK id=019e6281-9b66-7131-a529-5187af6daadf
==> get_by_id OK: name=shuta

push_schema() が生成する DDL はFK も sequence も無し。
DSQL の「FK 非対応・sequence 非対応」に元から当たらないため、secondary index が無ければそのまま動きます。

#[unique] / #[index] を付けると push_schema でエラー

一意制約やインデックスを持つモデルで push_schema() がどうなるか確認しました。

結果は#[unique] email を足すと、push_schema() が以下のようにエラー。

==> connected OK
Error: db error: ERROR: unsupported mode. please use CREATE INDEX ASYNC.

Toasty は CREATE UNIQUE INDEX ... を発行しますが、DSQL は同期的な CREATE INDEX を許可せず
CREATE INDEX ASYNC を要求します。
CREATE TABLE 自体は成功しており、エラーはindex作成だけ

なお、Toasty 側に ASYNC を出させる設定は現状ありません。
なので、手動でCREATE INDEX ASYNCを実行し、push_schema は呼ばないようにしました。

DDL を Toasty に任せず、psql で手動作成します。
CREATE INDEX ASYNCjob_id を返すだけで、その時点では未完了です。
後続の DDL/DML の前に sys.wait_for_job(job_id) で完了を待ちます。

なお DSQL は「1 トランザクションに DDL 1 文まで」なので、
下の文は1 文ずつ別トランザクションで実行します。

CREATE TABLE users (id uuid NOT NULL, name text NOT NULL, email text NOT NULL, PRIMARY KEY (id));

-- 非同期インデックス作成
CREATE UNIQUE INDEX ASYNC index_users_by_email ON users (email);
--           job_id
-- ----------------------------
--  xxxxxxxxxxxxxxxxxxxxxxxxx

-- 完了を待ってから先へ
SELECT sys.wait_for_job('xxxxxxxxxxxxxxxxxxxxxxxxx');

アプリ側は #[unique] 付きのモデルをそのまま定義し、db.push_schema() を呼ばないにようにする。
その結果、#[unique] 付きモデルでもCRUD処理は問題なく動作しました。

==> skip push_schema (schema は手動作成: CREATE INDEX ASYNC)
==> insert OK id=019e628e-06ce-71b3-816a-53a0dc88a50e
==> get_by_id OK: name=Shuta
==> get_by_email OK: id=019e628e-06ce-71b3-816a-53a0dc88a50e
==> update OK: name=Shuta Updated
==> delete OK: get_by_id is_err=true

DDL は DSQL 流に自分で実行(index は ASYNC)し、
Toasty は接続とクエリに使うのが現状の動作方法です。
※今後ToastyがCREATE INDEX ASYNCを実行するようになれば解決と思われる

リレーション(#[has_many] / #[belongs_to])の確認

リレーションの作成・ロードが DSQL で動くかも確認してみます。

UserTodo を定義(Todo 側に #[index] user_id#[belongs_to])。
Toasty は リレーションに FK を張らず user_id に index を作るだけなので、
その index を ASYNC で手動作成すれば動きます。

==> create todo (in user.todos)
==> create user2 + 2 todos
==> has_many load: user.todos()=1, user2.todos()=2
==> belongs_to load: todo.user().name=John Doe

nested create / バッチ nested create / has_manybelongs_to ロードが
すべて DSQLで成功しました。

AWS Lambda(cargo lambda)から DSQL に接続

ついでに Lambda 上でも動くか確認しました。
ここでは実行ロールの一時クレデンシャルから aws-sdk-dsql で接続トークンを生成します。

コードは「トークン生成して接続 → insert → 読み返して返す」だけのシンプルなもの
※テーブルは事前に手動作成

Setup の依存に加えて、Lambda 用に以下を追加します。

lambda_runtime = "1"
aws-config = { version = "1", features = ["behavior-version-latest"] }
aws-sdk-dsql = "1"
urlencoding = "2"
serde_json = "1"
use aws_config::{BehaviorVersion, Region};
use aws_sdk_dsql::auth_token::{AuthTokenGenerator, Config as DsqlAuthConfig};
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
use serde_json::{json, Value};

#[derive(Debug, toasty::Model)]
struct LambdaUser {
    #[key] #[auto] id: uuid::Uuid,
    note: String,
}

// 実行ロールのクレデンシャルから DSQL トークンを生成して接続
async fn connect() -> Result<toasty::Db, Error> {
    let endpoint = std::env::var("DSQL_ENDPOINT")?;
    let cfg = aws_config::load_defaults(BehaviorVersion::latest()).await;
    let token = AuthTokenGenerator::new(
        DsqlAuthConfig::builder()
            .hostname(&endpoint).region(Region::new("us-east-1")).build()?,
    )
    .db_connect_admin_auth_token(&cfg).await?;
    let url = format!(
        "postgresql://admin:{}@{endpoint}:5432/postgres?sslmode=require",
        urlencoding::encode(token.as_str()),
    );
    Ok(toasty::Db::builder().models(toasty::models!(crate::*)).connect(&url).await?)
}

async fn handler(_e: LambdaEvent<Value>) -> Result<Value, Error> {
    let mut db = connect().await?;
    let u = toasty::create!(LambdaUser { note: "hello from lambda" })
        .exec(&mut db).await?;
    let got = LambdaUser::get_by_id(&mut db, &u.id).await?;
    Ok(json!({ "id": got.id.to_string(), "note": got.note }))
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    run(service_fn(handler)).await
}

実行ロールには dsql:DbConnectAdmin(対象クラスター ARN)を付与。
*DSQL endpoint はパブリックなのでVPC不要

ビルドとデプロイして確認してみましょう。

% cargo lambda build --release --arm64
% cargo lambda deploy --iam-role <ROLE_ARN> --env-var DSQL_ENDPOINT=$ENDPOINT --timeout 30
% cargo lambda invoke --remote toasty-dsql-lambda --data-ascii '{}'

問題なく動いてることが確認できました。

{"id":"019e62b4-4e4c-7090-86b5-e31f09976e30","note":"hello from lambda ..."}

Summary

本記事ではToasty(v0.6.1) を Aurora DSQLで試してみました。
結果的には基本的な CRUD・リレーション・Lambda からの接続は動作しました。
#[unique] / #[index] を付けると push_schema() がエラーになりますが

接続は TLS + IAM トークンでOK.(TLS 対応は issue #660 / PR #663 で導入済み)

secondary indexは問題があり、#[unique] / #[index] を付けると
Toasty が素の CREATE INDEX を発行し、DSQL の「CREATE INDEX ASYNC 必須」でエラーとなり、
push_schema() が失敗します。Toasty 側に ASYNC を出させる設定は今のところ無いので、
index だけ自分で CREATE INDEX ASYNC を実行して、push_schema() は呼ばないようにする必要があります。

シンプルなCRUD なら Toasty は DSQL で使えるが、
スキーマ(index)は DSQL 流に自分で用意すればとりあえず問題なさそうです。
※DSQL は CDC 同様まだ preview なので挙動が変わる可能性があるので注意

References

この記事をシェアする

関連記事