feat: Handle FOREIGN KEY constraint violations gracefully during import (bd-koab)

When importing JSONL after merges that include deletions, FK constraint
violations can occur if an issue references a deleted issue. Previously,
import would fail completely. Now it continues and reports skipped dependencies.

Changes:
- Add SkippedDependencies field to Result/ImportResult structs
- Update importDependencies() to detect FK violations using IsForeignKeyConstraintError()
- Log warnings for each skipped dependency with issue IDs and type
- Continue importing remaining dependencies instead of failing
- Display summary of all skipped dependencies at end of import

Example output:
  Warning: Skipping dependency due to missing reference: bd-b → bd-a (blocks)

  ⚠️  Warning: Skipped 2 dependencies due to missing references:
    - bd-b → bd-a (blocks)
    - bd-c → bd-a (parent-child)

  This can happen after merges that delete issues referenced by other issues.
  The import continued successfully - you may want to review the skipped dependencies.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-23 23:32:34 -08:00
parent e2e04c011a
commit d45cff5085
4 changed files with 72 additions and 47 deletions

View File

@@ -40,16 +40,17 @@ type Options struct {
// Result contains statistics about the import operation
type Result struct {
Created int // New issues created
Updated int // Existing issues updated
Unchanged int // Existing issues that matched exactly (idempotent)
Skipped int // Issues skipped (duplicates, errors)
Collisions int // Collisions detected
IDMapping map[string]string // Mapping of remapped IDs (old -> new)
CollisionIDs []string // IDs that collided
PrefixMismatch bool // Prefix mismatch detected
ExpectedPrefix string // Database configured prefix
MismatchPrefixes map[string]int // Map of mismatched prefixes to count
Created int // New issues created
Updated int // Existing issues updated
Unchanged int // Existing issues that matched exactly (idempotent)
Skipped int // Issues skipped (duplicates, errors)
Collisions int // Collisions detected
IDMapping map[string]string // Mapping of remapped IDs (old -> new)
CollisionIDs []string // IDs that collided
PrefixMismatch bool // Prefix mismatch detected
ExpectedPrefix string // Database configured prefix
MismatchPrefixes map[string]int // Map of mismatched prefixes to count
SkippedDependencies []string // Dependencies skipped due to FK constraint violations
}
// ImportIssues handles the core import logic used by both manual and auto-import.
@@ -129,7 +130,7 @@ func ImportIssues(ctx context.Context, dbPath string, store storage.Storage, iss
}
// Import dependencies
if err := importDependencies(ctx, sqliteStore, issues, opts); err != nil {
if err := importDependencies(ctx, sqliteStore, issues, opts, result); err != nil {
return nil, err
}
@@ -615,7 +616,7 @@ if len(newIssues) > 0 {
}
// importDependencies imports dependency relationships
func importDependencies(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, issues []*types.Issue, opts Options) error {
func importDependencies(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, issues []*types.Issue, opts Options, result *Result) error {
for _, issue := range issues {
if len(issue.Dependencies) == 0 {
continue
@@ -643,6 +644,18 @@ func importDependencies(ctx context.Context, sqliteStore *sqlite.SQLiteStorage,
// Add dependency
if err := sqliteStore.AddDependency(ctx, dep, "import"); err != nil {
// Check for FOREIGN KEY constraint violation
if sqlite.IsForeignKeyConstraintError(err) {
// Log warning and track skipped dependency
depDesc := fmt.Sprintf("%s → %s (%s)", dep.IssueID, dep.DependsOnID, dep.Type)
fmt.Fprintf(os.Stderr, "Warning: Skipping dependency due to missing reference: %s\n", depDesc)
if result != nil {
result.SkippedDependencies = append(result.SkippedDependencies, depDesc)
}
continue
}
// For non-FK errors, respect strict mode
if opts.Strict {
return fmt.Errorf("error adding dependency %s → %s: %w", dep.IssueID, dep.DependsOnID, err)
}