Skip to content

Commit 0e17805

Browse files
committed
BUG32947160: Remove MySQLdb module dependency from Django backend
MySQL Connector/Python extends the Django built-in MySQL backend, which imports MySQLdb in base.py and introspection.py modules, and if it's not installed an exception is raised mentioning that the module is missing. This patch removes the need of using the built-ini MySQL base classes in base.py and introspection.py, by implementing the missing methods.
1 parent 960901b commit 0e17805

File tree

3 files changed

+125
-7
lines changed

3 files changed

+125
-7
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ v8.0.26
1515
- WL#14542: Deprecate TLS 1.0 and 1.1
1616
- WL#14440: Support for authentication_kerberos_client authentication plugin
1717
- WL#14237: Support query attributes
18+
- BUG#32947160: Remove MySQLdb module dependency from Django backend
1819
- BUG#32789076: MSI is missing required libraries to use auth_ldap_client plugin
1920
- BUG#32778827: Raise an error if the _id is different when replacing a document
2021
- BUG#32740486: Fix typo in docstring

lib/mysql/connector/django/base.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
from django.core.exceptions import ImproperlyConfigured
4949
from django.db import IntegrityError
5050
from django.db.backends.base.base import BaseDatabaseWrapper
51-
from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper
5251
from django.db import utils
5352
from django.utils.functional import cached_property
5453
from django.utils import dateparse, timezone
@@ -175,7 +174,7 @@ def __iter__(self):
175174
return iter(self.cursor)
176175

177176

178-
class DatabaseWrapper(MySQLDatabaseWrapper):
177+
class DatabaseWrapper(BaseDatabaseWrapper):
179178
vendor = 'mysql'
180179
# This dictionary maps Field objects to their associated MySQL column
181180
# types, as strings. Column-type strings can contain format strings; they'll
@@ -456,6 +455,30 @@ def data_type_check_constraints(self):
456455
return check_constraints
457456
return {}
458457

458+
@cached_property
459+
def mysql_server_data(self):
460+
with self.temporary_connection() as cursor:
461+
# Select some server variables and test if the time zone
462+
# definitions are installed. CONVERT_TZ returns NULL if 'UTC'
463+
# timezone isn't loaded into the mysql.time_zone table.
464+
cursor.execute("""
465+
SELECT VERSION(),
466+
@@sql_mode,
467+
@@default_storage_engine,
468+
@@sql_auto_is_null,
469+
@@lower_case_table_names,
470+
CONVERT_TZ('2001-01-01 01:00:00', 'UTC', 'UTC') IS NOT NULL
471+
""")
472+
row = cursor.fetchone()
473+
return {
474+
'version': row[0],
475+
'sql_mode': row[1],
476+
'default_storage_engine': row[2],
477+
'sql_auto_is_null': bool(row[3]),
478+
'lower_case_table_names': bool(row[4]),
479+
'has_zoneinfo_database': bool(row[5]),
480+
}
481+
459482
@cached_property
460483
def mysql_server_info(self):
461484
with self.temporary_connection() as cursor:
@@ -479,7 +502,6 @@ def sql_mode(self):
479502
@property
480503
def use_pure(self):
481504
return self._use_pure
482-
# return not HAVE_CEXT or self._use_pure
483505

484506

485507
class DjangoMySQLConverter(MySQLConverter):

lib/mysql/connector/django/introspection.py

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,6 @@
3636
from django.db.backends.base.introspection import (
3737
BaseDatabaseIntrospection, FieldInfo as BaseFieldInfo, TableInfo,
3838
)
39-
from django.db.backends.mysql.introspection import (
40-
DatabaseIntrospection as MySQLDatabaseIntrospection,
41-
)
4239
from django.db.models import Index
4340
from django.utils.datastructures import OrderedSet
4441

@@ -60,7 +57,57 @@
6057
)
6158

6259

63-
class DatabaseIntrospection(MySQLDatabaseIntrospection):
60+
class DatabaseIntrospection(BaseDatabaseIntrospection):
61+
62+
data_types_reverse = {
63+
FieldType.BLOB: 'TextField',
64+
FieldType.DECIMAL: 'DecimalField',
65+
FieldType.NEWDECIMAL: 'DecimalField',
66+
FieldType.DATE: 'DateField',
67+
FieldType.DATETIME: 'DateTimeField',
68+
FieldType.DOUBLE: 'FloatField',
69+
FieldType.FLOAT: 'FloatField',
70+
FieldType.INT24: 'IntegerField',
71+
FieldType.LONG: 'IntegerField',
72+
FieldType.LONGLONG: 'BigIntegerField',
73+
FieldType.SHORT: 'SmallIntegerField',
74+
FieldType.STRING: 'CharField',
75+
FieldType.TIME: 'TimeField',
76+
FieldType.TIMESTAMP: 'DateTimeField',
77+
FieldType.TINY: 'IntegerField',
78+
FieldType.TINY_BLOB: 'TextField',
79+
FieldType.MEDIUM_BLOB: 'TextField',
80+
FieldType.LONG_BLOB: 'TextField',
81+
FieldType.VAR_STRING: 'CharField',
82+
}
83+
84+
def get_field_type(self, data_type, description):
85+
field_type = super().get_field_type(data_type, description)
86+
if 'auto_increment' in description.extra:
87+
if field_type == 'IntegerField':
88+
return 'AutoField'
89+
elif field_type == 'BigIntegerField':
90+
return 'BigAutoField'
91+
elif field_type == 'SmallIntegerField':
92+
return 'SmallAutoField'
93+
if description.is_unsigned:
94+
if field_type == 'BigIntegerField':
95+
return 'PositiveBigIntegerField'
96+
elif field_type == 'IntegerField':
97+
return 'PositiveIntegerField'
98+
elif field_type == 'SmallIntegerField':
99+
return 'PositiveSmallIntegerField'
100+
# JSON data type is an alias for LONGTEXT in MariaDB, use check
101+
# constraints clauses to introspect JSONField.
102+
if description.has_json_constraint:
103+
return 'JSONField'
104+
return field_type
105+
106+
def get_table_list(self, cursor):
107+
"""Return a list of table and view names in the current database."""
108+
cursor.execute("SHOW FULL TABLES")
109+
return [TableInfo(row[0], {'BASE TABLE': 't', 'VIEW': 'v'}.get(row[1]))
110+
for row in cursor.fetchall()]
64111

65112
def get_table_description(self, cursor, table_name):
66113
"""
@@ -181,6 +228,54 @@ def get_primary_key_column(self, cursor, table_name):
181228
return column[0]
182229
return None
183230

231+
def get_sequences(self, cursor, table_name, table_fields=()):
232+
for field_info in self.get_table_description(cursor, table_name):
233+
if 'auto_increment' in field_info.extra:
234+
# MySQL allows only one auto-increment column per table.
235+
return [{'table': table_name, 'column': field_info.name}]
236+
return []
237+
238+
def get_relations(self, cursor, table_name):
239+
"""
240+
Return a dictionary of {field_name: (field_name_other_table, other_table)}
241+
representing all relationships to the given table.
242+
"""
243+
constraints = self.get_key_columns(cursor, table_name)
244+
relations = {}
245+
for my_fieldname, other_table, other_field in constraints:
246+
relations[my_fieldname] = (other_field, other_table)
247+
return relations
248+
249+
def get_key_columns(self, cursor, table_name):
250+
"""
251+
Return a list of (column_name, referenced_table_name, referenced_column_name)
252+
for all key columns in the given table.
253+
"""
254+
key_columns = []
255+
cursor.execute("""
256+
SELECT column_name, referenced_table_name, referenced_column_name
257+
FROM information_schema.key_column_usage
258+
WHERE table_name = %s
259+
AND table_schema = DATABASE()
260+
AND referenced_table_name IS NOT NULL
261+
AND referenced_column_name IS NOT NULL""", [table_name])
262+
key_columns.extend(cursor.fetchall())
263+
return key_columns
264+
265+
def get_storage_engine(self, cursor, table_name):
266+
"""
267+
Retrieve the storage engine for a given table. Return the default
268+
storage engine if the table doesn't exist.
269+
"""
270+
cursor.execute(
271+
"SELECT engine "
272+
"FROM information_schema.tables "
273+
"WHERE table_name = %s", [table_name])
274+
result = cursor.fetchone()
275+
if not result:
276+
return self.connection.features._mysql_storage_engine
277+
return result[0]
278+
184279
def get_constraints(self, cursor, table_name):
185280
"""
186281
Retrieve any constraints or keys (unique, pk, fk, check, index) across

0 commit comments

Comments
 (0)