diff --git "a/journals/20260223-0018-\343\203\207\343\203\274\343\202\277\343\203\231\343\203\274\343\202\271\343\201\256\343\203\236\343\202\244\343\202\260\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\345\257\276\345\277\234\357\274\210v0.2.5\342\206\222v0.3.0\357\274\211.md" "b/journals/20260223-0018-\343\203\207\343\203\274\343\202\277\343\203\231\343\203\274\343\202\271\343\201\256\343\203\236\343\202\244\343\202\260\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\345\257\276\345\277\234\357\274\210v0.2.5\342\206\222v0.3.0\357\274\211.md" new file mode 100644 index 0000000..12fb1b4 --- /dev/null +++ "b/journals/20260223-0018-\343\203\207\343\203\274\343\202\277\343\203\231\343\203\274\343\202\271\343\201\256\343\203\236\343\202\244\343\202\260\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\345\257\276\345\277\234\357\274\210v0.2.5\342\206\222v0.3.0\357\274\211.md" @@ -0,0 +1,33 @@ +# Journal: 20260223-0018-データベースのマイグレーション対応(v0.2.5→v0.3.0) + +## 1. 作業実施の理由 + +v0.3.0 で実施したテーブル構造の正規化により、以前のバージョン(v0.2.5)とのデータベース互換性が失われていた。既存ユーザーがデータを失うことなくスムーズに移行できるよう、自動マイグレーション機能を実装するため。 + +## 2. 指示(背景、観点、意図を含む) + +- **指示**: 「マイグレーション作れる?」→「ごー」 +- **背景**: 手動でのDB削除・再構築はバリアが高いため、起動時に自動でスキーマを変換したい。 +- **意図**: 既存の `items` テーブルからドキュメントメタデータを抽出し、新しい 1:N 構造(`documents` テーブルとの紐付け)に再編する。その際、ベクトルデータとの整合性を保つため `id` を維持する。 + +## 3. 指示事項とその対応 + +- **自動検知**: `PRAGMA table_info(items)` を使用して、古い `path` カラムの有無でマイグレーションの要否を判断。 +- **データ移行**: + - `documents` テーブルを新規作成し、ユニークなパスを移行。 + - `items` テーブルを新スキーマで再作成(`document_id` を付与し、`chunk_index` を自動生成)。 + - 全工程を一つのトランザクション内で実行し、整合性を担保。 + +## 4. 作業詳細 + +AIエージェントは以下の手順で作業を実施した。 + +1. `src/backend/src/db.rs` に `migrate_025_to_030` 関数を実装。 +2. `init_schema` の冒頭で上記関数を呼び出すように修正。 +3. `cargo check` によりコンパイルの整合性を確認。 +4. ウォークスルーを作成し、移行ロジックの妥当性を整理。 + +## 5. AI視点での結果 + +今回のマイグレーション対応により、ユーザーは特別な操作をすることなく TelosDB v0.3.0 へアップデート可能になりました。内部的にはテーブルのリネーム、データの再挿入、外部キー制約の一時解除など複雑な処理を行っていますが、これらをカプセル化することで、プロダクトとしての堅牢性とユーザー体験の両立を実現しました。 +以前のベクトル ID をそのまま引き継ぐ設計としたため、再インデックスの手間も最小限に抑えられています。 diff --git a/src/backend/src/db.rs b/src/backend/src/db.rs index c3b53c7..77aea66 100644 --- a/src/backend/src/db.rs +++ b/src/backend/src/db.rs @@ -45,6 +45,9 @@ .await .map_err(|e| e.to_string())?; + // v0.2.5 から v0.3.0 へのマイグレーション(必要であれば) + migrate_025_to_030(pool).await?; + // ドキュメント(親メタデータ)テーブル sqlx::query( "CREATE TABLE IF NOT EXISTS documents ( @@ -119,6 +122,110 @@ Ok(()) } +async fn migrate_025_to_030(pool: &SqlitePool) -> Result<(), String> { + // items テーブルの構造を確認 + let rows = sqlx::query("PRAGMA table_info(items)") + .fetch_all(pool) + .await + .map_err(|e| e.to_string())?; + + let has_path = rows.iter().any(|row| { + let name: String = row.get(1); + name == "path" + }); + + if !has_path { + // すでに新形式か、テーブルが存在しない場合はスキップ + return Ok(()); + } + + log::info!("Migrating database from v0.2.5 to v0.3.0..."); + + // 外部キー制約を一時的に無効化(テーブル置換のため) + sqlx::query("PRAGMA foreign_keys = OFF") + .execute(pool) + .await + .map_err(|e| e.to_string())?; + + let mut tx = pool.begin().await.map_err(|e| e.to_string())?; + + // 1. documents テーブルを作成 + sqlx::query( + "CREATE TABLE IF NOT EXISTS documents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + path TEXT UNIQUE, + mime TEXT, + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + )", + ) + .execute(&mut *tx) + .await + .map_err(|e| e.to_string())?; + + // 2. ユニークなパスを抽出して documents に移行 + sqlx::query("INSERT OR IGNORE INTO documents (path) SELECT DISTINCT path FROM items") + .execute(&mut *tx) + .await + .map_err(|e| e.to_string())?; + + // 3. 新スキーマの items テーブル(一時)を作成 + sqlx::query( + "CREATE TABLE items_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + document_id INTEGER NOT NULL, + chunk_index INTEGER NOT NULL, + content TEXT NOT NULL, + created_at TEXT, + updated_at TEXT, + FOREIGN KEY(document_id) REFERENCES documents(id) ON DELETE CASCADE + )", + ) + .execute(&mut *tx) + .await + .map_err(|e| e.to_string())?; + + // 4. データを移行。ID を維持し、各ドキュメント内の順序(chunk_index)を生成 + sqlx::query( + "INSERT INTO items_new (id, document_id, chunk_index, content, created_at, updated_at) + SELECT + i.id, + d.id, + (SELECT COUNT(*) FROM items i2 WHERE i2.path = i.path AND i2.id < i.id), + i.content, + i.created_at, + i.updated_at + FROM items i + JOIN documents d ON i.path = d.path", + ) + .execute(&mut *tx) + .await + .map_err(|e| e.to_string())?; + + // 5. 旧テーブルを削除し、新テーブルをリネーム + sqlx::query("DROP TABLE items") + .execute(&mut *tx) + .await + .map_err(|e| e.to_string())?; + + sqlx::query("ALTER TABLE items_new RENAME TO items") + .execute(&mut *tx) + .await + .map_err(|e| e.to_string())?; + + tx.commit().await.map_err(|e| e.to_string())?; + + // 外部キー制約を再有効化 + sqlx::query("PRAGMA foreign_keys = ON") + .execute(pool) + .await + .map_err(|e| e.to_string())?; + + log::info!("Migration v0.2.5 to v0.3.0 completed successfully."); + + Ok(()) +} + async fn check_and_init_vector_table(pool: &SqlitePool, dimension: usize) -> Result<(), String> { // 現在のテーブル定義を確認 let row = sqlx::query("SELECT sql FROM sqlite_master WHERE type='table' AND name='vec_items'")