Skip to content

Commit dca8052

Browse files
refactor: move dependency graph queries into adapter methods
Extract the backend-specific primary key and foreign key SQL from dependencies.py into load_primary_keys_sql() and load_foreign_keys_sql() adapter methods, eliminating the if/else backend fork. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4796d39 commit dca8052

File tree

4 files changed

+126
-84
lines changed

4 files changed

+126
-84
lines changed

src/datajoint/adapters/base.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,55 @@ def get_foreign_keys_sql(self, schema_name: str, table_name: str) -> str:
727727
"""
728728
...
729729

730+
@abstractmethod
731+
def load_primary_keys_sql(self, schemas_list: str, like_pattern: str) -> str:
732+
"""
733+
Generate query to load primary key columns for all tables across schemas.
734+
735+
Used by the dependency graph to build the schema graph.
736+
737+
Parameters
738+
----------
739+
schemas_list : str
740+
Comma-separated, quoted schema names for an IN clause.
741+
like_pattern : str
742+
SQL LIKE pattern to exclude (e.g., "'~%%'" for internal tables).
743+
744+
Returns
745+
-------
746+
str
747+
SQL query returning rows with columns:
748+
- tab: fully qualified table name (quoted)
749+
- column_name: primary key column name
750+
"""
751+
...
752+
753+
@abstractmethod
754+
def load_foreign_keys_sql(self, schemas_list: str, like_pattern: str) -> str:
755+
"""
756+
Generate query to load foreign key relationships across schemas.
757+
758+
Used by the dependency graph to build the schema graph.
759+
760+
Parameters
761+
----------
762+
schemas_list : str
763+
Comma-separated, quoted schema names for an IN clause.
764+
like_pattern : str
765+
SQL LIKE pattern to exclude (e.g., "'~%%'" for internal tables).
766+
767+
Returns
768+
-------
769+
str
770+
SQL query returning rows (as dicts) with columns:
771+
- constraint_name: FK constraint name
772+
- referencing_table: fully qualified child table name (quoted)
773+
- referenced_table: fully qualified parent table name (quoted)
774+
- column_name: FK column in child table
775+
- referenced_column_name: referenced column in parent table
776+
"""
777+
...
778+
730779
@abstractmethod
731780
def get_constraint_info_sql(self, constraint_name: str, schema_name: str, table_name: str) -> str:
732781
"""

src/datajoint/adapters/mysql.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,32 @@ def get_foreign_keys_sql(self, schema_name: str, table_name: str) -> str:
656656
f"ORDER BY constraint_name, ordinal_position"
657657
)
658658

659+
def load_primary_keys_sql(self, schemas_list: str, like_pattern: str) -> str:
660+
"""Query to load all primary key columns across schemas."""
661+
tab_expr = "concat('`', table_schema, '`.`', table_name, '`')"
662+
return (
663+
f"SELECT {tab_expr} as tab, column_name "
664+
f"FROM information_schema.key_column_usage "
665+
f"WHERE table_name NOT LIKE {like_pattern} "
666+
f"AND table_schema in ({schemas_list}) "
667+
f"AND constraint_name='PRIMARY'"
668+
)
669+
670+
def load_foreign_keys_sql(self, schemas_list: str, like_pattern: str) -> str:
671+
"""Query to load all foreign key relationships across schemas."""
672+
tab_expr = "concat('`', table_schema, '`.`', table_name, '`')"
673+
ref_tab_expr = "concat('`', referenced_table_schema, '`.`', referenced_table_name, '`')"
674+
return (
675+
f"SELECT constraint_name, "
676+
f"{tab_expr} as referencing_table, "
677+
f"{ref_tab_expr} as referenced_table, "
678+
f"column_name, referenced_column_name "
679+
f"FROM information_schema.key_column_usage "
680+
f"WHERE referenced_table_name NOT LIKE {like_pattern} "
681+
f"AND (referenced_table_schema in ({schemas_list}) "
682+
f"OR referenced_table_schema is not NULL AND table_schema in ({schemas_list}))"
683+
)
684+
659685
def get_constraint_info_sql(self, constraint_name: str, schema_name: str, table_name: str) -> str:
660686
"""Query to get FK constraint details from information_schema."""
661687
return (

src/datajoint/adapters/postgres.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,44 @@ def get_foreign_keys_sql(self, schema_name: str, table_name: str) -> str:
799799
f"ORDER BY kcu.constraint_name, kcu.ordinal_position"
800800
)
801801

802+
def load_primary_keys_sql(self, schemas_list: str, like_pattern: str) -> str:
803+
"""Query to load all primary key columns across schemas."""
804+
tab_expr = "'\"' || kcu.table_schema || '\".\"' || kcu.table_name || '\"'"
805+
return (
806+
f"SELECT {tab_expr} as tab, kcu.column_name "
807+
f"FROM information_schema.key_column_usage kcu "
808+
f"JOIN information_schema.table_constraints tc "
809+
f"ON kcu.constraint_name = tc.constraint_name "
810+
f"AND kcu.table_schema = tc.table_schema "
811+
f"WHERE kcu.table_name NOT LIKE {like_pattern} "
812+
f"AND kcu.table_schema in ({schemas_list}) "
813+
f"AND tc.constraint_type = 'PRIMARY KEY'"
814+
)
815+
816+
def load_foreign_keys_sql(self, schemas_list: str, like_pattern: str) -> str:
817+
"""Query to load all foreign key relationships across schemas."""
818+
return (
819+
f"SELECT "
820+
f"c.conname as constraint_name, "
821+
f"'\"' || ns1.nspname || '\".\"' || cl1.relname || '\"' as referencing_table, "
822+
f"'\"' || ns2.nspname || '\".\"' || cl2.relname || '\"' as referenced_table, "
823+
f"a1.attname as column_name, "
824+
f"a2.attname as referenced_column_name "
825+
f"FROM pg_constraint c "
826+
f"JOIN pg_class cl1 ON c.conrelid = cl1.oid "
827+
f"JOIN pg_namespace ns1 ON cl1.relnamespace = ns1.oid "
828+
f"JOIN pg_class cl2 ON c.confrelid = cl2.oid "
829+
f"JOIN pg_namespace ns2 ON cl2.relnamespace = ns2.oid "
830+
f"CROSS JOIN LATERAL unnest(c.conkey, c.confkey) WITH ORDINALITY AS cols(conkey, confkey, ord) "
831+
f"JOIN pg_attribute a1 ON a1.attrelid = cl1.oid AND a1.attnum = cols.conkey "
832+
f"JOIN pg_attribute a2 ON a2.attrelid = cl2.oid AND a2.attnum = cols.confkey "
833+
f"WHERE c.contype = 'f' "
834+
f"AND cl1.relname NOT LIKE {like_pattern} "
835+
f"AND (ns2.nspname in ({schemas_list}) "
836+
f"OR ns1.nspname in ({schemas_list})) "
837+
f"ORDER BY c.conname, cols.ord"
838+
)
839+
802840
def get_constraint_info_sql(self, constraint_name: str, schema_name: str, table_name: str) -> str:
803841
"""
804842
Query to get FK constraint details from information_schema.

src/datajoint/dependencies.py

Lines changed: 13 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -164,92 +164,21 @@ def load(self, force: bool = True) -> None:
164164
# Build schema list for IN clause
165165
schemas_list = ", ".join(adapter.quote_string(s) for s in self._conn.schemas)
166166

167-
# Backend-specific queries for primary keys and foreign keys
168-
# Note: Both PyMySQL and psycopg2 use %s placeholders, so escape % as %%
167+
# Load primary keys and foreign keys via adapter methods
168+
# Note: Both PyMySQL and psycopg use %s placeholders, so escape % as %%
169169
like_pattern = "'~%%'"
170170

171-
if adapter.backend == "mysql":
172-
# MySQL: use concat() and MySQL-specific information_schema columns
173-
tab_expr = "concat('`', table_schema, '`.`', table_name, '`')"
174-
175-
# load primary key info (MySQL uses constraint_name='PRIMARY')
176-
keys = self._conn.query(
177-
f"""
178-
SELECT {tab_expr} as tab, column_name
179-
FROM information_schema.key_column_usage
180-
WHERE table_name NOT LIKE {like_pattern}
181-
AND table_schema in ({schemas_list})
182-
AND constraint_name='PRIMARY'
183-
"""
184-
)
185-
pks = defaultdict(set)
186-
for key in keys:
187-
pks[key[0]].add(key[1])
188-
189-
# load foreign keys (MySQL has referenced_* columns)
190-
ref_tab_expr = "concat('`', referenced_table_schema, '`.`', referenced_table_name, '`')"
191-
fk_keys = self._conn.query(
192-
f"""
193-
SELECT constraint_name,
194-
{tab_expr} as referencing_table,
195-
{ref_tab_expr} as referenced_table,
196-
column_name, referenced_column_name
197-
FROM information_schema.key_column_usage
198-
WHERE referenced_table_name NOT LIKE {like_pattern}
199-
AND (referenced_table_schema in ({schemas_list})
200-
OR referenced_table_schema is not NULL AND table_schema in ({schemas_list}))
201-
""",
202-
as_dict=True,
203-
)
204-
else:
205-
# PostgreSQL: use || concatenation and different query structure
206-
tab_expr = "'\"' || kcu.table_schema || '\".\"' || kcu.table_name || '\"'"
207-
208-
# load primary key info (PostgreSQL uses constraint_type='PRIMARY KEY')
209-
keys = self._conn.query(
210-
f"""
211-
SELECT {tab_expr} as tab, kcu.column_name
212-
FROM information_schema.key_column_usage kcu
213-
JOIN information_schema.table_constraints tc
214-
ON kcu.constraint_name = tc.constraint_name
215-
AND kcu.table_schema = tc.table_schema
216-
WHERE kcu.table_name NOT LIKE {like_pattern}
217-
AND kcu.table_schema in ({schemas_list})
218-
AND tc.constraint_type = 'PRIMARY KEY'
219-
"""
220-
)
221-
pks = defaultdict(set)
222-
for key in keys:
223-
pks[key[0]].add(key[1])
224-
225-
# load foreign keys using pg_constraint system catalogs
226-
# The information_schema approach creates a Cartesian product for composite FKs
227-
# because constraint_column_usage doesn't have ordinal_position.
228-
# Using pg_constraint with unnest(conkey, confkey) WITH ORDINALITY gives correct mapping.
229-
fk_keys = self._conn.query(
230-
f"""
231-
SELECT
232-
c.conname as constraint_name,
233-
'"' || ns1.nspname || '"."' || cl1.relname || '"' as referencing_table,
234-
'"' || ns2.nspname || '"."' || cl2.relname || '"' as referenced_table,
235-
a1.attname as column_name,
236-
a2.attname as referenced_column_name
237-
FROM pg_constraint c
238-
JOIN pg_class cl1 ON c.conrelid = cl1.oid
239-
JOIN pg_namespace ns1 ON cl1.relnamespace = ns1.oid
240-
JOIN pg_class cl2 ON c.confrelid = cl2.oid
241-
JOIN pg_namespace ns2 ON cl2.relnamespace = ns2.oid
242-
CROSS JOIN LATERAL unnest(c.conkey, c.confkey) WITH ORDINALITY AS cols(conkey, confkey, ord)
243-
JOIN pg_attribute a1 ON a1.attrelid = cl1.oid AND a1.attnum = cols.conkey
244-
JOIN pg_attribute a2 ON a2.attrelid = cl2.oid AND a2.attnum = cols.confkey
245-
WHERE c.contype = 'f'
246-
AND cl1.relname NOT LIKE {like_pattern}
247-
AND (ns2.nspname in ({schemas_list})
248-
OR ns1.nspname in ({schemas_list}))
249-
ORDER BY c.conname, cols.ord
250-
""",
251-
as_dict=True,
252-
)
171+
# load primary key info
172+
keys = self._conn.query(adapter.load_primary_keys_sql(schemas_list, like_pattern))
173+
pks = defaultdict(set)
174+
for key in keys:
175+
pks[key[0]].add(key[1])
176+
177+
# load foreign keys
178+
fk_keys = self._conn.query(
179+
adapter.load_foreign_keys_sql(schemas_list, like_pattern),
180+
as_dict=True,
181+
)
253182

254183
# add nodes to the graph
255184
for n, pk in pks.items():

0 commit comments

Comments
 (0)