gastownhall/beads · error

db: CommentSQLRepository.InsertRecord: comment must not be n

Error message

db: CommentSQLRepository.InsertRecord: comment must not be nil

What it means

InsertRecord validates its comment pointer before doing any SQL work. A nil *types.Comment means the caller has no record to insert, so the repository fails fast with this guard error rather than panicking on the subsequent struct dereference (copy := *comment). It is a pure programming/usage error, never a data or database problem.

Source

Thrown at internal/storage/domain/db/comment.go:130

	}
	return storage.NewSliceIter(bulk[issueID]), nil
}

func (r *commentSQLRepositoryImpl) Insert(ctx context.Context, issueID, author, text string, opts domain.CommentOpts) (*types.Comment, error) {
	// Live add: advance past the issue's newest comment so a burst inside one
	// second still reads back in write order (issueops.NextLiveCommentTime).
	// InsertRecord honors a supplied CreatedAt verbatim, which is what keeps
	// imported comments on their original timestamps.
	stamp, err := issueops.NextLiveCommentTime(ctx, r.runner, pickCommentTable(opts.UseWispsTable), issueID, time.Now())
	if err != nil {
		return nil, fmt.Errorf("db: CommentSQLRepository.Insert: %w", err)
	}
	return r.InsertRecord(ctx, &types.Comment{IssueID: issueID, Author: author, Text: text, CreatedAt: stamp}, opts)
}

func (r *commentSQLRepositoryImpl) InsertRecord(ctx context.Context, comment *types.Comment, opts domain.CommentOpts) (*types.Comment, error) {
	if comment == nil {
		return nil, fmt.Errorf("db: CommentSQLRepository.InsertRecord: comment must not be nil")
	}
	copy := *comment
	if copy.IssueID == "" {
		return nil, fmt.Errorf("db: CommentSQLRepository.InsertRecord: issueID must not be empty")
	}

	issueTable := pickIssueTable(opts.UseWispsTable)
	var exists bool
	//nolint:gosec // G201: issueTable is one of two hardcoded constants
	if err := r.runner.QueryRowContext(ctx,
		fmt.Sprintf("SELECT EXISTS(SELECT 1 FROM %s WHERE id = ?)", issueTable), copy.IssueID).Scan(&exists); err != nil {
		return nil, fmt.Errorf("db: CommentSQLRepository.InsertRecord: check issue existence: %w", err)
	}
	if !exists {
		return nil, fmt.Errorf("db: CommentSQLRepository.InsertRecord: issue %s not found", copy.IssueID)
	}

	if copy.CreatedAt.IsZero() {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Ensure the *types.Comment is non-nil before calling InsertRecord (allocate with &types.Comment{...}).
  2. If wrapping Insert, build the record inside the wrapper as Insert does rather than forwarding a possibly-nil pointer.
  3. Check any caller that does if c != nil { InsertRecord(ctx, c, ...) } — invert the early-return so nil never reaches the call.

Example fix

// before
var c *types.Comment
repo.InsertRecord(ctx, c, opts) // guard error
// after
c := &types.Comment{IssueID: id, Author: author, Text: text}
repo.InsertRecord(ctx, c, opts)
Defensive patterns

Strategy: validation

Validate before calling

func validateComment(c *types.Comment) error {
    if c == nil {
        return fmt.Errorf("comment must not be nil")
    }
    return nil
}

Type guard

func commentIsNotNil(c *types.Comment) bool { return c != nil }

Try / catch

if _, err := repo.InsertRecord(ctx, comment, opts); err != nil {
    if strings.Contains(err.Error(), "comment must not be nil") {
        // fix caller: allocate a non-nil *types.Comment before calling
    }
}

Prevention

When it happens

Trigger: Calling CommentSQLRepository.InsertRecord(ctx, nil, opts) directly, or a wrapper that returns nil on an error path but passes the nil result straight into InsertRecord.

Common situations: Code that unmarshals comments from JSON where the field was absent (yielding nil), refactorings where an earlier nil-check was removed, or tests exercising the guard.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/9b0fb06e7eb68fdb. Report an issue: GitHub.