Skip to content

Commit 407ac56

Browse files
committed
BUG32435181: Add support for Django 3.2
This patch adds support for Django 3.2, maintaining backward compatibility with Django 2.2, 3.0 and 3.1. Tests got adjusted for regression.
1 parent 791850c commit 407ac56

File tree

5 files changed

+148
-248
lines changed

5 files changed

+148
-248
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ v8.0.24
1515
- WL#14239: Remove Python 2.7 support
1616
- WL#14212: Support connection close notification
1717
- WL#14027: Add support for Python 3.9
18+
- BUG#32435181: Add support for Django 3.2
1819
- BUG#32029891: Add context manager support for pooled connections
1920

2021
v8.0.23

lib/mysql/connector/django/base.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2020, Oracle and/or its affiliates.
1+
# Copyright (c) 2020, 2021, Oracle and/or its affiliates.
22
#
33
# This program is free software; you can redistribute it and/or modify
44
# it under the terms of the GNU General Public License, version 2.0, as
@@ -48,6 +48,7 @@
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
5152
from django.db import utils
5253
from django.utils.functional import cached_property
5354
from django.utils import dateparse, timezone
@@ -174,7 +175,7 @@ def __iter__(self):
174175
return iter(self.cursor)
175176

176177

177-
class DatabaseWrapper(BaseDatabaseWrapper):
178+
class DatabaseWrapper(MySQLDatabaseWrapper):
178179
vendor = 'mysql'
179180
# This dictionary maps Field objects to their associated MySQL column
180181
# types, as strings. Column-type strings can contain format strings; they'll

lib/mysql/connector/django/introspection.py

Lines changed: 105 additions & 173 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2020, Oracle and/or its affiliates.
1+
# Copyright (c) 2020, 2021, Oracle and/or its affiliates.
22
#
33
# This program is free software; you can redistribute it and/or modify
44
# it under the terms of the GNU General Public License, version 2.0, as
@@ -32,101 +32,84 @@
3232

3333
from mysql.connector.constants import FieldType
3434

35+
from django import VERSION as DJANGO_VERSION
3536
from django.db.backends.base.introspection import (
3637
BaseDatabaseIntrospection, FieldInfo as BaseFieldInfo, TableInfo,
3738
)
39+
from django.db.backends.mysql.introspection import (
40+
DatabaseIntrospection as MySQLDatabaseIntrospection,
41+
)
3842
from django.db.models import Index
3943
from django.utils.datastructures import OrderedSet
4044

41-
FieldInfo = namedtuple('FieldInfo', BaseFieldInfo._fields + ('extra', 'is_unsigned', 'has_json_constraint'))
42-
InfoLine = namedtuple('InfoLine', 'col_name data_type max_len num_prec num_scale extra column_default is_unsigned')
43-
44-
45-
class DatabaseIntrospection(BaseDatabaseIntrospection):
46-
data_types_reverse = {
47-
FieldType.BLOB: 'TextField',
48-
FieldType.DECIMAL: 'DecimalField',
49-
FieldType.NEWDECIMAL: 'DecimalField',
50-
FieldType.DATE: 'DateField',
51-
FieldType.DATETIME: 'DateTimeField',
52-
FieldType.DOUBLE: 'FloatField',
53-
FieldType.FLOAT: 'FloatField',
54-
FieldType.INT24: 'IntegerField',
55-
FieldType.JSON: 'JSONField',
56-
FieldType.LONG: 'IntegerField',
57-
FieldType.LONGLONG: 'BigIntegerField',
58-
FieldType.SHORT: 'SmallIntegerField',
59-
FieldType.STRING: 'CharField',
60-
FieldType.TIME: 'TimeField',
61-
FieldType.TIMESTAMP: 'DateTimeField',
62-
FieldType.TINY: 'IntegerField',
63-
FieldType.TINY_BLOB: 'TextField',
64-
FieldType.MEDIUM_BLOB: 'TextField',
65-
FieldType.LONG_BLOB: 'TextField',
66-
FieldType.VAR_STRING: 'CharField',
67-
}
45+
FieldInfo = namedtuple(
46+
'FieldInfo',
47+
BaseFieldInfo._fields + ('extra', 'is_unsigned', 'has_json_constraint')
48+
)
49+
if DJANGO_VERSION < (3, 2, 0):
50+
InfoLine = namedtuple(
51+
'InfoLine',
52+
'col_name data_type max_len num_prec num_scale extra column_default '
53+
'is_unsigned'
54+
)
55+
else:
56+
InfoLine = namedtuple(
57+
'InfoLine',
58+
'col_name data_type max_len num_prec num_scale extra column_default '
59+
'collation is_unsigned'
60+
)
6861

69-
def get_field_type(self, data_type, description):
70-
field_type = super().get_field_type(data_type, description)
71-
if 'auto_increment' in description.extra:
72-
if field_type == 'IntegerField':
73-
return 'AutoField'
74-
elif field_type == 'BigIntegerField':
75-
return 'BigAutoField'
76-
elif field_type == 'SmallIntegerField':
77-
return 'SmallAutoField'
78-
if description.is_unsigned:
79-
if field_type == 'BigIntegerField':
80-
return 'PositiveBigIntegerField'
81-
elif field_type == 'IntegerField':
82-
return 'PositiveIntegerField'
83-
elif field_type == 'SmallIntegerField':
84-
return 'PositiveSmallIntegerField'
85-
# JSON data type is an alias for LONGTEXT in MariaDB, use check
86-
# constraints clauses to introspect JSONField.
87-
if description.has_json_constraint:
88-
return 'JSONField'
89-
return field_type
9062

91-
def get_table_list(self, cursor):
92-
"""Return a list of table and view names in the current database."""
93-
cursor.execute("SHOW FULL TABLES")
94-
return [TableInfo(row[0], {'BASE TABLE': 't', 'VIEW': 'v'}.get(row[1]))
95-
for row in cursor.fetchall()]
63+
class DatabaseIntrospection(MySQLDatabaseIntrospection):
9664

9765
def get_table_description(self, cursor, table_name):
9866
"""
9967
Return a description of the table with the DB-API cursor.description
10068
interface."
10169
"""
10270
json_constraints = {}
103-
if self.connection.mysql_is_mariadb and self.connection.features.can_introspect_json_field:
104-
# JSON data type is an alias for LONGTEXT in MariaDB, select
105-
# JSON_VALID() constraints to introspect JSONField.
106-
cursor.execute("""
107-
SELECT c.constraint_name AS column_name
108-
FROM information_schema.check_constraints AS c
109-
WHERE
110-
c.table_name = %s AND
111-
LOWER(c.check_clause) = 'json_valid(`' + LOWER(c.constraint_name) + '`)' AND
112-
c.constraint_schema = DATABASE()
113-
""", [table_name])
114-
json_constraints = {row[0] for row in cursor.fetchall()}
71+
# A default collation for the given table.
72+
cursor.execute("""
73+
SELECT table_collation
74+
FROM information_schema.tables
75+
WHERE table_schema = DATABASE()
76+
AND table_name = %s
77+
""", [table_name])
78+
row = cursor.fetchone()
79+
default_column_collation = row[0] if row else ''
11580
# information_schema database gives more accurate results for some figures:
11681
# - varchar length returned by cursor.description is an internal length,
11782
# not visible length (#5725)
11883
# - precision and scale (for decimal fields) (#5014)
11984
# - auto_increment is not available in cursor.description
120-
cursor.execute("""
121-
SELECT
122-
column_name, data_type, character_maximum_length,
123-
numeric_precision, numeric_scale, extra, column_default,
124-
CASE
125-
WHEN column_type LIKE '%% unsigned' THEN 1
126-
ELSE 0
127-
END AS is_unsigned
128-
FROM information_schema.columns
129-
WHERE table_name = %s AND table_schema = DATABASE()""", [table_name])
85+
if DJANGO_VERSION < (3, 2, 0):
86+
cursor.execute("""
87+
SELECT
88+
column_name, data_type, character_maximum_length,
89+
numeric_precision, numeric_scale, extra, column_default,
90+
CASE
91+
WHEN column_type LIKE '%% unsigned' THEN 1
92+
ELSE 0
93+
END AS is_unsigned
94+
FROM information_schema.columns
95+
WHERE table_name = %s AND table_schema = DATABASE()
96+
""", [table_name])
97+
else:
98+
cursor.execute("""
99+
SELECT
100+
column_name, data_type, character_maximum_length,
101+
numeric_precision, numeric_scale, extra, column_default,
102+
CASE
103+
WHEN collation_name = %s THEN NULL
104+
ELSE collation_name
105+
END AS collation_name,
106+
CASE
107+
WHEN column_type LIKE '%% unsigned' THEN 1
108+
ELSE 0
109+
END AS is_unsigned
110+
FROM information_schema.columns
111+
WHERE table_name = %s AND table_schema = DATABASE()
112+
""", [default_column_collation, table_name])
130113
field_info = {line[0]: InfoLine(*line) for line in cursor.fetchall()}
131114

132115
cursor.execute("SELECT * FROM %s LIMIT 1" % self.connection.ops.quote_name(table_name))
@@ -137,56 +120,33 @@ def to_int(i):
137120
fields = []
138121
for line in cursor.description:
139122
info = field_info[line[0]]
140-
name, type_code, display_size = line[:3]
141-
fields.append(FieldInfo(
142-
name,
143-
type_code,
144-
display_size,
145-
to_int(info.max_len) or line[3],
146-
to_int(info.num_prec) or line[4],
147-
to_int(info.num_scale) or line[5],
148-
line[6],
149-
info.column_default,
150-
info.extra,
151-
info.is_unsigned,
152-
line[0] in json_constraints
153-
))
123+
if DJANGO_VERSION < (3, 2, 0):
124+
fields.append(FieldInfo(
125+
*line[:3],
126+
to_int(info.max_len) or line[3],
127+
to_int(info.num_prec) or line[4],
128+
to_int(info.num_scale) or line[5],
129+
line[6],
130+
info.column_default,
131+
info.extra,
132+
info.is_unsigned,
133+
line[0] in json_constraints
134+
))
135+
else:
136+
fields.append(FieldInfo(
137+
*line[:3],
138+
to_int(info.max_len) or line[3],
139+
to_int(info.num_prec) or line[4],
140+
to_int(info.num_scale) or line[5],
141+
line[6],
142+
info.column_default,
143+
info.collation,
144+
info.extra,
145+
info.is_unsigned,
146+
line[0] in json_constraints,
147+
))
154148
return fields
155149

156-
def get_sequences(self, cursor, table_name, table_fields=()):
157-
for field_info in self.get_table_description(cursor, table_name):
158-
if 'auto_increment' in field_info.extra:
159-
# MySQL allows only one auto-increment column per table.
160-
return [{'table': table_name, 'column': field_info.name}]
161-
return []
162-
163-
def get_relations(self, cursor, table_name):
164-
"""
165-
Return a dictionary of {field_name: (field_name_other_table, other_table)}
166-
representing all relationships to the given table.
167-
"""
168-
constraints = self.get_key_columns(cursor, table_name)
169-
relations = {}
170-
for my_fieldname, other_table, other_field in constraints:
171-
relations[my_fieldname] = (other_field, other_table)
172-
return relations
173-
174-
def get_key_columns(self, cursor, table_name):
175-
"""
176-
Return a list of (column_name, referenced_table_name, referenced_column_name)
177-
for all key columns in the given table.
178-
"""
179-
key_columns = []
180-
cursor.execute("""
181-
SELECT column_name, referenced_table_name, referenced_column_name
182-
FROM information_schema.key_column_usage
183-
WHERE table_name = %s
184-
AND table_schema = DATABASE()
185-
AND referenced_table_name IS NOT NULL
186-
AND referenced_column_name IS NOT NULL""", [table_name])
187-
key_columns.extend(cursor.fetchall())
188-
return key_columns
189-
190150
def get_indexes(self, cursor, table_name):
191151
cursor.execute("SHOW INDEX FROM {0}"
192152
"".format(self.connection.ops.quote_name(table_name)))
@@ -221,33 +181,6 @@ def get_primary_key_column(self, cursor, table_name):
221181
return column[0]
222182
return None
223183

224-
def get_storage_engine(self, cursor, table_name):
225-
"""
226-
Retrieve the storage engine for a given table. Return the default
227-
storage engine if the table doesn't exist.
228-
"""
229-
cursor.execute(
230-
"SELECT engine "
231-
"FROM information_schema.tables "
232-
"WHERE table_name = %s", [table_name])
233-
result = cursor.fetchone()
234-
if not result:
235-
return self.connection.features._mysql_storage_engine
236-
return result[0]
237-
238-
def _parse_constraint_columns(self, check_clause, columns):
239-
check_columns = OrderedSet()
240-
statement = sqlparse.parse(check_clause)[0]
241-
tokens = (token for token in statement.flatten() if not token.is_whitespace)
242-
for token in tokens:
243-
if (
244-
token.ttype == sqlparse.tokens.Name and
245-
self.connection.ops.quote_name(token.value) == token.value and
246-
token.value[1:-1] in columns
247-
):
248-
check_columns.add(token.value[1:-1])
249-
return check_columns
250-
251184
def get_constraints(self, cursor, table_name):
252185
"""
253186
Retrieve any constraints or keys (unique, pk, fk, check, index) across
@@ -275,6 +208,8 @@ def get_constraints(self, cursor, table_name):
275208
'check': False,
276209
'foreign_key': (ref_table, ref_column) if ref_column else None,
277210
}
211+
if self.connection.features.supports_index_column_ordering:
212+
constraints[constraint]['orders'] = []
278213
constraints[constraint]['columns'].add(column)
279214
# Now get the constraint types
280215
type_query = """
@@ -295,27 +230,18 @@ def get_constraints(self, cursor, table_name):
295230
if self.connection.features.can_introspect_check_constraints:
296231
unnamed_constraints_index = 0
297232
columns = {info.name for info in self.get_table_description(cursor, table_name)}
298-
if self.connection.mysql_is_mariadb:
299-
type_query = """
300-
SELECT c.constraint_name, c.check_clause
301-
FROM information_schema.check_constraints AS c
302-
WHERE
303-
c.constraint_schema = DATABASE() AND
304-
c.table_name = %s
305-
"""
306-
else:
307-
type_query = """
308-
SELECT cc.constraint_name, cc.check_clause
309-
FROM
310-
information_schema.check_constraints AS cc,
311-
information_schema.table_constraints AS tc
312-
WHERE
313-
cc.constraint_schema = DATABASE() AND
314-
tc.table_schema = cc.constraint_schema AND
315-
cc.constraint_name = tc.constraint_name AND
316-
tc.constraint_type = 'CHECK' AND
317-
tc.table_name = %s
318-
"""
233+
type_query = """
234+
SELECT cc.constraint_name, cc.check_clause
235+
FROM
236+
information_schema.check_constraints AS cc,
237+
information_schema.table_constraints AS tc
238+
WHERE
239+
cc.constraint_schema = DATABASE() AND
240+
tc.table_schema = cc.constraint_schema AND
241+
cc.constraint_name = tc.constraint_name AND
242+
tc.constraint_type = 'CHECK' AND
243+
tc.table_name = %s
244+
"""
319245
cursor.execute(type_query, [table_name])
320246
for constraint, check_clause in cursor.fetchall():
321247
constraint_columns = self._parse_constraint_columns(check_clause, columns)
@@ -335,7 +261,9 @@ def get_constraints(self, cursor, table_name):
335261
}
336262
# Now add in the indexes
337263
cursor.execute("SHOW INDEX FROM %s" % self.connection.ops.quote_name(table_name))
338-
for table, non_unique, index, colseq, column, type_ in [x[:5] + (x[10],) for x in cursor.fetchall()]:
264+
for table, non_unique, index, colseq, column, order, type_ in [
265+
x[:6] + (x[10],) for x in cursor.fetchall()
266+
]:
339267
if index not in constraints:
340268
constraints[index] = {
341269
'columns': OrderedSet(),
@@ -344,9 +272,13 @@ def get_constraints(self, cursor, table_name):
344272
'check': False,
345273
'foreign_key': None,
346274
}
275+
if self.connection.features.supports_index_column_ordering:
276+
constraints[index]['orders'] = []
347277
constraints[index]['index'] = True
348278
constraints[index]['type'] = Index.suffix if type_ == 'BTREE' else type_.lower()
349279
constraints[index]['columns'].add(column)
280+
if self.connection.features.supports_index_column_ordering:
281+
constraints[index]['orders'].append('DESC' if order == 'D' else 'ASC')
350282
# Convert the sorted sets to lists
351283
for constraint in constraints.values():
352284
constraint['columns'] = list(constraint['columns'])

0 commit comments

Comments
 (0)