@@ -660,3 +660,228 @@ fn test_transaction_receipts_in_block_function() {
660660 assert ! ( receipts. is_empty( ) )
661661 } )
662662}
663+
664+ // ---- rebuild_storage tests ----
665+
666+ /// Helper that runs a test only on NETWORK_NAME (private storage).
667+ /// rebuild_storage is not supported on shared storage.
668+ fn run_rebuild_test < R , F > ( chain : FakeBlockList , test : F )
669+ where
670+ F : Fn ( Arc < DieselChainStore > , Arc < DieselStore > ) -> R + Send + Sync + ' static ,
671+ R : Future < Output = ( ) > + Send + ' static ,
672+ {
673+ run_test_sequentially ( |store| async move {
674+ block_store:: set_chain ( chain. clone ( ) , NETWORK_NAME ) . await ;
675+
676+ let chain_store = store
677+ . block_store ( )
678+ . chain_store ( NETWORK_NAME )
679+ . await
680+ . expect ( "chain store for NETWORK_NAME" ) ;
681+
682+ test ( chain_store, store) . await ;
683+ } ) ;
684+ }
685+
686+ #[ test]
687+ fn rebuild_storage_with_existing_namespace ( ) {
688+ let chain = vec ! [ & * GENESIS_BLOCK , & * BLOCK_ONE , & * BLOCK_TWO ] ;
689+
690+ run_rebuild_test ( chain, |chain_store, store| async move {
691+ let block_store = store. block_store ( ) ;
692+
693+ // Look up the chain from primary metadata
694+ let mut conn = PRIMARY_POOL . get ( ) . await . unwrap ( ) ;
695+ let chain = graph_store_postgres:: find_chain ( & mut conn, NETWORK_NAME )
696+ . await
697+ . unwrap ( )
698+ . expect ( "chain exists in public.chains" ) ;
699+ drop ( conn) ;
700+
701+ let ident = chain. network_identifier ( ) . unwrap ( ) ;
702+
703+ // Verify blocks are present before rebuild
704+ let hashes = chain_store. block_hashes_by_block_number ( 1 ) . await . unwrap ( ) ;
705+ assert ! ( !hashes. is_empty( ) , "block 1 should exist before rebuild" ) ;
706+
707+ // Verify namespace exists
708+ assert ! (
709+ block_store. has_namespace( & chain) . await . unwrap( ) ,
710+ "namespace should exist before rebuild"
711+ ) ;
712+
713+ // Rebuild storage (should drop and recreate)
714+ block_store
715+ . rebuild_chain_storage ( NETWORK_NAME , & ident)
716+ . await
717+ . expect ( "rebuild_chain_storage succeeds" ) ;
718+
719+ // Verify namespace still exists after rebuild
720+ assert ! (
721+ block_store. has_namespace( & chain) . await . unwrap( ) ,
722+ "namespace should exist after rebuild"
723+ ) ;
724+
725+ // Verify blocks are gone after rebuild (tables are fresh)
726+ let hashes = chain_store. block_hashes_by_block_number ( 1 ) . await . unwrap ( ) ;
727+ assert ! ( hashes. is_empty( ) , "blocks should be gone after rebuild" ) ;
728+
729+ // Verify chain identity is intact
730+ let ident_after = chain_store. chain_identifier ( ) . await . unwrap ( ) ;
731+ assert_eq ! ( ident. net_version, ident_after. net_version) ;
732+ assert_eq ! ( ident. genesis_block_hash, ident_after. genesis_block_hash) ;
733+
734+ // Verify head is reset to null
735+ let head = chain_store
736+ . clone ( )
737+ . chain_head_ptr ( )
738+ . await
739+ . expect ( "chain_head_ptr succeeds" ) ;
740+ assert ! ( head. is_none( ) , "head should be null after rebuild" ) ;
741+ } ) ;
742+ }
743+
744+ #[ test]
745+ fn rebuild_storage_with_missing_namespace ( ) {
746+ let chain = vec ! [ & * GENESIS_BLOCK , & * BLOCK_ONE ] ;
747+
748+ run_rebuild_test ( chain, |chain_store, store| async move {
749+ let block_store = store. block_store ( ) ;
750+
751+ let mut conn = PRIMARY_POOL . get ( ) . await . unwrap ( ) ;
752+ let chain = graph_store_postgres:: find_chain ( & mut conn, NETWORK_NAME )
753+ . await
754+ . unwrap ( )
755+ . expect ( "chain exists in public.chains" ) ;
756+ drop ( conn) ;
757+
758+ let ident = chain. network_identifier ( ) . unwrap ( ) ;
759+ let nsp = chain. storage . to_string ( ) ;
760+
761+ // Drop the namespace manually to simulate missing storage
762+ {
763+ let mut conn = chain_store. get_conn_for_test ( ) . await . unwrap ( ) ;
764+ diesel:: sql_query ( format ! ( "DROP SCHEMA IF EXISTS {nsp} CASCADE" ) )
765+ . execute ( & mut conn)
766+ . await
767+ . unwrap ( ) ;
768+ }
769+
770+ // Verify namespace is gone
771+ assert ! (
772+ !block_store. has_namespace( & chain) . await . unwrap( ) ,
773+ "namespace should be gone after manual drop"
774+ ) ;
775+
776+ // Rebuild should recreate the missing namespace
777+ block_store
778+ . rebuild_chain_storage ( NETWORK_NAME , & ident)
779+ . await
780+ . expect ( "rebuild_chain_storage succeeds on missing namespace" ) ;
781+
782+ // Verify namespace exists again
783+ assert ! (
784+ block_store. has_namespace( & chain) . await . unwrap( ) ,
785+ "namespace should exist after rebuild"
786+ ) ;
787+
788+ // Verify chain identity is correct
789+ let ident_after = chain_store. chain_identifier ( ) . await . unwrap ( ) ;
790+ assert_eq ! ( ident. net_version, ident_after. net_version) ;
791+ assert_eq ! ( ident. genesis_block_hash, ident_after. genesis_block_hash) ;
792+ } ) ;
793+ }
794+
795+ #[ test]
796+ fn rebuild_storage_repairs_ethereum_networks_row ( ) {
797+ let chain = vec ! [ & * GENESIS_BLOCK ] ;
798+
799+ run_rebuild_test ( chain, |chain_store, store| async move {
800+ let block_store = store. block_store ( ) ;
801+
802+ let mut conn = PRIMARY_POOL . get ( ) . await . unwrap ( ) ;
803+ let chain = graph_store_postgres:: find_chain ( & mut conn, NETWORK_NAME )
804+ . await
805+ . unwrap ( )
806+ . expect ( "chain exists in public.chains" ) ;
807+ drop ( conn) ;
808+
809+ let ident = chain. network_identifier ( ) . unwrap ( ) ;
810+ let nsp = chain. storage . to_string ( ) ;
811+
812+ // Drop the namespace AND delete the ethereum_networks row to
813+ // simulate a fully broken state
814+ {
815+ let mut conn = chain_store. get_conn_for_test ( ) . await . unwrap ( ) ;
816+ diesel:: sql_query ( format ! ( "DROP SCHEMA IF EXISTS {nsp} CASCADE" ) )
817+ . execute ( & mut conn)
818+ . await
819+ . unwrap ( ) ;
820+ diesel:: sql_query ( format ! (
821+ "DELETE FROM public.ethereum_networks WHERE name = '{}'" ,
822+ NETWORK_NAME
823+ ) )
824+ . execute ( & mut conn)
825+ . await
826+ . unwrap ( ) ;
827+ }
828+
829+ // Rebuild should recreate both the namespace and the
830+ // ethereum_networks row via upsert
831+ block_store
832+ . rebuild_chain_storage ( NETWORK_NAME , & ident)
833+ . await
834+ . expect ( "rebuild_chain_storage succeeds with missing ethereum_networks row" ) ;
835+
836+ // Verify the chain identity was restored from the ident we passed in
837+ let ident_after = chain_store. chain_identifier ( ) . await . unwrap ( ) ;
838+ assert_eq ! ( ident. net_version, ident_after. net_version) ;
839+ assert_eq ! ( ident. genesis_block_hash, ident_after. genesis_block_hash) ;
840+
841+ // Verify namespace exists
842+ assert ! (
843+ block_store. has_namespace( & chain) . await . unwrap( ) ,
844+ "namespace should exist after rebuild"
845+ ) ;
846+ } ) ;
847+ }
848+
849+ #[ test]
850+ fn has_namespace_returns_false_for_missing_schema ( ) {
851+ let chain = vec ! [ & * GENESIS_BLOCK ] ;
852+
853+ run_rebuild_test ( chain, |chain_store, store| async move {
854+ let block_store = store. block_store ( ) ;
855+
856+ let mut conn = PRIMARY_POOL . get ( ) . await . unwrap ( ) ;
857+ let chain = graph_store_postgres:: find_chain ( & mut conn, NETWORK_NAME )
858+ . await
859+ . unwrap ( )
860+ . expect ( "chain exists" ) ;
861+ drop ( conn) ;
862+
863+ // Namespace should exist initially
864+ assert ! ( block_store. has_namespace( & chain) . await . unwrap( ) ) ;
865+
866+ let nsp = chain. storage . to_string ( ) ;
867+
868+ // Drop the schema
869+ {
870+ let mut conn = chain_store. get_conn_for_test ( ) . await . unwrap ( ) ;
871+ diesel:: sql_query ( format ! ( "DROP SCHEMA IF EXISTS {nsp} CASCADE" ) )
872+ . execute ( & mut conn)
873+ . await
874+ . unwrap ( ) ;
875+ }
876+
877+ // Should return false now
878+ assert ! ( !block_store. has_namespace( & chain) . await . unwrap( ) ) ;
879+
880+ // Rebuild to leave things clean for other tests
881+ let ident = chain. network_identifier ( ) . unwrap ( ) ;
882+ block_store
883+ . rebuild_chain_storage ( NETWORK_NAME , & ident)
884+ . await
885+ . unwrap ( ) ;
886+ } ) ;
887+ }
0 commit comments