fix(sqlite): handle text timestamps in scan for cross-driver compatibility

The ncruces/go-sqlite3 driver does not always auto-convert TEXT columns
to time.Time. This caused scan errors on updated_at/created_at fields,
blocking witness startup.

Fix: Scan timestamps into sql.NullString and parse with parseTimeString()
helper that handles RFC3339Nano, RFC3339, and SQLite native formats.

Fixes: bd-4dqmy

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beads/crew/collins
2026-01-20 19:07:24 -08:00
committed by Steve Yegge
parent 90344b9939
commit 458fb7197a
4 changed files with 93 additions and 7 deletions

View File

@@ -885,6 +885,8 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
// First pass: scan all issues
for rows.Next() {
var issue types.Issue
var createdAtStr sql.NullString // TEXT column - must parse manually for cross-driver compatibility
var updatedAtStr sql.NullString // TEXT column - must parse manually for cross-driver compatibility
var contentHash sql.NullString
var closedAt sql.NullTime
var estimatedMinutes sql.NullInt64
@@ -927,7 +929,7 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
&issue.CreatedAt, &issue.CreatedBy, &owner, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo, &closeReason,
&createdAtStr, &issue.CreatedBy, &owner, &updatedAtStr, &closedAt, &externalRef, &sourceRepo, &closeReason,
&deletedAt, &deletedBy, &deleteReason, &originalType,
&sender, &wisp, &pinned, &isTemplate, &crystallizes,
&awaitType, &awaitID, &timeoutNs, &waiters,
@@ -938,6 +940,14 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
return nil, fmt.Errorf("failed to scan issue: %w", err)
}
// Parse timestamp strings (TEXT columns require manual parsing)
if createdAtStr.Valid {
issue.CreatedAt = parseTimeString(createdAtStr.String)
}
if updatedAtStr.Valid {
issue.UpdatedAt = parseTimeString(updatedAtStr.String)
}
if contentHash.Valid {
issue.ContentHash = contentHash.String
}
@@ -1065,6 +1075,8 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
var results []*types.IssueWithDependencyMetadata
for rows.Next() {
var issue types.Issue
var createdAtStr sql.NullString // TEXT column - must parse manually for cross-driver compatibility
var updatedAtStr sql.NullString // TEXT column - must parse manually for cross-driver compatibility
var contentHash sql.NullString
var closedAt sql.NullTime
var estimatedMinutes sql.NullInt64
@@ -1096,7 +1108,7 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
&issue.CreatedAt, &issue.CreatedBy, &owner, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo,
&createdAtStr, &issue.CreatedBy, &owner, &updatedAtStr, &closedAt, &externalRef, &sourceRepo,
&deletedAt, &deletedBy, &deleteReason, &originalType,
&sender, &wisp, &pinned, &isTemplate, &crystallizes,
&awaitType, &awaitID, &timeoutNs, &waiters,
@@ -1106,6 +1118,14 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
return nil, fmt.Errorf("failed to scan issue with dependency type: %w", err)
}
// Parse timestamp strings (TEXT columns require manual parsing)
if createdAtStr.Valid {
issue.CreatedAt = parseTimeString(createdAtStr.String)
}
if updatedAtStr.Valid {
issue.UpdatedAt = parseTimeString(updatedAtStr.String)
}
if contentHash.Valid {
issue.ContentHash = contentHash.String
}