Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions internal/storage/dolt/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func (s *DoltStore) GetReadyWork(ctx context.Context, filter types.WorkFilter) (
}
// Exclude future-deferred issues unless IncludeDeferred is set
if !filter.IncludeDeferred {
whereClauses = append(whereClauses, "(defer_until IS NULL OR defer_until <= NOW())")
whereClauses = append(whereClauses, "(defer_until IS NULL OR defer_until <= UTC_TIMESTAMP())")
}
// Exclude children of future-deferred parents (GH#1190)
// Pre-compute excluded IDs using separate single-table queries to avoid
Expand Down Expand Up @@ -773,7 +773,7 @@ func (s *DoltStore) getChildrenOfDeferredParents(ctx context.Context) ([]string,
// Step 1: Get IDs of issues with future defer_until
deferredRows, err := s.queryContext(ctx, `
SELECT id FROM issues
WHERE defer_until IS NOT NULL AND defer_until > NOW()
WHERE defer_until IS NOT NULL AND defer_until > UTC_TIMESTAMP()
`)
if err != nil {
return nil, wrapQueryError("deferred parents: get deferred issues", err)
Expand Down
103 changes: 103 additions & 0 deletions internal/storage/dolt/queries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,109 @@ func TestGetReadyWork_CustomStatusBlockerStillBlocks(t *testing.T) {
}
}

// TestGetReadyWork_PastDeferredIssueIsReady verifies that an issue whose
// defer_until is in the past appears in ready work. Regression test for a
// timezone bug: Go stores defer_until as UTC, but Dolt's NOW() returns local
// time. On non-UTC machines, the comparison defer_until <= NOW() would
// incorrectly exclude past-deferred issues. The fix uses UTC_TIMESTAMP().
func TestGetReadyWork_PastDeferredIssueIsReady(t *testing.T) {
store, cleanup := setupTestStore(t)
defer cleanup()

ctx, cancel := testContext(t)
defer cancel()

// Create an issue and set defer_until to 1 hour in the past (UTC).
pastDeferred := &types.Issue{
ID: "rw-past-deferred",
Title: "Past Deferred Task",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, pastDeferred, "tester"); err != nil {
t.Fatalf("failed to create issue: %v", err)
}
pastTime := time.Now().UTC().Add(-1 * time.Hour)
if err := store.UpdateIssue(ctx, pastDeferred.ID, map[string]interface{}{
"defer_until": pastTime,
}, "tester"); err != nil {
t.Fatalf("failed to set defer_until: %v", err)
}

// Create a normal issue (no defer) as a control.
normal := &types.Issue{
ID: "rw-normal",
Title: "Normal Task",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, normal, "tester"); err != nil {
t.Fatalf("failed to create issue: %v", err)
}

work, err := store.GetReadyWork(ctx, types.WorkFilter{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

foundPastDeferred := false
foundNormal := false
for _, w := range work {
if w.ID == pastDeferred.ID {
foundPastDeferred = true
}
if w.ID == normal.ID {
foundNormal = true
}
}
if !foundNormal {
t.Error("normal issue should appear in ready work")
}
if !foundPastDeferred {
t.Error("past-deferred issue (defer_until in the past) should appear in ready work")
}
}

// TestGetReadyWork_FutureDeferredIssueExcluded verifies that an issue whose
// defer_until is in the future does NOT appear in ready work.
func TestGetReadyWork_FutureDeferredIssueExcluded(t *testing.T) {
store, cleanup := setupTestStore(t)
defer cleanup()

ctx, cancel := testContext(t)
defer cancel()

futureDeferred := &types.Issue{
ID: "rw-future-deferred",
Title: "Future Deferred Task",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, futureDeferred, "tester"); err != nil {
t.Fatalf("failed to create issue: %v", err)
}
futureTime := time.Now().UTC().Add(24 * time.Hour)
if err := store.UpdateIssue(ctx, futureDeferred.ID, map[string]interface{}{
"defer_until": futureTime,
}, "tester"); err != nil {
t.Fatalf("failed to set defer_until: %v", err)
}

work, err := store.GetReadyWork(ctx, types.WorkFilter{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

for _, w := range work {
if w.ID == futureDeferred.ID {
t.Error("future-deferred issue should NOT appear in ready work")
}
}
}

// =============================================================================
// GetBlockedIssues tests
// =============================================================================
Expand Down
8 changes: 4 additions & 4 deletions internal/storage/dolt/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,14 +294,14 @@ LEFT JOIN blocked_transitively bt ON bt.issue_id = i.id
WHERE i.status = 'open'
AND (i.ephemeral = 0 OR i.ephemeral IS NULL)
AND bt.issue_id IS NULL
AND (i.defer_until IS NULL OR i.defer_until <= NOW())
AND (i.defer_until IS NULL OR i.defer_until <= UTC_TIMESTAMP())
AND NOT EXISTS (
SELECT 1 FROM dependencies d_parent
JOIN issues parent ON parent.id = d_parent.depends_on_id
WHERE d_parent.issue_id = i.id
AND d_parent.type = 'parent-child'
AND parent.defer_until IS NOT NULL
AND parent.defer_until > NOW()
AND parent.defer_until > UTC_TIMESTAMP()
);
`

Expand Down Expand Up @@ -378,14 +378,14 @@ LEFT JOIN blocked_transitively bt ON bt.issue_id = i.id
WHERE i.status IN (` + statusList + `)
AND (i.ephemeral = 0 OR i.ephemeral IS NULL)
AND bt.issue_id IS NULL
AND (i.defer_until IS NULL OR i.defer_until <= NOW())
AND (i.defer_until IS NULL OR i.defer_until <= UTC_TIMESTAMP())
AND NOT EXISTS (
SELECT 1 FROM dependencies d_parent
JOIN issues parent ON parent.id = d_parent.depends_on_id
WHERE d_parent.issue_id = i.id
AND d_parent.type = 'parent-child'
AND parent.defer_until IS NOT NULL
AND parent.defer_until > NOW()
AND parent.defer_until > UTC_TIMESTAMP()
);
`
}
Expand Down