Wire OrphanHandling through import pipeline (bd-8072)
- Added OrphanHandling type to sqlite package with 4 modes: strict/resurrect/skip/allow - Updated EnsureIDs() to accept orphanHandling parameter and implement mode logic - Added CreateIssuesWithOptions() that passes orphan handling through batch creation - Made importer.OrphanHandling an alias to sqlite.OrphanHandling - Importer now respects opts.OrphanHandling during batch issue creation Next: Add import.orphan_handling config and wire through CLI commands Amp-Thread-ID: https://ampcode.com/threads/T-bb7ffdd9-f444-4975-b5f7-bfff97cb92ff Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -33,7 +33,7 @@ func validateBatchIssues(issues []*types.Issue) error {
|
||||
}
|
||||
|
||||
// generateBatchIDs generates IDs for all issues that need them atomically
|
||||
func (s *SQLiteStorage) generateBatchIDs(ctx context.Context, conn *sql.Conn, issues []*types.Issue, actor string) error {
|
||||
func (s *SQLiteStorage) generateBatchIDs(ctx context.Context, conn *sql.Conn, issues []*types.Issue, actor string, orphanHandling OrphanHandling) error {
|
||||
// Get prefix from config (needed for both generation and validation)
|
||||
var prefix string
|
||||
err := conn.QueryRowContext(ctx, `SELECT value FROM config WHERE key = ?`, "issue_prefix").Scan(&prefix)
|
||||
@@ -45,7 +45,7 @@ func (s *SQLiteStorage) generateBatchIDs(ctx context.Context, conn *sql.Conn, is
|
||||
}
|
||||
|
||||
// Generate or validate IDs for all issues
|
||||
if err := EnsureIDs(ctx, conn, prefix, issues, actor); err != nil {
|
||||
if err := EnsureIDs(ctx, conn, prefix, issues, actor, orphanHandling); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -122,6 +122,12 @@ func bulkMarkDirty(ctx context.Context, conn *sql.Conn, issues []*types.Issue) e
|
||||
// - Single issue creation (use CreateIssue for simplicity)
|
||||
// - Interactive user operations (use CreateIssue)
|
||||
func (s *SQLiteStorage) CreateIssues(ctx context.Context, issues []*types.Issue, actor string) error {
|
||||
// Default to OrphanResurrect for backward compatibility
|
||||
return s.CreateIssuesWithOptions(ctx, issues, actor, OrphanResurrect)
|
||||
}
|
||||
|
||||
// CreateIssuesWithOptions creates multiple issues with configurable orphan handling
|
||||
func (s *SQLiteStorage) CreateIssuesWithOptions(ctx context.Context, issues []*types.Issue, actor string, orphanHandling OrphanHandling) error {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -150,7 +156,7 @@ func (s *SQLiteStorage) CreateIssues(ctx context.Context, issues []*types.Issue,
|
||||
}()
|
||||
|
||||
// Phase 3: Generate IDs for issues that need them
|
||||
if err := s.generateBatchIDs(ctx, conn, issues, actor); err != nil {
|
||||
if err := s.generateBatchIDs(ctx, conn, issues, actor, orphanHandling); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -183,11 +183,21 @@ func tryResurrectParent(parentID string, issues []*types.Issue) bool {
|
||||
return false // Parent not in this batch
|
||||
}
|
||||
|
||||
// OrphanHandling defines how to handle missing parent issues during import
|
||||
type OrphanHandling string
|
||||
|
||||
const (
|
||||
OrphanStrict OrphanHandling = "strict" // Fail import on missing parent
|
||||
OrphanResurrect OrphanHandling = "resurrect" // Auto-resurrect from batch
|
||||
OrphanSkip OrphanHandling = "skip" // Skip orphaned issues
|
||||
OrphanAllow OrphanHandling = "allow" // Allow orphans (default)
|
||||
)
|
||||
|
||||
// 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)
|
||||
// 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 {
|
||||
// For hierarchical IDs with missing parents, behavior depends on orphanHandling mode
|
||||
func EnsureIDs(ctx context.Context, conn *sql.Conn, prefix string, issues []*types.Issue, actor string, orphanHandling OrphanHandling) error {
|
||||
usedIDs := make(map[string]bool)
|
||||
|
||||
// First pass: record explicitly provided IDs
|
||||
@@ -210,11 +220,24 @@ func EnsureIDs(ctx context.Context, conn *sql.Conn, prefix string, issues []*typ
|
||||
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)
|
||||
// Handle missing parent based on mode
|
||||
switch orphanHandling {
|
||||
case OrphanStrict:
|
||||
return fmt.Errorf("parent issue %s does not exist (strict mode)", parentID)
|
||||
case OrphanResurrect:
|
||||
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
|
||||
case OrphanSkip:
|
||||
// Mark issue for skipping by clearing its ID (will be filtered out later)
|
||||
issues[i].ID = ""
|
||||
continue
|
||||
case OrphanAllow:
|
||||
// Allow orphan - no validation
|
||||
default:
|
||||
// Default to allow for backward compatibility
|
||||
}
|
||||
// Parent will be created in this batch (due to depth-sorting), so allow this child
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user