Reporting · Money · Engineering

One cap, three right answers

Three reports totalled their newest rows and called it everything. One now sums in the database, one refuses the window, one says it is capped.


There is a shape we have now found three times in our own code. A report fetches the newest fifty, sixty or five hundred rows so the screen stays quick, and then adds them up and presents the sum as if it were about everything. The total makes a claim about its input, and it has been handed a sample. The first time it was the voucher ledger, where the function whose own comment calls it the check that the stored balance and the story agree was given the fifty rows the screen showed instead of all of them.

A census of report handlers that both cap their rows and total them in code returned five. Two were clean. Three were the bug, and each needed a different fix, which is the whole point of writing this down.

Total it in the database

The card reconciliation history showed an unexplained figure, the sum of every day's difference, under a comment that said across every day somebody has checked. It was a sum over the newest sixty days. True until a venue had checked sixty-one, and then it quietly meant something smaller, the voucher-ledger fault word for word. It is now summed in the database over every row, with the number shown and the total number of rows beside it, because a cap that does not say so is how a short list and a truncated one come to look identical. A database sum is safe here in a way it is not everywhere: it is the same arithmetic on the same column, not a second definition of anything.

Refuse the window

The timesheet report totalled pay and minutes over the newest five hundred shifts. The dates come from whoever opens it, so a manager opening a quarter with six hundred shifts in it got the newest five hundred added up and presented as the period's pay, and the only sign was the shift count reading exactly five hundred.

A database sum would have been the wrong fix, and that is the interesting part. Pay on a shift uses the hourly rate snapshotted at clock-in, so a pay rise never silently rewrites what someone was owed last month, and an open shift grows its minutes against the clock. Reproducing that in a query makes a second place where pay is decided, which is exactly the fault we keep margin calculation in one place to avoid. So this one follows the rule from an earlier fix: refuse, never truncate. The report now says those dates hold 501 shifts, it answers for up to 500 at a time because the hours and the pay are added up from the shifts on it, and asks for narrower dates.

Say the cap out loud

The price watch report lists recent ingredient price moves and summarises them: rises, drops, average change, biggest jump. All of it came off the newest five hundred changes. Neither of the other fixes fits. The figures are an average and a maximum over a derived percentage, not a sum of a column, and a report of recent price moves is still useful on the newest five hundred; refusing would delete a screen that people use. What was missing was that the reader could not tell. It now says how many it is showing, how many there are, and whether it is capped.

So the lesson is not that caps are bad. Is there a cap above this total is one question with three right answers: total it in the database, refuse the window, or disclose the cap. Picking the wrong one either invents a second definition of money or removes a screen somebody opens every week.

Two footnotes. An existing guard caught the fix itself, because the first version wrote the page size into the query text rather than binding it, and a check written three releases earlier refuses that pattern on sight. And the live estate had nothing near any of these limits, which proves nothing either way, so the bug was proved against seventy seeded reconciliations and five hundred and one seeded shifts, locally, and deleted afterwards.


Try it on tonight’s service.

Nothing to install, no card. Not better by the weekend? Close the tab.