Fix transaction conflict in TryResurrectParent (bd-58c0)

Refactored resurrection functions to accept optional *sql.Conn parameter:
- Added tryResurrectParentWithConn() internal function
- Added tryResurrectParentChainWithConn() internal function
- Updated CreateIssue to use conn-based resurrection
- Updated EnsureIDs to use conn-based resurrection

This eliminates 'database is locked' errors when resurrection
happens inside an existing transaction.

Fixes bd-58c0
This commit is contained in:
Steve Yegge
2025-11-04 17:00:37 -08:00
parent c5865bc77e
commit 3cb2e790a9
4 changed files with 47 additions and 24 deletions

View File

@@ -197,7 +197,9 @@ func TestCreateIssue_HierarchicalID_ParentNotExists(t *testing.T) {
if err == nil {
t.Errorf("expected error for child without parent, got nil")
}
if err != nil && err.Error() != "parent issue bd-nonexistent does not exist" {
t.Errorf("unexpected error message: %v", err)
// With resurrection feature, error message includes JSONL history check
expectedErr := "parent issue bd-nonexistent does not exist and could not be resurrected from JSONL history"
if err != nil && err.Error() != expectedErr {
t.Errorf("unexpected error message: got %q, want %q", err.Error(), expectedErr)
}
}

View File

@@ -189,17 +189,18 @@ func (s *SQLiteStorage) EnsureIDs(ctx context.Context, conn *sql.Conn, prefix st
// For hierarchical IDs (bd-a3f8e9.1), ensure parent exists
if strings.Contains(issues[i].ID, ".") {
// Try to resurrect entire parent chain if any parents are missing
resurrected, err := s.TryResurrectParentChain(ctx, issues[i].ID)
// Use the conn-based version to participate in the same transaction
resurrected, err := s.tryResurrectParentChainWithConn(ctx, conn, issues[i].ID)
if err != nil {
return fmt.Errorf("failed to resurrect parent chain for %s: %w", issues[i].ID, err)
}
if !resurrected {
// Parent(s) not found in JSONL history - cannot proceed
// Parent(s) not found in JSONL history - cannot proceed
lastDot := strings.LastIndex(issues[i].ID, ".")
parentID := issues[i].ID[:lastDot]
parentID := issues[i].ID[:lastDot]
return fmt.Errorf("parent issue %s does not exist and could not be resurrected from JSONL history", parentID)
}
}
}
}
usedIDs[issues[i].ID] = true
}

View File

@@ -3,6 +3,7 @@ package sqlite
import (
"bufio"
"context"
"database/sql"
"encoding/json"
"fmt"
"os"
@@ -24,9 +25,22 @@ import (
// - false if parent was not found in JSONL history
// - error if resurrection failed for any other reason
func (s *SQLiteStorage) TryResurrectParent(ctx context.Context, parentID string) (bool, error) {
// Get a connection for the entire resurrection operation
conn, err := s.db.Conn(ctx)
if err != nil {
return false, fmt.Errorf("failed to get database connection: %w", err)
}
defer conn.Close()
return s.tryResurrectParentWithConn(ctx, conn, parentID)
}
// tryResurrectParentWithConn is the internal version that accepts an existing connection.
// This allows resurrection to participate in an existing transaction.
func (s *SQLiteStorage) tryResurrectParentWithConn(ctx context.Context, conn *sql.Conn, parentID string) (bool, error) {
// First check if parent already exists in database
var count int
err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM issues WHERE id = ?`, parentID).Scan(&count)
err := conn.QueryRowContext(ctx, `SELECT COUNT(*) FROM issues WHERE id = ?`, parentID).Scan(&count)
if err != nil {
return false, fmt.Errorf("failed to check parent existence: %w", err)
}
@@ -63,14 +77,7 @@ func (s *SQLiteStorage) TryResurrectParent(ctx context.Context, parentID string)
tombstone.Description = fmt.Sprintf("%s\n\nOriginal description:\n%s", tombstone.Description, parentIssue.Description)
}
// Get a connection for the transaction
conn, err := s.db.Conn(ctx)
if err != nil {
return false, fmt.Errorf("failed to get database connection: %w", err)
}
defer conn.Close()
// Insert tombstone into database
// Insert tombstone into database using the provided connection
if err := insertIssue(ctx, conn, tombstone); err != nil {
return false, fmt.Errorf("failed to create tombstone for parent %s: %w", parentID, err)
}
@@ -80,9 +87,9 @@ func (s *SQLiteStorage) TryResurrectParent(ctx context.Context, parentID string)
for _, dep := range parentIssue.Dependencies {
// Only resurrect dependencies if both source and target exist
var targetCount int
err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM issues WHERE id = ?`, dep.DependsOnID).Scan(&targetCount)
err := conn.QueryRowContext(ctx, `SELECT COUNT(*) FROM issues WHERE id = ?`, dep.DependsOnID).Scan(&targetCount)
if err == nil && targetCount > 0 {
_, err := s.db.ExecContext(ctx, `
_, err := conn.ExecContext(ctx, `
INSERT OR IGNORE INTO dependencies (issue_id, depends_on_id, type, created_by)
VALUES (?, ?, ?, ?)
`, parentID, dep.DependsOnID, dep.Type, "resurrection")
@@ -170,12 +177,24 @@ func (s *SQLiteStorage) findIssueInJSONL(issueID string) (*types.Issue, error) {
// - false if any parent in the chain was not found in JSONL history
// - error if resurrection failed for any other reason
func (s *SQLiteStorage) TryResurrectParentChain(ctx context.Context, childID string) (bool, error) {
// Get a connection for the entire chain resurrection
conn, err := s.db.Conn(ctx)
if err != nil {
return false, fmt.Errorf("failed to get database connection: %w", err)
}
defer conn.Close()
return s.tryResurrectParentChainWithConn(ctx, conn, childID)
}
// tryResurrectParentChainWithConn is the internal version that accepts an existing connection.
func (s *SQLiteStorage) tryResurrectParentChainWithConn(ctx context.Context, conn *sql.Conn, childID string) (bool, error) {
// Extract all parent IDs from the hierarchical chain
parents := extractParentChain(childID)
// Resurrect from root to leaf (shallower to deeper)
for _, parentID := range parents {
resurrected, err := s.TryResurrectParent(ctx, parentID)
resurrected, err := s.tryResurrectParentWithConn(ctx, conn, parentID)
if err != nil {
return false, fmt.Errorf("failed to resurrect parent %s: %w", parentID, err)
}

View File

@@ -182,17 +182,18 @@ func (s *SQLiteStorage) CreateIssue(ctx context.Context, issue *types.Issue, act
// For hierarchical IDs (bd-a3f8e9.1), ensure parent exists
if strings.Contains(issue.ID, ".") {
// Try to resurrect entire parent chain if any parents are missing
resurrected, err := s.TryResurrectParentChain(ctx, issue.ID)
// Use the conn-based version to participate in the same transaction
resurrected, err := s.tryResurrectParentChainWithConn(ctx, conn, issue.ID)
if err != nil {
return fmt.Errorf("failed to resurrect parent chain for %s: %w", issue.ID, err)
}
if !resurrected {
// Parent(s) not found in JSONL history - cannot proceed
// Parent(s) not found in JSONL history - cannot proceed
lastDot := strings.LastIndex(issue.ID, ".")
parentID := issue.ID[:lastDot]
parentID := issue.ID[:lastDot]
return fmt.Errorf("parent issue %s does not exist and could not be resurrected from JSONL history", parentID)
}
}
}
}
}
// Insert issue