Typed CRUD
Delete with a deliberate boundary
Delete only the intended row, consider soft deletion honestly, and verify the effect instead of assuming a successful query changed data.
Delete has the same trap as update. db.delete(notes) with no .where() empties the table, and Drizzle will happily run it. So we use the same boundary we built for updates: the note ID plus the owner.
import { and, eq } from 'drizzle-orm'
const [deleted] = await db
.delete(notes)
.where(and(eq(notes.id, noteId), eq(notes.authorId, userId)))
.returning({ id: notes.id })
.returning({ id: notes.id }) gives back just the ID of the row that was removed. If deleted is undefined, nothing was deleted: the note didn’t exist or wasn’t this user’s. Same as with update, I answer both cases the same way.
A successful query is not a changed row
This is worth repeating. The delete above “succeeds” even when it matches zero rows. No error, no exception. The only proof that data changed is the returned row, or a changes count if you use .run() instead of .returning().
So don’t log “note deleted” because the statement didn’t throw. Check the result.
Guards help, but they don’t think
Some teams add a lint rule or a wrapper that refuses update and delete without .where(). I like that guard. It catches the forgotten line.
It can’t catch a wrong line. .where(gt(notes.id, 0)) has a condition and still matches every real row. The guard sees a where and is satisfied. Only a human reading the condition, or a test with two users’ data, notices the problem.
Soft delete, honestly
A soft delete doesn’t remove the row. It sets a timestamp like archivedAt and every read filters it out. We already added that column, and the list query in the select lesson skips archived notes with isNull(notes.archivedAt).
Soft delete gives users an undo. But be honest about the costs. Every query needs the filter, forever, and forgetting it once shows “deleted” data. The data is still on disk, so a privacy request to erase it isn’t satisfied. And it is not a backup: a bug that archives everything is as bad as one that deletes everything.
Pick the behavior from the product, not from habit. A notes app might archive with a 30-day window, then hard delete. An audit log might forbid deletes entirely. Invoices might need retention rules set by law. Write the decision down.
To see the boundary work, delete one note as its owner, then attempt the same delete as another user. The first returns { id: 7 } or whatever the ID was. The second returns undefined, and a select confirms the note is gone only once. Then decide, for your own project, whether this table needs hard delete, archiving, or a retention rule.
Lesson completed