Skip to main content

<assert> — a config that checks its own output

Use it when the shape of your data matters to whoever consumes it, and you want the run to stop rather than hand over a file that quietly drifted.

An assertion states a property the finished run must have. If it holds, nothing happens. If it doesn't, the run stops with your own sentence, before a single line is written.

<assert that="Tracked == 700" says="every shipped order should carry a tracking number"/>

It lives in <env>, beside <uniq> and <distinct>, because like them it says something about the whole run rather than about one column.

What is worth asserting

Not what the config already states. You wrote percent="70" and you assert 70 percent — you have tested that TDC can count.

The value is in what the config does not state. Here a filter and a condition stack up, and the share that reaches the file is nowhere in the text:

<tdc>
<env count="1000" seed="orders" local="en">
<sequence name="Status"><gen type="text" value="shipped,pending" percent="70,30"/></sequence>
<sequence name="Tracking" parent="Status.shipped">
<gen type="regex" value="[A-Z]{2}[0-9]{9}" if="Status == 'shipped' && _count % 4 != 0"/>
</sequence>
<sequence name="Tracked"><gen type="stat" of="Tracking" op="count"/></sequence>

<assert that="Tracked == 700" says="every shipped order should carry a tracking number"/>
</env>
<block>
<line><data>${{Status}},${{Tracking}}</data></line>
</block>
</tdc>
./run orders.tdc
tdcv2: assert failed: every shipped order should carry a tracking number
Tracked == 700   with Tracked = 522

Nothing else in TDC has an opinion about this config. It parses, it validates, it runs, and 178 shipped orders come out with an empty tracking number. That is the failure an assertion is for.

The exit code is 1, so CI stops on it.

At a glance

AttributeRequiredWhat it does
thatyesThe condition, in the same language as if=
saysyesThe sentence a reader is given when it fails

Both are required. An assertion that fires with only its expression to show has made the reader work out what it was for, months later, in a CI log.

There is no flag. An assertion runs because it is written: a check that has to be remembered is a check nobody ran, on a config that looks verified.

Where the numbers come from

that= reads columns, and the columns worth reading are usually <gen type="stat"> — one number for the whole run.

<env count="500" seed="clinic" local="en">
<sequence name="Visit"><gen type="date" from="2026-01-01" to="2026-06-30" format="YYYY-MM-DD"/></sequence>
<sequence name="Follow"><gen type="date" of="Visit" plus="7..30d" format="YYYY-MM-DD"/></sequence>
<sequence name="Ward"><gen type="text" value="A,B,C" percent="50,30,20"/></sequence>

<sequence name="Rows"><gen type="stat" of="Visit" op="count"/></sequence>
<sequence name="Wards"><gen type="stat" of="Ward" op="count"/></sequence>

<assert that="Rows == _total" says="every row has a visit date"/>
<assert that="Wards == _total" says="every row has a ward"/>
</env>

_total is the row count, and it is the one built-in an assertion may read.

The rule that keeps it honest

Every name in that= must be the same on every row. A stat column is, by construction. A one-value text column is, as a matter of fact. A drawn column is not, and is refused:

<sequence name="Amount"><gen type="number" value="1..500"/></sequence>
<assert that="Amount > 0" says="every amount is positive"/>
./run amounts.tdc
tdcv2: assert ("Amount > 0"): "Amount" is not the same on every row, so this would have checked the first row and called the run verified. An assertion reads whole-run values: give it a <gen type="stat" of="Amount" op="…"/> column, or _total.

Without this rule, that="Amount > 0" would read row 0 and report on one row out of five hundred — a check that passed because it barely looked, wearing a badge that says verified. That is the same disease the whole feature exists to cure.

A column that a parent= filter leaves empty on some rows is refused for the same reason: it has no single value for the run, so the condition would compare against whatever row 0 happened to hold. Summarise it with op="count" instead.

What it does not do yet

  • Per-row assertions ("every amount is positive"). A different feature: it needs a row loop and a report that names failing rows rather than one number.
  • Asserting a check digit. That is the trap — <compute> produced it, and recomputing it asserts that the same code agrees with itself.

Engines

An assertion reads stat columns, and stat already routes a config to the in-memory engine. So assertions add no engine consequence of their own; see large outputs.

See also

  • stat — where the numbers come from
  • Expressions — the language that= is written in
  • Uniqueness — the other whole-run statements