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:
+15
-15
File diff suppressed because one or more lines are too long
@@ -366,6 +366,16 @@ NOTE: Import requires direct database access and does not work with daemon mode.
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
|
||||
// Print skipped dependencies summary if any
|
||||
if len(result.SkippedDependencies) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "\n⚠️ Warning: Skipped %d dependencies due to missing references:\n", len(result.SkippedDependencies))
|
||||
for _, dep := range result.SkippedDependencies {
|
||||
fmt.Fprintf(os.Stderr, " - %s\n", dep)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "\nThis can happen after merges that delete issues referenced by other issues.\n")
|
||||
fmt.Fprintf(os.Stderr, "The import continued successfully - you may want to review the skipped dependencies.\n")
|
||||
}
|
||||
|
||||
// Print force message if metadata was updated despite no changes
|
||||
if force && result.Created == 0 && result.Updated == 0 && len(result.IDMapping) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "Metadata updated (database already in sync with JSONL)\n")
|
||||
|
||||
@@ -179,6 +179,7 @@ type ImportResult struct {
|
||||
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
|
||||
}
|
||||
|
||||
// importIssuesCore handles the core import logic used by both manual and auto-import.
|
||||
@@ -238,6 +239,7 @@ func importIssuesCore(ctx context.Context, dbPath string, store storage.Storage,
|
||||
PrefixMismatch: result.PrefixMismatch,
|
||||
ExpectedPrefix: result.ExpectedPrefix,
|
||||
MismatchPrefixes: result.MismatchPrefixes,
|
||||
SkippedDependencies: result.SkippedDependencies,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ type Result struct {
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user