diff --git a/bd-test b/bd-test index 5314d303..5f377d35 100755 Binary files a/bd-test and b/bd-test differ diff --git a/internal/importer/importer.go b/internal/importer/importer.go index 9220c5f7..4e93e86e 100644 --- a/internal/importer/importer.go +++ b/internal/importer/importer.go @@ -537,19 +537,24 @@ func upsertIssues(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, issues } } - // Batch create all new issues with topological sorting - // 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 { - SortByDepth(newIssues) +// Batch create all new issues +// Sort by hierarchy depth to ensure parents are created before children +if len(newIssues) > 0 { + sort.Slice(newIssues, func(i, j int) bool { + 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) - // This handles parent-child pairs in the same import batch +// Create in batches by depth level (max depth 3) for depth := 0; depth <= 3; depth++ { - var batchForDepth []*types.Issue - for _, issue := range newIssues { - if GetHierarchyDepth(issue.ID) == depth { - batchForDepth = append(batchForDepth, issue) + var batchForDepth []*types.Issue + for _, issue := range newIssues { + if strings.Count(issue.ID, ".") == depth { + batchForDepth = append(batchForDepth, issue) } } if len(batchForDepth) > 0 { diff --git a/internal/storage/sqlite/ids.go b/internal/storage/sqlite/ids.go index 60136cb2..13e4641d 100644 --- a/internal/storage/sqlite/ids.go +++ b/internal/storage/sqlite/ids.go @@ -172,10 +172,22 @@ func GenerateBatchIssueIDs(ctx context.Context, conn *sql.Conn, prefix string, i 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 // 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) -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) // 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 if strings.Contains(issues[i].ID, ".") { - // Try to resurrect entire parent chain if any parents are missing - // 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) + // Extract parent ID (everything before the last dot) + lastDot := strings.LastIndex(issues[i].ID, ".") + parentID := issues[i].ID[:lastDot] + + 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