openlang

rows.Next said false and the error went nowhere

Small tool on database/sql returned half a list. No error. The code.

func names(ctx context.Context, db *sql.DB) ([]string, error) {
	rows, err := db.QueryContext(ctx, `SELECT name FROM users ORDER BY id`)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("users: %w", err)
	}
	defer rows.Close()

	var out []string
	for rows.Next() {
		var n string
		if err := rows.Scan(&n); err != nil {
			return nil, fmt.Errorf("scan: %w", err)
		}
		out = append(out, n)
	}
	return out, nil
}

Context with a deadline. The deadline fired in the middle of the fetch, rows.Next() returned false, the loop ended, and the function handed back what it had collected plus nil. The error was sitting in rows.Err() and nothing asked for it.

Fix is two lines after the loop, if err := rows.Err(); err != nil and a return with %w. Done. Now hunting for the other loops like it.

The conversation continues 3

Follow the reasoning
  1. Permalink

    Next has two reasons to return false and one bool to say it with. End of rows or an error, and only Err tells them apart. With a deadline in play there's a counting detail too. The close on cancellation happens on another goroutine, so the number of names collected before the false varies from run to run. And if the query reaches its last row before the deadline fires, Err is nil, which is correct. A test for the fix needs more rows than the deadline can cover, otherwise it passes some of the time for the wrong reason.

  2. Permalink

    One way out is a small helper that runs the loop, calls a scan function per row and returns the result together with rows.Err(), so the check after the loop lives in one place.

Other sides of the same question

  1. count(*) arrived as a string and 12 + 1 made 121

    A todo list screen. The header says 12 items, I add one, and to bump the number before the server answers I do total + 1. The…

  2. UPDATE rechecks its WHERE after the wait

    Two sessions under PostgreSQL Read Committed, one account with a balance of 15, and both sessions run the same statement. Ses…

    Replies 0sql
  3. count(*) OVER () or Repeatable Read for a paged report

    Read Committed lets a paged report disagree with itself. The reporting screen counts 12 matching orders. Another session comm…

    Replies 9sql