Skip to content

Commit 4e4c788

Browse files
Merge pull request #1439 from datajoint/fix/simplify-indexes-and-warn-mariadb
refactor: simplify index query, warn on MariaDB connections
2 parents dde6471 + 7f8af35 commit 4e4c788

6 files changed

Lines changed: 67 additions & 43 deletions

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export DOCKER_HOST=unix://$HOME/.docker/run/docker.sock
5656

5757
### PostgreSQL Backend
5858

59-
DataJoint supports PostgreSQL 15+ as an alternative to MySQL 8+. To install the PostgreSQL driver:
59+
DataJoint supports MySQL 8.0.13+ and PostgreSQL 15+ as production database backends. To install the PostgreSQL driver:
6060

6161
```bash
6262
pip install -e ".[postgres]" # Installs psycopg2-binary

src/datajoint/adapters/mysql.py

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ def create_table_sql(
423423
fk_cols = ", ".join(self.quote_identifier(col) for col in fk["columns"])
424424
ref_cols = ", ".join(self.quote_identifier(col) for col in fk["ref_columns"])
425425
lines.append(
426-
f"FOREIGN KEY ({fk_cols}) REFERENCES {fk['ref_table']} ({ref_cols}) " f"ON UPDATE CASCADE ON DELETE RESTRICT"
426+
f"FOREIGN KEY ({fk_cols}) REFERENCES {fk['ref_table']} ({ref_cols}) ON UPDATE CASCADE ON DELETE RESTRICT"
427427
)
428428

429429
# Indexes
@@ -735,26 +735,7 @@ def parse_foreign_key_error(self, error_message: str) -> dict[str, str | list[st
735735
return result
736736

737737
def get_indexes_sql(self, schema_name: str, table_name: str) -> str:
738-
"""Query to get index definitions.
739-
740-
Note: For MySQL 8.0.13+, EXPRESSION column contains the expression for
741-
functional indexes. COLUMN_NAME is NULL for such indexes.
742-
On MySQL < 8.0.13 and MariaDB, the EXPRESSION column does not exist;
743-
heading.py falls back to get_indexes_sql_fallback() in that case.
744-
"""
745-
return (
746-
f"SELECT INDEX_NAME as index_name, "
747-
f"COALESCE(COLUMN_NAME, CONCAT('(', EXPRESSION, ')')) as column_name, "
748-
f"NON_UNIQUE as non_unique, SEQ_IN_INDEX as seq_in_index "
749-
f"FROM information_schema.statistics "
750-
f"WHERE table_schema = {self.quote_string(schema_name)} "
751-
f"AND table_name = {self.quote_string(table_name)} "
752-
f"AND index_name != 'PRIMARY' "
753-
f"ORDER BY index_name, seq_in_index"
754-
)
755-
756-
def get_indexes_sql_fallback(self, schema_name: str, table_name: str) -> str:
757-
"""Fallback index query for MySQL < 8.0.13 and MariaDB (no EXPRESSION column)."""
738+
"""Query to get index definitions. Functional indexes (NULL COLUMN_NAME) are skipped downstream."""
758739
return (
759740
f"SELECT INDEX_NAME as index_name, "
760741
f"COLUMN_NAME as column_name, "

src/datajoint/connection.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,18 @@ def conn(
108108
return conn.connection
109109

110110

111+
def _warn_if_mariadb(version_str: str) -> None:
112+
"""Emit a UserWarning if `version_str` looks like MariaDB. No-op for MySQL."""
113+
if "MariaDB" in version_str:
114+
warnings.warn(
115+
f"MariaDB is not officially supported by DataJoint 2.x "
116+
f"(server reports {version_str}). Compatibility is best-effort "
117+
f"and may break in future releases.",
118+
UserWarning,
119+
stacklevel=3,
120+
)
121+
122+
111123
class EmulatedCursor:
112124
"""acts like a cursor"""
113125

@@ -221,6 +233,8 @@ def __init__(
221233
f"{self.conn_info['user']}@{self.conn_info['host']}:{self.conn_info['port']}{db_str}"
222234
)
223235
self.connection_id = self.adapter.get_connection_id(self._conn)
236+
if self.adapter.backend == "mysql":
237+
_warn_if_mariadb(self.query("SELECT @@version").fetchone()[0])
224238
else:
225239
raise errors.LostConnectionError(
226240
f"Connection failed {self.conn_info['user']}@{self.conn_info['host']}:{self.conn_info['port']}"

src/datajoint/heading.py

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -551,30 +551,13 @@ def _init_from_database(self) -> None:
551551

552552
# Read and tabulate secondary indexes
553553
keys = defaultdict(dict)
554-
try:
555-
index_rows = conn.query(
556-
adapter.get_indexes_sql(database, table_name),
557-
as_dict=True,
558-
)
559-
except Exception:
560-
# Fall back for MySQL < 8.0.13 / MariaDB (no EXPRESSION column)
561-
index_rows = (
562-
conn.query(
563-
adapter.get_indexes_sql_fallback(database, table_name),
564-
as_dict=True,
565-
)
566-
if hasattr(adapter, "get_indexes_sql_fallback")
567-
else []
568-
)
569-
for item in index_rows:
570-
# Note: adapter.get_indexes_sql() already filters out PRIMARY key
571-
# MySQL/PostgreSQL adapters return: index_name, column_name, non_unique
554+
for item in conn.query(
555+
adapter.get_indexes_sql(database, table_name),
556+
as_dict=True,
557+
):
572558
index_name = item.get("index_name") or item.get("Key_name")
573559
seq = item.get("seq_in_index") or item.get("Seq_in_index") or len(keys[index_name]) + 1
574560
column = item.get("column_name") or item.get("Column_name")
575-
# MySQL EXPRESSION column stores escaped single quotes - unescape them
576-
if column:
577-
column = column.replace("\\'", "'")
578561
non_unique = item.get("non_unique") or item.get("Non_unique")
579562
nullable = item.get("nullable") or (item.get("Null", "NO").lower() == "yes")
580563

tests/integration/test_json.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ def test_insert_update(schema_json):
119119
assert not q
120120

121121

122+
@pytest.mark.skip(
123+
reason="Functional indexes are not currently round-tripped through Heading.indexes; "
124+
"describe() drops them. Re-enable when functional-index introspection is restored."
125+
)
122126
def test_describe(schema_json):
123127
rel = Team()
124128
context = inspect.currentframe().f_globals
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Unit tests for the MariaDB compatibility warning emitted at connect time."""
2+
3+
import warnings
4+
5+
import pytest
6+
7+
from datajoint.connection import _warn_if_mariadb
8+
9+
10+
@pytest.mark.parametrize(
11+
"version_str",
12+
[
13+
"10.11.5-MariaDB",
14+
"10.5.5-MariaDB-1~bionic",
15+
"5.5.68-MariaDB",
16+
],
17+
)
18+
def test_warn_on_mariadb(version_str):
19+
with warnings.catch_warnings(record=True) as caught:
20+
warnings.simplefilter("always")
21+
_warn_if_mariadb(version_str)
22+
assert len(caught) == 1
23+
assert issubclass(caught[0].category, UserWarning)
24+
assert "MariaDB is not officially supported" in str(caught[0].message)
25+
assert version_str in str(caught[0].message)
26+
27+
28+
@pytest.mark.parametrize(
29+
"version_str",
30+
[
31+
"8.0.40",
32+
"8.0.13",
33+
"8.0.40-0ubuntu0.22.04.1",
34+
"8.4.2-log",
35+
"9.0.0",
36+
],
37+
)
38+
def test_no_warn_on_mysql(version_str):
39+
with warnings.catch_warnings(record=True) as caught:
40+
warnings.simplefilter("always")
41+
_warn_if_mariadb(version_str)
42+
assert caught == []

0 commit comments

Comments
 (0)