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>
This commit is contained in:
@@ -82,7 +82,7 @@ func (s *DoltStore) SetExportHash(ctx context.Context, issueID, contentHash stri
|
|||||||
INSERT INTO export_hashes (issue_id, content_hash, exported_at)
|
INSERT INTO export_hashes (issue_id, content_hash, exported_at)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE content_hash = VALUES(content_hash), exported_at = VALUES(exported_at)
|
ON DUPLICATE KEY UPDATE content_hash = VALUES(content_hash), exported_at = VALUES(exported_at)
|
||||||
`, issueID, contentHash, time.Now())
|
`, issueID, contentHash, time.Now().UTC())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to set export hash: %w", err)
|
return fmt.Errorf("failed to set export hash: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func (s *DoltStore) AddIssueComment(ctx context.Context, issueID, author, text s
|
|||||||
result, err := s.db.ExecContext(ctx, `
|
result, err := s.db.ExecContext(ctx, `
|
||||||
INSERT INTO comments (issue_id, author, text, created_at)
|
INSERT INTO comments (issue_id, author, text, created_at)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
`, issueID, author, text, time.Now())
|
`, issueID, author, text, time.Now().UTC())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to add comment: %w", err)
|
return nil, fmt.Errorf("failed to add comment: %w", err)
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ func (s *DoltStore) AddIssueComment(ctx context.Context, issueID, author, text s
|
|||||||
IssueID: issueID,
|
IssueID: issueID,
|
||||||
Author: author,
|
Author: author,
|
||||||
Text: text,
|
Text: text,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now().UTC(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func (s *DoltStore) CreateIssue(ctx context.Context, issue *types.Issue, actor s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set timestamps
|
// Set timestamps
|
||||||
now := time.Now()
|
now := time.Now().UTC()
|
||||||
if issue.CreatedAt.IsZero() {
|
if issue.CreatedAt.IsZero() {
|
||||||
issue.CreatedAt = now
|
issue.CreatedAt = now
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ func (s *DoltStore) CreateIssues(ctx context.Context, issues []*types.Issue, act
|
|||||||
defer func() { _ = tx.Rollback() }()
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
now := time.Now()
|
now := time.Now().UTC()
|
||||||
if issue.CreatedAt.IsZero() {
|
if issue.CreatedAt.IsZero() {
|
||||||
issue.CreatedAt = now
|
issue.CreatedAt = now
|
||||||
}
|
}
|
||||||
@@ -239,7 +239,7 @@ func (s *DoltStore) UpdateIssue(ctx context.Context, id string, updates map[stri
|
|||||||
|
|
||||||
// Build update query
|
// Build update query
|
||||||
setClauses := []string{"updated_at = ?"}
|
setClauses := []string{"updated_at = ?"}
|
||||||
args := []interface{}{time.Now()}
|
args := []interface{}{time.Now().UTC()}
|
||||||
|
|
||||||
for key, value := range updates {
|
for key, value := range updates {
|
||||||
if !isAllowedUpdateField(key) {
|
if !isAllowedUpdateField(key) {
|
||||||
@@ -289,7 +289,7 @@ func (s *DoltStore) UpdateIssue(ctx context.Context, id string, updates map[stri
|
|||||||
|
|
||||||
// CloseIssue closes an issue with a reason
|
// CloseIssue closes an issue with a reason
|
||||||
func (s *DoltStore) CloseIssue(ctx context.Context, id string, reason string, actor string, session string) error {
|
func (s *DoltStore) CloseIssue(ctx context.Context, id string, reason string, actor string, session string) error {
|
||||||
now := time.Now()
|
now := time.Now().UTC()
|
||||||
|
|
||||||
tx, err := s.db.BeginTx(ctx, nil)
|
tx, err := s.db.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -597,7 +597,7 @@ func markDirty(ctx context.Context, tx *sql.Tx, issueID string) error {
|
|||||||
INSERT INTO dirty_issues (issue_id, marked_at)
|
INSERT INTO dirty_issues (issue_id, marked_at)
|
||||||
VALUES (?, ?)
|
VALUES (?, ?)
|
||||||
ON DUPLICATE KEY UPDATE marked_at = VALUES(marked_at)
|
ON DUPLICATE KEY UPDATE marked_at = VALUES(marked_at)
|
||||||
`, issueID, time.Now())
|
`, issueID, time.Now().UTC())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,7 +645,7 @@ func manageClosedAt(oldIssue *types.Issue, updates map[string]interface{}, setCl
|
|||||||
}
|
}
|
||||||
|
|
||||||
if newStatus == string(types.StatusClosed) {
|
if newStatus == string(types.StatusClosed) {
|
||||||
now := time.Now()
|
now := time.Now().UTC()
|
||||||
setClauses = append(setClauses, "closed_at = ?")
|
setClauses = append(setClauses, "closed_at = ?")
|
||||||
args = append(args, now)
|
args = append(args, now)
|
||||||
} else if oldIssue.Status == types.StatusClosed {
|
} else if oldIssue.Status == types.StatusClosed {
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ func (s *DoltStore) SearchIssues(ctx context.Context, query string, filter types
|
|||||||
}
|
}
|
||||||
if filter.Overdue {
|
if filter.Overdue {
|
||||||
whereClauses = append(whereClauses, "due_at IS NOT NULL AND due_at < ? AND status != ?")
|
whereClauses = append(whereClauses, "due_at IS NOT NULL AND due_at < ? AND status != ?")
|
||||||
args = append(args, time.Now().Format(time.RFC3339), types.StatusClosed)
|
args = append(args, time.Now().UTC().Format(time.RFC3339), types.StatusClosed)
|
||||||
}
|
}
|
||||||
|
|
||||||
whereSQL := ""
|
whereSQL := ""
|
||||||
@@ -402,7 +402,7 @@ func (s *DoltStore) GetEpicsEligibleForClosure(ctx context.Context) ([]*types.Ep
|
|||||||
|
|
||||||
// GetStaleIssues returns issues that haven't been updated recently
|
// GetStaleIssues returns issues that haven't been updated recently
|
||||||
func (s *DoltStore) GetStaleIssues(ctx context.Context, filter types.StaleFilter) ([]*types.Issue, error) {
|
func (s *DoltStore) GetStaleIssues(ctx context.Context, filter types.StaleFilter) ([]*types.Issue, error) {
|
||||||
cutoff := time.Now().AddDate(0, 0, -filter.Days)
|
cutoff := time.Now().UTC().AddDate(0, 0, -filter.Days)
|
||||||
|
|
||||||
statusClause := "status IN ('open', 'in_progress')"
|
statusClause := "status IN ('open', 'in_progress')"
|
||||||
if filter.Status != "" {
|
if filter.Status != "" {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func (s *DoltStore) UpdateIssueID(ctx context.Context, oldID, newID string, issu
|
|||||||
UPDATE issues
|
UPDATE issues
|
||||||
SET id = ?, title = ?, description = ?, design = ?, acceptance_criteria = ?, notes = ?, updated_at = ?
|
SET id = ?, title = ?, description = ?, design = ?, acceptance_criteria = ?, notes = ?, updated_at = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`, newID, issue.Title, issue.Description, issue.Design, issue.AcceptanceCriteria, issue.Notes, time.Now(), oldID)
|
`, newID, issue.Title, issue.Description, issue.Design, issue.AcceptanceCriteria, issue.Notes, time.Now().UTC(), oldID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to update issue ID: %w", err)
|
return fmt.Errorf("failed to update issue ID: %w", err)
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ func (s *DoltStore) UpdateIssueID(ctx context.Context, oldID, newID string, issu
|
|||||||
INSERT INTO dirty_issues (issue_id, marked_at)
|
INSERT INTO dirty_issues (issue_id, marked_at)
|
||||||
VALUES (?, ?)
|
VALUES (?, ?)
|
||||||
ON DUPLICATE KEY UPDATE marked_at = VALUES(marked_at)
|
ON DUPLICATE KEY UPDATE marked_at = VALUES(marked_at)
|
||||||
`, newID, time.Now())
|
`, newID, time.Now().UTC())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to mark issue dirty: %w", err)
|
return fmt.Errorf("failed to mark issue dirty: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func (s *DoltStore) RunInTransaction(ctx context.Context, fn func(tx storage.Tra
|
|||||||
|
|
||||||
// CreateIssue creates an issue within the transaction
|
// CreateIssue creates an issue within the transaction
|
||||||
func (t *doltTransaction) CreateIssue(ctx context.Context, issue *types.Issue, actor string) error {
|
func (t *doltTransaction) CreateIssue(ctx context.Context, issue *types.Issue, actor string) error {
|
||||||
now := time.Now()
|
now := time.Now().UTC()
|
||||||
if issue.CreatedAt.IsZero() {
|
if issue.CreatedAt.IsZero() {
|
||||||
issue.CreatedAt = now
|
issue.CreatedAt = now
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ func (t *doltTransaction) SearchIssues(ctx context.Context, query string, filter
|
|||||||
// UpdateIssue updates an issue within the transaction
|
// UpdateIssue updates an issue within the transaction
|
||||||
func (t *doltTransaction) UpdateIssue(ctx context.Context, id string, updates map[string]interface{}, actor string) error {
|
func (t *doltTransaction) UpdateIssue(ctx context.Context, id string, updates map[string]interface{}, actor string) error {
|
||||||
setClauses := []string{"updated_at = ?"}
|
setClauses := []string{"updated_at = ?"}
|
||||||
args := []interface{}{time.Now()}
|
args := []interface{}{time.Now().UTC()}
|
||||||
|
|
||||||
for key, value := range updates {
|
for key, value := range updates {
|
||||||
if !isAllowedUpdateField(key) {
|
if !isAllowedUpdateField(key) {
|
||||||
@@ -145,7 +145,7 @@ func (t *doltTransaction) UpdateIssue(ctx context.Context, id string, updates ma
|
|||||||
|
|
||||||
// CloseIssue closes an issue within the transaction
|
// CloseIssue closes an issue within the transaction
|
||||||
func (t *doltTransaction) CloseIssue(ctx context.Context, id string, reason string, actor string, session string) error {
|
func (t *doltTransaction) CloseIssue(ctx context.Context, id string, reason string, actor string, session string) error {
|
||||||
now := time.Now()
|
now := time.Now().UTC()
|
||||||
_, err := t.tx.ExecContext(ctx, `
|
_, err := t.tx.ExecContext(ctx, `
|
||||||
UPDATE issues SET status = ?, closed_at = ?, updated_at = ?, close_reason = ?, closed_by_session = ?
|
UPDATE issues SET status = ?, closed_at = ?, updated_at = ?, close_reason = ?, closed_by_session = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
|
|||||||
Reference in New Issue
Block a user