CHANGES: 1. Merge logic (internal/merge/merge.go): - Added mergeStatus() enforcing closed ALWAYS wins over open - Fixed closed_at handling: only set when status='closed' - Changed deletion handling: deletion ALWAYS wins over modification 2. Deletion tracking (cmd/bd/snapshot_manager.go): - Updated ComputeAcceptedDeletions to accept all merge deletions - Removed "unchanged locally" check (deletion wins regardless) 3. FK constraint helper (internal/storage/sqlite/util.go): - Added IsForeignKeyConstraintError() for bd-koab - Detects FK violations for graceful import handling TESTS UPDATED: - TestMergeStatus: comprehensive status merge tests - TestIsForeignKeyConstraintError: FK constraint detection - bd-pq5k test: validates no invalid state (status=open with closed_at) - Deletion tests: reflect new deletion-wins behavior - All tests pass ✓ This ensures issues never get stuck in invalid states and prevents the insane situation where issues never die! 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
66 lines
1.9 KiB
Go
66 lines
1.9 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"strings"
|
|
)
|
|
|
|
// QueryContext exposes the underlying database QueryContext method for advanced queries
|
|
func (s *SQLiteStorage) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
|
|
return s.db.QueryContext(ctx, query, args...)
|
|
}
|
|
|
|
// BeginTx starts a new database transaction
|
|
// This is used by commands that need to perform multiple operations atomically
|
|
func (s *SQLiteStorage) BeginTx(ctx context.Context) (*sql.Tx, error) {
|
|
return s.db.BeginTx(ctx, nil)
|
|
}
|
|
|
|
// withTx executes a function within a database transaction.
|
|
// If the function returns an error, the transaction is rolled back.
|
|
// Otherwise, the transaction is committed.
|
|
func (s *SQLiteStorage) withTx(ctx context.Context, fn func(*sql.Tx) error) error {
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return wrapDBError("begin transaction", err)
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
if err := fn(tx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return wrapDBError("commit transaction", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ExecInTransaction is deprecated. Use withTx instead.
|
|
func (s *SQLiteStorage) ExecInTransaction(ctx context.Context, fn func(*sql.Tx) error) error {
|
|
return s.withTx(ctx, fn)
|
|
}
|
|
|
|
// IsUniqueConstraintError checks if an error is a UNIQUE constraint violation
|
|
func IsUniqueConstraintError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
return strings.Contains(err.Error(), "UNIQUE constraint failed")
|
|
}
|
|
|
|
// IsForeignKeyConstraintError checks if an error is a FOREIGN KEY constraint violation
|
|
// This can occur when importing issues that reference deleted issues (e.g., after merge)
|
|
func IsForeignKeyConstraintError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
errStr := err.Error()
|
|
return strings.Contains(errStr, "FOREIGN KEY constraint failed") ||
|
|
strings.Contains(errStr, "foreign key constraint failed")
|
|
}
|
|
|
|
|