> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-vortex-format.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# ClickHouse Managed Postgres で Rust (sqlx) を使用する

> sqlx と axum を使って Rust アプリを ClickHouse Managed Postgres に接続し、sqlx-cli で移行を実行して、検証済みの TLS 接続経由でクエリを実行する方法を説明します

export const BetaBadge = ({link, galaxyTrack, galaxyEvent}) => {
  if (link) {
    return <a href={link} target="_blank" rel="noopener noreferrer" className="betaBadge" onClick={galaxyTrack && galaxyEvent ? galaxyOnClick(galaxyEvent) : undefined}>
                <span>ベータ</span>
            </a>;
  }
  return <a href="https://clickhouse.com/docs/reference/settings/beta-and-experimental-features#beta-features" className="betaBadge">
            <span>ベータ機能</span>
        </a>;
};

<BetaBadge link="https://clickhouse.com/cloud/postgres" galaxyTrack={true} galaxyEvent="docs.managed-postgres.guides-rust-beta" />

[sqlx](https://github.com/launchbadge/sqlx) は、接続poolと移行機能を標準で備えた、純粋な Rust 実装の非同期 SQL ツールキットです。このガイドでは、`sqlx-cli` の移行を使って `todos` テーブルを作成し、小さな [axum](https://github.com/tokio-rs/axum) サーバーで作成・読み取り・更新・削除のクエリを処理します。すべての接続で、証明書を完全に検証する TLS を使用します。

<h2 id="prerequisites">
  前提条件
</h2>

* [Rust](https://www.rust-lang.org/tools/install) 1.94 以降。本ガイドは Rust 1.98.1、`sqlx` 0.9.0、`sqlx-cli` 0.9.0、`axum` 0.8.9 で動作確認しています。
* ClickHouse Cloud アカウント
* データベース作成用の [`psql`](https://www.postgresql.org/download/)。`CREATE DATABASE` ステートメントは [SQL コンソール](/ja/integrations/connectors/sql-clients/sql-console) で実行することもできます。

<h2 id="create-service">
  ClickHouse Managed Postgres サービスを作成する
</h2>

ClickHouse Cloud コンソールで **New service** をクリックし、**Postgres** を選択します。インスタンスは数分で利用可能になります。詳しい手順については、[クイックスタート](/ja/products/managed-postgres/quickstart)を参照してください。

<h2 id="connection-details">
  接続情報を取得する
</h2>

サービスの左サイドバーにある **Connect** をクリックします。**Directly** は選択したままにして、**Use SSL** をオンにします。これで、**url** タブに表示される接続 URL の末尾が `sslmode=verify-full&sslrootcert=<service-name>-ca-certificate.pem` になり、ポートは `5432` になります。

**Download CA certificate** をクリックします。証明書は後から **Settings → CA Certificate** でダウンロードすることもできます。この証明書はインスタンスごとに固有のものなので、ドライバーはこれを使って、接続先が自分のサーバーであることを検証できます。

このガイドでは、アプリと `sqlx-cli` のどちらも、ポート `5432` で Postgres に**直接**接続します。axum サーバーは長時間稼働するプロセスであり、`PgPool` が独自の接続 pool を保持するため、Postgres の前段に別途プーラーを置く必要はありません。移行には必ず直接接続を使用してください。多数のアプリインスタンスを実行する場合は、[PgBouncer を使用する](#pgbouncer)を参照してください。

<h2 id="project-setup">
  プロジェクトをセットアップする
</h2>

プロジェクトを作成し、Postgres ドライバー、Tokio ランタイム、`rustls` TLS バックエンドを有効にした `sqlx` を追加します。あわせて、axum といくつかの補助クレートも追加します。

```bash theme={null}
cargo new todo-api && cd todo-api
cargo add sqlx --features postgres,runtime-tokio,tls-rustls,chrono
cargo add axum dotenvy
cargo add tokio --features full
cargo add serde --features derive
cargo add chrono --features serde
```

同じ TLS バックエンドを指定して `sqlx-cli` をインストールします。

```bash theme={null}
cargo install sqlx-cli --no-default-features --features rustls,postgres
```

<Warning>
  **macOS では `native-tls` ではなく `rustls` を使用してください**

  ClickHouse Managed Postgres は TLS 1.3 のみをサポートしています。macOS では、`native-tls` バックエンドがシステムの Secure Transport ライブラリを使用しますが、このライブラリは TLS 1.3 に対応していません。オプションを指定せずに `cargo install sqlx-cli` を実行すると `native-tls` が使用され、すべてのコマンドが `error occurred while attempting to establish a TLS connection: One or more parameters passed to a function were not valid.`(または `bad protocol version`)というエラーで失敗します。`tls-native-tls` フィーチャーを有効にしてビルドしたアプリも同様です。Linux では `native-tls` が OpenSSL を使用するため接続自体は可能ですが、`ca-certificate.pem` に含まれる最初の証明書しか読み込みません。`rustls` はすべてのプラットフォームで TLS 1.3 をサポートし、ファイル内のすべての証明書を読み込みます。
</Warning>

<Tip>
  **既存のアプリに追加する場合**

  `cargo new` は不要です。プロジェクト内で上記の `cargo add` および `cargo install` コマンドを実行し、[接続を設定する](#configure-connection)に進んでください。アプリで sqlx を SQLite と組み合わせて使用している場合は、次の変更を行います。

  * `Cargo.toml` の `sqlx` フィーチャーから `sqlite` を削除し、`SqlitePool` を `PgPool` に置き換えます。
  * `?` プレースホルダーを `$1`、`$2` などに変更します。
  * 移行を Postgres の構文で書き直します。たとえば、`INTEGER PRIMARY KEY AUTOINCREMENT` の代わりに `bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY` を使用します。
</Tip>

<h2 id="configure-connection">
  接続を設定する
</h2>

ダウンロードした CA 証明書をプロジェクトディレクトリに移動し、ファイル名を `ca-certificate.pem` にリネームします。

アプリ用のデータベースを作成します。`<PASSWORD>` とホストは、**Connect** モーダルに表示される値に置き換えてください。

```bash theme={null}
psql "postgresql://postgres:<PASSWORD>@your-instance.pg.clickhouse.cloud:5432/postgres?sslmode=verify-full&sslrootcert=ca-certificate.pem" -c "CREATE DATABASE guide_rust;"
```

```text theme={null}
CREATE DATABASE
```

プロジェクトのルートに `.env` ファイルを作成するか、既存の `.env` ファイル内の `DATABASE_URL` を更新して、`guide_rust` データベースを参照するように設定します。アプリと `sqlx-cli` はどちらも、このファイルから `DATABASE_URL` を読み込みます。

```bash title=".env" theme={null}
DATABASE_URL="postgresql://postgres:<PASSWORD>@your-instance.pg.clickhouse.cloud:5432/guide_rust?sslmode=verify-full&sslrootcert=ca-certificate.pem"
```

sqlx は URL から `sslmode` と `sslrootcert` を読み取ります。`verify-full` を指定すると、サーバー証明書がインスタンスの CA によって署名されていること、および証明書がホスト名と一致することが検証されます。`sslrootcert` のパスは、コマンドを実行するディレクトリを基準とした相対パスとして解決されます。`.env` はバージョン管理の対象から除外してください。また、パスワードに `@`、`/`、`#` などの文字が含まれる場合は、パーセントエンコードしてください。

<Warning>
  **必ず `sslmode=verify-full` を設定してください**

  `sslmode` を指定しない場合、sqlx は `prefer` を使用します。この場合、接続は暗号化されますが、`sslrootcert` を設定していてもあらゆる証明書を受け入れてしまいます。`verify-full` を指定した場合、CA 証明書が存在しないか誤っていると `UnknownIssuer` で失敗し、IP アドレスなど証明書と一致しないホストを指定すると `NotValidForNameContext` (`sqlx-cli` では `certificate not valid for name`) で失敗します。`sslrootcert` のパスが誤っている場合は `No such file or directory` で失敗します。
</Warning>

<h2 id="migrations">
  移行でテーブルを作成する
</h2>

移行ファイルを作成します:

```bash theme={null}
sqlx migrate add create_todos
```

```text theme={null}
Creating migrations/20261001142218_create_todos.sql
```

`migrations` ディレクトリ内に作成された新しいファイルを開き、テーブル定義を追加します。

```sql title="migrations/<timestamp>_create_todos.sql" theme={null}
CREATE TABLE todos (
    id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    title      text NOT NULL,
    done       boolean NOT NULL DEFAULT false,
    created_at timestamptz NOT NULL DEFAULT now()
);
```

次のコマンドで適用します:

```bash theme={null}
sqlx migrate run
```

```text theme={null}
Applied 20261001142218/migrate create todos (233.971666ms)
```

`sqlx migrate run` は適用済みの移行を `_sqlx_migrations` テーブルに記録し、以降の実行では未適用の移行のみを実行します。`migrations` ディレクトリはコードと一緒にコミットしてください。アプリの起動時に移行を適用したい場合は、代わりに `main` 内で `sqlx::migrate!().run(&pool).await?` を呼び出します。ただし、pool が直接接続を使用している必要があります。

<h2 id="query">
  API を構築する
</h2>

`src/main.rs` を、ルートごとに 1 つのクエリを実行する小さな axum サーバーに置き換えます。既存のアプリケーションに組み込む場合は、`main` と同じ方法で pool を作成し、`Todo` 構造体、ハンドラー、ルートをお使いのルーターに追加してください。

```rust title="src/main.rs" theme={null}
use std::str::FromStr;

use axum::{
    Json, Router,
    extract::{Path, State},
    http::StatusCode,
    routing::{get, patch},
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use sqlx::postgres::{PgConnectOptions, PgPoolOptions};

#[derive(Serialize, sqlx::FromRow)]
struct Todo {
    id: i64,
    title: String,
    done: bool,
    created_at: DateTime<Utc>,
}

#[derive(Deserialize)]
struct NewTodo {
    title: String,
}

type ApiResult<T> = Result<Json<T>, (StatusCode, String)>;

// Return 404 when no row matches, and 500 for any other database error.
fn db_error(err: sqlx::Error) -> (StatusCode, String) {
    match err {
        sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, "Not found\n".into()),
        err => {
            eprintln!("Database error: {err}");
            (StatusCode::INTERNAL_SERVER_ERROR, "Database error\n".into())
        }
    }
}

// Read
async fn list_todos(State(pool): State<PgPool>) -> ApiResult<Vec<Todo>> {
    let todos = sqlx::query_as::<_, Todo>("SELECT * FROM todos ORDER BY id")
        .fetch_all(&pool)
        .await
        .map_err(db_error)?;
    Ok(Json(todos))
}

// Create
async fn create_todo(State(pool): State<PgPool>, Json(new): Json<NewTodo>) -> ApiResult<Todo> {
    let todo = sqlx::query_as::<_, Todo>("INSERT INTO todos (title) VALUES ($1) RETURNING *")
        .bind(new.title)
        .fetch_one(&pool)
        .await
        .map_err(db_error)?;
    Ok(Json(todo))
}

// Update: toggle the done flag
async fn toggle_todo(State(pool): State<PgPool>, Path(id): Path<i64>) -> ApiResult<Todo> {
    let todo = sqlx::query_as::<_, Todo>("UPDATE todos SET done = NOT done WHERE id = $1 RETURNING *")
        .bind(id)
        .fetch_one(&pool)
        .await
        .map_err(db_error)?;
    Ok(Json(todo))
}

// Delete
async fn delete_todo(State(pool): State<PgPool>, Path(id): Path<i64>) -> ApiResult<Todo> {
    let todo = sqlx::query_as::<_, Todo>("DELETE FROM todos WHERE id = $1 RETURNING *")
        .bind(id)
        .fetch_one(&pool)
        .await
        .map_err(db_error)?;
    Ok(Json(todo))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Load DATABASE_URL from .env, if the variable isn't already set
    dotenvy::dotenv().ok();

    // sslmode and sslrootcert come from the URL. Not sending extra_float_digits
    // lets the same code also connect through PgBouncer, which rejects it.
    let options = PgConnectOptions::from_str(&std::env::var("DATABASE_URL")?)?
        .extra_float_digits(None);
    let pool = PgPoolOptions::new()
        .max_connections(10)
        .connect_with(options)
        .await?;

    // Confirm that the connection is encrypted
    let tls: Option<String> =
        sqlx::query_scalar("SELECT version FROM pg_stat_ssl WHERE pid = pg_backend_pid()")
            .fetch_one(&pool)
            .await?;
    println!("Connected to Postgres, TLS version: {}", tls.as_deref().unwrap_or("none"));

    let app = Router::new()
        .route("/todos", get(list_todos).post(create_todo))
        .route("/todos/{id}", patch(toggle_todo).delete(delete_todo))
        .with_state(pool);

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3117").await?;
    println!("Listening on http://localhost:3117/todos");
    axum::serve(listener, app).await?;
    Ok(())
}
```

値は常に `.bind` を使ってパラメータ (`$1`) として渡され、SQL 文字列に直接埋め込まれることはありません。個々の SQL 文字列は接続ごとに一度だけプリペアされ、最大 100 個のステートメントを保持するステートメントキャッシュに格納されます。キャッシュから追い出されたステートメントは、sqlx によってクローズされます。

<Tip>
  **コンパイル時にチェックされるクエリ**

  このガイドでは `query_as` 関数を使用します。この関数は実行時に SQL をチェックするため、`cargo build` の実行時にデータベースは必要ありません。一方、`query!` および `query_as!` マクロは、コンパイル時にデータベースに照らして SQL とカラム型をチェックします。これらのマクロはビルド時に `.env` から `DATABASE_URL` を読み込むため、この値は直接接続の設定のままにしておいてください。CI などデータベースのない環境でビルドするには、`cargo sqlx prepare` を実行し、生成された `.sqlx` ディレクトリをコミットしたうえで、`SQLX_OFFLINE=true` を設定してください。
</Tip>

<h2 id="verify">
  実行と動作確認
</h2>

サーバーを起動します。

```bash theme={null}
cargo run
```

```text theme={null}
Connected to Postgres, TLS version: TLSv1.3
Listening on http://localhost:3117/todos
```

1行目は `pg_stat_ssl` からの出力で、session が暗号化されていることを示しています。別の端末を開いて todo を2件作成し、1件目を完了にしてから、2件目を削除します。

```bash theme={null}
curl -w '\n' -X POST localhost:3117/todos -H 'content-type: application/json' -d '{"title": "Try sqlx"}'
curl -w '\n' -X POST localhost:3117/todos -H 'content-type: application/json' -d '{"title": "Write a migration"}'
curl -w '\n' -X PATCH localhost:3117/todos/1
curl -w '\n' -X DELETE localhost:3117/todos/2
```

各リクエストは、影響を受けた行を返します。`-w '\n'` オプションは、各レスポンスの後に改行を追加するためだけのものです。

```text theme={null}
{"id":1,"title":"Try sqlx","done":false,"created_at":"2026-10-01T14:22:32.475739Z"}
{"id":2,"title":"Write a migration","done":false,"created_at":"2026-10-01T14:22:32.627680Z"}
{"id":1,"title":"Try sqlx","done":true,"created_at":"2026-10-01T14:22:32.475739Z"}
{"id":2,"title":"Write a migration","done":false,"created_at":"2026-10-01T14:22:32.627680Z"}
```

ブラウザで `http://localhost:3117/todos` を開くと、残りの todo を一覧表示できます。

```json theme={null}
[{"id":1,"title":"Try sqlx","done":true,"created_at":"2026-10-01T14:22:32.475739Z"}]
```

コンソールでデータを確認するには、サービスの左サイドバーで **SQL コンソール** を開き、`guide_rust`、`public` の順に展開して、`todos` テーブルをクリックします。その隣にある `_sqlx_migrations` テーブルには、移行履歴が格納されています。`todos` テーブルには、残っている行が格納されています。

| id | title | done | created\_at |
| - | - | - | - |
| 1 | Try sqlx | true | 2026-10-01 14:22:32.475739+00 |

<h2 id="pgbouncer">
  PgBouncer を使用する
</h2>

同梱の [PgBouncer](/ja/products/managed-postgres/connection#pgbouncer) 経由でアプリを接続するには、**Connect** モーダルで **via PgBouncer** を選択し、アプリの `DATABASE_URL` でポート `6432` を指定します。CA 証明書は同じものをそのまま使用できます。たとえば、`sqlx-cli` 用の `.env` は直接接続の設定のままにしておき、アプリの起動時に変数を設定します。

```bash theme={null}
DATABASE_URL="postgresql://postgres:<PASSWORD>@your-instance.pg.clickhouse.cloud:6432/guide_rust?sslmode=verify-full&sslrootcert=ca-certificate.pem" cargo run
```

```text theme={null}
Connected to Postgres, TLS version: none
Listening on http://localhost:3117/todos
```

PgBouncer を経由する場合、`pg_stat_ssl` が報告するのは PgBouncer 自身から Postgres への接続であるため、このチェックでは `none` が出力されます。ただし、アプリケーションから PgBouncer への接続は引き続き検証されます。誤った CA 証明書を使用した場合は、同様に `UnknownIssuer` で失敗します。

PgBouncer はトランザクションプーリングモードで動作するため、トランザクションごとに異なる Postgres 接続上で実行される可能性があります。sqlx を使用する場合は、次の点に注意してください。

* **接続** するには、`main.rs` と同様に `.extra_float_digits(None)` を指定する必要があります。sqlx はデフォルトで `extra_float_digits` を起動パラメータとして送信しますが、PgBouncer は `unsupported startup parameter: extra_float_digits` を返して接続を拒否します。同じ理由で、`PgConnectOptions::options` も `unsupported startup parameter in options` で失敗します。
* **プリペアドステートメント** はデフォルト設定のままで動作します。sqlx はキャッシュから追い出したステートメントをプロトコルレベルの `Close` メッセージで解放し、PgBouncer はトランザクション内であってもこれを処理します。`statement_cache_capacity(0)` は設定しないでください。この設定は不要なうえ、キャッシュを無効にしても sqlx は名前付きステートメントを準備し続け、それらを一切クローズしなくなります。
* トランザクション外で実行するクエリには **`.persistent(false)` を使用しないでください**。この場合、sqlx は名前なしステートメントを準備してから 2 回目のラウンドトリップで実行しますが、PgBouncer がこれを別の Postgres 接続に送信する可能性があります。その結果、クエリが `unnamed prepared statement does not exist` で失敗したり、別のクライアントのステートメントが気付かないうちに実行されて誤った結果が返されたりすることがあります。
* `SET` で変更した **セッション設定** は、poolの `after_connect` フック内で変更したものも含め、自身の接続に保持されず、他のクライアントに影響が及ぶ可能性があります。代わりに、トランザクション内で `SET LOCAL` を使用してください。
* **移行** には直接接続を使用する必要があります。`sqlx-cli` と `query!` マクロは常に `extra_float_digits` を送信するため、PgBouncer 経由では接続できません。また、`sqlx::migrate!` はセッションレベルのアドバイザリロック (`pg_advisory_lock`) を取得しますが、PgBouncer 経由ではロック解除が別の Postgres 接続に送られる可能性があります。その場合、ロックはpool内の接続で保持されたままとなり、以降の移行は直接接続によるものも含め、PgBouncer がその接続を閉じるまでハングします。

<h2 id="next-steps">
  次のステップ
</h2>

* [接続](/ja/products/managed-postgres/connection): 接続文字列、PgBouncer、TLS
* [設定](/ja/products/managed-postgres/settings): `max_connections` などの Postgres および PgBouncer のパラメータ
* [読み取りレプリカ](/ja/products/managed-postgres/read-replicas): 2 つ目の `PgPool` を使用して読み取り専用クエリをレプリカに送信
* [セキュリティ](/ja/products/managed-postgres/security): IP アクセスリストとプライベートネットワーキング
