Files
beads/internal/storage/dolt/dirty.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

110 lines
3.3 KiB
Go

package dolt
import (
"context"
"fmt"
"strings"
"time"
)
// GetDirtyIssues returns IDs of issues that have been modified since last export
func (s *DoltStore) GetDirtyIssues(ctx context.Context) ([]string, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT issue_id FROM dirty_issues ORDER BY marked_at ASC
`)
if err != nil {
return nil, fmt.Errorf("failed to get dirty issues: %w", err)
}
defer rows.Close()
var ids []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, fmt.Errorf("failed to scan issue id: %w", err)
}
ids = append(ids, id)
}
return ids, rows.Err()
}
// GetDirtyIssueHash returns the dirty hash for a specific issue
func (s *DoltStore) GetDirtyIssueHash(ctx context.Context, issueID string) (string, error) {
var hash string
err := s.db.QueryRowContext(ctx, `
SELECT i.content_hash FROM issues i
JOIN dirty_issues d ON i.id = d.issue_id
WHERE d.issue_id = ?
`, issueID).Scan(&hash)
if err != nil {
return "", fmt.Errorf("failed to get dirty issue hash: %w", err)
}
return hash, nil
}
// ClearDirtyIssuesByID removes specific issues from the dirty list
func (s *DoltStore) ClearDirtyIssuesByID(ctx context.Context, issueIDs []string) error {
if len(issueIDs) == 0 {
return nil
}
placeholders := make([]string, len(issueIDs))
args := make([]interface{}, len(issueIDs))
for i, id := range issueIDs {
placeholders[i] = "?"
args[i] = id
}
// nolint:gosec // G201: placeholders contains only ? markers, actual values passed via args
query := fmt.Sprintf("DELETE FROM dirty_issues WHERE issue_id IN (%s)", strings.Join(placeholders, ","))
_, err := s.db.ExecContext(ctx, query, args...)
if err != nil {
return fmt.Errorf("failed to clear dirty issues: %w", err)
}
return nil
}
// GetExportHash returns the last export hash for an issue
func (s *DoltStore) GetExportHash(ctx context.Context, issueID string) (string, error) {
var hash string
err := s.db.QueryRowContext(ctx, `
SELECT content_hash FROM export_hashes WHERE issue_id = ?
`, issueID).Scan(&hash)
if err != nil {
return "", nil // Not found is OK
}
return hash, nil
}
// SetExportHash stores the export hash for an issue
func (s *DoltStore) SetExportHash(ctx context.Context, issueID, contentHash string) error {
_, err := s.db.ExecContext(ctx, `
INSERT INTO export_hashes (issue_id, content_hash, exported_at)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE content_hash = VALUES(content_hash), exported_at = VALUES(exported_at)
`, issueID, contentHash, time.Now().UTC())
if err != nil {
return fmt.Errorf("failed to set export hash: %w", err)
}
return nil
}
// ClearAllExportHashes removes all export hashes (for full re-export)
func (s *DoltStore) ClearAllExportHashes(ctx context.Context) error {
_, err := s.db.ExecContext(ctx, "DELETE FROM export_hashes")
if err != nil {
return fmt.Errorf("failed to clear export hashes: %w", err)
}
return nil
}
// GetJSONLFileHash returns the stored JSONL file hash
func (s *DoltStore) GetJSONLFileHash(ctx context.Context) (string, error) {
return s.GetMetadata(ctx, "jsonl_file_hash")
}
// SetJSONLFileHash stores the JSONL file hash
func (s *DoltStore) SetJSONLFileHash(ctx context.Context, fileHash string) error {
return s.SetMetadata(ctx, "jsonl_file_hash", fileHash)
}