@@ -387,3 +387,202 @@ def test_schema_with_special_characters(self, snowflake_config, test_table_name,
387387 assert row ['first name' ] == 'Alice'
388388 assert abs (row ['total$amount' ] - 100.0 ) < 0.001
389389 assert row ['2024_data' ] == 'a'
390+
391+ def test_handle_reorg_no_metadata_column (self , snowflake_config , test_table_name , cleanup_tables ):
392+ """Test reorg handling when table lacks metadata column"""
393+ from src .amp .streaming .types import BlockRange
394+
395+ cleanup_tables .append (test_table_name )
396+ loader = SnowflakeLoader (snowflake_config )
397+
398+ with loader :
399+ # Create table without metadata column
400+ data = pa .table ({'id' : [1 , 2 , 3 ], 'block_num' : [100 , 150 , 200 ], 'value' : [10.0 , 20.0 , 30.0 ]})
401+ loader .load_table (data , test_table_name , create_table = True )
402+
403+ # Call handle reorg
404+ invalidation_ranges = [BlockRange (network = 'ethereum' , start = 150 , end = 250 )]
405+
406+ # Should log warning and not modify data
407+ loader ._handle_reorg (invalidation_ranges , test_table_name )
408+
409+ # Verify data unchanged
410+ loader .cursor .execute (f'SELECT COUNT(*) FROM { test_table_name } ' )
411+ count = loader .cursor .fetchone ()['COUNT(*)' ]
412+ assert count == 3
413+
414+ def test_handle_reorg_single_network (self , snowflake_config , test_table_name , cleanup_tables ):
415+ """Test reorg handling for single network data"""
416+ import json
417+
418+ from src .amp .streaming .types import BlockRange
419+
420+ cleanup_tables .append (test_table_name )
421+ loader = SnowflakeLoader (snowflake_config )
422+
423+ with loader :
424+ # Create table with metadata
425+ block_ranges = [
426+ [{'network' : 'ethereum' , 'start' : 100 , 'end' : 110 }],
427+ [{'network' : 'ethereum' , 'start' : 150 , 'end' : 160 }],
428+ [{'network' : 'ethereum' , 'start' : 200 , 'end' : 210 }],
429+ ]
430+
431+ data = pa .table (
432+ {
433+ 'id' : [1 , 2 , 3 ],
434+ 'block_num' : [105 , 155 , 205 ],
435+ '_meta_block_ranges' : [json .dumps (ranges ) for ranges in block_ranges ],
436+ }
437+ )
438+
439+ # Load initial data
440+ result = loader .load_table (data , test_table_name , create_table = True )
441+ assert result .success
442+ assert result .rows_loaded == 3
443+
444+ # Verify all data exists
445+ loader .cursor .execute (f'SELECT COUNT(*) FROM { test_table_name } ' )
446+ count = loader .cursor .fetchone ()['COUNT(*)' ]
447+ assert count == 3
448+
449+ # Reorg from block 155 - should delete rows 2 and 3
450+ invalidation_ranges = [BlockRange (network = 'ethereum' , start = 155 , end = 300 )]
451+ loader ._handle_reorg (invalidation_ranges , test_table_name )
452+
453+ # Verify only first row remains
454+ loader .cursor .execute (f'SELECT COUNT(*) FROM { test_table_name } ' )
455+ count = loader .cursor .fetchone ()['COUNT(*)' ]
456+ assert count == 1
457+
458+ loader .cursor .execute (f'SELECT id FROM { test_table_name } ' )
459+ remaining_id = loader .cursor .fetchone ()['ID' ]
460+ assert remaining_id == 1
461+
462+ def test_handle_reorg_multi_network (self , snowflake_config , test_table_name , cleanup_tables ):
463+ """Test reorg handling preserves data from unaffected networks"""
464+ import json
465+
466+ from src .amp .streaming .types import BlockRange
467+
468+ cleanup_tables .append (test_table_name )
469+ loader = SnowflakeLoader (snowflake_config )
470+
471+ with loader :
472+ # Create data from multiple networks
473+ block_ranges = [
474+ [{'network' : 'ethereum' , 'start' : 100 , 'end' : 110 }],
475+ [{'network' : 'polygon' , 'start' : 100 , 'end' : 110 }],
476+ [{'network' : 'ethereum' , 'start' : 150 , 'end' : 160 }],
477+ [{'network' : 'polygon' , 'start' : 150 , 'end' : 160 }],
478+ ]
479+
480+ data = pa .table (
481+ {
482+ 'id' : [1 , 2 , 3 , 4 ],
483+ 'network' : ['ethereum' , 'polygon' , 'ethereum' , 'polygon' ],
484+ '_meta_block_ranges' : [json .dumps ([r ]) for r in block_ranges ],
485+ }
486+ )
487+
488+ # Load initial data
489+ result = loader .load_table (data , test_table_name , create_table = True )
490+ assert result .success
491+ assert result .rows_loaded == 4
492+
493+ # Reorg only ethereum from block 150
494+ invalidation_ranges = [BlockRange (network = 'ethereum' , start = 150 , end = 200 )]
495+ loader ._handle_reorg (invalidation_ranges , test_table_name )
496+
497+ # Verify ethereum row 3 deleted, but polygon rows preserved
498+ loader .cursor .execute (f'SELECT id FROM { test_table_name } ORDER BY id' )
499+ remaining_ids = [row ['ID' ] for row in loader .cursor .fetchall ()]
500+ assert remaining_ids == [1 , 2 , 4 ] # Row 3 deleted
501+
502+ def test_handle_reorg_overlapping_ranges (self , snowflake_config , test_table_name , cleanup_tables ):
503+ """Test reorg with overlapping block ranges"""
504+ import json
505+
506+ from src .amp .streaming .types import BlockRange
507+
508+ cleanup_tables .append (test_table_name )
509+ loader = SnowflakeLoader (snowflake_config )
510+
511+ with loader :
512+ # Create data with overlapping ranges
513+ block_ranges = [
514+ [{'network' : 'ethereum' , 'start' : 90 , 'end' : 110 }], # Overlaps with reorg
515+ [{'network' : 'ethereum' , 'start' : 140 , 'end' : 160 }], # Overlaps with reorg
516+ [{'network' : 'ethereum' , 'start' : 170 , 'end' : 190 }], # After reorg
517+ ]
518+
519+ data = pa .table ({'id' : [1 , 2 , 3 ], '_meta_block_ranges' : [json .dumps (ranges ) for ranges in block_ranges ]})
520+
521+ # Load initial data
522+ result = loader .load_table (data , test_table_name , create_table = True )
523+ assert result .success
524+ assert result .rows_loaded == 3
525+
526+ # Reorg from block 150 - should delete rows where end >= 150
527+ invalidation_ranges = [BlockRange (network = 'ethereum' , start = 150 , end = 200 )]
528+ loader ._handle_reorg (invalidation_ranges , test_table_name )
529+
530+ # Only first row should remain (ends at 110 < 150)
531+ loader .cursor .execute (f'SELECT COUNT(*) FROM { test_table_name } ' )
532+ count = loader .cursor .fetchone ()['COUNT(*)' ]
533+ assert count == 1
534+
535+ loader .cursor .execute (f'SELECT id FROM { test_table_name } ' )
536+ remaining_id = loader .cursor .fetchone ()['ID' ]
537+ assert remaining_id == 1
538+
539+ def test_streaming_with_reorg (self , snowflake_config , test_table_name , cleanup_tables ):
540+ """Test streaming data with reorg support"""
541+ from src .amp .streaming .types import BatchMetadata , BlockRange , ResponseBatch , ResponseBatchWithReorg
542+
543+ cleanup_tables .append (test_table_name )
544+ loader = SnowflakeLoader (snowflake_config )
545+
546+ with loader :
547+ # Create streaming data with metadata
548+ data1 = pa .RecordBatch .from_pydict ({'id' : [1 , 2 ], 'value' : [100 , 200 ]})
549+
550+ data2 = pa .RecordBatch .from_pydict ({'id' : [3 , 4 ], 'value' : [300 , 400 ]})
551+
552+ # Create response batches
553+ response1 = ResponseBatchWithReorg (
554+ is_reorg = False ,
555+ data = ResponseBatch (
556+ data = data1 , metadata = BatchMetadata (ranges = [BlockRange (network = 'ethereum' , start = 100 , end = 110 )])
557+ ),
558+ )
559+
560+ response2 = ResponseBatchWithReorg (
561+ is_reorg = False ,
562+ data = ResponseBatch (
563+ data = data2 , metadata = BatchMetadata (ranges = [BlockRange (network = 'ethereum' , start = 150 , end = 160 )])
564+ ),
565+ )
566+
567+ # Simulate reorg event
568+ reorg_response = ResponseBatchWithReorg (
569+ is_reorg = True , invalidation_ranges = [BlockRange (network = 'ethereum' , start = 150 , end = 200 )]
570+ )
571+
572+ # Process streaming data
573+ stream = [response1 , response2 , reorg_response ]
574+ results = list (loader .load_stream_continuous (iter (stream ), test_table_name ))
575+
576+ # Verify results
577+ assert len (results ) == 3
578+ assert results [0 ].success
579+ assert results [0 ].rows_loaded == 2
580+ assert results [1 ].success
581+ assert results [1 ].rows_loaded == 2
582+ assert results [2 ].success
583+ assert results [2 ].is_reorg
584+
585+ # Verify reorg deleted the second batch
586+ loader .cursor .execute (f'SELECT id FROM { test_table_name } ORDER BY id' )
587+ remaining_ids = [row ['ID' ] for row in loader .cursor .fetchall ()]
588+ assert remaining_ids == [1 , 2 ] # 3 and 4 deleted by reorg
0 commit comments