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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user