Add pinned column to the issues table to support persistent context markers that should not be treated as work items (bd-7h5). Changes: - Add pinned column to schema.go CREATE TABLE - Add migration 023_pinned_column.go for existing databases - Update all issue queries to include pinned column - Update scanIssues and scanIssuesWithDependencyType to handle pinned field - Add Pinned field to types.Issue struct with JSON serialization - Fix migrations_test.go to include pinned in legacy schema test 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
117 lines
5.6 KiB
Go
117 lines
5.6 KiB
Go
// Package sqlite - database migrations
|
|
package sqlite
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
|
|
"github.com/steveyegge/beads/internal/storage/sqlite/migrations"
|
|
)
|
|
|
|
// Migration represents a single database migration
|
|
type Migration struct {
|
|
Name string
|
|
Func func(*sql.DB) error
|
|
}
|
|
|
|
// migrations is the ordered list of all migrations to run
|
|
// Migrations are run in order during database initialization
|
|
var migrationsList = []Migration{
|
|
{"dirty_issues_table", migrations.MigrateDirtyIssuesTable},
|
|
{"external_ref_column", migrations.MigrateExternalRefColumn},
|
|
{"composite_indexes", migrations.MigrateCompositeIndexes},
|
|
{"closed_at_constraint", migrations.MigrateClosedAtConstraint},
|
|
{"compaction_columns", migrations.MigrateCompactionColumns},
|
|
{"snapshots_table", migrations.MigrateSnapshotsTable},
|
|
{"compaction_config", migrations.MigrateCompactionConfig},
|
|
{"compacted_at_commit_column", migrations.MigrateCompactedAtCommitColumn},
|
|
{"export_hashes_table", migrations.MigrateExportHashesTable},
|
|
{"content_hash_column", migrations.MigrateContentHashColumn},
|
|
{"external_ref_unique", migrations.MigrateExternalRefUnique},
|
|
{"source_repo_column", migrations.MigrateSourceRepoColumn},
|
|
{"repo_mtimes_table", migrations.MigrateRepoMtimesTable},
|
|
{"child_counters_table", migrations.MigrateChildCountersTable},
|
|
{"blocked_issues_cache", migrations.MigrateBlockedIssuesCache},
|
|
{"orphan_detection", migrations.MigrateOrphanDetection},
|
|
{"close_reason_column", migrations.MigrateCloseReasonColumn},
|
|
{"tombstone_columns", migrations.MigrateTombstoneColumns},
|
|
{"messaging_fields", migrations.MigrateMessagingFields},
|
|
{"edge_consolidation", migrations.MigrateEdgeConsolidation},
|
|
{"migrate_edge_fields", migrations.MigrateEdgeFields},
|
|
{"drop_edge_columns", migrations.MigrateDropEdgeColumns},
|
|
{"pinned_column", migrations.MigratePinnedColumn},
|
|
}
|
|
|
|
// MigrationInfo contains metadata about a migration for inspection
|
|
type MigrationInfo struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
// ListMigrations returns list of all registered migrations with descriptions
|
|
// Note: This returns ALL registered migrations, not just pending ones (all are idempotent)
|
|
func ListMigrations() []MigrationInfo {
|
|
result := make([]MigrationInfo, len(migrationsList))
|
|
for i, m := range migrationsList {
|
|
result[i] = MigrationInfo{
|
|
Name: m.Name,
|
|
Description: getMigrationDescription(m.Name),
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// getMigrationDescription returns a human-readable description for a migration
|
|
func getMigrationDescription(name string) string {
|
|
descriptions := map[string]string{
|
|
"dirty_issues_table": "Adds dirty_issues table for auto-export tracking",
|
|
"external_ref_column": "Adds external_ref column to issues table",
|
|
"composite_indexes": "Adds composite indexes for better query performance",
|
|
"closed_at_constraint": "Adds constraint ensuring closed issues have closed_at timestamp",
|
|
"compaction_columns": "Adds compaction tracking columns (compacted_at, compacted_at_commit)",
|
|
"snapshots_table": "Adds snapshots table for issue history",
|
|
"compaction_config": "Adds config entries for compaction",
|
|
"compacted_at_commit_column": "Adds compacted_at_commit to snapshots table",
|
|
"export_hashes_table": "Adds export_hashes table for idempotent exports",
|
|
"content_hash_column": "Adds content_hash column for collision resolution",
|
|
"external_ref_unique": "Adds UNIQUE constraint on external_ref column",
|
|
"source_repo_column": "Adds source_repo column for multi-repo support",
|
|
"repo_mtimes_table": "Adds repo_mtimes table for multi-repo hydration caching",
|
|
"child_counters_table": "Adds child_counters table for hierarchical ID generation with ON DELETE CASCADE",
|
|
"blocked_issues_cache": "Adds blocked_issues_cache table for GetReadyWork performance optimization (bd-5qim)",
|
|
"orphan_detection": "Detects orphaned child issues and logs them for user action (bd-3852)",
|
|
"close_reason_column": "Adds close_reason column to issues table for storing closure explanations (bd-uyu)",
|
|
"tombstone_columns": "Adds tombstone columns (deleted_at, deleted_by, delete_reason, original_type) for inline soft-delete (bd-vw8)",
|
|
"messaging_fields": "Adds messaging fields (sender, ephemeral, replies_to, relates_to, duplicate_of, superseded_by) for inter-agent communication (bd-kwro)",
|
|
"edge_consolidation": "Adds metadata and thread_id columns to dependencies table for edge schema consolidation (Decision 004)",
|
|
"migrate_edge_fields": "Migrates existing issue fields (replies_to, relates_to, duplicate_of, superseded_by) to dependency edges (Decision 004 Phase 3)",
|
|
"drop_edge_columns": "Drops deprecated edge columns (replies_to, relates_to, duplicate_of, superseded_by) from issues table (Decision 004 Phase 4)",
|
|
"pinned_column": "Adds pinned column for persistent context markers (bd-7h5)",
|
|
}
|
|
|
|
if desc, ok := descriptions[name]; ok {
|
|
return desc
|
|
}
|
|
return "Unknown migration"
|
|
}
|
|
|
|
// RunMigrations executes all registered migrations in order with invariant checking
|
|
func RunMigrations(db *sql.DB) error {
|
|
snapshot, err := captureSnapshot(db)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to capture pre-migration snapshot: %w", err)
|
|
}
|
|
|
|
for _, migration := range migrationsList {
|
|
if err := migration.Func(db); err != nil {
|
|
return fmt.Errorf("migration %s failed: %w", migration.Name, err)
|
|
}
|
|
}
|
|
|
|
if err := verifyInvariants(db, snapshot); err != nil {
|
|
return fmt.Errorf("post-migration validation failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|