Updating, Deleting, and Knowing Whether It Happened
res, err := db.Exec(`UPDATE tasks SET done = ? WHERE id = ?`, true, id)
If no row has that id, err is nil. The statement ran, it was valid, it matched nothing, and SQL considers that a complete success. Your API almost certainly does not — a PATCH /tasks/999 should be a 404, and this is the only place you can tell.
n, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("update task %d: %w", id, err)
}
if n == 0 {
return ErrNotFound
}
That check is the difference between an endpoint that reports what happened and one that cheerfully returns 204 for a task that never existed. The same applies to DELETE: deleting nothing is not an error, so RowsAffected() == 0 is how "there was nothing to delete" reaches the caller.
Zero rows affected is not always "not found"
One subtlety worth knowing before it confuses you. Some databases (MySQL, by default) report zero affected rows for an UPDATE that matched a row but changed nothing — setting done = true on a task already done. Under that behaviour, n == 0 conflates "no such row" with "no change needed", and a 404 would be wrong.
SQLite and PostgreSQL count matched rows, so n == 0 really does mean "no such row" there. If you need certainty across databases, check existence separately, or use RETURNING.
Exec for writes, always
Reaching for Query on an UPDATE still works and still leaks a connection. The rule doesn't change: statements that return rows use Query/QueryRow; statements that don't use Exec.
The pattern this completes
You now have the four operations the SQL-backed store needs, and each reports absence in the same vocabulary:
| Operation | How "not there" arrives |
|---|---|
| Get one | sql.ErrNoRows from Scan |
| List | an empty slice — absence is not an error |
| Update | RowsAffected() == 0 |
| Delete | RowsAffected() == 0 |
Three different mechanisms, one ErrNotFound at your boundary. That translation is what lets the layer above stay ignorant of SQL entirely — which is exactly what the next part builds.
Try it
Implement update and delete, and make both tell the caller when there was nothing to act on.
Practice
1. Update and Delete a Task
IntermediateImplement two functions.
SetDone(db *sql.DB, id int64, done bool) error- Update that task's
donecolumn. - Return
ErrNotFoundwhen no row has that id.
DeleteTask(db *sql.DB, id int64) error- Delete the task.
- Return
ErrNotFoundwhen there was nothing to delete.
Both return a wrapped error on a genuine failure. Remember that a statement matching zero rows is not an error in SQL — you have to ask.
package main import ( "database/sql" "errors" "fmt" _ "modernc.org/sqlite" ) var ErrNotFound = errors.New("task not found") // SetDone marks a task done or not done. func SetDone(db *sql.DB, id int64, done bool) error { // TODO: implement return fmt.Errorf("not implemented") } // DeleteTask removes a task. func DeleteTask(db *sql.DB, id int64) error { // TODO: implement return fmt.Errorf("not implemented") } func main() {}Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- Update that task's