@@ -354,6 +354,236 @@ def test_data_persistence(self, lmdb_config, sample_test_data, test_table_name):
354354
355355 loader2 .disconnect ()
356356
357+ def test_handle_reorg_empty_db (self , lmdb_config ):
358+ """Test reorg handling on empty database"""
359+ from src .amp .streaming .types import BlockRange
360+
361+ loader = LMDBLoader (lmdb_config )
362+ loader .connect ()
363+
364+ # Call handle reorg on empty database
365+ invalidation_ranges = [BlockRange (network = 'ethereum' , start = 100 , end = 200 )]
366+
367+ # Should not raise any errors
368+ loader ._handle_reorg (invalidation_ranges , 'test_reorg_empty' )
369+
370+ loader .disconnect ()
371+
372+ def test_handle_reorg_no_metadata (self , lmdb_config ):
373+ """Test reorg handling when data lacks metadata column"""
374+ from src .amp .streaming .types import BlockRange
375+
376+ config = {** lmdb_config , 'key_column' : 'id' }
377+ loader = LMDBLoader (config )
378+ loader .connect ()
379+
380+ # Create data without metadata column
381+ data = pa .table ({'id' : [1 , 2 , 3 ], 'block_num' : [100 , 150 , 200 ], 'value' : [10.0 , 20.0 , 30.0 ]})
382+ loader .load_table (data , 'test_reorg_no_meta' , mode = LoadMode .OVERWRITE )
383+
384+ # Call handle reorg
385+ invalidation_ranges = [BlockRange (network = 'ethereum' , start = 150 , end = 250 )]
386+
387+ # Should not delete any data (no metadata to check)
388+ loader ._handle_reorg (invalidation_ranges , 'test_reorg_no_meta' )
389+
390+ # Verify data still exists
391+ with loader .env .begin () as txn :
392+ assert txn .get (b'1' ) is not None
393+ assert txn .get (b'2' ) is not None
394+ assert txn .get (b'3' ) is not None
395+
396+ loader .disconnect ()
397+
398+ def test_handle_reorg_single_network (self , lmdb_config ):
399+ """Test reorg handling for single network data"""
400+ import json
401+
402+ from src .amp .streaming .types import BlockRange
403+
404+ config = {** lmdb_config , 'key_column' : 'id' }
405+ loader = LMDBLoader (config )
406+ loader .connect ()
407+
408+ # Create table with metadata
409+ block_ranges = [
410+ [{'network' : 'ethereum' , 'start' : 100 , 'end' : 110 }],
411+ [{'network' : 'ethereum' , 'start' : 150 , 'end' : 160 }],
412+ [{'network' : 'ethereum' , 'start' : 200 , 'end' : 210 }],
413+ ]
414+
415+ data = pa .table (
416+ {
417+ 'id' : [1 , 2 , 3 ],
418+ 'block_num' : [105 , 155 , 205 ],
419+ '_meta_block_ranges' : [json .dumps (ranges ) for ranges in block_ranges ],
420+ }
421+ )
422+
423+ # Load initial data
424+ result = loader .load_table (data , 'test_reorg_single' , mode = LoadMode .OVERWRITE )
425+ assert result .success
426+ assert result .rows_loaded == 3
427+
428+ # Verify all data exists
429+ with loader .env .begin () as txn :
430+ assert txn .get (b'1' ) is not None
431+ assert txn .get (b'2' ) is not None
432+ assert txn .get (b'3' ) is not None
433+
434+ # Reorg from block 155 - should delete rows 2 and 3
435+ invalidation_ranges = [BlockRange (network = 'ethereum' , start = 155 , end = 300 )]
436+ loader ._handle_reorg (invalidation_ranges , 'test_reorg_single' )
437+
438+ # Verify only first row remains
439+ with loader .env .begin () as txn :
440+ assert txn .get (b'1' ) is not None
441+ assert txn .get (b'2' ) is None # Deleted
442+ assert txn .get (b'3' ) is None # Deleted
443+
444+ loader .disconnect ()
445+
446+ def test_handle_reorg_multi_network (self , lmdb_config ):
447+ """Test reorg handling preserves data from unaffected networks"""
448+ import json
449+
450+ from src .amp .streaming .types import BlockRange
451+
452+ config = {** lmdb_config , 'key_column' : 'id' }
453+ loader = LMDBLoader (config )
454+ loader .connect ()
455+
456+ # Create data from multiple networks
457+ block_ranges = [
458+ [{'network' : 'ethereum' , 'start' : 100 , 'end' : 110 }],
459+ [{'network' : 'polygon' , 'start' : 100 , 'end' : 110 }],
460+ [{'network' : 'ethereum' , 'start' : 150 , 'end' : 160 }],
461+ [{'network' : 'polygon' , 'start' : 150 , 'end' : 160 }],
462+ ]
463+
464+ data = pa .table (
465+ {
466+ 'id' : [1 , 2 , 3 , 4 ],
467+ 'network' : ['ethereum' , 'polygon' , 'ethereum' , 'polygon' ],
468+ '_meta_block_ranges' : [json .dumps (r ) for r in block_ranges ],
469+ }
470+ )
471+
472+ # Load initial data
473+ result = loader .load_table (data , 'test_reorg_multi' , mode = LoadMode .OVERWRITE )
474+ assert result .success
475+ assert result .rows_loaded == 4
476+
477+ # Reorg only ethereum from block 150
478+ invalidation_ranges = [BlockRange (network = 'ethereum' , start = 150 , end = 200 )]
479+ loader ._handle_reorg (invalidation_ranges , 'test_reorg_multi' )
480+
481+ # Verify ethereum row 3 deleted, but polygon rows preserved
482+ with loader .env .begin () as txn :
483+ assert txn .get (b'1' ) is not None # ethereum block 100
484+ assert txn .get (b'2' ) is not None # polygon block 100
485+ assert txn .get (b'3' ) is None # ethereum block 150 (deleted)
486+ assert txn .get (b'4' ) is not None # polygon block 150
487+
488+ loader .disconnect ()
489+
490+ def test_handle_reorg_overlapping_ranges (self , lmdb_config ):
491+ """Test reorg with overlapping block ranges"""
492+ import json
493+
494+ from src .amp .streaming .types import BlockRange
495+
496+ config = {** lmdb_config , 'key_column' : 'id' }
497+ loader = LMDBLoader (config )
498+ loader .connect ()
499+
500+ # Create data with overlapping ranges
501+ block_ranges = [
502+ [{'network' : 'ethereum' , 'start' : 90 , 'end' : 110 }], # Overlaps with reorg
503+ [{'network' : 'ethereum' , 'start' : 140 , 'end' : 160 }], # Overlaps with reorg
504+ [{'network' : 'ethereum' , 'start' : 170 , 'end' : 190 }], # After reorg
505+ ]
506+
507+ data = pa .table ({'id' : [1 , 2 , 3 ], '_meta_block_ranges' : [json .dumps (ranges ) for ranges in block_ranges ]})
508+
509+ # Load initial data
510+ result = loader .load_table (data , 'test_reorg_overlap' , mode = LoadMode .OVERWRITE )
511+ assert result .success
512+ assert result .rows_loaded == 3
513+
514+ # Reorg from block 150 - should delete rows where end >= 150
515+ invalidation_ranges = [BlockRange (network = 'ethereum' , start = 150 , end = 200 )]
516+ loader ._handle_reorg (invalidation_ranges , 'test_reorg_overlap' )
517+
518+ # Only first row should remain (ends at 110 < 150)
519+ with loader .env .begin () as txn :
520+ assert txn .get (b'1' ) is not None
521+ assert txn .get (b'2' ) is None # Deleted (end=160 >= 150)
522+ assert txn .get (b'3' ) is None # Deleted (end=190 >= 150)
523+
524+ loader .disconnect ()
525+
526+ def test_streaming_with_reorg (self , lmdb_config ):
527+ """Test streaming data with reorg support"""
528+ from src .amp .streaming .types import (
529+ BatchMetadata ,
530+ BlockRange ,
531+ ResponseBatch ,
532+ ResponseBatchType ,
533+ ResponseBatchWithReorg ,
534+ )
535+
536+ config = {** lmdb_config , 'key_column' : 'id' }
537+ loader = LMDBLoader (config )
538+ loader .connect ()
539+
540+ # Create streaming data with metadata
541+ data1 = pa .RecordBatch .from_pydict ({'id' : [1 , 2 ], 'value' : [100 , 200 ]})
542+
543+ data2 = pa .RecordBatch .from_pydict ({'id' : [3 , 4 ], 'value' : [300 , 400 ]})
544+
545+ # Create response batches
546+ response1 = ResponseBatchWithReorg (
547+ batch_type = ResponseBatchType .DATA ,
548+ data = ResponseBatch (
549+ data = data1 , metadata = BatchMetadata (ranges = [BlockRange (network = 'ethereum' , start = 100 , end = 110 )])
550+ ),
551+ )
552+
553+ response2 = ResponseBatchWithReorg (
554+ batch_type = ResponseBatchType .DATA ,
555+ data = ResponseBatch (
556+ data = data2 , metadata = BatchMetadata (ranges = [BlockRange (network = 'ethereum' , start = 150 , end = 160 )])
557+ ),
558+ )
559+
560+ # Simulate reorg event
561+ reorg_response = ResponseBatchWithReorg (
562+ batch_type = ResponseBatchType .REORG , invalidation_ranges = [BlockRange (network = 'ethereum' , start = 150 , end = 200 )]
563+ )
564+
565+ # Process streaming data
566+ stream = [response1 , response2 , reorg_response ]
567+ results = list (loader .load_stream_continuous (iter (stream ), 'test_streaming_reorg' ))
568+
569+ # Verify results
570+ assert len (results ) == 3
571+ assert results [0 ].success
572+ assert results [0 ].rows_loaded == 2
573+ assert results [1 ].success
574+ assert results [1 ].rows_loaded == 2
575+ assert results [2 ].success
576+ assert results [2 ].is_reorg
577+
578+ # Verify reorg deleted the second batch
579+ with loader .env .begin () as txn :
580+ assert txn .get (b'1' ) is not None
581+ assert txn .get (b'2' ) is not None
582+ assert txn .get (b'3' ) is None # Deleted by reorg
583+ assert txn .get (b'4' ) is None # Deleted by reorg
584+
585+ loader .disconnect ()
586+
357587
358588if __name__ == '__main__' :
359589 pytest .main ([__file__ , '-v' ])
0 commit comments