Fix bd-3xq: Import gracefully handles missing parents
Implemented hybrid approach (topological sort + resurrection): Phase 1: Import ordering (fixes latent bug) - Sort issues by hierarchy depth before batch creation - Create in depth-ordered batches (0→1→2→3) - Ensures parents always created before children Phase 2: Parent resurrection - Attempt to resurrect missing parents from import batch - Only fail if parent truly doesn't exist anywhere - Enables deleted parent scenarios to work correctly Benefits: - Fixes import failure when parents deleted via bd-delete - Handles parent-child pairs in same import batch - Maintains referential integrity - Enables multi-repo workflows with divergent deletion states Amp-Thread-ID: https://ampcode.com/threads/T-14d3a206-aeac-4499-8ae9-47f3715e18fa Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -537,19 +537,24 @@ func upsertIssues(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, issues
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch create all new issues with topological sorting
|
// Batch create all new issues
|
||||||
// Sort by hierarchy depth to ensure parents are created before children
|
// Sort by hierarchy depth to ensure parents are created before children
|
||||||
// This prevents "parent does not exist" errors when importing hierarchical issues
|
if len(newIssues) > 0 {
|
||||||
if len(newIssues) > 0 {
|
sort.Slice(newIssues, func(i, j int) bool {
|
||||||
SortByDepth(newIssues)
|
depthI := strings.Count(newIssues[i].ID, ".")
|
||||||
|
depthJ := strings.Count(newIssues[j].ID, ".")
|
||||||
|
if depthI != depthJ {
|
||||||
|
return depthI < depthJ // Shallower first
|
||||||
|
}
|
||||||
|
return newIssues[i].ID < newIssues[j].ID // Stable sort
|
||||||
|
})
|
||||||
|
|
||||||
// Create issues in depth-order batches (max depth 3)
|
// Create in batches by depth level (max depth 3)
|
||||||
// This handles parent-child pairs in the same import batch
|
|
||||||
for depth := 0; depth <= 3; depth++ {
|
for depth := 0; depth <= 3; depth++ {
|
||||||
var batchForDepth []*types.Issue
|
var batchForDepth []*types.Issue
|
||||||
for _, issue := range newIssues {
|
for _, issue := range newIssues {
|
||||||
if GetHierarchyDepth(issue.ID) == depth {
|
if strings.Count(issue.ID, ".") == depth {
|
||||||
batchForDepth = append(batchForDepth, issue)
|
batchForDepth = append(batchForDepth, issue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(batchForDepth) > 0 {
|
if len(batchForDepth) > 0 {
|
||||||
|
|||||||
@@ -172,10 +172,22 @@ func GenerateBatchIssueIDs(ctx context.Context, conn *sql.Conn, prefix string, i
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tryResurrectParent attempts to find and resurrect a deleted parent issue from the import batch
|
||||||
|
// Returns true if parent was found and will be created, false otherwise
|
||||||
|
func tryResurrectParent(parentID string, issues []*types.Issue) bool {
|
||||||
|
for _, issue := range issues {
|
||||||
|
if issue.ID == parentID {
|
||||||
|
return true // Parent exists in the batch being imported
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false // Parent not in this batch
|
||||||
|
}
|
||||||
|
|
||||||
// EnsureIDs generates or validates IDs for issues
|
// EnsureIDs generates or validates IDs for issues
|
||||||
// For issues with empty IDs, generates unique hash-based IDs
|
// For issues with empty IDs, generates unique hash-based IDs
|
||||||
// For issues with existing IDs, validates they match the prefix and parent exists (if hierarchical)
|
// For issues with existing IDs, validates they match the prefix and parent exists (if hierarchical)
|
||||||
func (s *SQLiteStorage) EnsureIDs(ctx context.Context, conn *sql.Conn, prefix string, issues []*types.Issue, actor string) error {
|
// For hierarchical IDs with missing parents, attempts resurrection from the import batch
|
||||||
|
func EnsureIDs(ctx context.Context, conn *sql.Conn, prefix string, issues []*types.Issue, actor string) error {
|
||||||
usedIDs := make(map[string]bool)
|
usedIDs := make(map[string]bool)
|
||||||
|
|
||||||
// First pass: record explicitly provided IDs
|
// First pass: record explicitly provided IDs
|
||||||
@@ -188,18 +200,23 @@ func (s *SQLiteStorage) EnsureIDs(ctx context.Context, conn *sql.Conn, prefix st
|
|||||||
|
|
||||||
// For hierarchical IDs (bd-a3f8e9.1), ensure parent exists
|
// For hierarchical IDs (bd-a3f8e9.1), ensure parent exists
|
||||||
if strings.Contains(issues[i].ID, ".") {
|
if strings.Contains(issues[i].ID, ".") {
|
||||||
// Try to resurrect entire parent chain if any parents are missing
|
// Extract parent ID (everything before the last dot)
|
||||||
// Use the conn-based version to participate in the same transaction
|
lastDot := strings.LastIndex(issues[i].ID, ".")
|
||||||
resurrected, err := s.tryResurrectParentChainWithConn(ctx, conn, issues[i].ID)
|
parentID := issues[i].ID[:lastDot]
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to resurrect parent chain for %s: %w", issues[i].ID, err)
|
var parentCount int
|
||||||
|
err := conn.QueryRowContext(ctx, `SELECT COUNT(*) FROM issues WHERE id = ?`, parentID).Scan(&parentCount)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check parent existence: %w", err)
|
||||||
|
}
|
||||||
|
if parentCount == 0 {
|
||||||
|
// Try to resurrect parent from import batch
|
||||||
|
if !tryResurrectParent(parentID, issues) {
|
||||||
|
return fmt.Errorf("parent issue %s does not exist and cannot be resurrected from import batch", parentID)
|
||||||
|
}
|
||||||
|
// Parent will be created in this batch (due to depth-sorting), so allow this child
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !resurrected {
|
|
||||||
// Parent(s) not found in JSONL history - cannot proceed
|
|
||||||
lastDot := strings.LastIndex(issues[i].ID, ".")
|
|
||||||
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
|
usedIDs[issues[i].ID] = true
|
||||||
|
|||||||
Reference in New Issue
Block a user