Skip to content

Commit 297b24c

Browse files
committed
BUG19163169: Add support for Django 1.7
This patch makes connector/python compatible with Django 1.7. Django 1.7 supports schema migration, this patch adds a file schema.py required by django for schema migrations. Also it fixes issues with django datetime fields.
1 parent 4fc176f commit 297b24c

12 files changed

+411
-146
lines changed

lib/mysql/connector/cursor.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,9 @@ def execute(self, operation, params=None, multi=False):
464464
"""
465465
if not operation:
466466
return None
467+
468+
if not self._connection:
469+
raise errors.ProgrammingError("Cursor is not connected.")
467470
if self._connection.unread_result is True:
468471
raise errors.InternalError("Unread result found.")
469472

lib/mysql/connector/custom_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import sys
2828

29+
2930
class HexLiteral(str):
3031

3132
"""Class holding MySQL hex literals"""

lib/mysql/connector/django/base.py

Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
from __future__ import unicode_literals
1919

2020
import sys
21+
2122
import django
23+
from django.utils.functional import cached_property
2224

2325
try:
2426
import mysql.connector
@@ -41,7 +43,12 @@
4143
"is required; you have %s" % mysql.connector.__version__)
4244

4345
from django.db import utils
44-
from django.db.backends import *
46+
if django.VERSION < (1, 7):
47+
from django.db.backends import util
48+
else:
49+
from django.db.backends import utils as backend_utils
50+
from django.db.backends import (BaseDatabaseFeatures, BaseDatabaseOperations,
51+
BaseDatabaseWrapper)
4552
from django.db.backends.signals import connection_created
4653
from django.utils import (six, timezone, dateparse)
4754
from django.conf import settings
@@ -50,6 +57,8 @@
5057
from mysql.connector.django.creation import DatabaseCreation
5158
from mysql.connector.django.introspection import DatabaseIntrospection
5259
from mysql.connector.django.validation import DatabaseValidation
60+
if django.VERSION >= (1, 7):
61+
from mysql.connector.django.schema import DatabaseSchemaEditor
5362

5463
try:
5564
import pytz
@@ -69,7 +78,7 @@ def _TIME_to_python(self, value, dsc=None):
6978
7079
Returns datetime.time()
7180
"""
72-
return dateparse.parse_time(value)
81+
return dateparse.parse_time(value.decode('utf-8'))
7382

7483
def _DATETIME_to_python(self, value, dsc=None):
7584
"""Connector/Python always returns naive datetime.datetime
@@ -102,8 +111,10 @@ def _execute_wrapper(self, method, query, args):
102111
"""Wrapper around execute() and executemany()"""
103112
try:
104113
return method(query, args)
105-
except (mysql.connector.ProgrammingError,
106-
mysql.connector.IntegrityError) as err:
114+
except (mysql.connector.ProgrammingError) as err:
115+
six.reraise(utils.ProgrammingError,
116+
utils.ProgrammingError(err.msg), sys.exc_info()[2])
117+
except (mysql.connector.IntegrityError) as err:
107118
six.reraise(utils.IntegrityError,
108119
utils.IntegrityError(err.msg), sys.exc_info()[2])
109120
except mysql.connector.OperationalError as err:
@@ -127,7 +138,7 @@ def executemany(self, query, args):
127138
This wrapper method around the executemany()-method of the cursor is
128139
mainly needed to re-raise using different exceptions.
129140
"""
130-
return self._execute_wrapper(self.cursor.execute, query, args)
141+
return self._execute_wrapper(self.cursor.executemany, query, args)
131142

132143
def __getattr__(self, attr):
133144
"""Return attribute of wrapped cursor"""
@@ -137,6 +148,12 @@ def __iter__(self):
137148
"""Returns iterator over wrapped cursor"""
138149
return iter(self.cursor)
139150

151+
def __enter__(self):
152+
return self
153+
154+
def __exit__(self, exc_type, exc_value, exc_traceback):
155+
self.close()
156+
140157

141158
class DatabaseFeatures(BaseDatabaseFeatures):
142159
"""Features specific to MySQL
@@ -154,14 +171,19 @@ class DatabaseFeatures(BaseDatabaseFeatures):
154171
has_select_for_update_nowait = False
155172
supports_forward_references = False
156173
supports_long_model_names = False
174+
supports_binary_field = six.PY2
157175
supports_microsecond_precision = False # toggled in __init__()
158176
supports_regex_backreferencing = False
159177
supports_date_lookup_using_string = False
178+
can_introspect_binary_field = False
179+
can_introspect_boolean_field = False
160180
supports_timezones = False
161181
requires_explicit_null_ordering_when_grouping = True
182+
allows_auto_pk_0 = False
162183
allows_primary_key_0 = False
163184
uses_savepoints = True
164185
atomic_transactions = False
186+
supports_column_check_constraints = False
165187

166188
def __init__(self, connection):
167189
super(DatabaseFeatures, self).__init__(connection)
@@ -172,6 +194,7 @@ def _microseconds_precision(self):
172194
return True
173195
return False
174196

197+
@cached_property
175198
def _mysql_storage_engine(self):
176199
"""Get default storage engine of MySQL
177200
@@ -180,28 +203,30 @@ def _mysql_storage_engine(self):
180203
181204
Used by Django tests.
182205
"""
183-
cursor = self.connection.cursor()
184206
tblname = 'INTROSPECT_TEST'
185207

186208
droptable = 'DROP TABLE IF EXISTS {table}'.format(table=tblname)
209+
with self.connection.cursor() as cursor:
210+
cursor.execute(droptable)
211+
cursor.execute('CREATE TABLE {table} (X INT)'.format(table=tblname))
212+
213+
if self.connection.server_version >= (5, 0, 0):
214+
cursor.execute(
215+
"SELECT ENGINE FROM INFORMATION_SCHEMA.TABLES "
216+
"WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s",
217+
(self.connection.settings_dict['NAME'], tblname))
218+
engine = cursor.fetchone()[0]
219+
else:
220+
# Very old MySQL servers..
221+
cursor.execute("SHOW TABLE STATUS WHERE Name='{table}'".format(
222+
table=tblname))
223+
engine = cursor.fetchone()[1]
224+
cursor.execute(droptable)
187225

188-
cursor.execute(droptable)
189-
cursor.execute('CREATE TABLE {table} (X INT)'.format(table=tblname))
190-
191-
if self.connection.server_version >= (5, 0, 0):
192-
cursor.execute(
193-
"SELECT ENGINE FROM INFORMATION_SCHEMA.TABLES "
194-
"WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s",
195-
(self.connection.settings_dict['NAME'], tblname))
196-
engine = cursor.fetchone()[0]
197-
else:
198-
# Very old MySQL servers..
199-
cursor.execute("SHOW TABLE STATUS WHERE Name='{table}'".format(
200-
table=tblname))
201-
engine = cursor.fetchone()[1]
202-
cursor.execute(droptable)
226+
self._cached_storage_engine = engine
203227
return engine
204228

229+
@cached_property
205230
def can_introspect_foreign_keys(self):
206231
"""Confirm support for introspected foreign keys
207232
@@ -210,6 +235,7 @@ def can_introspect_foreign_keys(self):
210235
"""
211236
return self._mysql_storage_engine == 'InnoDB'
212237

238+
@cached_property
213239
def has_zoneinfo_database(self):
214240
"""Tests if the time zone definitions are installed
215241
@@ -223,14 +249,21 @@ def has_zoneinfo_database(self):
223249
if not HAVE_PYTZ:
224250
return False
225251

226-
cursor = self.connection.cursor()
227-
cursor.execute("SELECT 1 FROM mysql.time_zone LIMIT 1")
228-
return cursor.fetchall() != []
252+
with self.connection.cursor() as cursor:
253+
cursor.execute("SELECT 1 FROM mysql.time_zone LIMIT 1")
254+
return cursor.fetchall() != []
229255

230256

231257
class DatabaseOperations(BaseDatabaseOperations):
232258
compiler_module = "mysql.connector.django.compiler"
233259

260+
# MySQL stores positive fields as UNSIGNED ints.
261+
if django.VERSION >= (1, 7):
262+
integer_field_ranges = dict(BaseDatabaseOperations.integer_field_ranges,
263+
PositiveSmallIntegerField=(0, 4294967295),
264+
PositiveIntegerField=(
265+
0, 18446744073709551615),)
266+
234267
def date_extract_sql(self, lookup_type, field_name):
235268
# http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html
236269
if lookup_type == 'week_day':
@@ -250,7 +283,7 @@ def date_trunc_sql(self, lookup_type, field_name):
250283
The field_name is returned when lookup_type is not supported.
251284
"""
252285
fields = ['year', 'month', 'day', 'hour', 'minute', 'second']
253-
format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s')
286+
format = ('%Y-', '%m', '-%d', ' %H:', '%i', ':%s')
254287
format_def = ('0000-', '01', '-01', ' 00:', '00', ':00')
255288
try:
256289
i = fields.index(lookup_type) + 1
@@ -290,7 +323,7 @@ def datetime_trunc_sql(self, lookup_type, field_name, tzname):
290323
else:
291324
params = []
292325
fields = ['year', 'month', 'day', 'hour', 'minute', 'second']
293-
format_ = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s')
326+
format_ = ('%Y-', '%m', '-%d', ' %H:', '%i', ':%s')
294327
format_def = ('0000-', '01', '-01', ' 00:', '00', ':00')
295328
try:
296329
i = fields.index(lookup_type) + 1
@@ -379,7 +412,7 @@ def sequence_reset_by_name_sql(self, style, sequences):
379412

380413
def validate_autopk_value(self, value):
381414
# MySQLism: zero in AUTO_INCREMENT field does not work. Refs #17653.
382-
if not value:
415+
if value == 0:
383416
raise ValueError('The database backend does not accept 0 as a '
384417
'value for AutoField.')
385418
return value
@@ -454,6 +487,15 @@ def savepoint_commit_sql(self, sid):
454487
def savepoint_rollback_sql(self, sid):
455488
return "ROLLBACK TO SAVEPOINT {0}".format(sid)
456489

490+
def combine_expression(self, connector, sub_expressions):
491+
"""
492+
MySQL requires special cases for ^ operators in query expressions
493+
"""
494+
if connector == '^':
495+
return 'POW(%s)' % ','.join(sub_expressions)
496+
return super(DatabaseOperations, self).combine_expression(
497+
connector, sub_expressions)
498+
457499

458500
class DatabaseWrapper(BaseDatabaseWrapper):
459501
vendor = 'mysql'
@@ -681,8 +723,18 @@ def _rollback(self):
681723

682724
def _set_autocommit(self, autocommit):
683725
# Django 1.6
684-
self.connection.autocommit = autocommit
726+
with self.wrap_database_errors:
727+
self.connection.autocommit = autocommit
728+
729+
def schema_editor(self, *args, **kwargs):
730+
"""Returns a new instance of this backend's SchemaEditor"""
731+
# Django 1.7
732+
return DatabaseSchemaEditor(self, *args, **kwargs)
685733

686734
def is_usable(self):
687735
# Django 1.6
688736
return self.connection.is_connected()
737+
738+
@property
739+
def mysql_version(self):
740+
return self.server_version

lib/mysql/connector/django/compiler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def resolve_columns(self, row, fields=()):
1313
bool_fields = ("BooleanField", "NullBooleanField")
1414
for value, field in zip_longest(row[index_extra_select:], fields):
1515
if (field and field.get_internal_type() in bool_fields and
16-
value in (0, 1)):
16+
value in (0, 1)):
1717
value = bool(value)
1818
values.append(value)
1919
return row[:index_extra_select] + tuple(values)

lib/mysql/connector/django/creation.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
import django
44
from django.db import models
55
from django.db.backends.creation import BaseDatabaseCreation
6-
from django.db.backends.util import truncate_name
76

7+
if django.VERSION < (1, 7):
8+
from django.db.backends.util import truncate_name
9+
else:
10+
from django.db.backends.utils import truncate_name
811

912
class DatabaseCreation(BaseDatabaseCreation):
1013
"""Maps Django Field object with MySQL data types
@@ -47,12 +50,21 @@ def __init__(self, connection):
4750

4851
def sql_table_creation_suffix(self):
4952
suffix = []
50-
if self.connection.settings_dict['TEST_CHARSET']:
51-
suffix.append('CHARACTER SET {0}'.format(
52-
self.connection.settings_dict['TEST_CHARSET']))
53-
if self.connection.settings_dict['TEST_COLLATION']:
54-
suffix.append('COLLATE {0}'.format(
55-
self.connection.settings_dict['TEST_COLLATION']))
53+
if django.VERSION < (1, 7):
54+
if self.connection.settings_dict['TEST_CHARSET']:
55+
suffix.append('CHARACTER SET {0}'.format(
56+
self.connection.settings_dict['TEST_CHARSET']))
57+
if self.connection.settings_dict['TEST_COLLATION']:
58+
suffix.append('COLLATE {0}'.format(
59+
self.connection.settings_dict['TEST_COLLATION']))
60+
61+
else:
62+
test_settings = self.connection.settings_dict['TEST']
63+
if test_settings['CHARSET']:
64+
suffix.append('CHARACTER SET %s' % test_settings['CHARSET'])
65+
if test_settings['COLLATION']:
66+
suffix.append('COLLATE %s' % test_settings['COLLATION'])
67+
5668
return ' '.join(suffix)
5769

5870
if django.VERSION < (1, 6):

0 commit comments

Comments
 (0)