BREAKING CHANGE: API field and CLI command renamed - types.Issue.Wisp → types.Issue.Ephemeral - JSON field: "wisp" → "ephemeral" - CLI: bd wisp → bd ephemeral - Flags: --wisp → --ephemeral - ID prefix: wisp → eph The SQLite column already uses 'ephemeral' so no schema migration needed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
130 lines
4.4 KiB
Go
130 lines
4.4 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/steveyegge/beads/internal/types"
|
|
)
|
|
|
|
// isUniqueConstraintError checks if error is a UNIQUE constraint violation
|
|
// Used to detect and handle duplicate IDs in JSONL imports gracefully
|
|
func isUniqueConstraintError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
errMsg := err.Error()
|
|
return strings.Contains(errMsg, "UNIQUE constraint failed") ||
|
|
strings.Contains(errMsg, "constraint failed: UNIQUE")
|
|
}
|
|
|
|
// insertIssue inserts a single issue into the database
|
|
func insertIssue(ctx context.Context, conn *sql.Conn, issue *types.Issue) error {
|
|
sourceRepo := issue.SourceRepo
|
|
if sourceRepo == "" {
|
|
sourceRepo = "." // Default to primary repo
|
|
}
|
|
|
|
wisp := 0
|
|
if issue.Ephemeral {
|
|
wisp = 1
|
|
}
|
|
pinned := 0
|
|
if issue.Pinned {
|
|
pinned = 1
|
|
}
|
|
isTemplate := 0
|
|
if issue.IsTemplate {
|
|
isTemplate = 1
|
|
}
|
|
|
|
_, err := conn.ExecContext(ctx, `
|
|
INSERT OR IGNORE INTO issues (
|
|
id, content_hash, title, description, design, acceptance_criteria, notes,
|
|
status, priority, issue_type, assignee, estimated_minutes,
|
|
created_at, created_by, updated_at, closed_at, external_ref, source_repo, close_reason,
|
|
deleted_at, deleted_by, delete_reason, original_type,
|
|
sender, ephemeral, pinned, is_template,
|
|
await_type, await_id, timeout_ns, waiters
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`,
|
|
issue.ID, issue.ContentHash, issue.Title, issue.Description, issue.Design,
|
|
issue.AcceptanceCriteria, issue.Notes, issue.Status,
|
|
issue.Priority, issue.IssueType, issue.Assignee,
|
|
issue.EstimatedMinutes, issue.CreatedAt, issue.CreatedBy, issue.UpdatedAt,
|
|
issue.ClosedAt, issue.ExternalRef, sourceRepo, issue.CloseReason,
|
|
issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
|
|
issue.Sender, wisp, pinned, isTemplate,
|
|
issue.AwaitType, issue.AwaitID, int64(issue.Timeout), formatJSONStringArray(issue.Waiters),
|
|
)
|
|
if err != nil {
|
|
// INSERT OR IGNORE should handle duplicates, but driver may still return error
|
|
// Explicitly ignore UNIQUE constraint errors (expected for duplicate IDs in JSONL)
|
|
if !isUniqueConstraintError(err) {
|
|
return fmt.Errorf("failed to insert issue: %w", err)
|
|
}
|
|
// Duplicate ID detected and ignored (INSERT OR IGNORE succeeded)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// insertIssues bulk inserts multiple issues using a prepared statement
|
|
func insertIssues(ctx context.Context, conn *sql.Conn, issues []*types.Issue) error {
|
|
stmt, err := conn.PrepareContext(ctx, `
|
|
INSERT OR IGNORE INTO issues (
|
|
id, content_hash, title, description, design, acceptance_criteria, notes,
|
|
status, priority, issue_type, assignee, estimated_minutes,
|
|
created_at, created_by, updated_at, closed_at, external_ref, source_repo, close_reason,
|
|
deleted_at, deleted_by, delete_reason, original_type,
|
|
sender, ephemeral, pinned, is_template,
|
|
await_type, await_id, timeout_ns, waiters
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to prepare statement: %w", err)
|
|
}
|
|
defer func() { _ = stmt.Close() }()
|
|
|
|
for _, issue := range issues {
|
|
sourceRepo := issue.SourceRepo
|
|
if sourceRepo == "" {
|
|
sourceRepo = "." // Default to primary repo
|
|
}
|
|
|
|
wisp := 0
|
|
if issue.Ephemeral {
|
|
wisp = 1
|
|
}
|
|
pinned := 0
|
|
if issue.Pinned {
|
|
pinned = 1
|
|
}
|
|
isTemplate := 0
|
|
if issue.IsTemplate {
|
|
isTemplate = 1
|
|
}
|
|
|
|
_, err = stmt.ExecContext(ctx,
|
|
issue.ID, issue.ContentHash, issue.Title, issue.Description, issue.Design,
|
|
issue.AcceptanceCriteria, issue.Notes, issue.Status,
|
|
issue.Priority, issue.IssueType, issue.Assignee,
|
|
issue.EstimatedMinutes, issue.CreatedAt, issue.CreatedBy, issue.UpdatedAt,
|
|
issue.ClosedAt, issue.ExternalRef, sourceRepo, issue.CloseReason,
|
|
issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
|
|
issue.Sender, wisp, pinned, isTemplate,
|
|
issue.AwaitType, issue.AwaitID, int64(issue.Timeout), formatJSONStringArray(issue.Waiters),
|
|
)
|
|
if err != nil {
|
|
// INSERT OR IGNORE should handle duplicates, but driver may still return error
|
|
// Explicitly ignore UNIQUE constraint errors (expected for duplicate IDs in JSONL)
|
|
if !isUniqueConstraintError(err) {
|
|
return fmt.Errorf("failed to insert issue %s: %w", issue.ID, err)
|
|
}
|
|
// Duplicate ID detected and ignored (INSERT OR IGNORE succeeded)
|
|
}
|
|
}
|
|
return nil
|
|
}
|