@@ -13,6 +13,7 @@ public class TableMetadata
1313 public int RecordCount { get ; set ; }
1414 public int RecordSize { get ; set ; }
1515 public long DataStartOffset { get ; set ; }
16+ public int ReservedRecordCount { get ; set ; }
1617 /// <summary>
1718 /// The index of this table in the file header metadata section.
1819 /// This ensures consistent offset calculations across reloads.
@@ -88,13 +89,14 @@ private void CreateDatabase(Dictionary<string, Type> tableTypes)
8889 RecordCount = 0 ,
8990 RecordSize = metadata . RecordSize ,
9091 DataStartOffset = currentOffset ,
92+ ReservedRecordCount = INCREASE_RECORD_SIZE ,
9193 TableIndex = tableIndex
9294 } ;
9395 _tables [ kvp . Key ] = tableMetadata ;
9496
9597 WriteTableMetadata ( writer , tableMetadata ) ;
9698 tableIndex ++ ;
97- currentOffset += metadata . RecordSize * INCREASE_RECORD_SIZE ;
99+ currentOffset += metadata . RecordSize * tableMetadata . ReservedRecordCount ;
98100 }
99101
100102 // Freeze the metadata cache after initialization
@@ -117,9 +119,10 @@ private void WriteTableMetadata(BinaryWriter writer, TableMetadata metadata)
117119 writer . Write ( metadata . RecordCount ) ;
118120 writer . Write ( metadata . RecordSize ) ;
119121 writer . Write ( metadata . DataStartOffset ) ;
122+ writer . Write ( metadata . ReservedRecordCount ) ;
120123 writer . Write ( metadata . TableIndex ) ;
121124
122- Span < byte > reserved = stackalloc byte [ 44 ] ;
125+ Span < byte > reserved = stackalloc byte [ 40 ] ;
123126 reserved . Clear ( ) ;
124127 writer . Write ( reserved ) ;
125128 }
@@ -149,15 +152,17 @@ private void LoadDatabase()
149152 var recordCount = reader . ReadInt32 ( ) ;
150153 var recordSize = reader . ReadInt32 ( ) ;
151154 var dataStartOffset = reader . ReadInt64 ( ) ;
155+ var reservedRecordCount = reader . ReadInt32 ( ) ;
152156 var tableIndex = reader . ReadInt32 ( ) ;
153- reader . ReadBytes ( 44 ) ; // Skip reserved
157+ reader . ReadBytes ( 40 ) ; // Skip reserved
154158
155159 _tables [ tableName ] = new TableMetadata
156160 {
157161 TableName = tableName ,
158162 RecordCount = recordCount ,
159163 RecordSize = recordSize ,
160164 DataStartOffset = dataStartOffset ,
165+ ReservedRecordCount = reservedRecordCount > 0 ? reservedRecordCount : INCREASE_RECORD_SIZE ,
161166 TableIndex = tableIndex
162167 } ;
163168 }
@@ -173,6 +178,11 @@ private void LoadDatabase()
173178 return result ;
174179
175180 var entityMetadata = GetOrCreateEntityMetadata ( typeof ( T ) ) ;
181+ if ( tableMetadata . RecordSize != entityMetadata . RecordSize )
182+ {
183+ throw new InvalidDataException (
184+ $ "Schema mismatch for table '{ tableName } ': file RecordSize={ tableMetadata . RecordSize } , expected RecordSize={ entityMetadata . RecordSize } for entity '{ typeof ( T ) . FullName } '.") ;
185+ }
176186 byte [ ] ? rentedBuffer = null ;
177187
178188 try
@@ -222,13 +232,19 @@ private async Task SaveChangesInternalAsync<T>(string tableName, List<T> added,
222232 {
223233 var tableMetadata = _tables [ tableName ] ;
224234 var entityMetadata = GetOrCreateEntityMetadata ( typeof ( T ) ) ;
235+ if ( tableMetadata . RecordSize != entityMetadata . RecordSize )
236+ {
237+ throw new InvalidDataException (
238+ $ "Schema mismatch for table '{ tableName } ': file RecordSize={ tableMetadata . RecordSize } , expected RecordSize={ entityMetadata . RecordSize } for entity '{ typeof ( T ) . FullName } '.") ;
239+ }
225240
226241 await using var file = new FileStream ( _filePath , FileMode . Open , FileAccess . ReadWrite , FileShare . Read ,
227242 bufferSize : 4096 , useAsync : true ) ;
228243
229244 // Handle added records
230245 foreach ( var entity in added )
231246 {
247+ await EnsureCapacityForAppendAsync ( tableName , file , cancellationToken ) ;
232248 var buffer = SerializeRecord ( entity , entityMetadata ) ;
233249 file . Seek ( tableMetadata . DataStartOffset + ( tableMetadata . RecordCount * tableMetadata . RecordSize ) , SeekOrigin . Begin ) ;
234250 await file . WriteAsync ( buffer , cancellationToken ) ;
@@ -274,6 +290,85 @@ private async Task SaveChangesInternalAsync<T>(string tableName, List<T> added,
274290 await file . FlushAsync ( cancellationToken ) ;
275291 }
276292
293+ private async Task EnsureCapacityForAppendAsync ( string tableName , FileStream file , CancellationToken cancellationToken )
294+ {
295+ var tableMetadata = _tables [ tableName ] ;
296+ if ( tableMetadata . RecordCount < tableMetadata . ReservedRecordCount )
297+ {
298+ return ;
299+ }
300+
301+ int growByRecords = Math . Max ( INCREASE_RECORD_SIZE , tableMetadata . ReservedRecordCount ) ;
302+ long growByBytes = ( long ) growByRecords * tableMetadata . RecordSize ;
303+
304+ var ordered = _tables . Values
305+ . OrderBy ( t => t . DataStartOffset )
306+ . ToArray ( ) ;
307+
308+ int tableIndex = Array . FindIndex ( ordered , t => string . Equals ( t . TableName , tableName , StringComparison . Ordinal ) ) ;
309+ if ( tableIndex < 0 )
310+ {
311+ throw new InvalidOperationException ( $ "Table '{ tableName } ' not found.") ;
312+ }
313+
314+ // Shift subsequent tables (data + offsets) backwards from end to avoid overwrite.
315+ for ( int i = ordered . Length - 1 ; i > tableIndex ; i -- )
316+ {
317+ var t = ordered [ i ] ;
318+ if ( t . RecordCount <= 0 )
319+ {
320+ t . DataStartOffset += growByBytes ;
321+ continue ;
322+ }
323+
324+ long srcStart = t . DataStartOffset ;
325+ long bytesToMove = ( long ) t . RecordCount * t . RecordSize ;
326+ long srcEndExclusive = srcStart + bytesToMove ;
327+ long destStart = srcStart + growByBytes ;
328+
329+ await MoveRegionAsync ( file , srcStart , srcEndExclusive , destStart , cancellationToken ) ;
330+ t . DataStartOffset = destStart ;
331+ }
332+
333+ tableMetadata . ReservedRecordCount += growByRecords ;
334+
335+ foreach ( var t in ordered )
336+ {
337+ await UpdateTableMetadataAsync ( t . TableName , file , cancellationToken ) ;
338+ }
339+ }
340+
341+ private static async Task MoveRegionAsync ( FileStream file , long srcStart , long srcEndExclusive , long destStart , CancellationToken cancellationToken )
342+ {
343+ const int bufferSize = 64 * 1024 ;
344+ byte [ ] buffer = ArrayPool < byte > . Shared . Rent ( bufferSize ) ;
345+ try
346+ {
347+ long remaining = srcEndExclusive - srcStart ;
348+ while ( remaining > 0 )
349+ {
350+ int toRead = ( int ) Math . Min ( buffer . Length , remaining ) ;
351+ long readPos = srcStart + ( remaining - toRead ) ;
352+ long writePos = destStart + ( remaining - toRead ) ;
353+
354+ file . Seek ( readPos , SeekOrigin . Begin ) ;
355+ int read = await file . ReadAsync ( buffer . AsMemory ( 0 , toRead ) , cancellationToken ) ;
356+ if ( read != toRead )
357+ {
358+ throw new EndOfStreamException ( $ "Unexpected EOF while moving data region. Expected { toRead } bytes, got { read } .") ;
359+ }
360+
361+ file . Seek ( writePos , SeekOrigin . Begin ) ;
362+ await file . WriteAsync ( buffer . AsMemory ( 0 , toRead ) , cancellationToken ) ;
363+ remaining -= toRead ;
364+ }
365+ }
366+ finally
367+ {
368+ ArrayPool < byte > . Shared . Return ( buffer ) ;
369+ }
370+ }
371+
277372 private async Task UpdateTableMetadataAsync ( string tableName , FileStream file , CancellationToken cancellationToken = default )
278373 {
279374 var tableMetadata = _tables [ tableName ] ;
0 commit comments