five days since the last entry: suggestions now queue up for review instead of landing straight on the ballot, polls can have deadlines for voting and for suggesting, duplicate suggestions merge into one entry, you can set a password on an account you made with google, and renaming yourself in settings finally renames you everywhere instead of just on the account page.
i ended devlog 003 saying nothing stopped a poll from filling up with fifteen books nobody would read, and that i wanted a suggestion period that ran before the ballot rather than alongside it. both of those are in now. the queue was the easy half: a suggestion sits in its own list until an admin moves it onto the ballot, so members can throw books at the wall without the ballot growing every time they do.
the part i want to write about is a two-line change i made in passing.
the nicety
if two people suggest the same book, you shouldn't get the same book twice. so suggestions started merging — same open library key, or same title and author, and it's one entry with a count on it. "×2 from ada and bo" instead of two identical rows sitting on top of each other.
that's it. that's the whole feature. it took about twenty minutes.
what it broke
removing a suggestion had been written back when a suggestion had exactly one suggester, because at the time that was true. the query was roughly:
DELETE FROM book_poll_candidates
WHERE id = $1 AND suggested_by = $2
which reads fine. you can only delete it if you're the one who suggested it. that's the permission check, right there in the WHERE clause, and it was correct on the day i wrote it.
the merge changed who "it" belonged to. one row, two people behind it, and suggested_by now means "whoever happened to suggest this first" rather than "the person who owns this". so:
if ada suggested a book and bo backed the same one, ada pressing remove deleted it for both of them. bo's backing went with it, and bo never did anything wrong.
bo, meanwhile, couldn't remove it at all. he wasn't suggested_by, so the delete matched zero rows and the app told him he could only remove books he'd suggested — which he had.
and the bad one: once an admin promoted that suggestion to the ballot, it was still the same row. ada could still press remove. the votes hung off that row with ON DELETE CASCADE, so a member could delete a book off the ballot and take everyone's votes with it, days into the voting.
nobody hit that last one. i found it while renaming a button.
the fix
the actual mistake was treating "who suggested this" as an ownership column. it never was — it was a convenience that happened to line up with ownership while only one person could suggest a book.
so backing is its own table now, one row per person per book, and removing is withdrawing your own backing rather than deleting anything. the book disappears only when nobody is left backing it and it never made it to the ballot. once it's on the ballot, no member action can delete it, because at that point it isn't anybody's suggestion any more — it's the club's ballot.
the same session turned up a smaller version of the same thing. i had one flag called isOpen that meant "the poll is running", and i'd been using it for both "members can still vote" and "admins can still manage this". then voting deadlines shipped, the two came apart, and admins lost the ability to pick a winner the moment voting closed — which is precisely when you'd want to. one word doing two jobs, fine until one of them changed.
what i keep thinking about
002 was about storing something i should have computed. this is the same failure one layer down: not a stale value, a stale assumption, written into a WHERE clause where it looks like logic rather than a belief about the world.
the thing that makes it nasty is that nothing failed when the assumption broke. no error, no type change, no test to go red — suggested_by was still a real column with a real value in it. it just quietly stopped meaning what the query thought it meant, and it kept running perfectly for a while, doing the wrong thing.
so the question i should have asked, and didn't, is small enough to write on a post-it: when i let a second person touch this row, who owns it now? i think that's worth asking every single time the answer changes from "one person" to "more than one", because that's the exact moment every permission check written against it silently becomes a guess.

Comments (0)