@@ -190,6 +190,7 @@ def compile_foreign_key(
190190 index_sql : list [str ],
191191 adapter ,
192192 fk_attribute_map : dict [str , tuple [str , str ]] | None = None ,
193+ fk_index_candidates : list [list [str ]] | None = None ,
193194) -> None :
194195 """
195196 Parse a foreign key line and update declaration components.
@@ -215,6 +216,11 @@ def compile_foreign_key(
215216 Database adapter for backend-specific SQL generation.
216217 fk_attribute_map : dict, optional
217218 Mapping of ``child_attr -> (parent_table, parent_attr)``. Updated in place.
219+ fk_index_candidates : list, optional
220+ PostgreSQL only. Collects each non-unique foreign key's columns as a
221+ candidate supporting index; the redundancy/coverage decision is made
222+ post-parse (once the full primary key and index list are known).
223+ Updated in place.
218224
219225 Raises
220226 ------
@@ -301,27 +307,22 @@ def compile_foreign_key(
301307 f"FOREIGN KEY ({ fk_cols } ) REFERENCES { ref_table_name } ({ pk_cols } ) ON UPDATE CASCADE ON DELETE RESTRICT"
302308 )
303309
304- # Declare a supporting index on the foreign-key columns.
310+ # Supporting index on the foreign-key columns.
305311 #
306- # MySQL/InnoDB creates one implicitly for every foreign key, so we emit an
307- # explicit index only on the PostgreSQL path — Postgres never auto-indexes
308- # the referencing (child) columns and offers no server setting to make it
309- # (see #1512). The `unique` case keeps its UNIQUE INDEX on every backend.
310- #
311- # Coverage-aware: skip when the foreign-key columns are already a left-prefix
312- # of the child's primary key, since the primary-key index then already serves
313- # foreign-key lookups and cascades. That holds exactly for a *leading*
314- # primary foreign key; a secondary foreign key (`primary_key is None`) or a
315- # non-leading primary foreign key is not covered and needs its own index.
312+ # The `unique` option is a uniqueness constraint, always emitted, on every
313+ # backend. For the non-unique case: MySQL/InnoDB indexes every foreign key
314+ # implicitly, but PostgreSQL never indexes the referencing (child) columns
315+ # and offers no server setting to make it (see #1512), so DataJoint must.
316+ # Whether that index is *redundant* (the columns are already a left-prefix of
317+ # the primary key or of another declared index) can only be decided once the
318+ # whole definition is parsed, so here we merely record the candidate; the
319+ # coverage decision happens post-parse in `prepare_declare`.
316320 fk_attrs = list (ref .primary_key )
317321 if is_unique :
318322 index_cols = ", " .join (adapter .quote_identifier (attr ) for attr in fk_attrs )
319323 index_sql .append (f"UNIQUE INDEX ({ index_cols } )" )
320- elif adapter .backend == "postgresql" :
321- covered_by_pk = primary_key is not None and list (primary_key [: len (fk_attrs )]) == fk_attrs
322- if not covered_by_pk :
323- index_cols = ", " .join (adapter .quote_identifier (attr ) for attr in fk_attrs )
324- index_sql .append (f"INDEX ({ index_cols } )" )
324+ elif adapter .backend == "postgresql" and fk_index_candidates is not None :
325+ fk_index_candidates .append (fk_attrs )
325326
326327
327328def prepare_declare (
@@ -368,6 +369,7 @@ def prepare_declare(
368369 external_stores = []
369370 fk_attribute_map = {} # child_attr -> (parent_table, parent_attr)
370371 column_comments = {} # column_name -> comment (for PostgreSQL COMMENT ON)
372+ fk_index_candidates = [] # PostgreSQL: FK column-lists that may need a support index (#1512)
371373
372374 for line in definition :
373375 if not line or line .startswith ("#" ): # ignore additional comments
@@ -385,6 +387,7 @@ def prepare_declare(
385387 index_sql ,
386388 adapter ,
387389 fk_attribute_map ,
390+ fk_index_candidates ,
388391 )
389392 elif re .match (r"^(unique\s+)?index\s*\(.*\)\s*(#.*)?$" , line , re .I ): # index
390393 compile_index (re .sub (r"\s*#.*$" , "" , line ), index_sql , adapter )
@@ -400,6 +403,33 @@ def prepare_declare(
400403 if comment :
401404 column_comments [name ] = comment
402405
406+ # Foreign-key support indexes (PostgreSQL; #1512). Now that the whole
407+ # definition is parsed, emit an index on each candidate foreign key's columns
408+ # UNLESS they are already a left-prefix of the primary key, of a declared
409+ # index, or of a longer FK-support index already emitted — in which case that
410+ # existing index already serves foreign-key lookups and cascades. Candidates
411+ # are only collected on PostgreSQL (MySQL/InnoDB indexes FKs implicitly), so
412+ # this loop is a no-op elsewhere.
413+ if fk_index_candidates :
414+
415+ def _index_columns (index_def : str ) -> list [str ]:
416+ match = re .match (r"(?:unique\s+)?index\s*\(([^)]+)\)" , index_def , re .I )
417+ return [col .strip ().strip ('`"' ) for col in match .group (1 ).split ("," )] if match else []
418+
419+ # Existing prefixes an FK index could be redundant against: the primary
420+ # key and every already-declared index. Grows as we accept FK indexes.
421+ existing_prefixes = [list (primary_key )] + [_index_columns (s ) for s in index_sql ]
422+
423+ def _covered (candidate : list [str ]) -> bool :
424+ return any (prefix [: len (candidate )] == candidate for prefix in existing_prefixes )
425+
426+ # Longest first, so a shorter FK index left-covered by a longer one is skipped.
427+ for candidate in sorted (fk_index_candidates , key = len , reverse = True ):
428+ if not _covered (candidate ):
429+ index_cols = ", " .join (adapter .quote_identifier (attr ) for attr in candidate )
430+ index_sql .append (f"INDEX ({ index_cols } )" )
431+ existing_prefixes .append (candidate )
432+
403433 return (
404434 table_comment ,
405435 primary_key ,
0 commit comments