|
| 1 | +# Struct-Based API Design and Implementation |
| 2 | + |
| 3 | +## 概要 |
| 4 | + |
| 5 | +本文書では、sqlc-rust-postgresプラグインにおける構造体ベースAPI(Struct-Based API)の設計、実装戦略、および技術的判断の根拠について詳述する。この機能は、現在の関数ベースAPIの課題を解決しつつ、ゼロコスト抽象化を実現する新しいAPIパターンを提供する。 |
| 6 | + |
| 7 | +## 背景と問題意識 |
| 8 | + |
| 9 | +### 現在の課題 |
| 10 | + |
| 11 | +現在のsqlc-rust-postgresプラグインが生成するAPIには、以下の重要な問題がある: |
| 12 | + |
| 13 | +#### 1. 関数引数の肥大化問題 |
| 14 | + |
| 15 | +SQLクエリのパラメータが増加すると、生成される関数の引数が過度に多くなる: |
| 16 | + |
| 17 | +```rust |
| 18 | +// 問題のある例:引数が多すぎて可読性・保守性が低い |
| 19 | +pub async fn update_user_profile( |
| 20 | + client: &Client, |
| 21 | + user_id: i64, |
| 22 | + name: &str, |
| 23 | + email: &str, |
| 24 | + phone: &str, |
| 25 | + address: &str, |
| 26 | + birth_date: Option<chrono::NaiveDate>, |
| 27 | + is_verified: bool, |
| 28 | + updated_at: chrono::DateTime<chrono::Utc>, |
| 29 | +) -> Result<UpdateUserProfileRow, Error> |
| 30 | +``` |
| 31 | + |
| 32 | +#### 2. パラメータ順序の誤用リスク |
| 33 | + |
| 34 | +引数の順序は型システムでは検証されないため、SQLクエリの変更により引数順序が変わっても、型が一致していればコンパイルエラーにならない: |
| 35 | + |
| 36 | +```rust |
| 37 | +// SQLクエリ変更前:WHERE user_id = $1 AND status = $2 |
| 38 | +get_user_by_status(client, user_id, status).await?; |
| 39 | + |
| 40 | +// SQLクエリ変更後:WHERE status = $1 AND user_id = $2 |
| 41 | +// ↓ 引数順序が入れ替わったが、型は同じなのでコンパイルエラーにならない |
| 42 | +get_user_by_status(client, user_id, status).await?; // バグ! |
| 43 | +``` |
| 44 | + |
| 45 | +#### 3. 型安全性の欠如 |
| 46 | + |
| 47 | +関数の引数リストでは、どの値がどのSQLパラメータに対応するかが位置に依存するため、リファクタリング時の安全性が低い。 |
| 48 | + |
| 49 | +### 解決すべき要求仕様 |
| 50 | + |
| 51 | +上記課題を踏まえ、以下の要求仕様を満たすAPIを設計する: |
| 52 | + |
| 53 | +1. **型安全性の向上**: パラメータの設定漏れや重複設定をコンパイル時に検出 |
| 54 | +2. **可読性の向上**: どの値がどのパラメータに対応するかが明確 |
| 55 | +3. **保守性の向上**: SQLクエリの変更に対してより堅牢 |
| 56 | +4. **パフォーマンス**: ゼロコスト抽象化の実現 |
| 57 | +5. **後方互換性**: 既存APIを破壊しない |
| 58 | + |
| 59 | +## 設計アプローチ |
| 60 | + |
| 61 | +### 基本設計思想 |
| 62 | + |
| 63 | +本実装では **型状態パターン(Type State Pattern)** を採用し、以下の原則に基づいて設計する: |
| 64 | + |
| 65 | +#### 1. コンパイル時安全性の最大化 |
| 66 | + |
| 67 | +Rustの型システムを活用し、実行時エラーを可能な限りコンパイル時エラーに変換する: |
| 68 | + |
| 69 | +```rust |
| 70 | +// コンパイル時に全パラメータの設定を強制 |
| 71 | +let query = GetUser::builder() |
| 72 | + .user_id(123) // 必須パラメータ |
| 73 | + .include_deleted(false) // 必須パラメータ |
| 74 | + .build(); // 全パラメータ設定済みの場合のみコンパイル成功 |
| 75 | +``` |
| 76 | + |
| 77 | +#### 2. ゼロコスト抽象化の実現 |
| 78 | + |
| 79 | +builderパターンの実装において、実行時コストを排除する: |
| 80 | + |
| 81 | +- `Option<T>`を使わずに型状態でフィールドの設定状態を管理 |
| 82 | +- `unwrap()`等の実行時チェックを排除 |
| 83 | +- 最終的な`build()`メソッドは単純なメモリ操作のみ |
| 84 | + |
| 85 | +#### 3. copy_types最適化の統合 |
| 86 | + |
| 87 | +既存の`copy_types`設定と連携し、パラメータの渡し方を最適化する: |
| 88 | + |
| 89 | +- Copy型(i32, bool等): 値渡しで効率化 |
| 90 | +- 非Copy型(String等): `Cow<'a, T>`で借用/所有を適応的に選択 |
| 91 | + |
| 92 | +## 実装戦略 |
| 93 | + |
| 94 | +### 1. 型状態パターンの実装 |
| 95 | + |
| 96 | +#### 基本構造 |
| 97 | + |
| 98 | +```rust |
| 99 | +// クエリパラメータを保持する最終構造体 |
| 100 | +pub struct GetUser<'a> { |
| 101 | + user_id: i32, // Copy型:値渡し |
| 102 | + name: Cow<'a, str>, // 非Copy型:Cowで最適化 |
| 103 | +} |
| 104 | + |
| 105 | +// 型状態を管理するbuilder |
| 106 | +pub struct GetUserBuilder<'a, Fields = ((), ())> { |
| 107 | + fields: Fields, // 実際の値を型レベルで管理 |
| 108 | + phantom: PhantomData<&'a ()>, // ライフタイム保持 |
| 109 | +} |
| 110 | +``` |
| 111 | + |
| 112 | +#### 段階的型状態の変化 |
| 113 | + |
| 114 | +```rust |
| 115 | +// 初期状態:未設定 |
| 116 | +GetUserBuilder<'a, ((), ())> |
| 117 | + |
| 118 | +// user_id設定後:第1フィールドのみ設定済み |
| 119 | +GetUserBuilder<'a, (i32, ())> |
| 120 | + |
| 121 | +// name設定後:全フィールド設定済み |
| 122 | +GetUserBuilder<'a, (i32, Cow<'a, str>)> |
| 123 | +``` |
| 124 | + |
| 125 | +#### ゼロコストbuilder実装 |
| 126 | + |
| 127 | +```rust |
| 128 | +impl<'a, V2> GetUserBuilder<'a, ((), V2)> { |
| 129 | + pub fn user_id(self, user_id: i32) -> GetUserBuilder<'a, (i32, V2)> { |
| 130 | + let ((), name) = self.fields; |
| 131 | + GetUserBuilder { |
| 132 | + fields: (user_id, name), // 単純な値の移動のみ |
| 133 | + phantom: PhantomData, |
| 134 | + } |
| 135 | + } |
| 136 | +} |
| 137 | + |
| 138 | +// build()メソッド:unwrap()なしの単純なdestructuring |
| 139 | +impl<'a> GetUserBuilder<'a, (i32, Cow<'a, str>)> { |
| 140 | + pub fn build(self) -> GetUser<'a> { |
| 141 | + let (user_id, name) = self.fields; // ゼロコスト |
| 142 | + GetUser { user_id, name } |
| 143 | + } |
| 144 | +} |
| 145 | +``` |
| 146 | + |
| 147 | +### 2. QueryAnnotationに基づく実行メソッド |
| 148 | + |
| 149 | +SQLCのクエリアノテーションに基づいて、適切な実行メソッドを生成する: |
| 150 | + |
| 151 | +```rust |
| 152 | +impl<'a> GetUser<'a> { |
| 153 | + // :one アノテーション → query_one & query_opt |
| 154 | + pub async fn query_one(&self, client: &Client) -> Result<UserRow, Error> |
| 155 | + pub async fn query_opt(&self, client: &Client) -> Result<Option<UserRow>, Error> |
| 156 | + |
| 157 | + // :many アノテーション → query_many & query_raw |
| 158 | + pub async fn query_many(&self, client: &Client) -> Result<Vec<UserRow>, Error> |
| 159 | + pub async fn query_raw(&self, client: &Client) -> Result<impl Iterator<Item=Result<UserRow, Error>>, Error> |
| 160 | + |
| 161 | + // :exec アノテーション → execute |
| 162 | + pub async fn execute(&self, client: &Client) -> Result<u64, Error> |
| 163 | +} |
| 164 | +``` |
| 165 | + |
| 166 | +### 3. copy_types最適化の統合 |
| 167 | + |
| 168 | +既存の`copy_types`設定と統合し、型ごとに最適なパラメータ渡しを実現する: |
| 169 | + |
| 170 | +#### 自動判定ルール |
| 171 | + |
| 172 | +1. **Copy trait実装型かつ16バイト以下**: 値渡し |
| 173 | +2. **String系**: `Cow<'a, str>`で最適化 |
| 174 | +3. **ユーザー定義copy_types**: 設定に従って値渡し |
| 175 | +4. **その他**: 参照渡し |
| 176 | + |
| 177 | +#### 実装例 |
| 178 | + |
| 179 | +```rust |
| 180 | +// 生成される構造体(copy_types最適化適用後) |
| 181 | +pub struct CreateUser<'a> { |
| 182 | + id: i32, // Copy型:値渡し |
| 183 | + name: Cow<'a, str>, // 文字列:Cowで最適化 |
| 184 | + email: Cow<'a, str>, // 文字列:Cowで最適化 |
| 185 | + is_active: bool, // Copy型:値渡し |
| 186 | + metadata: &'a serde_json::Value, // 大きな型:参照渡し |
| 187 | +} |
| 188 | +``` |
| 189 | + |
| 190 | +## 後方互換性戦略 |
| 191 | + |
| 192 | +### 既存APIの保持 |
| 193 | + |
| 194 | +新しい構造体ベースAPIは**追加機能**として実装し、既存の関数ベースAPIを破壊しない: |
| 195 | + |
| 196 | +```rust |
| 197 | +// 既存API:そのまま維持(内部実装のみ最適化) |
| 198 | +pub async fn get_user(client: &Client, user_id: i32, name: &str) -> Result<UserRow, Error> { |
| 199 | + // 内部でGetUser構造体を使用(外部には露出しない) |
| 200 | + let query = GetUser { |
| 201 | + user_id, |
| 202 | + name: Cow::Borrowed(name), |
| 203 | + }; |
| 204 | + query.query_one(client).await |
| 205 | +} |
| 206 | + |
| 207 | +// 新しいAPI:オプションとして提供 |
| 208 | +let user = GetUser::builder() |
| 209 | + .user_id(123) |
| 210 | + .name("test") |
| 211 | + .build() |
| 212 | + .query_one(&client) |
| 213 | + .await?; |
| 214 | +``` |
| 215 | + |
| 216 | +### 段階的移行戦略 |
| 217 | + |
| 218 | +1. **フェーズ1**: 構造体ベースAPIの実装と検証 |
| 219 | +2. **フェーズ2**: ドキュメント整備とサンプル提供 |
| 220 | +3. **フェーズ3**: ユーザーフィードバックに基づく改善 |
| 221 | +4. **フェーズ4**: 既存APIの非推奨化(長期計画) |
| 222 | + |
| 223 | +## 実装の技術的詳細 |
| 224 | + |
| 225 | +### 1. コード生成フロー |
| 226 | + |
| 227 | +``` |
| 228 | +SQL解析 → パラメータ抽出 → copy_types適用 → 構造体生成 → builder生成 → 実行メソッド生成 |
| 229 | +``` |
| 230 | + |
| 231 | +#### 主要な生成フェーズ |
| 232 | + |
| 233 | +1. **パラメータ分析**: SQLクエリからパラメータを抽出 |
| 234 | +2. **型最適化**: copy_types設定に基づく型変換 |
| 235 | +3. **構造体生成**: 最適化された型でクエリ構造体を生成 |
| 236 | +4. **builder生成**: 型状態パターンでbuilderを生成 |
| 237 | +5. **メソッド生成**: QueryAnnotationに基づく実行メソッド生成 |
| 238 | + |
| 239 | +### 2. 実装上の制約と対処 |
| 240 | + |
| 241 | +#### 制約1: Rustの型システムの限界 |
| 242 | + |
| 243 | +**問題**: タプルの要素数には限界があるため、極端に多いパラメータを持つクエリでは型状態パターンが適用できない。 |
| 244 | + |
| 245 | +**対処**: パラメータ数が閾値(例:16個)を超える場合は、従来の`Option<T>`ベースのbuilderにフォールバック。 |
| 246 | + |
| 247 | +#### 制約2: ライフタイム管理の複雑さ |
| 248 | + |
| 249 | +**問題**: 複数の借用パラメータが存在する場合、ライフタイム管理が複雑になる。 |
| 250 | + |
| 251 | +**対処**: |
| 252 | +- `PhantomData<&'a ()>`でライフタイムを統一 |
| 253 | +- 必要に応じて複数ライフタイムパラメータを使用 |
| 254 | +- `Cow<'a, T>`で借用と所有を適応的に切り替え |
| 255 | + |
| 256 | +### 3. パフォーマンス考慮事項 |
| 257 | + |
| 258 | +#### ゼロコスト抽象化の検証 |
| 259 | + |
| 260 | +以下の観点でパフォーマンスを検証する: |
| 261 | + |
| 262 | +1. **コンパイル時最適化**: リリースビルドでbuilderパターンが完全に最適化されるか |
| 263 | +2. **メモリ使用量**: 構造体のメモリレイアウトが効率的か |
| 264 | +3. **実行時コスト**: 従来の関数型APIと比較して性能劣化がないか |
| 265 | + |
| 266 | +#### 最適化指針 |
| 267 | + |
| 268 | +```rust |
| 269 | +// 最適化前(実行時コストあり) |
| 270 | +struct Builder { |
| 271 | + user_id: Option<i32>, // Option型によるオーバーヘッド |
| 272 | + name: Option<String>, // unwrap()による実行時チェック |
| 273 | +} |
| 274 | + |
| 275 | +// 最適化後(ゼロコスト) |
| 276 | +struct Builder<Fields = ((), ())> { |
| 277 | + fields: Fields, // 型レベルでの状態管理 |
| 278 | + phantom: PhantomData<()>, // コンパイル時のみ |
| 279 | +} |
| 280 | +``` |
| 281 | + |
| 282 | +## 設定オプションの拡張 |
| 283 | + |
| 284 | +### 既存設定の活用 |
| 285 | + |
| 286 | +構造体ベースAPIは既存の`copy_types`設定を活用し、新たな設定項目は追加しない方針とする。これにより設定の複雑化を避け、シンプルな利用体験を維持する。 |
| 287 | + |
| 288 | +```json |
| 289 | +{ |
| 290 | + "plugins": [ |
| 291 | + { |
| 292 | + "name": "rust-postgres", |
| 293 | + "options": { |
| 294 | + "copy_types": ["uuid::Uuid", "CustomId"], |
| 295 | + "existing_options": "..." |
| 296 | + } |
| 297 | + } |
| 298 | + ] |
| 299 | +} |
| 300 | +``` |
| 301 | + |
| 302 | +## 今後の拡張計画 |
| 303 | + |
| 304 | +### 短期計画(3ヶ月以内) |
| 305 | + |
| 306 | +1. **基本実装の完成**: 型状態パターンの実装 |
| 307 | +2. **copy_types統合**: 既存最適化との連携 |
| 308 | +3. **テストケース整備**: 包括的なテストスイート |
| 309 | +4. **ドキュメント整備**: API仕様とサンプルコード |
| 310 | + |
| 311 | +### 中期計画(6ヶ月以内) |
| 312 | + |
| 313 | +1. **エラーメッセージ改善**: コンパイルエラーの分かりやすさ向上 |
| 314 | +2. **IDE統合**: rust-analyzerでの補完とエラー表示改善 |
| 315 | +3. **パフォーマンス検証**: ベンチマークとプロファイリング |
| 316 | +4. **エコシステム対応**: 主要なPostgreSQLクレートとの互換性確認 |
| 317 | + |
| 318 | +### 長期計画(1年以内) |
| 319 | + |
| 320 | +1. **バッチ操作対応**: BatchExec等のアノテーション対応 |
| 321 | +2. **トランザクション統合**: 型安全なトランザクション管理 |
| 322 | +3. **マイグレーション支援**: 既存コードの移行ツール |
| 323 | +4. **他データベース対応**: MySQL、SQLite等への展開 |
| 324 | + |
| 325 | +## リスク分析と対策 |
| 326 | + |
| 327 | +### 技術的リスク |
| 328 | + |
| 329 | +#### リスク1: コンパイル時間の増加 |
| 330 | + |
| 331 | +**原因**: 複雑な型状態パターンによるコンパイル負荷増加 |
| 332 | + |
| 333 | +**対策**: |
| 334 | +- 型の複雑さを適切に制限 |
| 335 | +- インクリメンタルコンパイルの最適化 |
| 336 | +- 必要に応じてマクロによる実装簡素化 |
| 337 | + |
| 338 | +#### リスク2: エラーメッセージの複雑化 |
| 339 | + |
| 340 | +**原因**: 型状態パターンの型エラーが分かりにくい |
| 341 | + |
| 342 | +**対策**: |
| 343 | +- 適切な型エイリアスの提供 |
| 344 | +- カスタムコンパイルエラーメッセージ |
| 345 | +- 詳細なドキュメントと例示 |
| 346 | + |
| 347 | +### 採用リスク |
| 348 | + |
| 349 | +#### リスク1: 学習コストの増加 |
| 350 | + |
| 351 | +**原因**: 新しいAPIパターンの習得が必要 |
| 352 | + |
| 353 | +**対策**: |
| 354 | +- 段階的な移行計画 |
| 355 | +- 豊富なサンプルコードとチュートリアル |
| 356 | +- 既存APIとの併用期間の確保 |
| 357 | + |
| 358 | +#### リスク2: エコシステムの分断 |
| 359 | + |
| 360 | +**原因**: 新旧APIの並存による混乱 |
| 361 | + |
| 362 | +**対策**: |
| 363 | +- 明確な移行ガイドライン |
| 364 | +- ツールによる自動変換支援 |
| 365 | +- コミュニティとの十分な対話 |
| 366 | + |
| 367 | +## 結論 |
| 368 | + |
| 369 | +構造体ベースAPIの導入により、以下の価値を提供できる: |
| 370 | + |
| 371 | +1. **開発者体験の向上**: 型安全性と可読性の大幅な改善 |
| 372 | +2. **保守性の向上**: SQLクエリ変更に対する堅牢性 |
| 373 | +3. **パフォーマンス**: ゼロコスト抽象化による効率性 |
| 374 | +4. **段階的移行**: 既存コードへの影響を最小化 |
| 375 | + |
| 376 | +本実装は、Rustの型システムの力を最大限に活用し、データベースアクセスレイヤーの安全性と効率性を大幅に向上させる重要な機能である。慎重な実装と十分な検証を通じて、sqlc-rust-postgresプラグインの価値を更に高めることができると確信している。 |
| 377 | + |
| 378 | +--- |
| 379 | + |
| 380 | +*本文書の内容は実装の進行に伴い更新される予定である。最新の情報については、GitHubのissueとPRを参照されたい。* |
0 commit comments