Add SetHistoryColumn temporal test helper - #1051
Merged
Merged
Conversation
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.