Tak's Notebook

Kaggle, Machine Learning, Engineering

Rusqlite クレートを使って Rust で SQLite を利用する

概要

Rust で SQLite を使うためのラッパークレートである Rusqlite を使ってみる。

PythonSQLite バイナリを作成し、それを Rust で Rusqlite を使って読み込みクエリを実行する流れのサンプルコードを書いてみた。

github.com

Python で作成した SQLite バイナリを Rust で読み込んでクエリを実行する

ディレクトリ構成

以下のようなディレクトリ構成になってる想定でコードを書いていく。

.
├── python
│  └── python3.10
│     ├── data
│     │  └── users.sqlite
│     ├── poetry.lock
│     ├── pyproject.toml
│     ├── README.md
│     ├── src
│     │  ├── __init__.py
│     │  └── __main__.py
│     └── tasks.py
└── rust
   ├── Cargo.lock
   ├── Cargo.toml
   ├── Makefile.toml
   ├── README.md
   └── src
      └── main.rs

PythonSQLite のバイナリを作成する

使用するサードパーティライブラリはないので pyproject.toml は不要かもしれない。

  • pyproject.toml
[tool.poetry]
name = "python3-10"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
packages = [{include = "src"}]

[tool.poetry.dependencies]
python = "~3.10"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

以下のように、テーブル作成・値の挿入を行ったデータベースを保存する。

テスト代わりに Python 側でクエリを実行できるかを最後に確認している。

  • main.py
import sqlite3


CREATE_QUERY = """
    CREATE TABLE users (
        id INTEGER,
        age INTEGER NOT NULL,
        name TEXT NOT NULL,
        PRIMARY KEY (id)
);
"""

INSERT_QUERY = """
    INSERT INTO users
    VALUES
        (1, 20, 'Alice'),
        (2, 30, 'Bob'),
        (3, 40, 'Carol');
"""

SELECT_QUERY = """
    SELECT *
    FROM users;
"""

def main():
    path = "data/users.sqlite"

    with open(path, mode="w") as f:
        conn = sqlite3.connect(f.name)
        cursor = conn.cursor()
        cursor.execute(CREATE_QUERY)
        cursor.execute(INSERT_QUERY)
        conn.commit()
        conn.close()

    with open(path, mode="r") as f:
        conn = sqlite3.connect(f.name)
        cursor = conn.cursor()
        cursor.execute(SELECT_QUERY)
        print(cursor.fetchall())


if __name__ == "__main__":
    main()
  • 出力
[(1, 20, 'Alice'), (2, 30, 'Bob'), (3, 40, 'Carol')]

参考: Python: テストで SQLite3 のインメモリデータベースを使うときの問題点と解決策 - CUBE SUGAR CONTAINER

Rust で SQLite バイナリを読み込んでクエリを実行する

Cargo.toml に rusqlite を追加する。

features には bundledbackup を指定する。

前者はユーザーシステムの SQLite への自動リンクを可能にし、後者は SQLite バイナリからデータベースを復元する際に必要となる。

  • Cargo.toml
[package]
name = "rust"
version = "0.1.0"
edition = "2021"

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

Connection::open_in_memory() でインメモリな SQLite データベースを構築し、それに対して restore() することで復元する。

その後はクエリを実行し、Rust で定義した構造体にクエリの実行結果をマップする。

出力内容が Python のものと一致していることが確認できる。

  • main.rs
use rusqlite::{Connection, DatabaseName};

#[derive(Debug)]
#[allow(dead_code)]
struct User {
    id: i32,
    age: i32,
    name: String,
}

fn main() {
    let path = "python/python3.10/data/users.sqlite";
    let path = format!("{}/../{}", env!("CARGO_MANIFEST_DIR"), path);
    let mut conn = Connection::open_in_memory().unwrap();
    conn.restore(DatabaseName::Main, &path, Some(|_| {})).unwrap();

    let mut stmt = conn.prepare("SELECT id, age, name FROM users").unwrap();
    let user_iter = stmt.query_map([], |row| {
        Ok(User {
            id: row.get(0).unwrap(),
            age: row.get(1).unwrap(),
            name: row.get(2).unwrap(),
        })
    }).unwrap();

    for user in user_iter {
        println!("Found user {:?}", user.unwrap());
    }
}
  • 出力
Found user User { id: 1, age: 20, name: "Alice" }
Found user User { id: 2, age: 30, name: "Bob" }
Found user User { id: 3, age: 40, name: "Carol" }

参考: GitHub - rusqlite/rusqlite: Ergonomic bindings to SQLite for Rust

まとめ

Rustqlite を使えば Rust でも SQLite を気軽に利用できそう。