From 0a14abcef5eefec23aa57b8c97fcde101b9023c1 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 26 Dec 2025 23:18:04 -0800 Subject: [PATCH] fix: migration 028 handles missing created_by column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The migration was using SELECT * which fails when migrating databases that predate the created_by column (34 columns vs 35). Now explicitly lists columns and provides empty default for created_by if missing. Also fixes missed Wisp→Ephemeral rename in multirepo_test.go. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../migrations/028_tombstone_closed_at.go | 71 +++++++++++++++++-- internal/storage/sqlite/multirepo_test.go | 2 +- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/internal/storage/sqlite/migrations/028_tombstone_closed_at.go b/internal/storage/sqlite/migrations/028_tombstone_closed_at.go index 32d7b66c..966eff72 100644 --- a/internal/storage/sqlite/migrations/028_tombstone_closed_at.go +++ b/internal/storage/sqlite/migrations/028_tombstone_closed_at.go @@ -95,10 +95,73 @@ func MigrateTombstoneClosedAt(db *sql.DB) error { } // Step 2: Copy data from old table to new table - _, err = db.Exec(` - INSERT INTO issues_new - SELECT * FROM issues - `) + // We need to check if created_by column exists in the old table + // If not, we insert a default empty string for it + var hasCreatedBy bool + rows, err := db.Query(`PRAGMA table_info(issues)`) + if err != nil { + return fmt.Errorf("failed to get table info: %w", err) + } + for rows.Next() { + var cid int + var name, ctype string + var notnull, pk int + var dflt interface{} + if err := rows.Scan(&cid, &name, &ctype, ¬null, &dflt, &pk); err != nil { + rows.Close() + return fmt.Errorf("failed to scan table info: %w", err) + } + if name == "created_by" { + hasCreatedBy = true + break + } + } + rows.Close() + + var insertSQL string + if hasCreatedBy { + // Old table has created_by, copy all columns directly + insertSQL = ` + INSERT INTO issues_new ( + id, content_hash, title, description, design, acceptance_criteria, notes, + status, priority, issue_type, assignee, estimated_minutes, created_at, + created_by, updated_at, closed_at, external_ref, source_repo, compaction_level, + compacted_at, compacted_at_commit, original_size, deleted_at, deleted_by, + delete_reason, original_type, sender, ephemeral, close_reason, pinned, + is_template, await_type, await_id, timeout_ns, waiters + ) + SELECT + id, content_hash, title, description, design, acceptance_criteria, notes, + status, priority, issue_type, assignee, estimated_minutes, created_at, + created_by, updated_at, closed_at, external_ref, source_repo, compaction_level, + compacted_at, compacted_at_commit, original_size, deleted_at, deleted_by, + delete_reason, original_type, sender, ephemeral, close_reason, pinned, + is_template, await_type, await_id, timeout_ns, waiters + FROM issues + ` + } else { + // Old table doesn't have created_by, use empty string default + insertSQL = ` + INSERT INTO issues_new ( + id, content_hash, title, description, design, acceptance_criteria, notes, + status, priority, issue_type, assignee, estimated_minutes, created_at, + created_by, updated_at, closed_at, external_ref, source_repo, compaction_level, + compacted_at, compacted_at_commit, original_size, deleted_at, deleted_by, + delete_reason, original_type, sender, ephemeral, close_reason, pinned, + is_template, await_type, await_id, timeout_ns, waiters + ) + SELECT + id, content_hash, title, description, design, acceptance_criteria, notes, + status, priority, issue_type, assignee, estimated_minutes, created_at, + '', updated_at, closed_at, external_ref, source_repo, compaction_level, + compacted_at, compacted_at_commit, original_size, deleted_at, deleted_by, + delete_reason, original_type, sender, ephemeral, close_reason, pinned, + is_template, await_type, await_id, timeout_ns, waiters + FROM issues + ` + } + + _, err = db.Exec(insertSQL) if err != nil { return fmt.Errorf("failed to copy issues data: %w", err) } diff --git a/internal/storage/sqlite/multirepo_test.go b/internal/storage/sqlite/multirepo_test.go index 5741fd60..18d229b4 100644 --- a/internal/storage/sqlite/multirepo_test.go +++ b/internal/storage/sqlite/multirepo_test.go @@ -909,7 +909,7 @@ func TestUpsertPreservesGateFields(t *testing.T) { Status: types.StatusOpen, Priority: 1, IssueType: types.TypeGate, - Wisp: true, + Ephemeral: true, AwaitType: "gh:run", AwaitID: "123456789", Timeout: 30 * 60 * 1000000000, // 30 minutes in nanoseconds