Rustで作る自作データベース:第1回「基本的なSQL対応RDBMSの設計と実装」
この記事では、前回
🔸モノリスではコード量が多くなる可能性
🔸将来的な機能追加を考慮
🔸依存関係を整理
以上の点からクリーンアーキテクチャを採用しています。ただし、従来のクリーンアーキテクチャのようにユースケースを一つ一つ実装していくのはデータベースを実装する上では難しいので、ディレクトリ構成やDIパターンに基づいて実装しています。
※コード量が多いので、お急ぎの方はまずは保存して、時間のあるときにじっくり読むことを推奨します
それでは、これからRustで実装した基本的なSQLデータベースの構造とコードを解説します。繰り返しになりますが、クリーンアーキテクチャに基づいた設計で、SQLクエリのパース、インメモリストレージ、REST APIインターフェースまでを含んでいます。
プロジェクト構造🗃️
rustydb/
├── src/
│ ├── domain/ # ドメイン層(中心的な概念)
│ │ ├── entity/ # 基本エンティティ
│ │ │ ├── data_type.rs # データ型定義
│ │ │ ├── value.rs # 値の表現
│ │ │ ├── column.rs # カラム定義
│ │ │ ├── table.rs # テーブル構造
│ │ │ └── mod.rs # エンティティモジュール
│ │ ├── repository/ # データアクセス抽象化
│ │ │ ├── table_repository.rs # リポジトリインターフェース
│ │ │ └── mod.rs # リポジトリモジュール
│ │ └── mod.rs # ドメイン層モジュール
│ ├── application/ # アプリケーション層(未実装)
│ │ └── mod.rs
│ ├── infrastructure/ # インフラストラクチャ層(具体的実装)
│ │ ├── parser/ # SQLパーサー
│ │ │ ├── sql_parser.rs # SQL解析実装
│ │ │ └── mod.rs # パーサーモジュール
│ │ ├── storage/ # ストレージエンジン
│ │ │ ├── memory.rs # インメモリ実装
│ │ │ └── mod.rs # ストレージモジュール
│ │ ├── repository/ # リポジトリ実装
│ │ │ ├── memory_repository.rs # インメモリリポジトリ
│ │ │ └── mod.rs # リポジトリ実装モジュール
│ │ └── mod.rs # インフラ層モジュール
│ ├── interface/ # インターフェース層(UI/API)
│ │ ├── api/ # REST API
│ │ │ ├── server.rs # サーバー設定
│ │ │ ├── handler.rs # リクエストハンドラー
│ │ │ └── mod.rs # API モジュール
│ │ ├── cli/ # コマンドライン(未実装)
│ │ │ └── mod.rs
│ │ └── mod.rs # インターフェース層モジュール
│ ├── lib.rs # ライブラリルート
│ └── main.rs # エントリーポイント
├── examples/ # 使用例
│ ├── basic_usage.rs # 基本的な使用例
│ └── api_client.rs # API クライアント例
├── Cargo.toml # 依存関係定義
└── README.md # プロジェクト概要
Cargo.toml
[package]
name = "rustydb"
version = "0.1.0"
edition = "2021"
authors = ["yoshimura.hisa@gmail.com"]
description = "A Rust-based database system for educational purposes"
[dependencies]
# コア依存関係
sqlparser = "0.35" # SQLパーサー
serde = { version = "1.0", features = ["derive"] } # シリアライズ/デシリアライズ
serde_json = "1.0" # JSONサポート
thiserror = "1.0" # エラー定義
async-trait = "0.1" # 非同期トレイト
# Webフレームワーク
axum = "0.6" # Webサーバー
tower = "0.4" # HTTPミドルウェア
tower-http = { version = "0.4", features = ["cors", "trace"] }
# 非同期ランタイム
tokio = { version = "1", features = ["full"] }
# ロギング
tracing = "0.1" # 構造化ロギング
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# 開発効率化
derive_more = "0.99" # ボイラープレート削減
typed-builder = "0.16" # ビルダーパターン
strum = { version = "0.25", features = ["derive"] }
# ユーティリティ
bytes = "1.4" # バイト操作
itertools = "0.11" # イテレータ拡張
# 日付・時刻操作
chrono = { version = "0.4", features = ["serde"] }
# APIクライアントテスト用
reqwest = { version = "0.11", features = ["json"] }
[dev-dependencies]
# テスト用クレート
proptest = "1.2" # プロパティベーステスト
test-case = "3.1" # パラメータ化テスト
mockall = "0.11" # モックオブジェクト
criterion = "0.5" # ベンチマーク
[[bench]]
name = "query_benchmarks"
harness = false
主要コンポーネントの解説
src/domain/
)
1. ドメイン層 (ドメイン層はデータベースの中心的な概念を定義します。SQL文の解析結果やデータの操作方法に関わらず、データベースとして必要な基本的な概念はここに定義されます。
src/domain/entity/
)
1.1 エンティティ (データ型定義
// src/domain/entity/data_type.rs
use derive_more::Display;
use serde::{Deserialize, Serialize};
use std::fmt;
use strum::EnumString;
/// データベースでサポートされるデータ型
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize)]
pub enum DataType {
/// 整数型(64ビット符号付き整数)
#[strum(serialize = "INTEGER")]
Integer,
/// 浮動小数点型(64ビット)
#[strum(serialize = "FLOAT")]
Float,
/// 文字列型(UTF-8)
#[strum(serialize = "TEXT")]
Text,
/// 真偽値型
#[strum(serialize = "BOOLEAN")]
Boolean,
/// 日付時刻型
#[strum(serialize = "TIMESTAMP")]
Timestamp,
/// NULL値が許容される型を表す装飾子
#[strum(serialize = "NULL")]
Null,
}
/// SQL型制約の表現
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Constraint {
/// プライマリキー制約
PrimaryKey,
/// 一意制約
Unique,
/// NULLを許さない制約
NotNull,
/// デフォルト値制約
Default(String),
}
値の表現
// src/domain/entity/value.rs
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;
use crate::domain::entity::data_type::DataType;
use thiserror::Error;
/// 値型エラー
#[derive(Error, Debug, PartialEq)]
pub enum ValueError {
#[error("Type mismatch: expected {expected}, got {actual}")]
TypeMismatch {
expected: DataType,
actual: DataType,
},
#[error("Cannot convert from {0} to {1}")]
ConversionError(String, String),
#[error("Null value not allowed")]
NullValueError,
}
/// データベース内の値の表現
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Value {
/// 整数値
Integer(i64),
/// 浮動小数点値
Float(f64),
/// 文字列値
Text(String),
/// 真偽値
Boolean(bool),
/// 日時値
Timestamp(DateTime<Utc>),
/// NULL値
Null,
}
カラム定義
// src/domain/entity/column.rs
use crate::domain::entity::data_type::{Constraint, DataType};
use derive_more::Display;
use serde::{Deserialize, Serialize};
use typed_builder::TypedBuilder;
/// テーブルカラムを表現する構造体
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TypedBuilder)]
pub struct Column {
/// カラム名
pub name: String,
/// カラムのデータ型
pub data_type: DataType,
/// カラムに適用される制約のリスト
#[builder(default)]
pub constraints: Vec<Constraint>,
}
テーブル構造
// src/domain/entity/table.rs
use crate::domain::entity::column::Column;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
/// データベーステーブルを表現する構造体
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Table {
/// テーブル名
pub name: String,
/// テーブルのカラム
pub columns: Vec<Column>,
}
/// 1行のデータを表現する
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Row {
/// カラム名と値のマッピング
pub values: HashMap<String, crate::domain::entity::value::Value>,
}
/// クエリ結果セットを表現する
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ResultSet {
/// 結果セットのスキーマ(カラム定義)
pub columns: Vec<Column>,
/// 結果の行データ
pub rows: Vec<Row>,
}
src/domain/repository/
)
1.2 リポジトリ (リポジトリインターフェース
// src/domain/repository/table_repository.rs
use async_trait::async_trait;
use crate::domain::entity::table::{Table, Row, ResultSet};
use crate::domain::entity::value::Value;
use thiserror::Error;
use std::sync::Arc;
/// テーブルリポジトリのエラー型
#[derive(Error, Debug)]
pub enum RepositoryError {
#[error("Table {0} not found")]
TableNotFound(String),
#[error("Table {0} already exists")]
TableAlreadyExists(String),
#[error("Column {0} not found in table {1}")]
ColumnNotFound(String, String),
#[error("Storage error: {0}")]
StorageError(String),
#[error("Data error: {0}")]
DataError(String),
#[error("Internal error: {0}")]
InternalError(String),
}
/// クエリフィルター条件
#[derive(Debug, Clone)]
pub enum FilterCondition {
/// 単一条件(カラム名、演算子、値)
Simple {
column: String,
operator: FilterOperator,
value: Value,
},
/// 複数条件のAND結合
And(Vec<FilterCondition>),
/// 複数条件のOR結合
Or(Vec<FilterCondition>),
}
/// フィルター演算子
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FilterOperator {
Equal,
NotEqual,
Greater,
GreaterOrEqual,
Less,
LessOrEqual,
Like,
}
/// テーブルリポジトリ - データベーステーブルの永続化と取得のための抽象インターフェース
#[async_trait]
pub trait TableRepository: Send + Sync {
/// データベースに新しいテーブルを作成する
async fn create_table(&self, table: &Table) -> Result<(), RepositoryError>;
/// テーブルが存在するかどうかを確認する
async fn table_exists(&self, table_name: &str) -> Result<bool, RepositoryError>;
/// テーブルを削除する
async fn drop_table(&self, table_name: &str) -> Result<(), RepositoryError>;
/// 名前でテーブルを取得する
async fn get_table(&self, table_name: &str) -> Result<Table, RepositoryError>;
/// すべてのテーブル名を取得する
async fn get_table_names(&self) -> Result<Vec<String>, RepositoryError>;
/// テーブルに1行のデータを挿入する
async fn insert(&self, table_name: &str, row: &Row) -> Result<(), RepositoryError>;
/// 複数行のデータを一括挿入する
async fn insert_many(&self, table_name: &str, rows: &[Row]) -> Result<(), RepositoryError>;
/// テーブルからデータを取得する
async fn select(
&self,
table_name: &str,
columns: &[String],
filter: Option<&FilterCondition>
) -> Result<ResultSet, RepositoryError>;
/// 条件に合致する行を更新する
async fn update(
&self,
table_name: &str,
updates: &[(String, Value)],
filter: Option<&FilterCondition>
) -> Result<usize, RepositoryError>;
/// 条件に合致する行を削除する
async fn delete(
&self,
table_name: &str,
filter: Option<&FilterCondition>
) -> Result<usize, RepositoryError>;
}
src/infrastructure/
)
2. インフラストラクチャ層 (インフラストラクチャ層はドメイン層で定義されたインターフェースの具体的な実装を提供します。
src/infrastructure/parser/
)
2.1 パーサー (SQL解析実装
// src/infrastructure/parser/sql_parser.rs
use sqlparser::dialect::GenericDialect;
use sqlparser::parser::Parser;
use sqlparser::ast::{Statement, Query, SetExpr, TableFactor, Values, Expr, Value as SqlValue,
SelectItem, ObjectName, Ident, TableWithJoins};
use crate::domain::entity::{DataType, Column, Value};
use crate::domain::repository::{FilterCondition, FilterOperator};
use thiserror::Error;
/// SQL解析エラー
#[derive(Error, Debug)]
pub enum ParseError {
#[error("SQL syntax error: {0}")]
SyntaxError(String),
#[error("Unsupported SQL feature: {0}")]
UnsupportedFeature(String),
#[error("Invalid data type: {0}")]
InvalidDataType(String),
#[error("Invalid value: {0}")]
InvalidValue(String),
#[error("Internal parser error: {0}")]
InternalError(String),
}
/// SQLパーサーの実装
pub struct SqlParser {
dialect: GenericDialect,
}
/// 解析されたSQL文
pub enum ParsedStatement {
CreateTable(CreateTableStatement),
Select(SelectStatement),
Insert(InsertStatement),
Update(UpdateStatement),
Delete(DeleteStatement),
DropTable(DropTableStatement),
}
src/infrastructure/storage/
)
2.2 ストレージ (インメモリストレージ実装
// src/infrastructure/storage/memory.rs
use std::collections::HashMap;
use std::sync::RwLock;
use crate::domain::entity::{Table, Column, Row, Value, DataType};
use crate::domain::repository::{FilterCondition, FilterOperator};
use thiserror::Error;
/// ストレージエラー
#[derive(Error, Debug)]
pub enum StorageError {
#[error("Table {0} not found")]
TableNotFound(String),
#[error("Table {0} already exists")]
TableAlreadyExists(String),
// ... 他のエラー
}
/// テーブルのデータを保持する構造体
#[derive(Debug, Clone)]
struct TableData {
schema: Table,
rows: Vec<Row>,
}
/// インメモリストレージの実装
#[derive(Debug, Default)]
pub struct MemoryStorage {
tables: RwLock<HashMap<String, TableData>>,
}
src/infrastructure/repository/
)
2.3 リポジトリ実装 (インメモリリポジトリ実装
// src/infrastructure/repository/memory_repository.rs
use std::sync::Arc;
use async_trait::async_trait;
use crate::domain::entity::{Table, Row, Value, ResultSet};
use crate::domain::repository::{TableRepository, RepositoryError, FilterCondition};
use crate::infrastructure::storage::{MemoryStorage, StorageError};
/// インメモリリポジトリの実装
pub struct MemoryTableRepository {
storage: Arc<MemoryStorage>,
}
#[async_trait]
impl TableRepository for MemoryTableRepository {
// TableRepositoryトレイトのメソッド実装
async fn create_table(&self, table: &Table) -> Result<(), RepositoryError> {
// 実装...
}
// 他のメソッド実装...
}
src/interface/
)
3. インターフェース層 (インターフェース層はユーザーがデータベースと対話するためのインターフェースを提供します。
src/interface/api/
)
3.1 API (サーバー設定
// src/interface/api/server.rs
use axum::{
Router,
routing::{get, post},
Extension,
Server,
};
use std::net::SocketAddr;
use std::sync::Arc;
use tracing::info;
use crate::domain::repository::TableRepository;
use crate::infrastructure::storage::MemoryStorage;
use crate::infrastructure::repository::MemoryTableRepository;
use crate::infrastructure::parser::SqlParser;
use crate::interface::api::handler::{
health_check_handler,
get_tables_handler,
get_table_handler,
execute_sql_handler
};
#[derive(Clone)]
pub struct ServerConfig {
pub port: u16,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
port: 8080, // デフォルトポート番号
}
}
}
pub async fn start_server(config: ServerConfig) -> Result<(), Box<dyn std::error::Error>> {
// ストレージとリポジトリの初期化
let storage = Arc::new(MemoryStorage::new());
let repository: Arc<dyn TableRepository> = Arc::new(MemoryTableRepository::new(storage.clone()));
// SQLパーサーの初期化
let parser = Arc::new(SqlParser::new());
// ルーターの設定
let app = Router::new()
.route("/health", get(health_check_handler))
.route("/api/tables", get(get_tables_handler))
.route("/api/tables/:table_name", get(get_table_handler))
.route("/api/query", post(execute_sql_handler))
.layer(Extension(repository)) // リポジトリの拡張
.layer(Extension(parser)); // パーサーの拡張
// サーバーのアドレス設定
let addr = SocketAddr::from(([0, 0, 0, 0], config.port));
info!("サーバーを{}で起動中...", addr);
// サーバーの起動
Server::bind(&addr)
.serve(app.into_make_service())
.await
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
}
リクエストハンドラー
// src/interface/api/handler.rs
use axum::{
extract::{Path, Json, Extension},
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use thiserror::Error;
use crate::domain::repository::TableRepository;
use crate::infrastructure::parser::{SqlParser, ParsedStatement};
/// SQLクエリのリクエスト
#[derive(Deserialize)]
pub struct QueryRequest {
sql: String,
}
/// クエリ実行結果
#[derive(Serialize)]
pub struct QueryResult {
#[serde(skip_serializing_if = "Option::is_none")]
columns: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
rows: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
affected_rows: Option<usize>,
statement_type: String,
}
/// SQL実行ハンドラー
pub async fn execute_sql_handler(
Extension(repository): Extension<Arc<dyn TableRepository>>,
Extension(parser): Extension<Arc<SqlParser>>,
Json(payload): Json<QueryRequest>,
) -> Result<Json<QueryResult>, ApiError> {
// SQLの解析と実行...
}
4. その他の重要なファイル
ライブラリルート
// src/lib.rs
pub mod domain;
pub mod application;
pub mod infrastructure;
pub mod interface;
/// RustyDB version
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
/// Database result type
pub type Result<T> = std::result::Result<T, Error>;
/// Database error type
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("SQL parsing error: {0}")]
Parse(String),
#[error("Schema error: {0}")]
Schema(String),
#[error("Execution error: {0}")]
Execution(String),
#[error("Storage error: {0}")]
Storage(String),
#[error("Internal error: {0}")]
Internal(String),
}
エントリーポイント
// src/main.rs
use tracing::info;
use rustydb::interface::api::{start_server, ServerConfig};
use rustydb::VERSION;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// ロガーを初期化
tracing_subscriber::fmt::init();
info!("Starting RustyDB v{}", VERSION);
// サーバー設定(デフォルト:localhost:8080)
let config = ServerConfig::default();
// サーバーの起動
start_server(config).await?;
Ok(())
}
examples/
)
使用例 (基本的な使用例
// examples/basic_usage.rs
use rustydb::domain::entity::{ Table, Row};
use rustydb::domain::repository::TableRepository;
use rustydb::infrastructure::parser::SqlParser;
use rustydb::infrastructure::storage::MemoryStorage;
use rustydb::infrastructure::repository::MemoryTableRepository;
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// ストレージとリポジトリの初期化
let storage = Arc::new(MemoryStorage::new());
let repository = MemoryTableRepository::new(storage.clone());
// SQLパーサーの初期化
let parser = SqlParser::new();
println!("=== RustyDB 基本動作チェック ===\n");
// 1. テーブル作成
println!("1. テーブルの作成");
let create_table_sql = "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER, active BOOLEAN DEFAULT true)";
println!("SQL: {}", create_table_sql);
let parsed = parser.parse(create_table_sql)?;
if let Some(rustydb::infrastructure::parser::ParsedStatement::CreateTable(stmt)) = parsed.first() {
let mut table = Table::new(&stmt.table_name);
for column in &stmt.columns {
table.add_column(column.clone())?;
}
repository.create_table(&table).await?;
println!("テーブル 'users' を作成しました\n");
}
// 2. データ挿入
println!("2. データの挿入");
let insert_sql = "INSERT INTO users (id, name, age, active) VALUES (1, 'Alice', 30, true), (2, 'Bob', 25, false), (3, 'Charlie', 35, true)";
println!("SQL: {}", insert_sql);
let parsed = parser.parse(insert_sql)?;
if let Some(rustydb::infrastructure::parser::ParsedStatement::Insert(stmt)) = parsed.first() {
for values in &stmt.values {
let mut row = Row::new();
for (i, value) in values.iter().enumerate() {
if i < stmt.columns.len() {
row.set(stmt.columns[i].clone(), value.clone());
}
}
repository.insert(&stmt.table_name, &row).await?;
}
println!("3行挿入しました\n");
}
// 3. データ取得
println!("3. データの取得");
let select_sql = "SELECT * FROM users";
println!("SQL: {}", select_sql);
let parsed = parser.parse(select_sql)?;
if let Some(rustydb::infrastructure::parser::ParsedStatement::Select(stmt)) = parsed.first() {
let cols = stmt.columns.as_ref().map_or(Vec::new(), |c| c.clone());
let result = repository.select(&stmt.table_name, &cols, stmt.filter.as_ref()).await?;
// 結果の表示
println!("\n結果:");
// ヘッダーの表示
for col in &result.columns {
print!("{}\t", col.name);
}
println!();
// 行の表示
for row in &result.rows {
for col in &result.columns {
match row.get(&col.name) {
Some(value) => print!("{}\t", value),
None => print!("NULL\t"),
}
}
println!();
}
println!();
}
// 4. データ更新
println!("4. データの更新");
let update_sql = "UPDATE users SET age = 31 WHERE id = 1";
println!("SQL: {}", update_sql);
let parsed = parser.parse(update_sql)?;
if let Some(rustydb::infrastructure::parser::ParsedStatement::Update(stmt)) = parsed.first() {
let count = repository.update(&stmt.table_name, &stmt.updates, stmt.filter.as_ref()).await?;
println!("{}行更新しました\n", count);
}
// 更新後のデータを表示
println!("5. 更新後のデータ確認");
let select_sql = "SELECT * FROM users WHERE id = 1";
println!("SQL: {}", select_sql);
let parsed = parser.parse(select_sql)?;
if let Some(rustydb::infrastructure::parser::ParsedStatement::Select(stmt)) = parsed.first() {
let cols = stmt.columns.as_ref().map_or(Vec::new(), |c| c.clone());
let result = repository.select(&stmt.table_name, &cols, stmt.filter.as_ref()).await?;
// 結果の表示
println!("\n結果:");
// ヘッダーの表示
for col in &result.columns {
print!("{}\t", col.name);
}
println!();
// 行の表示
for row in &result.rows {
for col in &result.columns {
match row.get(&col.name) {
Some(value) => print!("{}\t", value),
None => print!("NULL\t"),
}
}
println!();
}
println!();
}
// 6. データ削除
println!("6. データの削除");
let delete_sql = "DELETE FROM users WHERE active = false";
println!("SQL: {}", delete_sql);
let parsed = parser.parse(delete_sql)?;
if let Some(rustydb::infrastructure::parser::ParsedStatement::Delete(stmt)) = parsed.first() {
let count = repository.delete(&stmt.table_name, stmt.filter.as_ref()).await?;
println!("{}行削除しました\n", count);
}
// 削除後のデータを表示
println!("7. 削除後のデータ確認");
let select_sql = "SELECT * FROM users";
println!("SQL: {}", select_sql);
let parsed = parser.parse(select_sql)?;
if let Some(rustydb::infrastructure::parser::ParsedStatement::Select(stmt)) = parsed.first() {
let cols = stmt.columns.as_ref().map_or(Vec::new(), |c| c.clone());
let result = repository.select(&stmt.table_name, &cols, stmt.filter.as_ref()).await?;
// 結果の表示
println!("\n結果:");
// ヘッダーの表示
for col in &result.columns {
print!("{}\t", col.name);
}
println!();
// 行の表示
for row in &result.rows {
for col in &result.columns {
match row.get(&col.name) {
Some(value) => print!("{}\t", value),
None => print!("NULL\t"),
}
}
println!();
}
println!();
}
println!("テスト完了!");
Ok(())
}
APIクライアント例
// examples/api_client.rs
use reqwest::Client;
use serde_json::{json, Value};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let base_url = "http://localhost:8080";
println!("=== RustyDB API テスト ===\n");
// 1. ヘルスチェック
println!("1. ヘルスチェック");
let resp = client.get(format!("{}/health", base_url)).send().await?;
println!("ステータス: {}", resp.status());
println!("レスポンス: {}", resp.text().await?);
println!();
// 2. テーブル作成
println!("2. テーブル作成");
let create_query = json!({
"sql": "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER, active BOOLEAN DEFAULT true)"
});
let resp = client.post(format!("{}/api/query", base_url))
.json(&create_query)
.send()
.await?;
println!("ステータス: {}", resp.status());
println!("レスポンス: {}", resp.text().await?);
println!();
// 3. データ挿入
println!("3. データ挿入");
let insert_query = json!({
"sql": "INSERT INTO users (id, name, age, active) VALUES (1, 'Alice', 30, true), (2, 'Bob', 25, false), (3, 'Charlie', 35, true)"
});
let resp = client.post(format!("{}/api/query", base_url))
.json(&insert_query)
.send()
.await?;
println!("ステータス: {}", resp.status());
println!("レスポンス: {}", resp.text().await?);
println!();
// 4. データ取得
println!("4. データ取得");
let select_query = json!({
"sql": "SELECT * FROM users"
});
let resp = client.post(format!("{}/api/query", base_url))
.json(&select_query)
.send()
.await?;
println!("ステータス: {}", resp.status());
let result_text = resp.text().await?;
println!("レスポンス: {}", result_text);
// JSON形式のレスポンスをきれいに表示
if let Ok(result) = serde_json::from_str::<Value>(&result_text) {
println!("整形レスポンス: {}", serde_json::to_string_pretty(&result)?);
}
println!();
// 5. テーブル一覧取得
println!("5. テーブル一覧取得");
let resp = client.get(format!("{}/api/tables", base_url))
.send()
.await?;
println!("ステータス: {}", resp.status());
println!("レスポンス: {}", resp.text().await?);
println!();
// 6. テーブル詳細取得
println!("6. テーブル詳細取得");
let resp = client.get(format!("{}/api/tables/users", base_url))
.send()
.await?;
println!("ステータス: {}", resp.status());
let result_text = resp.text().await?;
println!("レスポンス: {}", result_text);
// JSON形式のレスポンスをきれいに表示
if let Ok(result) = serde_json::from_str::<Value>(&result_text) {
println!("整形レスポンス: {}", serde_json::to_string_pretty(&result)?);
}
println!();
// 7. データ更新
println!("7. データ更新");
let update_query = json!({
"sql": "UPDATE users SET age = 31 WHERE id = 1"
});
let resp = client.post(format!("{}/api/query", base_url))
.json(&update_query)
.send()
.await?;
println!("ステータス: {}", resp.status());
println!("レスポンス: {}", resp.text().await?);
println!();
// 8. 更新後のデータ確認
println!("8. 更新後のデータ確認");
let select_query = json!({
"sql": "SELECT * FROM users WHERE id = 1"
});
let resp = client.post(format!("{}/api/query", base_url))
.json(&select_query)
.send()
.await?;
println!("ステータス: {}", resp.status());
let result_text = resp.text().await?;
println!("レスポンス: {}", result_text);
// JSON形式のレスポンスをきれいに表示
if let Ok(result) = serde_json::from_str::<Value>(&result_text) {
println!("整形レスポンス: {}", serde_json::to_string_pretty(&result)?);
}
println!();
println!("APIテスト完了!");
Ok(())
}
まとめ
この実装では、クリーンアーキテクチャを採用することで、各レイヤーが明確に分離され、変更に強い設計になっています。ドメイン層でデータベースの基本概念を定義し、インフラストラクチャ層で具体的な実装を提供し、インターフェース層でユーザーとの対話を可能にしています。
現時点の実装はインメモリストレージのみをサポートしていますが、ドメインロジックを変更せずに、永続化ストレージやインデックス機能を追加することが可能です。また、RESTful APIを通じて、さまざまなクライアントからデータベースを操作できます。
次回は、B-Treeインデックスの実装とクエリ最適化に取り組み、より高速で効率的なデータベースを目指します。
見逃さないように僕のフォロー&いいねもお忘れなく🤗
リポジトリ
この連載のコードはこちらのGitHubリポジトリで公開しています:
実装を進める中で、質問やフィードバックがあれば、コメントやIssueでお気軽にお知らせください。
一緒にRustでデータベースを作る旅を楽しみましょう!
Discussion
記事の構造を改善しました。