Priya needed a demo database by Friday. A prospect wanted to see the product against something that looked like their own business, so she reached for the usual fixture library and generated a few thousand customers, orders, payments, and shipments. It looked great in a spreadsheet. Then she started actually using it, and it came apart one piece at a time.
The pieces it broke on are worth cataloguing, because they are the same ones every naive synthetic dataset breaks on. They share a property: none of them is visible in a single row.
Monday: the orders pointed nowhere
The first join returned almost nothing. Her order rows carried a customer_id drawn at random from a plausible range, and almost none of those IDs matched a customer she had actually created. Referential integrity is the floor, and column-at-a-time generation fails it by default. The harder version bit her the next day: a manager_id that was supposed to point back into the employees table, which cannot be filled from a list that does not exist yet while the employees are still being generated.
Tuesday: the totals lied
Her invoice demo asserted that an order total equalled the sum of its line items. It never did, because the total and the items were generated independently. She could have hand-computed each one, but that is the moment you realize you are doing the generator's job. The same trap was waiting on tax = subtotal × rate and on every date that was meant to be another date plus an offset.
Wednesday: the timeline ran backwards
She sorted a shipment's events by time to demo the tracking view. A package was marked delivered before it was picked up. Because the statuses had been chosen at random, roughly half of every multi-event history was physically impossible. What she actually needed was a state machine: statuses that only move along legal transitions, with believable gaps between them.
shipment status event_ts SHP-51 picked_up 09:14 SHP-51 in_transit 11:40 SHP-51 out_for_del Jul 12 SHP-51 delivered Jul 12 every entity's timeline now makes sense
Thursday: the money didn't reconcile
The wallet demo had a running balance column. It was full of believable numbers that had no relationship to the amounts beside them. A ledger is a chain: each balance is the previous one plus this row's amount, in time order, landing on a known closing figure. You cannot fake a chain with independent draws.
entry amount balance 1 open 2,400.00 2 -318.55 2,081.45 3 +540.10 2,621.55 balance = prior + amount, every row
By Thursday afternoon Priya had a list. Foreign keys that resolve. Totals that are actually derived. Statuses that walk a legal path. Balances that chain and foot. Allocations that sum to a target, shipments that fit in a truck without overflowing, org charts where a manager out-ranks their reports. Every item was a rule that lived between rows, and every one had been silently violated by a generator that only ever looked at one field at a time.
The thing they have in common
The reason these are hard is also the reason they are fixable. They only hold if every row is produced with the others in mind. That is the whole idea behind CrossRow: it generates each row in the context of the rest of the table, infers which of these rules fit your schema, and shows them in the plan for you to review. Then, after generation, it re-verifies every declared rule against the rows it actually produced and reports the share that comply. A rule that was asked for but not satisfied shows up as a failure, never a green checkmark it did not earn.
Priya rebuilt the demo in an afternoon by describing the business instead of hand-filling columns. The prospect never saw a package delivered before it shipped. If you have ever lost a day to fixtures that pass the eye test and fail the real one, you have met this list already, probably in the same order.