@@ -3062,6 +3062,10 @@ internal void DeleteMultipleKeys(List<(string Column, string Literal)> condition
30623062 // B1: decode only the columns the delete core touches (PK + loaded hash-index columns).
30633063 int [ ] deleteKeyColumns = BuildDeleteKeyColumns ( ) ;
30643064
3065+ // A1: when the (plaintext, legacy variable-length) data file is small enough, read it
3066+ // ONCE and resolve every target from memory instead of one pread pair per deleted row.
3067+ byte [ ] ? wholeFile = TryLoadWholeFileForDeleteResolution ( ) ;
3068+
30653069 var recordsToDelete = new List < ( long storagePosition , Dictionary < string , object > row ) > ( ) ;
30663070
30673071 foreach ( var ( col , literal ) in conditions )
@@ -3076,16 +3080,25 @@ internal void DeleteMultipleKeys(List<(string Column, string Literal)> condition
30763080 var fastSearch = this . Index . Search ( value ) ;
30773081 if ( fastSearch . Found )
30783082 {
3079- var fastData = engine . Read ( Name , fastSearch . Value ) ;
3080- if ( fastData != null )
3083+ Dictionary < string , object > ? fastRow = null ;
3084+ if ( wholeFile != null )
30813085 {
3082- var fastRow = DeserializeDeleteKeyRow ( fastData , deleteKeyColumns ) ?? DeserializeRowFromSpan ( fastData ) ;
3083- if ( fastRow != null )
3086+ fastRow = DeserializeDeleteKeyRowFromFile ( wholeFile , fastSearch . Value , deleteKeyColumns ) ;
3087+ }
3088+ else
3089+ {
3090+ var fastData = engine . Read ( Name , fastSearch . Value ) ;
3091+ if ( fastData != null )
30843092 {
3085- recordsToDelete . Add ( ( fastSearch . Value , fastRow ) ) ;
3093+ fastRow = DeserializeDeleteKeyRow ( fastData , deleteKeyColumns ) ?? DeserializeRowFromSpan ( fastData ) ;
30863094 }
30873095 }
30883096
3097+ if ( fastRow != null )
3098+ {
3099+ recordsToDelete . Add ( ( fastSearch . Value , fastRow ) ) ;
3100+ }
3101+
30893102 continue ;
30903103 }
30913104 }
@@ -3127,12 +3140,21 @@ _btreeManager is null &&
31273140
31283141 foreach ( var pos in hashIndex . LookupPositionsUnsafe ( key ) )
31293142 {
3130- var data = engine . Read ( Name , pos ) ;
3131- if ( data != null )
3143+ Dictionary < string , object > ? row = null ;
3144+ if ( wholeFile != null )
31323145 {
3133- var row = DeserializeDeleteKeyRow ( data , deleteKeyColumns ) ?? DeserializeRowFromSpan ( data ) ;
3134- if ( row != null ) recordsToDelete . Add ( ( pos , row ) ) ;
3146+ row = DeserializeDeleteKeyRowFromFile ( wholeFile , pos , deleteKeyColumns ) ;
3147+ }
3148+ else
3149+ {
3150+ var data = engine . Read ( Name , pos ) ;
3151+ if ( data != null )
3152+ {
3153+ row = DeserializeDeleteKeyRow ( data , deleteKeyColumns ) ?? DeserializeRowFromSpan ( data ) ;
3154+ }
31353155 }
3156+
3157+ if ( row != null ) recordsToDelete . Add ( ( pos , row ) ) ;
31363158 }
31373159
31383160 continue ;
@@ -3237,12 +3259,29 @@ private int[] BuildDeleteKeyColumns()
32373259 /// </summary>
32383260 private Dictionary < string , object > ? DeserializeDeleteKeyRow ( byte [ ] data , int [ ] wanted )
32393261 {
3240- if ( _fixedWidthRecords || data == null || data . Length == 0 || wanted . Length == 0 )
3262+ if ( data == null )
3263+ {
3264+ return null ;
3265+ }
3266+
3267+ return DeserializeDeleteKeyRow ( data . AsSpan ( ) , wanted ) ;
3268+ }
3269+
3270+ /// <summary>
3271+ /// B1: decodes only the columns listed in <paramref name="wanted"/> (ascending) from a legacy
3272+ /// variable-length serialized row, skipping the unneeded columns' payload parsing entirely.
3273+ /// Returns a minimal dictionary (pk + hash-index columns only) so <see cref="DeleteRecordsCore"/>
3274+ /// performs the identical PK/hash lookups without materializing the full row. Returns null for
3275+ /// fixed-width layouts (those go through the fixed-width codec) or corrupt rows — callers fall
3276+ /// back to full-row deserialization in that case.
3277+ /// </summary>
3278+ private Dictionary < string , object > ? DeserializeDeleteKeyRow ( ReadOnlySpan < byte > span , int [ ] wanted )
3279+ {
3280+ if ( _fixedWidthRecords || span . IsEmpty || wanted . Length == 0 )
32413281 {
32423282 return null ;
32433283 }
32443284
3245- ReadOnlySpan < byte > span = data . AsSpan ( ) ;
32463285 int offset = 0 ;
32473286 Dictionary < string , object > ? row = null ;
32483287 int wi = 0 ;
@@ -3274,6 +3313,44 @@ private int[] BuildDeleteKeyColumns()
32743313 return row ;
32753314 }
32763315
3316+ // A1: whole-file delete resolution reads the (small) data file once instead of one pread pair
3317+ // per deleted row. Guarded to plaintext legacy variable-length files below this size.
3318+ private const long WholeFileDeleteResolutionLimitBytes = 32 * 1024 * 1024 ;
3319+
3320+ private byte [ ] ? TryLoadWholeFileForDeleteResolution ( )
3321+ {
3322+ if ( _fixedWidthRecords ||
3323+ this . storage is null ||
3324+ this . storage . AreRecordsEncrypted ( DataFile ) )
3325+ {
3326+ return null ;
3327+ }
3328+
3329+ var fi = new System . IO . FileInfo ( DataFile ) ;
3330+ if ( ! fi . Exists || fi . Length <= 0 || fi . Length > WholeFileDeleteResolutionLimitBytes )
3331+ {
3332+ return null ;
3333+ }
3334+
3335+ return this . storage . ReadBytesRange ( DataFile , 0 , ( int ) fi . Length ) ;
3336+ }
3337+
3338+ private Dictionary < string , object > ? DeserializeDeleteKeyRowFromFile ( byte [ ] wholeFile , long position , int [ ] wanted )
3339+ {
3340+ if ( position < 0 || position + 4 > wholeFile . Length )
3341+ {
3342+ return null ;
3343+ }
3344+
3345+ int recordLength = System . Buffers . Binary . BinaryPrimitives . ReadInt32LittleEndian ( wholeFile . AsSpan ( ( int ) position , 4 ) ) ;
3346+ if ( recordLength <= 0 || position + 4 + recordLength > wholeFile . Length )
3347+ {
3348+ return null ;
3349+ }
3350+
3351+ return DeserializeDeleteKeyRow ( wholeFile . AsSpan ( ( int ) position + 4 , recordLength ) , wanted ) ;
3352+ }
3353+
32773354 /// <summary>
32783355 /// Shared B8/B9 probe: resolves each key's record position through the PK B-tree, requires the
32793356 /// positions to be physically adjacent at the fixed-width stride, reads the whole contiguous
0 commit comments