@@ -307,6 +307,10 @@ func (s *Store) migrate() error {
307307 }
308308 }
309309
310+ if err := s .migrateLegacyObservationsTable (); err != nil {
311+ return err
312+ }
313+
310314 if _ , err := s .db .Exec (`
311315 CREATE INDEX IF NOT EXISTS idx_obs_scope ON observations(scope);
312316 CREATE INDEX IF NOT EXISTS idx_obs_topic ON observations(topic_key, project, scope, updated_at DESC);
@@ -1357,6 +1361,138 @@ func (s *Store) addColumnIfNotExists(tableName, columnName, definition string) e
13571361 return err
13581362}
13591363
1364+ func (s * Store ) migrateLegacyObservationsTable () error {
1365+ rows , err := s .db .Query ("PRAGMA table_info(observations)" )
1366+ if err != nil {
1367+ return err
1368+ }
1369+ defer rows .Close ()
1370+
1371+ var hasID bool
1372+ var idIsPrimaryKey bool
1373+ for rows .Next () {
1374+ var cid int
1375+ var name , typ string
1376+ var notNull int
1377+ var defaultValue any
1378+ var pk int
1379+ if err := rows .Scan (& cid , & name , & typ , & notNull , & defaultValue , & pk ); err != nil {
1380+ return err
1381+ }
1382+ if name == "id" {
1383+ hasID = true
1384+ idIsPrimaryKey = pk == 1
1385+ break
1386+ }
1387+ }
1388+ if err := rows .Err (); err != nil {
1389+ return err
1390+ }
1391+
1392+ if ! hasID || idIsPrimaryKey {
1393+ return nil
1394+ }
1395+
1396+ tx , err := s .db .Begin ()
1397+ if err != nil {
1398+ return fmt .Errorf ("migrate legacy observations: begin tx: %w" , err )
1399+ }
1400+ defer tx .Rollback ()
1401+
1402+ if _ , err := tx .Exec (`
1403+ CREATE TABLE observations_migrated (
1404+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1405+ session_id TEXT NOT NULL,
1406+ type TEXT NOT NULL,
1407+ title TEXT NOT NULL,
1408+ content TEXT NOT NULL,
1409+ tool_name TEXT,
1410+ project TEXT,
1411+ scope TEXT NOT NULL DEFAULT 'project',
1412+ topic_key TEXT,
1413+ normalized_hash TEXT,
1414+ revision_count INTEGER NOT NULL DEFAULT 1,
1415+ duplicate_count INTEGER NOT NULL DEFAULT 1,
1416+ last_seen_at TEXT,
1417+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1418+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
1419+ deleted_at TEXT,
1420+ FOREIGN KEY (session_id) REFERENCES sessions(id)
1421+ );
1422+ ` ); err != nil {
1423+ return fmt .Errorf ("migrate legacy observations: create table: %w" , err )
1424+ }
1425+
1426+ if _ , err := tx .Exec (`
1427+ INSERT INTO observations_migrated (
1428+ id, session_id, type, title, content, tool_name, project,
1429+ scope, topic_key, normalized_hash, revision_count, duplicate_count,
1430+ last_seen_at, created_at, updated_at, deleted_at
1431+ )
1432+ SELECT
1433+ CASE
1434+ WHEN id IS NULL THEN NULL
1435+ WHEN ROW_NUMBER() OVER (PARTITION BY id ORDER BY rowid) = 1 THEN CAST(id AS INTEGER)
1436+ ELSE NULL
1437+ END,
1438+ session_id,
1439+ COALESCE(NULLIF(type, ''), 'manual'),
1440+ COALESCE(NULLIF(title, ''), 'Untitled observation'),
1441+ COALESCE(content, ''),
1442+ tool_name,
1443+ project,
1444+ CASE WHEN scope IS NULL OR scope = '' THEN 'project' ELSE scope END,
1445+ NULLIF(topic_key, ''),
1446+ normalized_hash,
1447+ CASE WHEN revision_count IS NULL OR revision_count < 1 THEN 1 ELSE revision_count END,
1448+ CASE WHEN duplicate_count IS NULL OR duplicate_count < 1 THEN 1 ELSE duplicate_count END,
1449+ last_seen_at,
1450+ COALESCE(NULLIF(created_at, ''), datetime('now')),
1451+ COALESCE(NULLIF(updated_at, ''), NULLIF(created_at, ''), datetime('now')),
1452+ deleted_at
1453+ FROM observations
1454+ ORDER BY rowid;
1455+ ` ); err != nil {
1456+ return fmt .Errorf ("migrate legacy observations: copy rows: %w" , err )
1457+ }
1458+
1459+ if _ , err := tx .Exec ("DROP TABLE observations" ); err != nil {
1460+ return fmt .Errorf ("migrate legacy observations: drop old table: %w" , err )
1461+ }
1462+
1463+ if _ , err := tx .Exec ("ALTER TABLE observations_migrated RENAME TO observations" ); err != nil {
1464+ return fmt .Errorf ("migrate legacy observations: rename table: %w" , err )
1465+ }
1466+
1467+ if _ , err := tx .Exec (`
1468+ DROP TRIGGER IF EXISTS obs_fts_insert;
1469+ DROP TRIGGER IF EXISTS obs_fts_update;
1470+ DROP TRIGGER IF EXISTS obs_fts_delete;
1471+ DROP TABLE IF EXISTS observations_fts;
1472+ CREATE VIRTUAL TABLE observations_fts USING fts5(
1473+ title,
1474+ content,
1475+ tool_name,
1476+ type,
1477+ project,
1478+ content='observations',
1479+ content_rowid='id'
1480+ );
1481+ INSERT INTO observations_fts(rowid, title, content, tool_name, type, project)
1482+ SELECT id, title, content, tool_name, type, project
1483+ FROM observations
1484+ WHERE deleted_at IS NULL;
1485+ ` ); err != nil {
1486+ return fmt .Errorf ("migrate legacy observations: rebuild fts: %w" , err )
1487+ }
1488+
1489+ if err := tx .Commit (); err != nil {
1490+ return fmt .Errorf ("migrate legacy observations: commit: %w" , err )
1491+ }
1492+
1493+ return nil
1494+ }
1495+
13601496func nullableString (s string ) * string {
13611497 if s == "" {
13621498 return nil
0 commit comments