1+ import functools
12import itertools
23import operator
34from collections .abc import Awaitable , Callable , Iterator , Sequence
45from datetime import UTC , datetime
5- from typing import Any , cast
6+ from typing import Any , Generic , cast , override
67
78import anyio
89import psycopg
@@ -78,9 +79,7 @@ async def _async_load(
7879 timeout = 600 ,
7980 ) as pool :
8081 await pool .wait ()
81- loader_cls = (
82- FullSyncBatchLoader if model .should_fully_sync_delete_diff () else BatchLoader
83- )
82+ loader_cls = FullSyncBatchLoader if model .should_delete_missing () else BatchLoader
8483 return await loader_cls (
8584 fetch_results ,
8685 model ,
@@ -94,7 +93,7 @@ async def _async_load(
9493 ).load ()
9594
9695
97- class BatchLoader :
96+ class BatchLoader ( Generic [ T ]): # noqa: UP046
9897 def __init__ (
9998 self ,
10099 fetch_results : Iterator [list [T ]],
@@ -175,28 +174,15 @@ def __init__(
175174
176175 async def load (self ) -> bool :
177176 timestamp = datetime .now (UTC )
178-
179177 self .full_load_timer .start ()
180178 async with self .pool .connection () as conn , conn .cursor (binary = True ) as cur :
181- self .progress_start_timer .start ()
182- await self ._insert_batch_start (cur )
183- await conn .commit ()
184- self .progress_start_timer .stop ()
179+ await self ._record_batch_start (conn , cur , commit = True )
185180
186- data_loaded = False
187- num_rows = 0
188181 batch_num = 1
189- while True :
190- self .idr_query_timer .start ()
191- # We unfortunately need to use a while true loop here since we need to wrap the
192- # iterator with the timer calls.
193- results = next (self .fetch_results , None )
194- self .idr_query_timer .stop ()
195- if not results :
196- break
197182
183+ async def _process_batch (results : list [T ]) -> None :
184+ nonlocal batch_num
198185 self .full_batch_timer .start ()
199- data_loaded = True
200186 logger .info (
201187 "{}-{}-{}: loading next {} results concurrently {} row(s) at a time" ,
202188 self .table ,
@@ -205,8 +191,6 @@ async def load(self) -> bool:
205191 len (results ),
206192 PER_BATCH_CONCURRENT_ROWS ,
207193 )
208- num_rows += len (results )
209-
210194 self .sort_batch_timer .start ()
211195 results .sort (key = operator .attrgetter (* self .ordered_pkeys ))
212196 self .sort_batch_timer .stop ()
@@ -262,6 +246,9 @@ async def _wrap_batch_chunk(
262246 batch_num += 1
263247 self .full_batch_timer .stop ()
264248
249+ num_rows = await self ._stage_all_batches (_process_batch )
250+ data_loaded = num_rows > 0
251+
265252 # Wait until the background worker signals that all pending loading tasks are completed
266253 # for the current partition before marking it totally complete
267254 self .worker_client .wait_until_done (self .model , self .partition )
@@ -421,29 +408,54 @@ async def _copy_data(
421408 [_remove_null_bytes (getattr (row , k )) for k in self .insert_cols ]
422409 )
423410
411+ async def _record_batch_start (
412+ self , conn : psycopg .AsyncConnection , cur : psycopg .AsyncCursor [Any ], commit : bool
413+ ) -> None :
414+ self .progress_start_timer .start ()
415+ await self ._insert_batch_start (cur )
416+ if commit :
417+ await conn .commit ()
418+ self .progress_start_timer .stop ()
419+
420+ def _next_batch (self ) -> list [T ] | None :
421+ self .idr_query_timer .start ()
422+ results = next (self .fetch_results , None )
423+ self .idr_query_timer .stop ()
424+ return results
425+
426+ async def _stage_all_batches (self , process_batch : Callable [[list [T ]], Awaitable [None ]]) -> int :
427+ num_rows = 0
428+
429+ while True :
430+ # We unfortunately need to use a while true loop here since we need to wrap the
431+ # iterator with the timer calls.
432+ self .idr_query_timer .start ()
433+ results = next (self .fetch_results , None )
434+ self .idr_query_timer .stop ()
435+ if not results :
436+ break
424437
425- class FullSyncBatchLoader (BatchLoader ):
438+ num_rows += len (results )
439+ await process_batch (results )
440+
441+ return num_rows
442+
443+
444+ class FullSyncBatchLoader (BatchLoader [T ]):
445+ @override
426446 async def load (self ) -> bool :
427447 timestamp = datetime .now (UTC )
428448 self .full_load_timer .start ()
429- num_rows = 0
449+ data_loaded = False
430450
431451 async with self .pool .connection () as conn , conn .cursor (binary = True ) as cur :
432- self .progress_start_timer .start ()
433- await self ._insert_batch_start (cur )
434- self .progress_start_timer .stop ()
435-
452+ await self ._record_batch_start (conn , cur , commit = False )
436453 full_temp_table = await self ._setup_temp_table (cur , "full_temp" )
437454
438- while True :
439- self .idr_query_timer .start ()
440- results = next (self .fetch_results , None )
441- self .idr_query_timer .stop ()
442- if not results :
443- break
444- num_rows += len (results )
445- await self ._copy_data (cur , full_temp_table , results )
446-
455+ num_rows = await self ._stage_all_batches (
456+ functools .partial (self ._copy_data , cur , full_temp_table )
457+ )
458+ data_loaded = num_rows > 0
447459 logger .info (
448460 "{}-{}: staged {} row(s) for full sync" ,
449461 self .table ,
@@ -472,19 +484,22 @@ async def load(self) -> bool:
472484 self .table ,
473485 self .partition .name ,
474486 )
475- return True
487+ return data_loaded
476488
477489 async def _delete_missing (self , cur : psycopg .AsyncCursor [Any ], temp_tablename : str ) -> int :
478490 # We have to exclude our synthetic data that also exists in prod from deletion
479- synthetic_data_filter = (
480- "" if self .load_mode == LoadMode .SYNTHETIC else "WHERE utn NOT LIKE '-%'"
491+ synthetic_data_filter = self .model .synthetic_data_filter ()
492+ synthetic_where_clause = (
493+ f"WHERE { synthetic_data_filter } "
494+ if synthetic_data_filter and self .load_mode != LoadMode .SYNTHETIC
495+ else ""
481496 )
482497 result = await cur .execute ( # type: ignore
483498 f'''
484499 DELETE FROM { self .table }
485500 WHERE ({ self .primary_keys_str } ) IN (
486501 SELECT { self .primary_keys_str } FROM { self .table }
487- { synthetic_data_filter }
502+ { synthetic_where_clause }
488503 EXCEPT
489504 SELECT { self .primary_keys_str } FROM "{ temp_tablename } "
490505 )
@@ -506,5 +521,5 @@ def _remove_null_bytes(val: DbType) -> DbType:
506521
507522
508523def should_track_load_progress (load_mode : LoadMode ) -> bool :
509- # Whether to read/write load progress, which is diabled for synthetic and testing loads.
524+ # Whether to read/write load progress, which is disabled for synthetic and testing loads.
510525 return load_mode == LoadMode .PROD or force_load_progress ()
0 commit comments