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:
committed by
Steve Yegge
parent
90344b9939
commit
458fb7197a
@@ -885,6 +885,8 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
|
|||||||
// First pass: scan all issues
|
// First pass: scan all issues
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var issue types.Issue
|
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 contentHash sql.NullString
|
||||||
var closedAt sql.NullTime
|
var closedAt sql.NullTime
|
||||||
var estimatedMinutes sql.NullInt64
|
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.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
||||||
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
|
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
|
||||||
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
&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,
|
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||||
&sender, &wisp, &pinned, &isTemplate, &crystallizes,
|
&sender, &wisp, &pinned, &isTemplate, &crystallizes,
|
||||||
&awaitType, &awaitID, &timeoutNs, &waiters,
|
&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)
|
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 {
|
if contentHash.Valid {
|
||||||
issue.ContentHash = contentHash.String
|
issue.ContentHash = contentHash.String
|
||||||
}
|
}
|
||||||
@@ -1065,6 +1075,8 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
|
|||||||
var results []*types.IssueWithDependencyMetadata
|
var results []*types.IssueWithDependencyMetadata
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var issue types.Issue
|
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 contentHash sql.NullString
|
||||||
var closedAt sql.NullTime
|
var closedAt sql.NullTime
|
||||||
var estimatedMinutes sql.NullInt64
|
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.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
||||||
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
|
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
|
||||||
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
&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,
|
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||||
&sender, &wisp, &pinned, &isTemplate, &crystallizes,
|
&sender, &wisp, &pinned, &isTemplate, &crystallizes,
|
||||||
&awaitType, &awaitID, &timeoutNs, &waiters,
|
&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)
|
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 {
|
if contentHash.Valid {
|
||||||
issue.ContentHash = contentHash.String
|
issue.ContentHash = contentHash.String
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,22 @@ func parseNullableTimeString(ns sql.NullString) *time.Time {
|
|||||||
return nil // Unparseable - shouldn't happen with valid data
|
return nil // Unparseable - shouldn't happen with valid data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseTimeString parses a time string from database TEXT columns (non-nullable).
|
||||||
|
// Similar to parseNullableTimeString but for required timestamp fields like created_at/updated_at.
|
||||||
|
// Returns zero time if parsing fails, which maintains backwards compatibility.
|
||||||
|
func parseTimeString(s string) time.Time {
|
||||||
|
if s == "" {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
// 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, s); err == nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Time{} // Unparseable - shouldn't happen with valid data
|
||||||
|
}
|
||||||
|
|
||||||
// parseJSONStringArray parses a JSON string array from database TEXT column.
|
// parseJSONStringArray parses a JSON string array from database TEXT column.
|
||||||
// Returns empty slice if the string is empty or invalid JSON.
|
// Returns empty slice if the string is empty or invalid JSON.
|
||||||
func parseJSONStringArray(s string) []string {
|
func parseJSONStringArray(s string) []string {
|
||||||
@@ -292,6 +308,8 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
|||||||
defer s.reconnectMu.RUnlock()
|
defer s.reconnectMu.RUnlock()
|
||||||
|
|
||||||
var issue types.Issue
|
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 closedAt sql.NullTime
|
var closedAt sql.NullTime
|
||||||
var estimatedMinutes sql.NullInt64
|
var estimatedMinutes sql.NullInt64
|
||||||
var assignee sql.NullString
|
var assignee sql.NullString
|
||||||
@@ -356,7 +374,7 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
|||||||
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
||||||
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
|
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
|
||||||
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
||||||
&issue.CreatedAt, &issue.CreatedBy, &owner, &issue.UpdatedAt, &closedAt, &externalRef,
|
&createdAtStr, &issue.CreatedBy, &owner, &updatedAtStr, &closedAt, &externalRef,
|
||||||
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
|
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
|
||||||
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||||
&sender, &wisp, &pinned, &isTemplate, &crystallizes,
|
&sender, &wisp, &pinned, &isTemplate, &crystallizes,
|
||||||
@@ -373,6 +391,14 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
|||||||
return nil, fmt.Errorf("failed to get issue: %w", err)
|
return nil, fmt.Errorf("failed to get 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 {
|
if contentHash.Valid {
|
||||||
issue.ContentHash = contentHash.String
|
issue.ContentHash = contentHash.String
|
||||||
}
|
}
|
||||||
@@ -581,6 +607,8 @@ func (s *SQLiteStorage) GetCloseReasonsForIssues(ctx context.Context, issueIDs [
|
|||||||
// GetIssueByExternalRef retrieves an issue by external reference
|
// GetIssueByExternalRef retrieves an issue by external reference
|
||||||
func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef string) (*types.Issue, error) {
|
func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef string) (*types.Issue, error) {
|
||||||
var issue types.Issue
|
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 closedAt sql.NullTime
|
var closedAt sql.NullTime
|
||||||
var estimatedMinutes sql.NullInt64
|
var estimatedMinutes sql.NullInt64
|
||||||
var assignee sql.NullString
|
var assignee sql.NullString
|
||||||
@@ -625,7 +653,7 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
|
|||||||
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
||||||
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
|
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
|
||||||
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
||||||
&issue.CreatedAt, &issue.CreatedBy, &owner, &issue.UpdatedAt, &closedAt, &externalRefCol,
|
&createdAtStr, &issue.CreatedBy, &owner, &updatedAtStr, &closedAt, &externalRefCol,
|
||||||
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
|
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
|
||||||
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||||
&sender, &wisp, &pinned, &isTemplate, &crystallizes,
|
&sender, &wisp, &pinned, &isTemplate, &crystallizes,
|
||||||
@@ -639,6 +667,14 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
|
|||||||
return nil, fmt.Errorf("failed to get issue by external_ref: %w", err)
|
return nil, fmt.Errorf("failed to get issue by external_ref: %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 {
|
if contentHash.Valid {
|
||||||
issue.ContentHash = contentHash.String
|
issue.ContentHash = contentHash.String
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -346,6 +346,8 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
|
|||||||
var issues []*types.Issue
|
var issues []*types.Issue
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var issue types.Issue
|
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 closedAt sql.NullTime
|
var closedAt sql.NullTime
|
||||||
var estimatedMinutes sql.NullInt64
|
var estimatedMinutes sql.NullInt64
|
||||||
var assignee sql.NullString
|
var assignee sql.NullString
|
||||||
@@ -378,7 +380,7 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
|
|||||||
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
||||||
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
|
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
|
||||||
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
||||||
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo,
|
&createdAtStr, &updatedAtStr, &closedAt, &externalRef, &sourceRepo,
|
||||||
&compactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &closeReason,
|
&compactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &closeReason,
|
||||||
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||||
&sender, &ephemeral, &pinned, &isTemplate,
|
&sender, &ephemeral, &pinned, &isTemplate,
|
||||||
@@ -388,6 +390,14 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
|
|||||||
return nil, fmt.Errorf("failed to scan stale issue: %w", err)
|
return nil, fmt.Errorf("failed to scan stale 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 {
|
if contentHash.Valid {
|
||||||
issue.ContentHash = contentHash.String
|
issue.ContentHash = contentHash.String
|
||||||
}
|
}
|
||||||
@@ -564,6 +574,8 @@ func (s *SQLiteStorage) GetBlockedIssues(ctx context.Context, filter types.WorkF
|
|||||||
var blocked []*types.BlockedIssue
|
var blocked []*types.BlockedIssue
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var issue types.BlockedIssue
|
var issue types.BlockedIssue
|
||||||
|
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 closedAt sql.NullTime
|
var closedAt sql.NullTime
|
||||||
var estimatedMinutes sql.NullInt64
|
var estimatedMinutes sql.NullInt64
|
||||||
var assignee sql.NullString
|
var assignee sql.NullString
|
||||||
@@ -575,13 +587,21 @@ func (s *SQLiteStorage) GetBlockedIssues(ctx context.Context, filter types.WorkF
|
|||||||
&issue.ID, &issue.Title, &issue.Description, &issue.Design,
|
&issue.ID, &issue.Title, &issue.Description, &issue.Design,
|
||||||
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
|
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
|
||||||
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
||||||
&issue.CreatedAt, &issue.CreatedBy, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo, &issue.BlockedByCount,
|
&createdAtStr, &issue.CreatedBy, &updatedAtStr, &closedAt, &externalRef, &sourceRepo, &issue.BlockedByCount,
|
||||||
&blockerIDsStr,
|
&blockerIDsStr,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to scan blocked issue: %w", err)
|
return nil, fmt.Errorf("failed to scan blocked 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 closedAt.Valid {
|
if closedAt.Valid {
|
||||||
issue.ClosedAt = &closedAt.Time
|
issue.ClosedAt = &closedAt.Time
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1291,6 +1291,8 @@ type scanner interface {
|
|||||||
// consistent scanning of issue rows.
|
// consistent scanning of issue rows.
|
||||||
func scanIssueRow(row scanner) (*types.Issue, error) {
|
func scanIssueRow(row scanner) (*types.Issue, error) {
|
||||||
var issue types.Issue
|
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 contentHash sql.NullString
|
||||||
var closedAt sql.NullTime
|
var closedAt sql.NullTime
|
||||||
var estimatedMinutes sql.NullInt64
|
var estimatedMinutes sql.NullInt64
|
||||||
@@ -1336,7 +1338,7 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
|
|||||||
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
||||||
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
|
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
|
||||||
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
||||||
&issue.CreatedAt, &issue.CreatedBy, &owner, &issue.UpdatedAt, &closedAt, &externalRef,
|
&createdAtStr, &issue.CreatedBy, &owner, &updatedAtStr, &closedAt, &externalRef,
|
||||||
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
|
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
|
||||||
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||||
&sender, &wisp, &pinned, &isTemplate, &crystallizes,
|
&sender, &wisp, &pinned, &isTemplate, &crystallizes,
|
||||||
@@ -1348,6 +1350,14 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
|
|||||||
return nil, fmt.Errorf("failed to scan issue: %w", err)
|
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 {
|
if contentHash.Valid {
|
||||||
issue.ContentHash = contentHash.String
|
issue.ContentHash = contentHash.String
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user