Back to Feed
article 3m read

Receipt check caught the database library dropping a digit

A repeating fraction lost one digit on its way into a JSON column, so an order receipt stopped matching its own hash. I kept the check strict and took JSON serialization away from the ORM instead.

Article DepthPractice

Flow

Keeping a hashed receipt exact

  1. 1Test with awkward numbers

    Write repeating fractions through the real database and read them back.

  2. 2Keep the check strict

    Never loosen the hash comparison or hash what comes back from the table.

  3. 3Write the JSON yourself

    Turn the payload into text in your code and let the database parse it.

  4. 4Use one helper

    Send every insert and completion step through the same small function.

Saving a receipt that didn't match itself was something the ordering system did. The copy of the request in the row no longer matched the hash saved next to it.

Why the receipt matters

When a buyer turns a plan into an order, the request gets a hash and is saved as a receipt. If the same request comes again, the system sends back the first answer, so a retry can't make a second order. As far as I know, a different request with the same key is refused. Anyone can also hash the saved copy later to check that it hasn't changed. That check is what failed.

How I found it

I had a big test that runs one supplier's whole catalog across every store, from the first draft to the next plan. It checks that the saved copy hashes to the saved hash. When it didn't, the test listed the field that changed. It was a repeating fraction that came back with one digit fewer.

Why one digit counts

A computer stores repeating fractions as a close guess. To read that exact guess back, you need every digit. Drop one and you get a slightly different number. Round numbers never show this, so clean test data hides it. You would never see the gap by eye, but a hash treats it as a whole new input.

Where the digit went

The database library rounded the number when it wrote the JSON column. The database kept what it was given. The test was right to fail.

Writing the JSON text yourself

Don't loosen the check, and don't hash what comes back from the table. Write the JSON text yourself and let the database parse it:

await db.run(
  `INSERT INTO receipts (key, hash, payload) VALUES ($1, $2, $3::jsonb)`,
  [key, hash, JSON.stringify(payload)])

Put the insert and the completion step in one small helper, and make every writer use it. The completion step should only change a row that is still in progress, and throw if it changes anything else. I didn't touch old receipts or add a migration.

Which tests count

Fast tests that fake the database can't prove digits survive. Only tests against the real database can. Mine caught this because its full test data had a fraction in it. Put awkward numbers in on purpose.

Other JSON columns

Other JSON columns may round the same way, and I haven't looked at them yet.

Keep reading

Pick up a connected idea or branch into a nearby one.

5 paths forward