Log Appliers
This page covers how Internals Viewer applies a transaction log record to a page image, for Log Records in the Query view. It follows the source in InternalsViewer.TransactionLog/Appliers.
Applying a record
Each log operation type (LOP_INSERT_ROWS, LOP_MODIFY_ROW, LOP_DELETE_ROWS, LOP_SET_BITS, LOP_MODIFY_HEADER, etc.) has a matching applier:
InsertRowsApplier,ModifyRowApplier,DeleteRowsApplier- row-level operationsModifyColumnsApplier- column-level modifications within a rowSetBitsApplier- single-bit changes, e.g. PFS byte flagsSetFreeSpaceApplier- page header free space accountingModifyHeaderApplier- other page header field changes
All of these derive from PageLogRecordApplier<TRecord>, which runs the checks common to every page-scoped record before handing off to the type-specific logic:
- The record's target page address must match the page being viewed
- The record's
PreviousPageLsnmust match the page's current LSN - i.e. the page is at the exact point in its history the record expects to be applied on top of - If both checks pass, the type-specific
ApplyRecordruns, and on success the record's own LSN is stamped into the page header
A failure at any step surfaces as the reason shown in the Page Viewer - a page/LSN mismatch, or an operation-specific reason such as a splice that would change a record's length without the row rebuild/relocate logic to support it.
Each change made to the page image is recorded as a ChangeSpan(Offset, Length, Description) - this is what drives the Region/Change/Offset/Length rows in the Page Log Operations panel, and the hex viewer highlight when a change is selected.
Splices vs page surgery
Where a change fits in the same space it previously occupied, it is applied as a same-size splice - the byte range is overwritten in place, after verifying the page's current bytes match the log record's before-image.
Where a row changes size - an update that grows or shrinks a variable-length column - the applier instead rebuilds the row and places it:
- If the row is shrinking, or it's the last row before the free data offset, it's rewritten in place
- Otherwise it's relocated to the current free data offset, and the slot's offset table entry is repointed to the new location, leaving the old bytes behind as a hole in the page
This is the same mechanic SQL Server itself uses - a growing row that doesn't fit in place gets moved within the page rather than triggering a page split for every update.