CLOVER🍀

That was when it all began.

RustでRusqliteを使ってSQLiteにアクセスしてみる

これは、なにをしたくて書いたもの?

RustからSQLiteにアクセスしてみましょう、ということで。

RustからSQLiteにアクセスする

RustからSQLiteにアクセスするには、以下のクレートを使うとよさそうです。

違いが気になるところですが、Rusqliteを使う、でよさそうです。

Rusqliteクレート

Rusqliteクレートのドキュメントはこちら。

rusqlite - Rust

GitHubリポジトリーはこちら。

GitHub - rusqlite/rusqlite: Ergonomic bindings to SQLite for Rust · GitHub

現在のバージョンは0.39.0です。

https://docs.rs/rusqlite/0.39.0/rusqlite/

RusqliteはRustからSQLiteを使うためのラッパーです。rust-postgresのAPIをベースにしていたようですが、互換性があるわけでも
なさそうです。

サポートしているSQLiteのバージョンは3.34.1以上です。

Rusqlite / Usage / Supported SQLite Versions

フィーチャーフラグはいろいろありますが、内部にlibsqlite3-sysというクレートを持っており、これを合わせてビルドする
bundledをまずは使うとよさそうです。

rusqlite 0.39.0 - Docs.rs

rusqlite/libsqlite3-sys at v0.39.0 · rusqlite/rusqlite · GitHub

Rusqlite / Notes on building rusqlite and libsqlite3-sys

サンプルはドキュメントを見るか、

Rusqlite / Usage

こちらのディレクトリーを参照するとよいでしょう。

https://github.com/rusqlite/rusqlite/tree/v0.39.0/examples

ちなみにasync/awaitには対応しておらず、その場合はこちらを使うようです。

tokio_rusqlite - Rust

それでは試してみます。

環境

今回の環境はこちら。

$ rustup --version
rustup 1.29.0 (28d1352db 2026-03-05)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: the currently active `rustc` version is `rustc 1.95.0 (59807616e 2026-04-14)`

準備

Cargoパッケージの作成。

$ cargo new --vcs none --lib rusqlite-getting-started
    Creating library `rusqlite-getting-started` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
$ cd rusqlite-getting-started

確認はテストコードで行うので、ライブラリークレートにしました。

Rusqliteクレートを使う

では、Rusqliteクレートを使っていきます。

依存関係の追加。

$ cargo add rusqlite --features bundled
$ cargo add --dev uuid --features v4

Cargo.toml

[package]
name = "rusqlite-getting-started"
version = "0.1.0"
edition = "2024"

[dependencies]
rusqlite = { version = "0.39.0", features = ["bundled"] }

[dev-dependencies]
uuid = { version = "1.23.1", features = ["v4"] }

uuidクレートはテスト用に使います。

uuid - Rust

Rusqliteのbundledというフィーチャーフラグですが、これを指定しない場合はその環境でSQLiteが利用できる必要があります。
bundledを指定すると、Rusqliteがこれを担います。

ビルド時にこういうエラーが発生した場合は、最初はbundledを指定しておくのがよいでしょう。

error: linking with `cc` failed: exit status: 1
  |
  = note:  "cc" "-m64" "省略" "-Wl,--gc-sections" "-pie" "-Wl,-z,relro,-z,now" "-nodefaultlibs"
  = note: some arguments are omitted. use `--verbose` to show all linker arguments
  = note: rust-lld: error: unable to find library -lsqlite3
          collect2: error: ld returned 1 exit status

使用するSQLiteをコントロールしたい場合は話が変わってきますね。

では、Rusqliteを使っていきましょう。テストコードの雛形はこちら。

src/lib.rs

#[cfg(test)]
mod tests {
    use rusqlite::{Connection, Result, named_params};
    use std::fs;
    use uuid::Uuid;

    // ここにテストを書く
}

まずはインメモリーでデータベースを開いてみます。

    #[test]
    fn connect_in_memory() {
        let connection = Connection::open_in_memory().unwrap();

        let result: i32 = connection
            .query_one("select 1", (), |row| row.get(0))
            .unwrap();

        assert_eq!(result, 1);

        assert_eq!(connection.is_autocommit(), true);
    }

Struct Connection / pub fn open_in_memory() -> Result

データをファイルに保存する場合。

    #[test]
    fn connect_file_database() {
        let connection = Connection::open("./first_db.db3").unwrap();

        let result: i32 = connection
            .query_one("select 1", (), |row| row.get(0))
            .unwrap();

        assert_eq!(result, 1);

        assert_eq!(connection.is_autocommit(), true);

        let _ = fs::remove_file("./first_db.db3");
    }

Struct Connection / pub fn open<P: AsRef>(path: P) -> Result

データベース接続を作る時にはフラグ(OpenFlags)を指定できます。デフォルトだと以下と同じ意味のようです。

Connection::open_with_flags(
    path,
    OpenFlags::SQLITE_OPEN_READ_WRITE
        | OpenFlags::SQLITE_OPEN_CREATE
        | OpenFlags::SQLITE_OPEN_URI
        | OpenFlags::SQLITE_OPEN_NO_MUTEX,
)

OpenFlags in rusqlite - Rust

詳しい意味はこちらへ。

Opening A New Database Connection

インメモリーデータベースだと、接続を跨いでデータを維持できないことを確認。

    #[test]
    fn test_in_memory() {
        let connection = Connection::open_in_memory().unwrap();

        connection
            .execute("create table t(id integer, primary key(id))", ())
            .unwrap();

        connection
            .execute("insert into t(id) values(10)", ())
            .unwrap();

        let result: i32 = connection
            .query_one("select id from t order by id limit 1", (), |row| row.get(0))
            .unwrap();

        assert_eq!(result, 10);

        let connection = Connection::open_in_memory().unwrap();

        match connection.query_one("select id from t order by id limit 1", (), |row| {
            row.get::<usize, i32>(0)
        }) {
            Ok(r) => panic!("unexpected result: {:?}", r),
            Err(err) => assert_eq!(err.to_string(), "no such table: t"),
        }
    }

このコードだとConnectionに割り当てている変数名が同じなので、新しいConnectionを開いた時点で前のConnection
Dropされます。

SQLの実行はexecutequery_〜を使います。

今回は値を直接入れ込んでいますが、バインドパラメーターも使えます。

ファイルを使った場合は、接続を跨いでもデータが維持されています。

    #[test]
    fn test_file() {
        let database_path = "./example_db.db3";

        let connection = Connection::open(database_path).unwrap();

        connection
            .execute("create table t(id integer, primary key(id))", ())
            .unwrap();

        connection
            .execute("insert into t(id) values(10)", ())
            .unwrap();

        let result: i32 = connection
            .query_one("select id from t order by id limit 1", (), |row| row.get(0))
            .unwrap();

        assert_eq!(result, 10);

        let connection = Connection::open(database_path).unwrap();

        let result: i32 = connection
            .query_one("select id from t order by id limit 1", (), |row| row.get(0))
            .unwrap();

        assert_eq!(result, 10);

        let _ = fs::remove_file(database_path);
    }

テスト内でデータベース接続とクリーンアップを行う関数を作成。

    fn with_connection<F>(consumer: F)
    where
        F: FnOnce(&mut Connection),
    {
        struct CleanupDatabase {
            path: String,
        }

        impl Drop for CleanupDatabase {
            fn drop(&mut self) {
                let _ = fs::remove_file(&self.path);
            }
        }

        let path = format!("{}_db.db3", Uuid::new_v4());
        let mut connection = Connection::open(&path).unwrap();

        let _cleanup = CleanupDatabase {
            path: path.to_string(),
        };

        consumer(&mut connection);
    }

複数行の取得。

    #[derive(Debug)]
    struct Person {
        id: i32,
        first_name: String,
        last_name: String,
        age: i32,
    }

    #[test]
    fn test_map_struct() {
        with_connection(|connection| {
            connection
                .execute(
                    "create table person(
                      id integer,
                      first_name text,
                      last_name text,
                      age integer,
                      primary key(id)
                    )",
                    (),
                )
                .unwrap();

            let mut statement = connection
                .prepare(
                    "insert into person(id, first_name, last_name, age) values(?1, ?2, ?3, ?4)",
                )
                .unwrap();

            statement.execute((1, "カツオ", "磯野", 11)).unwrap();
            statement.execute((2, "ワカメ", "磯野", 9)).unwrap();
            statement.execute((3, "タラオ", "フグ田", 3)).unwrap();

            let mut statement = connection
                .prepare("select id, first_name, last_name, age from person where age > :age order by age desc")
                .unwrap();

            let people: Vec<Person> = statement
                .query_map(named_params! {":age": 5}, |row| {
                    Ok(Person {
                        id: row.get(0).unwrap(),
                        first_name: row.get(1).unwrap(),
                        last_name: row.get(2).unwrap(),
                        age: row.get(3).unwrap(),
                    })
                })
                .unwrap()
                .collect::<Result<Vec<Person>>>()
                .unwrap();

            assert_eq!(people.len(), 2);

            let katsuo = &people[0];
            assert_eq!(katsuo.id, 1);
            assert_eq!(katsuo.first_name, "カツオ");
            assert_eq!(katsuo.last_name, "磯野");
            assert_eq!(katsuo.age, 11);

            let wakame = &people[1];
            assert_eq!(wakame.id, 2);
            assert_eq!(wakame.first_name, "ワカメ");
            assert_eq!(wakame.last_name, "磯野");
            assert_eq!(wakame.age, 9);
        });
    }

今回はStatementを1度作成し、バインドパラメーターを渡して実行するという方法をとっています。

パラメーターの渡し方は、位置指定と名前指定の2種類があります。

Params in rusqlite - Rust

最後はトランザクションの確認。

    #[test]
    fn test_transaction() {
        with_connection(|connection| {
            connection
                .execute(
                    "create table person(
                      id integer,
                      first_name text,
                      last_name text,
                      age integer,
                      primary key(id)
                    )",
                    (),
                )
                .unwrap();

            let transaction = connection.transaction().unwrap();

            transaction.execute(
                "insert into person(id, first_name, last_name, age) values(:id, :first_name, :last_name, :age)",
                named_params! {":id": 1, ":first_name": "カツオ", ":last_name": "磯野", ":age": 11}).unwrap();

            transaction.commit().unwrap();

            assert_eq!(
                connection
                    .query_one("select count(*) from person", (), |row| row
                        .get::<usize, i32>(0))
                    .unwrap(),
                1
            );
            assert_eq!(
                connection
                    .query_one(
                        "select first_name from person where id = :id",
                        named_params! {":id": 1},
                        |row| row.get::<usize, String>(0)
                    )
                    .unwrap(),
                "カツオ"
            );

            let transaction = connection.transaction().unwrap();

            transaction.execute(
                "insert into person(id, first_name, last_name, age) values(:id, :first_name, :last_name, :age)",
                named_params! {":id": 2, ":first_name": "ワカメ", ":last_name": "磯野", ":age": 9}).unwrap();

            transaction.rollback().unwrap();

            assert_eq!(
                connection
                    .query_one("select count(*) from person", (), |row| row
                        .get::<usize, i32>(0))
                    .unwrap(),
                1
            );
            assert_eq!(
                connection
                    .query_one(
                        "select first_name from person where id = :id",
                        named_params! {":id": 2},
                        |row| row.get::<usize, String>(0)
                    )
                    .unwrap_err()
                    .to_string(),
                "Query returned no rows"
            );
        })
    }

Transaction in rusqlite - Rust

デフォルトではdropされる時にトランザクションはロールバックされますが、これは変更可能なようです。コミットする時には
明示的に書いた方がよいとは思いますが。

またConnection#executeでバインドパラメーターを使う例にもしています。

こんなところでしょうか。

おわりに

Rusqliteを使ってSQLiteにアクセスしてみました。

まあ、苦労しました…。だいぶRustを忘れているところに引っ張られましたが、いい勉強にはなったのかなと。

SQLiteはいろいろ使いどころがありそうなので、もうちょっと深堀したいところです。