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:
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")
|
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
|
// Print force message if metadata was updated despite no changes
|
||||||
if force && result.Created == 0 && result.Updated == 0 && len(result.IDMapping) == 0 {
|
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")
|
fmt.Fprintf(os.Stderr, "Metadata updated (database already in sync with JSONL)\n")
|
||||||
|
|||||||
@@ -169,16 +169,17 @@ type ImportOptions struct {
|
|||||||
|
|
||||||
// ImportResult contains statistics about the import operation
|
// ImportResult contains statistics about the import operation
|
||||||
type ImportResult struct {
|
type ImportResult struct {
|
||||||
Created int // New issues created
|
Created int // New issues created
|
||||||
Updated int // Existing issues updated
|
Updated int // Existing issues updated
|
||||||
Unchanged int // Existing issues that matched exactly (idempotent)
|
Unchanged int // Existing issues that matched exactly (idempotent)
|
||||||
Skipped int // Issues skipped (duplicates, errors)
|
Skipped int // Issues skipped (duplicates, errors)
|
||||||
Collisions int // Collisions detected
|
Collisions int // Collisions detected
|
||||||
IDMapping map[string]string // Mapping of remapped IDs (old -> new)
|
IDMapping map[string]string // Mapping of remapped IDs (old -> new)
|
||||||
CollisionIDs []string // IDs that collided
|
CollisionIDs []string // IDs that collided
|
||||||
PrefixMismatch bool // Prefix mismatch detected
|
PrefixMismatch bool // Prefix mismatch detected
|
||||||
ExpectedPrefix string // Database configured prefix
|
ExpectedPrefix string // Database configured prefix
|
||||||
MismatchPrefixes map[string]int // Map of mismatched prefixes to count
|
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.
|
// importIssuesCore handles the core import logic used by both manual and auto-import.
|
||||||
@@ -228,16 +229,17 @@ func importIssuesCore(ctx context.Context, dbPath string, store storage.Storage,
|
|||||||
|
|
||||||
// Convert importer.Result to ImportResult
|
// Convert importer.Result to ImportResult
|
||||||
return &ImportResult{
|
return &ImportResult{
|
||||||
Created: result.Created,
|
Created: result.Created,
|
||||||
Updated: result.Updated,
|
Updated: result.Updated,
|
||||||
Unchanged: result.Unchanged,
|
Unchanged: result.Unchanged,
|
||||||
Skipped: result.Skipped,
|
Skipped: result.Skipped,
|
||||||
Collisions: result.Collisions,
|
Collisions: result.Collisions,
|
||||||
IDMapping: result.IDMapping,
|
IDMapping: result.IDMapping,
|
||||||
CollisionIDs: result.CollisionIDs,
|
CollisionIDs: result.CollisionIDs,
|
||||||
PrefixMismatch: result.PrefixMismatch,
|
PrefixMismatch: result.PrefixMismatch,
|
||||||
ExpectedPrefix: result.ExpectedPrefix,
|
ExpectedPrefix: result.ExpectedPrefix,
|
||||||
MismatchPrefixes: result.MismatchPrefixes,
|
MismatchPrefixes: result.MismatchPrefixes,
|
||||||
|
SkippedDependencies: result.SkippedDependencies,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,16 +40,17 @@ type Options struct {
|
|||||||
|
|
||||||
// Result contains statistics about the import operation
|
// Result contains statistics about the import operation
|
||||||
type Result struct {
|
type Result struct {
|
||||||
Created int // New issues created
|
Created int // New issues created
|
||||||
Updated int // Existing issues updated
|
Updated int // Existing issues updated
|
||||||
Unchanged int // Existing issues that matched exactly (idempotent)
|
Unchanged int // Existing issues that matched exactly (idempotent)
|
||||||
Skipped int // Issues skipped (duplicates, errors)
|
Skipped int // Issues skipped (duplicates, errors)
|
||||||
Collisions int // Collisions detected
|
Collisions int // Collisions detected
|
||||||
IDMapping map[string]string // Mapping of remapped IDs (old -> new)
|
IDMapping map[string]string // Mapping of remapped IDs (old -> new)
|
||||||
CollisionIDs []string // IDs that collided
|
CollisionIDs []string // IDs that collided
|
||||||
PrefixMismatch bool // Prefix mismatch detected
|
PrefixMismatch bool // Prefix mismatch detected
|
||||||
ExpectedPrefix string // Database configured prefix
|
ExpectedPrefix string // Database configured prefix
|
||||||
MismatchPrefixes map[string]int // Map of mismatched prefixes to count
|
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.
|
// 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
|
// Import dependencies
|
||||||
if err := importDependencies(ctx, sqliteStore, issues, opts); err != nil {
|
if err := importDependencies(ctx, sqliteStore, issues, opts, result); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,7 +616,7 @@ if len(newIssues) > 0 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// importDependencies imports dependency relationships
|
// 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 {
|
for _, issue := range issues {
|
||||||
if len(issue.Dependencies) == 0 {
|
if len(issue.Dependencies) == 0 {
|
||||||
continue
|
continue
|
||||||
@@ -643,6 +644,18 @@ func importDependencies(ctx context.Context, sqliteStore *sqlite.SQLiteStorage,
|
|||||||
|
|
||||||
// Add dependency
|
// Add dependency
|
||||||
if err := sqliteStore.AddDependency(ctx, dep, "import"); err != nil {
|
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 {
|
if opts.Strict {
|
||||||
return fmt.Errorf("error adding dependency %s → %s: %w", dep.IssueID, dep.DependsOnID, err)
|
return fmt.Errorf("error adding dependency %s → %s: %w", dep.IssueID, dep.DependsOnID, err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user