2121
2222from __future__ import annotations
2323
24+ import contextlib
2425import sqlite3
2526from contextlib import contextmanager
2627from datetime import UTC , datetime
4748# =============================================================================
4849
4950# Schema version for migrations
50- SCHEMA_VERSION = 1
51+ SCHEMA_VERSION = 2
5152
5253# SQL statements for schema creation
5354_CREATE_MEMORIES_TABLE = """
5859 summary TEXT NOT NULL,
5960 content TEXT NOT NULL,
6061 timestamp TEXT NOT NULL,
62+ repo_path TEXT,
6163 spec TEXT,
6264 phase TEXT,
6365 tags TEXT,
7476 "CREATE INDEX IF NOT EXISTS idx_memories_commit ON memories(commit_sha)" ,
7577 "CREATE INDEX IF NOT EXISTS idx_memories_timestamp ON memories(timestamp)" ,
7678 "CREATE INDEX IF NOT EXISTS idx_memories_status ON memories(status)" ,
79+ "CREATE INDEX IF NOT EXISTS idx_memories_repo_path ON memories(repo_path)" ,
7780]
7881
82+ # Migration SQL for schema version upgrades
83+ _MIGRATIONS = {
84+ 2 : [
85+ # Add repo_path column for per-repository memory isolation
86+ "ALTER TABLE memories ADD COLUMN repo_path TEXT" ,
87+ "CREATE INDEX IF NOT EXISTS idx_memories_repo_path ON memories(repo_path)" ,
88+ ],
89+ }
90+
7991_CREATE_VEC_TABLE = f"""
8092CREATE VIRTUAL TABLE IF NOT EXISTS vec_memories USING vec0(
8193 id TEXT PRIMARY KEY,
@@ -199,8 +211,48 @@ def _load_vec_extension(self) -> None:
199211 "Install sqlite-vec: pip install sqlite-vec" ,
200212 ) from e
201213
214+ def _get_current_schema_version (self ) -> int :
215+ """Get the current schema version from the database.
216+
217+ Returns:
218+ Current schema version, or 0 if metadata table doesn't exist.
219+ """
220+ if self ._conn is None :
221+ return 0
222+
223+ cursor = self ._conn .cursor ()
224+ try :
225+ cursor .execute ("SELECT value FROM metadata WHERE key = 'schema_version'" )
226+ row = cursor .fetchone ()
227+ return int (row [0 ]) if row else 1 # Default to v1 for existing DBs
228+ except sqlite3 .OperationalError :
229+ # Metadata table doesn't exist - new database
230+ return 0
231+
232+ def _run_migrations (self , from_version : int , to_version : int ) -> None :
233+ """Run schema migrations from one version to another.
234+
235+ Args:
236+ from_version: Current schema version.
237+ to_version: Target schema version.
238+ """
239+ if self ._conn is None :
240+ return
241+
242+ cursor = self ._conn .cursor ()
243+ for version in range (from_version + 1 , to_version + 1 ):
244+ if version in _MIGRATIONS :
245+ for sql in _MIGRATIONS [version ]:
246+ try :
247+ cursor .execute (sql )
248+ except sqlite3 .OperationalError as e :
249+ # Column may already exist from a partial migration
250+ if "duplicate column" not in str (e ).lower ():
251+ raise
252+ self ._conn .commit ()
253+
202254 def _create_schema (self ) -> None :
203- """Create database tables and indices."""
255+ """Create database tables and indices, running migrations if needed ."""
204256 if self ._conn is None :
205257 raise MemoryIndexError (
206258 "Database connection not established" ,
@@ -209,28 +261,36 @@ def _create_schema(self) -> None:
209261
210262 cursor = self ._conn .cursor ()
211263 try :
264+ # Check current schema version before creating tables
265+ current_version = self ._get_current_schema_version ()
266+
212267 # Create memories table
213268 cursor .execute (_CREATE_MEMORIES_TABLE )
214269
215- # Create indices
270+ # Create indices (ignore if they already exist)
216271 for index_sql in _CREATE_INDICES :
217- cursor .execute (index_sql )
272+ with contextlib .suppress (sqlite3 .OperationalError ):
273+ cursor .execute (index_sql )
218274
219275 # Create vector table
220276 cursor .execute (_CREATE_VEC_TABLE )
221277
222278 # Create metadata table
223279 cursor .execute (_CREATE_METADATA_TABLE )
224280
281+ # Run migrations if needed
282+ if 0 < current_version < SCHEMA_VERSION :
283+ self ._run_migrations (current_version , SCHEMA_VERSION )
284+
225285 # Set schema version
226286 cursor .execute (
227287 "INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)" ,
228288 ("schema_version" , str (SCHEMA_VERSION )),
229289 )
230290
231- # Set last sync to now
291+ # Set last sync to now (only if not already set)
232292 cursor .execute (
233- "INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)" ,
293+ "INSERT OR IGNORE INTO metadata (key, value) VALUES (?, ?)" ,
234294 ("last_sync" , datetime .now (UTC ).isoformat ()),
235295 )
236296
@@ -309,9 +369,9 @@ def insert(
309369 """
310370 INSERT INTO memories (
311371 id, commit_sha, namespace, summary, content,
312- timestamp, spec, phase, tags, status, relates_to,
372+ timestamp, repo_path, spec, phase, tags, status, relates_to,
313373 created_at, updated_at
314- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
374+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
315375 """ ,
316376 (
317377 memory .id ,
@@ -320,6 +380,7 @@ def insert(
320380 memory .summary ,
321381 memory .content ,
322382 memory .timestamp .isoformat (),
383+ memory .repo_path ,
323384 memory .spec ,
324385 memory .phase ,
325386 "," .join (memory .tags ) if memory .tags else None ,
0 commit comments