← Back to task

Commit e47da20d

commit e47da20db9dd5f5d0a09ffcf2b17ad4e1d78115b
Author: Ben Sima <ben@bensima.com>
Date:   Thu Nov 27 13:44:47 2025

    Implement proper schema migrations for ALTER TABLE
    
    All tests pass. The implementation adds:
    
    1. **`runMigrations`** - Called at the end of `initTaskDb` to run
    migrat 2. **`migrateTable`** - Compares expected columns against
    existing colum 3. **`getTableColumns`** - Uses `PRAGMA table_info` to
    get existing colu 4. **`addColumn`** - Runs `ALTER TABLE ADD COLUMN`
    for missing columns 5. **Column definitions** - Lists of expected
    columns for `task_activity
    
    When new columns are added to the schema in the future, you just
    add the
    
    Task-Id: t-152.3

diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs
index ffecd608..5af4ce49 100644
--- a/Omni/Task/Core.hs
+++ b/Omni/Task/Core.hs
@@ -374,6 +374,75 @@ initTaskDb = do
       \ tokens_used INTEGER, \
       \ FOREIGN KEY (task_id) REFERENCES tasks(id) \
       \)"
+    runMigrations conn
+
+-- | Run schema migrations to add missing columns to existing tables
+runMigrations :: SQL.Connection -> IO ()
+runMigrations conn = do
+  migrateTable conn "task_activity" taskActivityColumns
+  migrateTable conn "tasks" tasksColumns
+  migrateTable conn "retry_context" retryContextColumns
+
+-- | Expected columns for task_activity table (name, type, nullable)
+taskActivityColumns :: [(Text, Text)]
+taskActivityColumns =
+  [ ("id", "INTEGER"),
+    ("task_id", "TEXT"),
+    ("timestamp", "DATETIME"),
+    ("stage", "TEXT"),
+    ("message", "TEXT"),
+    ("metadata", "TEXT"),
+    ("amp_thread_url", "TEXT"),
+    ("started_at", "DATETIME"),
+    ("completed_at", "DATETIME"),
+    ("cost_cents", "INTEGER"),
+    ("tokens_used", "INTEGER")
+  ]
+
+-- | Expected columns for tasks table
+tasksColumns :: [(Text, Text)]
+tasksColumns =
+  [ ("id", "TEXT"),
+    ("title", "TEXT"),
+    ("type", "TEXT"),
+    ("parent", "TEXT"),
+    ("namespace", "TEXT"),
+    ("status", "TEXT"),
+    ("priority", "TEXT"),
+    ("dependencies", "TEXT"),
+    ("description", "TEXT"),
+    ("created_at", "TIMESTAMP"),
+    ("updated_at", "TIMESTAMP")
+  ]
+
+-- | Expected columns for retry_context table
+retryContextColumns :: [(Text, Text)]
+retryContextColumns =
+  [ ("task_id", "TEXT"),
+    ("original_commit", "TEXT"),
+    ("conflict_files", "TEXT"),
+    ("attempt", "INTEGER"),
+    ("reason", "TEXT")
+  ]
+
+-- | Migrate a table by adding any missing columns
+migrateTable :: SQL.Connection -> Text -> [(Text, Text)] -> IO ()
+migrateTable conn tableName expectedCols = do
+  existingCols <- getTableColumns conn tableName
+  let missingCols = filter (\(name, _) -> name `notElem` existingCols) expectedCols
+  traverse_ (addColumn conn tableName) missingCols
+
+-- | Get list of column names for a table using PRAGMA table_info
+getTableColumns :: SQL.Connection -> Text -> IO [Text]
+getTableColumns conn tableName = do
+  rows <- SQL.query conn "PRAGMA table_info(?)" (SQL.Only tableName) :: IO [(Int, Text, Text, Int, Maybe Text, Int)]
+  pure [colName | (_, colName, _, _, _, _) <- rows]
+
+-- | Add a column to a table
+addColumn :: SQL.Connection -> Text -> (Text, Text) -> IO ()
+addColumn conn tableName (colName, colType) = do
+  let sql = "ALTER TABLE " <> tableName <> " ADD COLUMN " <> colName <> " " <> colType
+  SQL.execute_ conn (SQL.Query sql)
 
 -- Generate a sequential task ID (t-1, t-2, t-3, ...)
 generateId :: IO Text