@@ -127,7 +127,7 @@ def _rename_attributes(table, props):
127127 )
128128
129129 if self ._key_source is None :
130- parents = self .target . parents (primary = True , as_objects = True , foreign_key_info = True )
130+ parents = self .parents (primary = True , as_objects = True , foreign_key_info = True )
131131 if not parents :
132132 raise DataJointError ("A table must have dependencies from its primary key for auto-populate to work" )
133133 self ._key_source = _rename_attributes (* parents [0 ])
@@ -204,15 +204,6 @@ def make(self, key):
204204 self .make_insert (key , * computed_result )
205205 yield
206206
207- @property
208- def target (self ):
209- """
210- :return: table to be populated.
211- In the typical case, dj.AutoPopulate is mixed into a dj.Table class by
212- inheritance and the target is self.
213- """
214- return self
215-
216207 def _jobs_to_do (self , restrictions ):
217208 """
218209 :return: the query yielding the keys to be computed (derived from self.key_source)
@@ -235,7 +226,7 @@ def _jobs_to_do(self, restrictions):
235226 raise DataJointError (
236227 "The populate target lacks attribute %s "
237228 "from the primary key of key_source"
238- % next (name for name in todo .heading .primary_key if name not in self .target . heading )
229+ % next (name for name in todo .heading .primary_key if name not in self .heading )
239230 )
240231 except StopIteration :
241232 pass
@@ -324,7 +315,7 @@ def _populate_direct(
324315 Computes keys directly from key_source, suitable for single-worker
325316 execution, development, and debugging.
326317 """
327- keys = (self ._jobs_to_do (restrictions ) - self . target ).fetch ("KEY" )
318+ keys = (self ._jobs_to_do (restrictions ) - self ).fetch ("KEY" )
328319
329320 logger .debug ("Found %d keys to populate" % len (keys ))
330321
@@ -493,14 +484,14 @@ def _populate1(self, key, jobs, suppress_errors, return_exception_objects, make_
493484 if not is_generator :
494485 self .connection .start_transaction ()
495486
496- if key in self . target : # already populated
487+ if key in self : # already populated
497488 if not is_generator :
498489 self .connection .cancel_transaction ()
499490 if jobs is not None :
500491 jobs .complete (key )
501492 return False
502493
503- logger .debug (f"Making { key } -> { self .target . full_table_name } " )
494+ logger .debug (f"Making { key } -> { self .full_table_name } " )
504495 self .__class__ ._allow_insert = True
505496
506497 try :
@@ -531,7 +522,7 @@ def _populate1(self, key, jobs, suppress_errors, return_exception_objects, make_
531522 exception = error .__class__ .__name__ ,
532523 msg = ": " + str (error ) if str (error ) else "" ,
533524 )
534- logger .debug (f"Error making { key } -> { self .target . full_table_name } - { error_message } " )
525+ logger .debug (f"Error making { key } -> { self .full_table_name } - { error_message } " )
535526 if jobs is not None :
536527 jobs .error (key , error_message = error_message , error_stack = traceback .format_exc ())
537528 if not suppress_errors or isinstance (error , SystemExit ):
@@ -542,7 +533,7 @@ def _populate1(self, key, jobs, suppress_errors, return_exception_objects, make_
542533 else :
543534 self .connection .commit_transaction ()
544535 duration = time .time () - start_time
545- logger .debug (f"Success making { key } -> { self .target . full_table_name } " )
536+ logger .debug (f"Success making { key } -> { self .full_table_name } " )
546537
547538 # Update hidden job metadata if table has the columns
548539 if self ._has_job_metadata_attrs ():
@@ -564,11 +555,61 @@ def _populate1(self, key, jobs, suppress_errors, return_exception_objects, make_
564555 def progress (self , * restrictions , display = False ):
565556 """
566557 Report the progress of populating the table.
558+
559+ Uses a single aggregation query to efficiently compute both total and
560+ remaining counts.
561+
562+ :param restrictions: conditions to restrict key_source
563+ :param display: if True, log the progress
567564 :return: (remaining, total) -- numbers of tuples to be populated
568565 """
569566 todo = self ._jobs_to_do (restrictions )
570- total = len (todo )
571- remaining = len (todo - self .target )
567+
568+ # Get primary key attributes from key_source for join condition
569+ # These are the "job keys" - the granularity at which populate() works
570+ pk_attrs = todo .primary_key
571+ assert pk_attrs , "key_source must have a primary key"
572+
573+ # Find common attributes between key_source and self for the join
574+ # This handles cases where self has additional PK attributes
575+ common_attrs = [attr for attr in pk_attrs if attr in self .heading .names ]
576+
577+ if not common_attrs :
578+ # No common attributes - fall back to two-query method
579+ total = len (todo )
580+ remaining = len (todo - self )
581+ else :
582+ # Build a single query that computes both total and remaining
583+ # Using LEFT JOIN with COUNT(DISTINCT) to handle 1:many relationships
584+ todo_sql = todo .make_sql ()
585+ target_sql = self .make_sql ()
586+
587+ # Build join condition on common attributes
588+ join_cond = " AND " .join (f"`$ks`.`{ attr } ` = `$tgt`.`{ attr } `" for attr in common_attrs )
589+
590+ # Build DISTINCT key expression for counting unique jobs
591+ # Use CONCAT for composite keys to create a single distinct value
592+ if len (pk_attrs ) == 1 :
593+ distinct_key = f"`$ks`.`{ pk_attrs [0 ]} `"
594+ null_check = f"`$tgt`.`{ common_attrs [0 ]} `"
595+ else :
596+ distinct_key = "CONCAT_WS('|', {})" .format (", " .join (f"`$ks`.`{ attr } `" for attr in pk_attrs ))
597+ null_check = f"`$tgt`.`{ common_attrs [0 ]} `"
598+
599+ # Single aggregation query:
600+ # - COUNT(DISTINCT key) gives total unique jobs in key_source
601+ # - Remaining = jobs where no matching target row exists
602+ sql = f"""
603+ SELECT
604+ COUNT(DISTINCT { distinct_key } ) AS total,
605+ COUNT(DISTINCT CASE WHEN { null_check } IS NULL THEN { distinct_key } END) AS remaining
606+ FROM ({ todo_sql } ) AS `$ks`
607+ LEFT JOIN ({ target_sql } ) AS `$tgt` ON { join_cond }
608+ """
609+
610+ result = self .connection .query (sql ).fetchone ()
611+ total , remaining = result
612+
572613 if display :
573614 logger .info (
574615 "%-20s" % self .__class__ .__name__
@@ -585,7 +626,7 @@ def progress(self, *restrictions, display=False):
585626 def _has_job_metadata_attrs (self ):
586627 """Check if table has hidden job metadata columns."""
587628 # Access _attributes directly to include hidden attributes
588- all_attrs = self .target . heading ._attributes
629+ all_attrs = self .heading ._attributes
589630 return all_attrs is not None and "_job_start_time" in all_attrs
590631
591632 def _update_job_metadata (self , key , start_time , duration , version ):
@@ -600,9 +641,9 @@ def _update_job_metadata(self, key, start_time, duration, version):
600641 """
601642 from .condition import make_condition
602643
603- pk_condition = make_condition (self . target , key , set ())
644+ pk_condition = make_condition (self , key , set ())
604645 self .connection .query (
605- f"UPDATE { self .target . full_table_name } SET "
646+ f"UPDATE { self .full_table_name } SET "
606647 "`_job_start_time`=%s, `_job_duration`=%s, `_job_version`=%s "
607648 f"WHERE { pk_condition } " ,
608649 args = (start_time , duration , version [:64 ] if version else "" ),
0 commit comments