Illegal Call Classes
Three kinds of write reach a universe that should never have been expressible. The contract closes two of them at the type. The third cannot be closed by any type, and the guard that catches it stays.
Class 1 — verb mismatch. Closes at the type.
Section titled “Class 1 — verb mismatch. Closes at the type.”A write that omits an act on a subject whose log requires one, or supplies an act to a subject that has no log to record it in.
Today both bind to one signature with the act defaulted, so the call compiles and is refused at runtime. Under the contract the act is a type parameter: on a logged subject it is non-nullable, so the act-less call has no overload to resolve against. The refusal moves from the run to the build.
Class 2 — a second writer inside fill. Closes through the open question.
Section titled “Class 2 — a second writer inside fill. Closes through the open question.”A table’s fill body can write the envelope’s own columns, so a subject can have two writers reaching the same column — the framework’s and the table’s. Last write wins, silently, because UpdateBuilder.set in Exposed 0.60.0 ends in a plain assignment with no already-initialized guard.
The contract does not close this by itself. fill’s receiver is the table, so a fill body on a declaring table still sees the envelope columns; closing it means narrowing that receiver or moving the columns off the table’s own interface — a change to the table, not to any interface in this design.
The cost of narrowing has been measured rather than argued. At the stack tip, all four adopting tables write no envelope column in fill; the two non-adopting tables carry their own varchar columns of the same name. Zero adopters break, so narrowing is pure hardening.
That measurement carries a caveat worth keeping. One rung below the tip, two of the adopting tables each write three envelope columns — the two-writer state is transient mid-stack, removed by the conversions before the tip. A measurement of this taken at the wrong rung reports the opposite and is not wrong about the rung it read.
Class 3 — schema drift. Does not close, and cannot.
Section titled “Class 3 — schema drift. Does not close, and cannot.”The declared columns and the columns the database actually has can disagree. Nothing in the type system can see that, because Kotlin cannot see the database: the compiler checks a declaration against itself, and the other side of the comparison exists only at runtime, in another process, under a migration history the build never reads.
So this class keeps its runtime guard. It is not a temporary measure pending a better type, and it should not be described as one — no arrangement of interfaces, carriers or bounds reaches it.
This is the class an on-call engineer meets at three in the morning as a bare Postgres 42703, and it is the first thing a summary of this design drops. It is stated here at equal weight for that reason.
What the split actually removes
Section titled “What the split actually removes”The split to Table and Persistence deletes both throwing branches of fillOccurrence, which retires two of the three runtime guards.
The schema-drift guard is the third, and it stays.
Counting the guards removed is the honest way to state the benefit. “The illegal call no longer compiles” is true of class 1, true of class 2 once the open question is settled, and false of class 3 — and a reader who takes the general form of that sentence will go looking for a compile error that will never appear.
Copyright: © Arda Systems 2025-2026, All rights reserved