Skip to content

Commit e49a2ef

Browse files
refactor: move get_master regex to adapter methods
Each adapter now has its own get_master_table_name() method with a backend-specific regex pattern: - MySQL: matches backtick-quoted names - PostgreSQL: matches double-quote-quoted names Updated utils.get_master() to accept optional adapter parameter. Updated table.py to pass adapter to get_master() calls. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b1ef634 commit e49a2ef

File tree

5 files changed

+49
-8
lines changed

5 files changed

+49
-8
lines changed

src/datajoint/adapters/base.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,24 @@ def quote_string(self, value: str) -> str:
186186
"""
187187
...
188188

189+
@abstractmethod
190+
def get_master_table_name(self, part_table: str) -> str | None:
191+
"""
192+
Extract master table name from a part table name.
193+
194+
Parameters
195+
----------
196+
part_table : str
197+
Full table name (e.g., `schema`.`master__part` for MySQL,
198+
"schema"."master__part" for PostgreSQL).
199+
200+
Returns
201+
-------
202+
str or None
203+
Master table name if part_table is a part table, None otherwise.
204+
"""
205+
...
206+
189207
@property
190208
@abstractmethod
191209
def parameter_placeholder(self) -> str:

src/datajoint/adapters/mysql.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,13 @@ def quote_string(self, value: str) -> str:
221221
escaped = client.converters.escape_string(value)
222222
return f"'{escaped}'"
223223

224+
def get_master_table_name(self, part_table: str) -> str | None:
225+
"""Extract master table name from part table (MySQL backtick format)."""
226+
import re
227+
# MySQL format: `schema`.`master__part`
228+
match = re.match(r"(?P<master>`\w+`.`#?\w+)__\w+`", part_table)
229+
return match["master"] + "`" if match else None
230+
224231
@property
225232
def parameter_placeholder(self) -> str:
226233
"""MySQL/pymysql uses %s placeholders."""

src/datajoint/adapters/postgres.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,13 @@ def quote_string(self, value: str) -> str:
238238
escaped = value.replace("'", "''")
239239
return f"'{escaped}'"
240240

241+
def get_master_table_name(self, part_table: str) -> str | None:
242+
"""Extract master table name from part table (PostgreSQL double-quote format)."""
243+
import re
244+
# PostgreSQL format: "schema"."master__part"
245+
match = re.match(r'(?P<master>"\w+"."#?\w+)__\w+"', part_table)
246+
return match["master"] + '"' if match else None
247+
241248
@property
242249
def parameter_placeholder(self) -> str:
243250
"""PostgreSQL/psycopg2 uses %s placeholders."""

src/datajoint/table.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -953,7 +953,7 @@ def strip_quotes(s):
953953
else:
954954
child &= table.proj()
955955

956-
master_name = get_master(child.full_table_name)
956+
master_name = get_master(child.full_table_name, table.connection.adapter)
957957
if (
958958
part_integrity == "cascade"
959959
and master_name
@@ -1009,7 +1009,7 @@ def strip_quotes(s):
10091009
if part_integrity == "enforce":
10101010
# Avoid deleting from part before master (See issue #151)
10111011
for part in deleted:
1012-
master = get_master(part)
1012+
master = get_master(part, self.connection.adapter)
10131013
if master and master not in deleted:
10141014
if transaction:
10151015
self.connection.cancel_transaction()
@@ -1100,7 +1100,7 @@ def drop(self, prompt: bool | None = None):
11001100

11011101
# avoid dropping part tables without their masters: See issue #374
11021102
for part in tables:
1103-
master = get_master(part)
1103+
master = get_master(part, self.connection.adapter)
11041104
if master and master not in tables:
11051105
raise DataJointError(
11061106
"Attempt to drop part table {part} before dropping its master. Drop {master} first.".format(

src/datajoint/utils.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,32 @@ def user_choice(prompt, choices=("yes", "no"), default=None):
2525
return response
2626

2727

28-
def get_master(full_table_name: str) -> str:
28+
def get_master(full_table_name: str, adapter=None) -> str:
2929
"""
3030
If the table name is that of a part table, then return what the master table name would be.
3131
This follows DataJoint's table naming convention where a master and a part must be in the
3232
same schema and the part table is prefixed with the master table name + ``__``.
3333
3434
Example:
35-
`ephys`.`session` -- master
36-
`ephys`.`session__recording` -- part
35+
`ephys`.`session` -- master (MySQL)
36+
`ephys`.`session__recording` -- part (MySQL)
37+
"ephys"."session__recording" -- part (PostgreSQL)
3738
3839
:param full_table_name: Full table name including part.
3940
:type full_table_name: str
41+
:param adapter: Optional database adapter for backend-specific parsing.
4042
:return: Supposed master full table name or empty string if not a part table name.
4143
:rtype: str
4244
"""
43-
match = re.match(r"(?P<master>`\w+`.`\w+)__(?P<part>\w+)`", full_table_name)
44-
return match["master"] + "`" if match else ""
45+
if adapter is not None:
46+
result = adapter.get_master_table_name(full_table_name)
47+
return result if result else ""
48+
49+
# Fallback: handle both MySQL backticks and PostgreSQL double quotes
50+
match = re.match(r'(?P<master>(?P<q>[`"])[\w]+(?P=q)\.(?P=q)[\w]+)__[\w]+(?P=q)', full_table_name)
51+
if match:
52+
return match["master"] + match["q"]
53+
return ""
4554

4655

4756
def is_camel_case(s):

0 commit comments

Comments
 (0)