@@ -1946,6 +1946,13 @@ internal void UpdateMultiple(List<(string where, Dictionary<string, object> upda
19461946 // from the file INCLUDING the stale record).
19471947 EnsureAllRegisteredIndexesLoaded ( ) ;
19481948
1949+ // Whole-file snapshot for the per-row fastPatch reads below (same guard as the DELETE
1950+ // path): reading the small plaintext file once replaces one pread pair per updated row.
1951+ // A position that already has a buffered in-place overwrite in this transaction is
1952+ // NEVER served from the snapshot (its disk bytes would be stale) — it falls back to the
1953+ // per-record read, which honors the write-behind buffer.
1954+ byte [ ] ? wholeFile = TryLoadWholeFileForRowAccess ( ) ;
1955+
19491956 // B8: single-pass contiguous UPDATE — when every operation is a `pk = <literal>` match on a
19501957 // plaintext fixed-width table with physically adjacent PK-ordered records, the old records
19511958 // are read as ONE contiguous byte range and patched in memory (no per-row pread). Strictly
@@ -2007,7 +2014,17 @@ internal void UpdateMultiple(List<(string where, Dictionary<string, object> upda
20072014 var fastSearch = this . Index . Search ( pkWhereVal ? . ToString ( ) ?? string . Empty ) ;
20082015 if ( fastSearch . Found )
20092016 {
2010- var fastData = engine . Read ( Name , fastSearch . Value ) ;
2017+ byte [ ] ? fastData ;
2018+ if ( fastPatch && wholeFile != null &&
2019+ ( this . storage is null || ! this . storage . HasBufferedOverwriteAt ( DataFile , fastSearch . Value ) ) )
2020+ {
2021+ fastData = TrySlicePayloadFromFile ( wholeFile , fastSearch . Value ) ;
2022+ }
2023+ else
2024+ {
2025+ fastData = engine . Read ( Name , fastSearch . Value ) ;
2026+ }
2027+
20112028 if ( fastData != null )
20122029 {
20132030 rows = fastPatch
@@ -2036,7 +2053,17 @@ internal void UpdateMultiple(List<(string where, Dictionary<string, object> upda
20362053 rows = [ ] ;
20372054 foreach ( var pos in hashIndex . LookupPositionsUnsafe ( key ) )
20382055 {
2039- var data = engine . Read ( Name , pos ) ;
2056+ byte [ ] ? data ;
2057+ if ( fastPatch && wholeFile != null &&
2058+ ( this . storage is null || ! this . storage . HasBufferedOverwriteAt ( DataFile , pos ) ) )
2059+ {
2060+ data = TrySlicePayloadFromFile ( wholeFile , pos ) ;
2061+ }
2062+ else
2063+ {
2064+ data = engine . Read ( Name , pos ) ;
2065+ }
2066+
20402067 if ( data != null )
20412068 {
20422069 if ( fastPatch )
@@ -3064,7 +3091,7 @@ internal void DeleteMultipleKeys(List<(string Column, string Literal)> condition
30643091
30653092 // A1: when the (plaintext, legacy variable-length) data file is small enough, read it
30663093 // ONCE and resolve every target from memory instead of one pread pair per deleted row.
3067- byte [ ] ? wholeFile = TryLoadWholeFileForDeleteResolution ( ) ;
3094+ byte [ ] ? wholeFile = TryLoadWholeFileForRowAccess ( ) ;
30683095
30693096 var recordsToDelete = new List < ( long storagePosition , Dictionary < string , object > row ) > ( ) ;
30703097
@@ -3317,7 +3344,7 @@ private int[] BuildDeleteKeyColumns()
33173344 // per deleted row. Guarded to plaintext legacy variable-length files below this size.
33183345 private const long WholeFileDeleteResolutionLimitBytes = 32 * 1024 * 1024 ;
33193346
3320- private byte [ ] ? TryLoadWholeFileForDeleteResolution ( )
3347+ private byte [ ] ? TryLoadWholeFileForRowAccess ( )
33213348 {
33223349 if ( _fixedWidthRecords ||
33233350 this . storage is null ||
@@ -3351,6 +3378,29 @@ this.storage is null ||
33513378 return DeserializeDeleteKeyRow ( wholeFile . AsSpan ( ( int ) position + 4 , recordLength ) , wanted ) ;
33523379 }
33533380
3381+ /// <summary>
3382+ /// Copies the raw (plaintext) payload of the record at <paramref name="position"/> out of a
3383+ /// whole-file snapshot. Returns null when the position/length is not fully contained in the
3384+ /// snapshot (caller falls back to the per-record read).
3385+ /// </summary>
3386+ private byte [ ] ? TrySlicePayloadFromFile ( byte [ ] wholeFile , long position )
3387+ {
3388+ if ( position < 0 || position + 4 > wholeFile . Length )
3389+ {
3390+ return null ;
3391+ }
3392+
3393+ int recordLength = System . Buffers . Binary . BinaryPrimitives . ReadInt32LittleEndian ( wholeFile . AsSpan ( ( int ) position , 4 ) ) ;
3394+ if ( recordLength <= 0 || position + 4 + recordLength > wholeFile . Length )
3395+ {
3396+ return null ;
3397+ }
3398+
3399+ var payload = new byte [ recordLength ] ;
3400+ wholeFile . AsSpan ( ( int ) position + 4 , recordLength ) . CopyTo ( payload ) ;
3401+ return payload ;
3402+ }
3403+
33543404 /// <summary>
33553405 /// Shared B8/B9 probe: resolves each key's record position through the PK B-tree, requires the
33563406 /// positions to be physically adjacent at the fixed-width stride, reads the whole contiguous
0 commit comments