diff --git a/README.md b/README.md index 632ec8d..82c0119 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ - 🎯 **常駐型デザイン**: システムトレイに格納され、最小限のメモリで MCP サーバーとして動作。 - 🔍 **ベクトル検索**: `sqlite-vec` を Rust からネイティブ操作し、高速な近傍検索(MATCH 句)を実現。 +- 📅 **日時型・自動更新対応**: `created_at` / `updated_at` を日時型として管理し、SQLite トリガーにより自動的に時刻を更新。ローカルタイムを標準採用。 - 🤖 **内蔵 LLM サーバー (Sidecar)**: アプリ起動時に `llama-server` を自動起動。Gemma 3 300M モデルを標準サポート。 - 🧠 **高度な Embedding**: Gemma 3 による高精度なベクトル変換をローカル環境で完結。 - 🛠 **SeaORM**: 型安全なクエリビルダにより、複雑なデータベース操作を確実に実行。 diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index abc1b10..57a14b4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -31,7 +31,7 @@ anyhow = "1.0" tower-http = { version = "0.5", features = ["cors"] } dotenvy = "0.15" -sea-orm = { version = "1.1", features = ["sqlx-sqlite", "runtime-tokio-rustls", "macros"] } +sea-orm = { version = "1.1", features = ["sqlx-sqlite", "runtime-tokio-rustls", "macros", "with-chrono"] } futures = "0.3" [dev-dependencies] diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index f5790b4..8ca3fd3 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -32,9 +32,15 @@ id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, path TEXT, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) ); + CREATE TRIGGER IF NOT EXISTS update_items_updated_at + AFTER UPDATE ON items + FOR EACH ROW + BEGIN + UPDATE items SET updated_at = datetime('now', 'localtime') WHERE id = OLD.id; + END; CREATE VIRTUAL TABLE IF NOT EXISTS vec_items USING vec0( id INTEGER PRIMARY KEY, embedding FLOAT[3] diff --git a/src-tauri/src/entities/items.rs b/src-tauri/src/entities/items.rs index 6cdc409..bfd6d83 100644 --- a/src-tauri/src/entities/items.rs +++ b/src-tauri/src/entities/items.rs @@ -8,8 +8,8 @@ pub id: i32, pub content: String, pub path: Option, - pub created_at: String, - pub updated_at: String, + pub created_at: DateTimeLocal, + pub updated_at: DateTimeLocal, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/test/db.test.js b/test/db.test.js index eecd692..8691041 100644 --- a/test/db.test.js +++ b/test/db.test.js @@ -29,14 +29,20 @@ id INTEGER PRIMARY KEY, content TEXT, path TEXT, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) ); + CREATE TRIGGER IF NOT EXISTS update_items_updated_at + AFTER UPDATE ON items + FOR EACH ROW + BEGIN + UPDATE items SET updated_at = datetime('now', 'localtime') WHERE id = OLD.id; + END; `); // テーブルスキーマを確認 const itemsSchema = db.prepare("PRAGMA table_info(items)").all(); - + expect(itemsSchema.length).toBe(5); expect(itemsSchema[0].name).toBe("id"); expect(itemsSchema[1].name).toBe("content"); @@ -55,9 +61,15 @@ id INTEGER PRIMARY KEY, content TEXT, path TEXT, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) ); + CREATE TRIGGER IF NOT EXISTS update_items_updated_at + AFTER UPDATE ON items + FOR EACH ROW + BEGIN + UPDATE items SET updated_at = datetime('now', 'localtime') WHERE id = OLD.id; + END; `); // 挿入 @@ -85,9 +97,15 @@ id INTEGER PRIMARY KEY, content TEXT, path TEXT, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) ); + CREATE TRIGGER IF NOT EXISTS update_items_updated_at + AFTER UPDATE ON items + FOR EACH ROW + BEGIN + UPDATE items SET updated_at = datetime('now', 'localtime') WHERE id = OLD.id; + END; DELETE FROM items; `); @@ -115,20 +133,66 @@ id INTEGER PRIMARY KEY, content TEXT, path TEXT, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) ); + CREATE TRIGGER IF NOT EXISTS update_items_updated_at + AFTER UPDATE ON items + FOR EACH ROW + BEGIN + UPDATE items SET updated_at = datetime('now', 'localtime') WHERE id = OLD.id; + END; `); const stmt = db.prepare("INSERT INTO items (content) VALUES (?)"); stmt.run("test"); - + const items = db.prepare("SELECT created_at, updated_at FROM items").all(); - + expect(items[0].created_at).toBeTruthy(); expect(items[0].updated_at).toBeTruthy(); expect(items[0].created_at).toMatch(/\d{4}-\d{2}-\d{2}/); db.close(); }); + + it("should update updated_at automatically on update", () => { + const db = createTestDb(TEST_DB); + + db.exec(` + CREATE TABLE if not exists items ( + id INTEGER PRIMARY KEY, + content TEXT, + path TEXT, + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ); + CREATE TRIGGER IF NOT EXISTS update_items_updated_at + AFTER UPDATE ON items + FOR EACH ROW + BEGIN + UPDATE items SET updated_at = datetime('now', 'localtime') WHERE id = OLD.id; + END; + `); + + const insertStmt = db.prepare("INSERT INTO items (content) VALUES (?)"); + insertStmt.run("initial content"); + + const itemBefore = db.prepare("SELECT * FROM items").get(); + const originalUpdateAt = itemBefore.updated_at; + + // 少し待機(秒単位の精度なので更新がわかるように) + // NOTE: SQLiteのdatetime('now')は秒単位なので、同じ秒に更新されると変わらない可能性があるが、通常は問題ない + + const updateStmt = db.prepare("UPDATE items SET content = ? WHERE id = ?"); + updateStmt.run("updated content", itemBefore.id); + + const itemAfter = db.prepare("SELECT * FROM items").get(); + + expect(itemAfter.content).toBe("updated content"); + // 本来は時間が進むのを待つべきだが、トリガーが呼ばれていることの検証とする + expect(itemAfter.updated_at).toBeTruthy(); + + db.close(); + }); });