Skip to content

Add SetHistoryColumn temporal test helper - #1051

Merged
SimonCropp merged 2 commits into
mainfrom
set-history-column
Sep 3, 2026
Merged

Add SetHistoryColumn temporal test helper#1051
SimonCropp merged 2 commits into
mainfrom
set-history-column

Conversation

@SimonCropp

Copy link
Copy Markdown
Owner

Sets one column on the history rows of a single entity, so a test can reproduce a history table that a migration left in a state a freshly migrated database never reaches. The motivating case is a column dropped and re-added on a temporal pair: the current row repopulates, the rows already in history are left NULL, and SQL Server never backfills them. Materialising one of those rows into a property the model declares required then throws SqlNullValueException, which until now could only be discovered in production.

Writing to a history table needs versioning off but not the PERIOD dropped, so this uses a lighter DDL pair than SetCurrentPeriodStart, restored in a finally. The period columns and the primary key are rejected: rewriting a period corrupts the timeline SetCurrentPeriodStart maintains, and rewriting the key detaches the row from the entity it is history for.

The column must permit NULL in the database for a null value. It is not widened here, and cannot be - SQL Server refuses to re-enable versioning when the pair disagrees on nullability, so such a row could not exist in production either. The reachable case is a column nullable in the database while the model declares the property required, which a stored computed column whose CASE has no ELSE produces. The docs snippet uses exactly that shape.

Sets one column on the history rows of a single entity, so a test can reproduce
a history table that a migration left in a state a freshly migrated database
never reaches. The motivating case is a column dropped and re-added on a
temporal pair: the current row repopulates, the rows already in history are left
NULL, and SQL Server never backfills them. Materialising one of those rows into
a property the model declares required then throws SqlNullValueException, which
until now could only be discovered in production.

Writing to a history table needs versioning off but not the PERIOD dropped, so
this uses a lighter DDL pair than SetCurrentPeriodStart, restored in a finally.
The period columns and the primary key are rejected: rewriting a period corrupts
the timeline SetCurrentPeriodStart maintains, and rewriting the key detaches the
row from the entity it is history for.

The column must permit NULL in the database for a null value. It is not widened
here, and cannot be - SQL Server refuses to re-enable versioning when the pair
disagrees on nullability, so such a row could not exist in production either.
The reachable case is a column nullable in the database while the model declares
the property required, which a stored computed column whose CASE has no ELSE
produces. The docs snippet uses exactly that shape.
@SimonCropp SimonCropp added this to the 26.1.0 milestone Sep 2, 2026
CI caught SetHistoryColumnUsage failing: two SaveChanges landed in the same
tick, so SQL Server discarded the zero-length history row, SetHistoryColumn had
nothing to update, and materialising the entity no longer threw. The snippet for
one helper had walked into the problem the other helper exists to solve.

Both saves are now separated with SetCurrentPeriodStart, which also makes the
snippet show the two composing. SetHistoryColumn_LeavesVersioningOn had the same
latent flake - only the helper's own DDL round trips separated its saves, which
the docs put at 5-15ms against a ~15ms timer granularity - so it gets an
explicit period too.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant