
【2026年8月版】
Alopex DB v0.8.8チュートリアル 第5回:サーバーで開く
公開日: 2026/08/27
読了時間: 約 14分
第4回までは、Pythonのプロセスの中でデータベースを開いてきました。第5回では、同じデータディレクトリをalopex-serverで開き、HTTPから同じSQLを実行します。
v0.8.8では、公開版パッケージだけを使ったリリース検証で、ライブラリ・組み込み・CLI・HTTP・gRPC・cluster-awareの各入口が同じデータを読めることまで確認されています。ここでは、その確認を手元で再現するためのサーバー操作を扱います。
アプリが育ってデータベースを別プロセスへ分けるとき、通常はデータの移行が要ります。ダンプして、変換して、投入して、件数と中身を突き合わせる作業です。Alopex DBでは、第1回から使ってきたデータディレクトリをそのまま開きます。
1. 環境
| 項目 | 値 |
|---|---|
| Alopex DB | 0.8.8(PyPI / crates.io 公開版) |
| Python | 3.11.11 |
| OS | Linux(WSL2、glibc 2.35) |
| 確認日 | 2026年8月17日 |
サーバーとCLIを入れます。
cargo install alopex-cli --version "=0.8.8" --locked
cargo install alopex-server --version "=0.8.8" --locked2. エンジンへの五つの入口
Alopex DBには、同じデータへ届く経路が五つあります。
| サーフェス | 到達経路 | データ |
|---|---|---|
| SF-MEM | Database.new() / CLI --in-memory | メモリのみ |
| SF-FILE | Database.open(path) / CLI --data-dir | データディレクトリ |
| SF-HTTP | alopex-server の /api/sql/query | データディレクトリ |
| SF-GRPC | alopex-server のgRPCサーフェス | データディレクトリ |
| SF-CLUSTER | alopex-server([cluster] mode=cluster_aware) | データディレクトリ |
第1回のDatabase.new()がSF-MEM、Database.open()がSF-FILEです。永続化しないのはSF-MEMだけで、残る四つは同じデータディレクトリを開けます。
3. データディレクトリを作る
CLIでarticlesテーブルを作ります。次の内容をsetup.sqlに書きます。
CREATE TABLE articles (
id INTEGER PRIMARY KEY,
title TEXT,
author TEXT,
views INTEGER,
rating REAL
);
INSERT INTO articles VALUES (1, 'Rustで書くLSMツリー', 'mio', 1200, 3.5);
INSERT INTO articles VALUES (2, 'SQLパーサをNimで実装する', 'mio', 800, 4.5);
INSERT INTO articles VALUES (3, 'ベクトル検索の基礎', 'ren', 1500, 4.5);--data-dirでディレクトリを指定して実行すると、そこにデータが書かれます。
alopex --batch --output json --data-dir ./srv-data sql -f setup.sql./srv-dataディレクトリができます。このディレクトリは、Pythonからも読めます。
from alopex import Database
db = Database.open("./srv-data")
print(db.execute_sql("SELECT id, title, views FROM articles ORDER BY id"))[{'id': 1, 'title': 'Rustで書くLSMツリー', 'views': 1200},
{'id': 2, 'title': 'SQLパーサをNimで実装する', 'views': 800},
{'id': 3, 'title': 'ベクトル検索の基礎', 'views': 1500}]CLIで書いたデータを、Pythonがそのまま読み出します。第1回からDatabase.open()で開いてきたのと同じディレクトリです。
4. サーバーで開く
同じディレクトリをalopex-serverで開きます。設定をalopex.tomlに書きます。
data_dir = "./srv-data"
http_bind = "127.0.0.1:18080"
grpc_bind = "127.0.0.1:19090"
admin_bind = "127.0.0.1:18081"Pythonのプロセスを終了してから起動します。
alopex-server --config alopex.tomlログに設定が出ます。
INFO alopex_server::server: Cluster startup configuration applied
cluster_mode=SingleNode node_id=local role=Gateway
http_bind=127.0.0.1:18080 grpc_bind=127.0.0.1:19090 admin_bind=127.0.0.1:18081HTTPが18080、gRPCが19090、管理用が18081で待ち受けます。
データディレクトリは、一度に一つのプロセスから開きます。Pythonで開いたままサーバーを起動しないでください。
5. HTTPから同じSQLを実行する
別の端末から/api/sql/queryへPOSTします。
curl -s -X POST http://127.0.0.1:18080/api/sql/query \
-H 'Content-Type: application/json' \
-d '{"sql":"SELECT id, title, views FROM articles ORDER BY id"}'{"columns":[{"name":"id","data_type":"INTEGER"},
{"name":"title","data_type":"TEXT"},
{"name":"views","data_type":"INTEGER"}],
"rows":[[{"Integer":1},{"Text":"Rustで書くLSMツリー"},{"Integer":1200}],
[{"Integer":2},{"Text":"SQLパーサをNimで実装する"},{"Integer":800}],
[{"Integer":3},{"Text":"ベクトル検索の基礎"},{"Integer":1500}]],
"affected_rows":null}SQLは3章のPythonと同じ文です。書き換えていません。
上のJSONは、記事に載せるためにcolumns・rows・affected_rowsだけを抜き出しています。実際の応答には、これらに加えてresultsとrouting_diagnosticsが入ります。
返る形式は違います。Pythonが辞書のリストを返したのに対し、HTTPは列の定義と行の配列を分けて返し、値には型名が付きます。アプリのテストでこの二つを比べるときは、形ではなく値と列名で照合します。実行時間のようなメタデータまで固定すると、サーバーの更新のたびにテストが落ちます。
ここで、v0.8.8のサーバーへHTTPで行を追加する瞬間を録画しています。レスポンスのaffected_rows: 1と、local_onlyのルーティング判断を実際の実行結果で確認できます。
6. サーバー経由で書いた行をファイルから読む
HTTPから1行追加します。
curl -s -X POST http://127.0.0.1:18080/api/sql/query \
-H 'Content-Type: application/json' \
-d "{\"sql\":\"INSERT INTO articles VALUES (4, 'HTTPから追加した記事', 'sora', 500, 4.0)\"}"{"columns":[],"rows":[],"affected_rows":1}サーバーを停止し、Pythonから開き直します。
from alopex import Database
db = Database.open("./srv-data")
for row in db.execute_sql("SELECT id, title, author FROM articles ORDER BY id"):
print(row){'id': 1, 'title': 'Rustで書くLSMツリー', 'author': 'mio'}
{'id': 2, 'title': 'SQLパーサをNimで実装する', 'author': 'mio'}
{'id': 3, 'title': 'ベクトル検索の基礎', 'author': 'ren'}
{'id': 4, 'title': 'HTTPから追加した記事', 'author': 'sora'}HTTP経由で追加したid = 4を、ファイル経路から読み出せます。データディレクトリは、どの経路から書いても同じ場所に残ります。
7. healthzとadmin/status
管理用のポートでヘルスチェックを返します。
curl -s http://127.0.0.1:18081/healthzより詳しい状態は/api/admin/statusにあります。
curl -s http://127.0.0.1:18080/api/admin/status{"version": "0.8.8",
"uptime_secs": 20,
"cluster": {
"mode": "single_node",
"identity": {"node_id": "local", "role": "gateway",
"lifecycle_state": "unconfigured"},
"membership": {"source": "local_default", "members": []},
"routing_capabilities": {"local_only": true,
"future_distributed_execution_required": true}}}modeがsingle_node、routing_capabilities.local_onlyがtrueです。この設定では、SQLの実行はこのノードの中で完結します。
8. cluster-awareで起動する
alopex.tomlに[cluster]を足すと、cluster-awareで起動します。
data_dir = "./srv-data"
http_bind = "127.0.0.1:18080"
grpc_bind = "127.0.0.1:19090"
admin_bind = "127.0.0.1:18081"
[cluster]
mode = "cluster_aware"
node_id = "node-1"
cluster_id = "blog-cluster"
advertised_endpoint = "127.0.0.1:19090"node_id・cluster_id・advertised_endpointの三つは必須で、どれかを欠くと起動時に止まります。
failed to load config: invalid config: cluster.cluster_id is required when cluster.mode is cluster_aware起動すると/api/admin/statusの内容が変わります。
{"cluster": {
"mode": "cluster_aware",
"identity": {"node_id": "node-1", "cluster_id": "blog-cluster",
"advertised_endpoint": "127.0.0.1:19090",
"role": "gateway", "lifecycle_state": "active"},
"membership": {"source": "chirps", "members": []},
"routing_capabilities": {"local_only": true},
"degraded": false}}lifecycle_stateはunconfiguredからactiveになり、membership.sourceはlocal_defaultからchirpsに変わりますが、local_onlyはtrueのままです。この設定は複数ノードへSQLを分散実行する機能ではなく、分散実行を必要とする操作はfuture_distributed_execution_requiredとして拒否されます。cluster-awareで得られるのは、ノードのidentityとmembershipを外から読めることです。
監視で見るのはdegradedです。membershipの取得元が利用できない場合、単一ノード相当のフォールバックで起動し、degradedがtrueになります。起動したかどうかだけを見ていると、この状態に気づけません。
9. io_statsによる統計の読み出し
メモリとI/Oの統計は、通常のSQLとして読めます。
alopex --batch --output json --data-dir ./srv-data sql "SELECT io_stats()"{"io_stats": "{\"wal_write_bytes\":0,\"sstable_read_bytes\":0,
\"buffer_pool_hit_rate\":1,\"buffer_pool_size_bytes\":0,
\"memtable_size_bytes\":1656,\"compaction_bytes_written\":0}"}WAL書き込み量、SSTable読み込み量、buffer pool hit rate、memtableサイズ、compaction書き込み量が返ります。値はJSON文字列なので、使うときはもう一度パースします。ほかにmemory_stats()とclear_cache()があります。
ダッシュボードへ送る前に、空のデータベースでどう見えるか、書き込み後にどの値が増えるかを手元で確かめておくと、異常値の判断がつきます。
10. ここまでで使ったもの
| 対象 | 役割 |
|---|---|
alopex --data-dir <dir> sql -f <file> | CLIからデータディレクトリを操作する |
alopex.toml の data_dir | サーバーが開くディレクトリを指定する |
POST /api/sql/query | HTTPからSQLを実行する |
GET /healthz(管理ポート) | 起動したかを確認する |
GET /api/admin/status | バージョンとcluster状態を読む |
SELECT io_stats() | I/Oの統計を読む |
Database::open(&Path) | Rustから組み込みでディレクトリを開く |
ureq::post(...).send_json(...) | RustからHTTPでSQLを実行する |
11. Rustから組み込みとHTTPで接続する
Rustからも二つの経路でデータを開けます。組み込みでディレクトリを直接開く経路と、HTTPでサーバーへ問い合わせる経路です。
組み込みで開く
6章でHTTPから追加したid = 4の行を、Rustのプログラムから読みます。サーバーを停止してから実行します。
[dependencies]
alopex-embedded = "=0.8.8"use alopex_embedded::{Database, SqlResult};
use std::path::Path;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let db = Database::open(Path::new("./srv-data"))?;
if let SqlResult::Query(q) = db.execute_sql("SELECT id, title, author FROM articles ORDER BY id")? {
for row in &q.rows {
let cells: Vec<String> = row.iter().map(|v| format!("{v:?}")).collect();
println!("{}", cells.join(" | "));
}
}
Ok(())
}Integer(1) | Text("Rustで書くLSMツリー") | Text("mio")
Integer(2) | Text("SQLパーサをNimで実装する") | Text("mio")
Integer(3) | Text("ベクトル検索の基礎") | Text("ren")
Integer(4) | Text("HTTPから追加した記事") | Text("sora")CLIで作り、HTTPから1行足したディレクトリを、Rustがそのまま読み出します。6章のPythonと同じ4行です。書いた経路と読む経路が違っても、データディレクトリは一つです。
HTTPで開く
同じ問い合わせを、今度はサーバー越しに実行します。alopex-embeddedは要りません。HTTPクライアントとJSONの読み取りがあれば足ります。
[dependencies]
ureq = { version = "2", features = ["json"] }
serde_json = "1"サーバーを起動し直してから実行します。
use serde_json::json;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let body: serde_json::Value = ureq::post("http://127.0.0.1:18080/api/sql/query")
.send_json(json!({"sql": "SELECT id, title, views FROM articles ORDER BY id"}))?
.into_json()?;
let names: Vec<&str> = body["columns"].as_array().unwrap().iter()
.map(|c| c["name"].as_str().unwrap()).collect();
println!("{}", names.join(" | "));
for row in body["rows"].as_array().unwrap() {
let cells: Vec<String> = row.as_array().unwrap().iter()
.map(|v| v.as_object().unwrap().values().next().unwrap().to_string())
.collect();
println!("{}", cells.join(" | "));
}
Ok(())
}id | title | views
1 | "Rustで書くLSMツリー" | 1200
2 | "SQLパーサをNimで実装する" | 800
3 | "ベクトル検索の基礎" | 1500
4 | "HTTPから追加した記事" | 500行の各要素は{"Integer": 1}のように、型名をキーにした一つ組のオブジェクトです。上のコードがvalues().next()で最初の値だけを取り出しているのは、この形のためです。組み込みのSqlValue::Integer(1)を、JSONで表したものが{"Integer":1}です。
二つのプログラムで、SQLの文は同じです。変えたのは接続の方法だけです。組み込みの側はDatabase::open()でディレクトリを指し、HTTPの側はURLを指しています。
アプリを組み込みで書き始めて、後からサーバーへ移すとき、書き換えるのはこの接続の部分です。SQLとデータディレクトリはそのまま使えます。
まとめ
この回では、一つの./srv-dataを四つの経路から開きました。CLIの--data-dirでarticlesを作り、PythonのDatabase.open()で読み、alopex-serverのHTTPでid = 4を追加し、RustのDatabase::open()でその4行を読み出しています。2章の表でいうSF-FILEとSF-HTTPです。v0.8.8の公開検証では、ここにgRPCとcluster-awareを加えた全サーフェスのモード一致も確認されています。
このときSELECT id, title, views FROM articles ORDER BY idという一つのSQL文を、経路を変えても書き換えていません。変えたのは、Database.open("./srv-data")とPOST /api/sql/queryという接続の指定だけです。ダンプと投入によるデータの移行は挟んでいません。
経路によって違うのは結果の形式です。Pythonは辞書のリスト、Rustの組み込みはSqlResult::Query、HTTPはcolumnsとrowsを分けたJSONを返します。値と列名は一致するので、経路をまたぐテストは形ではなくこの二つで照合します。
[cluster] mode=cluster_awareを足すと、/api/admin/statusのlifecycle_stateがactiveに、membership.sourceがchirpsに変わります。v0.8.8でここまでが実際に使えるようになった一方、local_onlyはtrueのままです。単一メンバーでのcluster-aware起動と既存データの読み出しはできますが、複数ノードへの分散実行を要する操作はfuture_distributed_execution_requiredとして拒否されます。この設定で外から読めるのは、ノードのidentityとmembershipです。