← Back to projects

A dry run counted about 3,400 service packages when 11,934 existed. Tracing the mismatch to unordered pagination, and to nulls the first filter dropped, made a bulk cost correction safe.

Role: Correction owner & verifier (current role) Period: May – Jun 2026 Status: delivered

TypeScriptMySQLClaude Code AI: Claude Code
about 3,400 vs 11,934

Packages the rehearsal run counted vs the real total (it saw less than a third)

The mismatch was caught before any data changed, so the bulk fix never ran on a wrong count.

Dry run vs production count, Jun 2026

about 8,506

Records a filter silently left out because their value was empty

The correction reached every affected record, not just most of them.

Production check, Jun 2026

about 21,539

Labor-cost records corrected in one controlled bulk update

Shops with wrong costs got them fixed without anyone editing thousands of records by hand.

Production validation, 4 Jun 2026

about 11,934

Service packages corrected, matching the real total

The final count equals the real number, so nothing was skipped.

Production validation, 4 Jun 2026

Architecture

ARCHITECTURE / PAGINATION-ROOT-CAUSE 01 Dry run about 3,400 found 02 Investigation count was 3.5x off 03 ORDER BY key stable pages: 11,934 04 Null handling +8,506 records 05 Single-use script optional account scope 06 Production validated, 4 Jun 2026 Dry run said 3,400; the real count was 11,934, about 3.5 times more.
Root cause: ordered pagination and null handling fixed a 3.5x undercount.

Context

A B2B SaaS for auto-repair shops (my current role) changed the formula that computes labor cost. Shops that used the quantity field to adjust the cost were left with wrong costs, and a bulk correction was needed across thousands of records. A bulk write is only as safe as the count it is based on, so the first job was making sure the dry run was telling the truth. This case is anonymous, and the demo reproduces the bug with synthetic data.

My role

  • Decided: that the correction would not run until the dry-run count could be explained, the explicit filtering rules, and that the script is single-use.
  • Verified: the dry-run output, the discrepancy that led to the root cause, and the final counts in production.
  • Delegated to AI: writing the correction script and the investigation queries with Claude Code. The AI wrote the code; I noticed the numbers did not add up and asked for the investigation.

Research

The dry run reported about 3,400 service packages. The real number was 11,934, roughly 3.5 times more. A number like that is not noise, so I asked for an investigation instead of adjusting the filter until the figure looked plausible.

The cause was pagination without a stable order. A query that reads page after page without an ORDER BY on a unique key gives the database permission to return rows in any order from one page to the next, so pages can repeat or skip records and the total silently shrinks. Fixing the order was the first correction. A second one surfaced right after: about 8,506 records with a null value were being excluded by the first filter, even though the shop’s front end treated them as valid.

Decisions

DecisionAlternatives rejectedWhy
Paginate with an ORDER BY on a stable keyPaginate without orderWithout a stable order the pages overlap and skip rows, so the count and the write set are wrong.
Treat nulls the same way the front end doesExclude them with the filterExcluding them left about 8,506 records uncorrected, so the correction would have been incomplete.
Mark the script as single-use, with an option to restrict it to one accountA re-runnable scriptA second run would double the effect. Making that impossible is safer than relying on someone remembering not to run it twice.

Validation & QA

The correction rules were explicit: only records from before the formula change, active, and with a cost. I reviewed the dry run before any write, and the final counts were validated in production on 4 June 2026.

The single-use marker and the account restriction came from the same thinking as the rest of my data-change standard: dry run, QA, production, verification afterward, and idempotent scripts wherever possible. Here idempotency was not possible by nature of the change, so the control moved to the script itself.

Results

  • The dry-run count went from about 3,400 to the real 11,934 packages once pagination was ordered.
  • About 8,506 null-valued records that the first filter excluded were included.
  • About 21,539 labor concepts and about 11,934 service packages corrected (about 17,799 embedded concepts), validated in production on 4 June 2026.

What it solves and where else it applies

Before changing thousands of records at once, the count of what will change has to be right. A reading bug made the rehearsal see less than a third of the real data. Finding why, instead of tuning the filter until the number looked plausible, kept a bulk write from leaving most records wrong.

Where else the same pattern applies:

  • Any bulk update of customer, price or billing data, where nothing should be written until the expected count is explained.
  • Reports and exports that read data page by page, in finance, e-commerce catalogs or health records, where totals can shrink without any error message.
  • Data-quality checks before an ERP or database migration.

What I’d do differently

I would make the dry run assert its own count: compare it, in the same script, with an independent count taken without pagination, and refuse to continue if they differ. In this case I found the gap by looking; it should have failed by itself. I would also add the “unordered pagination” check to the review of any script that reads in pages, since the bug is easy to write and invisible when the table is small.

Evidence

Full version: private repository, available on request.

Based on my current role; all data is fictional.