Files
beads/internal/storage/dolt/rename.go
LoomDeBWiles c40affd601 fix(storage): normalize timestamps to UTC to prevent validation failures (#1123)
All time.Now() calls in the dolt storage layer now use time.Now().UTC()
to ensure consistent timezone handling. Previously, timestamps could be
stored with mixed timezone formats (UTC 'Z' vs local '+01:00'), causing
bv validation to fail when updated_at appeared earlier than created_at
in absolute time.

Files modified:
- transaction.go: CreateIssue, UpdateIssue, CloseIssue
- issues.go: CreateIssue, CreateIssues, UpdateIssue, CloseIssue, markDirty, manageClosedAt
- rename.go: UpdateIssueID (2 locations)
- events.go: AddIssueComment (2 locations)
- dirty.go: SetExportHash
- queries.go: Overdue filter, GetStaleIssues

Fixes: bd-84gw9

Co-authored-by: LoomDeBWiles <loomenwiles@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 19:22:50 -08:00

130 lines
4.0 KiB
Go

package dolt
import (
"context"
"fmt"
"time"
"github.com/steveyegge/beads/internal/types"
)
// UpdateIssueID updates an issue ID and all its references
func (s *DoltStore) UpdateIssueID(ctx context.Context, oldID, newID string, issue *types.Issue, actor string) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer func() { _ = tx.Rollback() }()
// Update the issue itself
result, err := tx.ExecContext(ctx, `
UPDATE issues
SET id = ?, title = ?, description = ?, design = ?, acceptance_criteria = ?, notes = ?, updated_at = ?
WHERE id = ?
`, newID, issue.Title, issue.Description, issue.Design, issue.AcceptanceCriteria, issue.Notes, time.Now().UTC(), oldID)
if err != nil {
return fmt.Errorf("failed to update issue ID: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rows == 0 {
return fmt.Errorf("issue not found: %s", oldID)
}
// Update references in dependencies
_, err = tx.ExecContext(ctx, `UPDATE dependencies SET issue_id = ? WHERE issue_id = ?`, newID, oldID)
if err != nil {
return fmt.Errorf("failed to update issue_id in dependencies: %w", err)
}
_, err = tx.ExecContext(ctx, `UPDATE dependencies SET depends_on_id = ? WHERE depends_on_id = ?`, newID, oldID)
if err != nil {
return fmt.Errorf("failed to update depends_on_id in dependencies: %w", err)
}
// Update references in events
_, err = tx.ExecContext(ctx, `UPDATE events SET issue_id = ? WHERE issue_id = ?`, newID, oldID)
if err != nil {
return fmt.Errorf("failed to update events: %w", err)
}
// Update references in labels
_, err = tx.ExecContext(ctx, `UPDATE labels SET issue_id = ? WHERE issue_id = ?`, newID, oldID)
if err != nil {
return fmt.Errorf("failed to update labels: %w", err)
}
// Update references in comments
_, err = tx.ExecContext(ctx, `UPDATE comments SET issue_id = ? WHERE issue_id = ?`, newID, oldID)
if err != nil {
return fmt.Errorf("failed to update comments: %w", err)
}
// Update dirty_issues
_, err = tx.ExecContext(ctx, `
INSERT INTO dirty_issues (issue_id, marked_at)
VALUES (?, ?)
ON DUPLICATE KEY UPDATE marked_at = VALUES(marked_at)
`, newID, time.Now().UTC())
if err != nil {
return fmt.Errorf("failed to mark issue dirty: %w", err)
}
// Delete old dirty entry
_, err = tx.ExecContext(ctx, `DELETE FROM dirty_issues WHERE issue_id = ?`, oldID)
if err != nil {
return fmt.Errorf("failed to delete old dirty entry: %w", err)
}
// Record rename event
_, err = tx.ExecContext(ctx, `
INSERT INTO events (issue_id, event_type, actor, old_value, new_value)
VALUES (?, 'renamed', ?, ?, ?)
`, newID, actor, oldID, newID)
if err != nil {
return fmt.Errorf("failed to record rename event: %w", err)
}
return tx.Commit()
}
// RenameDependencyPrefix updates the prefix in all dependency records
func (s *DoltStore) RenameDependencyPrefix(ctx context.Context, oldPrefix, newPrefix string) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer func() { _ = tx.Rollback() }()
// Update issue_id column
_, err = tx.ExecContext(ctx, `
UPDATE dependencies
SET issue_id = CONCAT(?, SUBSTRING(issue_id, LENGTH(?) + 1))
WHERE issue_id LIKE CONCAT(?, '%')
`, newPrefix, oldPrefix, oldPrefix)
if err != nil {
return fmt.Errorf("failed to update issue_id in dependencies: %w", err)
}
// Update depends_on_id column
_, err = tx.ExecContext(ctx, `
UPDATE dependencies
SET depends_on_id = CONCAT(?, SUBSTRING(depends_on_id, LENGTH(?) + 1))
WHERE depends_on_id LIKE CONCAT(?, '%')
`, newPrefix, oldPrefix, oldPrefix)
if err != nil {
return fmt.Errorf("failed to update depends_on_id in dependencies: %w", err)
}
return tx.Commit()
}
// RenameCounterPrefix is a no-op with hash-based IDs
func (s *DoltStore) RenameCounterPrefix(ctx context.Context, oldPrefix, newPrefix string) error {
// Hash-based IDs don't use counters
return nil
}