fix(sqlite): handle deleted_at TEXT column scanning properly

The deleted_at column was defined as TEXT in the schema but code was
trying to scan into sql.NullTime. The ncruces/go-sqlite3 driver only
auto-converts TEXT to time.Time for columns declared as DATETIME/DATE/
TIME/TIMESTAMP. For TEXT columns, it returns raw strings which
sql.NullTime.Scan() cannot handle.

Added parseNullableTimeString() helper that manually parses time strings
and changed all deletedAt variables from sql.NullTime to sql.NullString.

Fixes import failure: "sql: Scan error on column index 22, name
deleted_at: unsupported Scan, storing driver.Value type string into
type *time.Time"

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-13 07:13:40 -08:00
parent 7a10377544
commit 18b1eb2e07
4 changed files with 29 additions and 24 deletions

View File

@@ -691,7 +691,7 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
var externalRef sql.NullString
var sourceRepo sql.NullString
var closeReason sql.NullString
var deletedAt sql.NullTime
var deletedAt sql.NullString // TEXT column, not DATETIME - must parse manually
var deletedBy sql.NullString
var deleteReason sql.NullString
var originalType sql.NullString
@@ -729,9 +729,7 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
if closeReason.Valid {
issue.CloseReason = closeReason.String
}
if deletedAt.Valid {
issue.DeletedAt = &deletedAt.Time
}
issue.DeletedAt = parseNullableTimeString(deletedAt)
if deletedBy.Valid {
issue.DeletedBy = deletedBy.String
}
@@ -773,7 +771,7 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
var assignee sql.NullString
var externalRef sql.NullString
var sourceRepo sql.NullString
var deletedAt sql.NullTime
var deletedAt sql.NullString // TEXT column, not DATETIME - must parse manually
var deletedBy sql.NullString
var deleteReason sql.NullString
var originalType sql.NullString
@@ -810,9 +808,7 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
if sourceRepo.Valid {
issue.SourceRepo = sourceRepo.String
}
if deletedAt.Valid {
issue.DeletedAt = &deletedAt.Time
}
issue.DeletedAt = parseNullableTimeString(deletedAt)
if deletedBy.Valid {
issue.DeletedBy = deletedBy.String
}

View File

@@ -11,6 +11,23 @@ import (
"github.com/steveyegge/beads/internal/types"
)
// parseNullableTimeString parses a nullable time string from database TEXT columns.
// The ncruces/go-sqlite3 driver only auto-converts TEXT→time.Time for columns declared
// as DATETIME/DATE/TIME/TIMESTAMP. For TEXT columns (like deleted_at), we must parse manually.
// Supports RFC3339, RFC3339Nano, and SQLite's native format.
func parseNullableTimeString(ns sql.NullString) *time.Time {
if !ns.Valid || ns.String == "" {
return nil
}
// Try RFC3339Nano first (more precise), then RFC3339, then SQLite format
for _, layout := range []string{time.RFC3339Nano, time.RFC3339, "2006-01-02 15:04:05"} {
if t, err := time.Parse(layout, ns.String); err == nil {
return &t
}
}
return nil // Unparseable - shouldn't happen with valid data
}
// REMOVED (bd-8e05): getNextIDForPrefix and AllocateNextID - sequential ID generation
// no longer needed with hash-based IDs
// Migration functions moved to migrations.go (bd-fc2d, bd-b245)
@@ -158,7 +175,7 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
var originalSize sql.NullInt64
var sourceRepo sql.NullString
var closeReason sql.NullString
var deletedAt sql.NullTime
var deletedAt sql.NullString // TEXT column, not DATETIME - must parse manually
var deletedBy sql.NullString
var deleteReason sql.NullString
var originalType sql.NullString
@@ -220,9 +237,7 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
if closeReason.Valid {
issue.CloseReason = closeReason.String
}
if deletedAt.Valid {
issue.DeletedAt = &deletedAt.Time
}
issue.DeletedAt = parseNullableTimeString(deletedAt)
if deletedBy.Valid {
issue.DeletedBy = deletedBy.String
}
@@ -331,7 +346,7 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
var compactedAtCommit sql.NullString
var sourceRepo sql.NullString
var closeReason sql.NullString
var deletedAt sql.NullTime
var deletedAt sql.NullString // TEXT column, not DATETIME - must parse manually
var deletedBy sql.NullString
var deleteReason sql.NullString
var originalType sql.NullString
@@ -391,9 +406,7 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
if closeReason.Valid {
issue.CloseReason = closeReason.String
}
if deletedAt.Valid {
issue.DeletedAt = &deletedAt.Time
}
issue.DeletedAt = parseNullableTimeString(deletedAt)
if deletedBy.Valid {
issue.DeletedBy = deletedBy.String
}

View File

@@ -170,7 +170,7 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
var compactedAtCommit sql.NullString
var originalSize sql.NullInt64
var closeReason sql.NullString
var deletedAt sql.NullTime
var deletedAt sql.NullString // TEXT column, not DATETIME - must parse manually
var deletedBy sql.NullString
var deleteReason sql.NullString
var originalType sql.NullString
@@ -221,9 +221,7 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
if closeReason.Valid {
issue.CloseReason = closeReason.String
}
if deletedAt.Valid {
issue.DeletedAt = &deletedAt.Time
}
issue.DeletedAt = parseNullableTimeString(deletedAt)
if deletedBy.Valid {
issue.DeletedBy = deletedBy.String
}

View File

@@ -1068,7 +1068,7 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
var sourceRepo sql.NullString
var compactedAtCommit sql.NullString
var closeReason sql.NullString
var deletedAt sql.NullTime
var deletedAt sql.NullString // TEXT column, not DATETIME - must parse manually
var deletedBy sql.NullString
var deleteReason sql.NullString
var originalType sql.NullString
@@ -1116,9 +1116,7 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
if closeReason.Valid {
issue.CloseReason = closeReason.String
}
if deletedAt.Valid {
issue.DeletedAt = &deletedAt.Time
}
issue.DeletedAt = parseNullableTimeString(deletedAt)
if deletedBy.Valid {
issue.DeletedBy = deletedBy.String
}