mirror of
https://github.com/Hopiu/django.git
synced 2026-04-26 17:54:48 +00:00
Thanks to Josh Smeaton for the idea of implementing server-side cursors in PostgreSQL from the iterator method, and Anssi Kääriäinen and Kevin Turner for their previous work. Also Simon Charette and Tim Graham for review.
370 lines
15 KiB
Python
370 lines
15 KiB
Python
"""
|
|
MySQL database backend for Django.
|
|
|
|
Requires mysqlclient: https://pypi.python.org/pypi/mysqlclient/
|
|
MySQLdb is supported for Python 2 only: http://sourceforge.net/projects/mysql-python
|
|
"""
|
|
from __future__ import unicode_literals
|
|
|
|
import datetime
|
|
import re
|
|
import sys
|
|
import warnings
|
|
|
|
from django.conf import settings
|
|
from django.db import utils
|
|
from django.db.backends import utils as backend_utils
|
|
from django.db.backends.base.base import BaseDatabaseWrapper
|
|
from django.utils import six, timezone
|
|
from django.utils.deprecation import RemovedInDjango20Warning
|
|
from django.utils.encoding import force_str
|
|
from django.utils.functional import cached_property
|
|
from django.utils.safestring import SafeBytes, SafeText
|
|
|
|
try:
|
|
import MySQLdb as Database
|
|
except ImportError as e:
|
|
from django.core.exceptions import ImproperlyConfigured
|
|
raise ImproperlyConfigured(
|
|
'Error loading MySQLdb module: %s.\n'
|
|
'Did you install mysqlclient or MySQL-python?' % e
|
|
)
|
|
|
|
from MySQLdb.constants import CLIENT, FIELD_TYPE # isort:skip
|
|
from MySQLdb.converters import Thing2Literal, conversions # isort:skip
|
|
|
|
# Some of these import MySQLdb, so import them after checking if it's installed.
|
|
from .client import DatabaseClient # isort:skip
|
|
from .creation import DatabaseCreation # isort:skip
|
|
from .features import DatabaseFeatures # isort:skip
|
|
from .introspection import DatabaseIntrospection # isort:skip
|
|
from .operations import DatabaseOperations # isort:skip
|
|
from .schema import DatabaseSchemaEditor # isort:skip
|
|
from .validation import DatabaseValidation # isort:skip
|
|
|
|
# We want version (1, 2, 1, 'final', 2) or later. We can't just use
|
|
# lexicographic ordering in this check because then (1, 2, 1, 'gamma')
|
|
# inadvertently passes the version test.
|
|
version = Database.version_info
|
|
if (version < (1, 2, 1) or (
|
|
version[:3] == (1, 2, 1) and (len(version) < 5 or version[3] != 'final' or version[4] < 2))):
|
|
from django.core.exceptions import ImproperlyConfigured
|
|
raise ImproperlyConfigured("MySQLdb-1.2.1p2 or newer is required; you have %s" % Database.__version__)
|
|
|
|
|
|
def adapt_datetime_warn_on_aware_datetime(value, conv):
|
|
# Remove this function and rely on the default adapter in Django 2.0.
|
|
if settings.USE_TZ and timezone.is_aware(value):
|
|
warnings.warn(
|
|
"The MySQL database adapter received an aware datetime (%s), "
|
|
"probably from cursor.execute(). Update your code to pass a "
|
|
"naive datetime in the database connection's time zone (UTC by "
|
|
"default).", RemovedInDjango20Warning)
|
|
# This doesn't account for the database connection's timezone,
|
|
# which isn't known. (That's why this adapter is deprecated.)
|
|
value = value.astimezone(timezone.utc).replace(tzinfo=None)
|
|
return Thing2Literal(value.strftime("%Y-%m-%d %H:%M:%S.%f"), conv)
|
|
|
|
|
|
# MySQLdb-1.2.1 returns TIME columns as timedelta -- they are more like
|
|
# timedelta in terms of actual behavior as they are signed and include days --
|
|
# and Django expects time, so we still need to override that. We also need to
|
|
# add special handling for SafeText and SafeBytes as MySQLdb's type
|
|
# checking is too tight to catch those (see Django ticket #6052).
|
|
django_conversions = conversions.copy()
|
|
django_conversions.update({
|
|
FIELD_TYPE.TIME: backend_utils.typecast_time,
|
|
FIELD_TYPE.DECIMAL: backend_utils.typecast_decimal,
|
|
FIELD_TYPE.NEWDECIMAL: backend_utils.typecast_decimal,
|
|
datetime.datetime: adapt_datetime_warn_on_aware_datetime,
|
|
})
|
|
|
|
# This should match the numerical portion of the version numbers (we can treat
|
|
# versions like 5.0.24 and 5.0.24a as the same).
|
|
server_version_re = re.compile(r'(\d{1,2})\.(\d{1,2})\.(\d{1,2})')
|
|
|
|
|
|
# MySQLdb-1.2.1 and newer automatically makes use of SHOW WARNINGS on
|
|
# MySQL-4.1 and newer, so the MysqlDebugWrapper is unnecessary. Since the
|
|
# point is to raise Warnings as exceptions, this can be done with the Python
|
|
# warning module, and this is setup when the connection is created, and the
|
|
# standard backend_utils.CursorDebugWrapper can be used. Also, using sql_mode
|
|
# TRADITIONAL will automatically cause most warnings to be treated as errors.
|
|
|
|
class CursorWrapper(object):
|
|
"""
|
|
A thin wrapper around MySQLdb's normal cursor class so that we can catch
|
|
particular exception instances and reraise them with the right types.
|
|
|
|
Implemented as a wrapper, rather than a subclass, so that we aren't stuck
|
|
to the particular underlying representation returned by Connection.cursor().
|
|
"""
|
|
codes_for_integrityerror = (1048,)
|
|
|
|
def __init__(self, cursor):
|
|
self.cursor = cursor
|
|
|
|
def execute(self, query, args=None):
|
|
try:
|
|
# args is None means no string interpolation
|
|
return self.cursor.execute(query, args)
|
|
except Database.OperationalError as e:
|
|
# Map some error codes to IntegrityError, since they seem to be
|
|
# misclassified and Django would prefer the more logical place.
|
|
if e.args[0] in self.codes_for_integrityerror:
|
|
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
|
|
raise
|
|
|
|
def executemany(self, query, args):
|
|
try:
|
|
return self.cursor.executemany(query, args)
|
|
except Database.OperationalError as e:
|
|
# Map some error codes to IntegrityError, since they seem to be
|
|
# misclassified and Django would prefer the more logical place.
|
|
if e.args[0] in self.codes_for_integrityerror:
|
|
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
|
|
raise
|
|
|
|
def __getattr__(self, attr):
|
|
if attr in self.__dict__:
|
|
return self.__dict__[attr]
|
|
else:
|
|
return getattr(self.cursor, attr)
|
|
|
|
def __iter__(self):
|
|
return iter(self.cursor)
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, type, value, traceback):
|
|
# Ticket #17671 - Close instead of passing thru to avoid backend
|
|
# specific behavior.
|
|
self.close()
|
|
|
|
|
|
class DatabaseWrapper(BaseDatabaseWrapper):
|
|
vendor = 'mysql'
|
|
# This dictionary maps Field objects to their associated MySQL column
|
|
# types, as strings. Column-type strings can contain format strings; they'll
|
|
# be interpolated against the values of Field.__dict__ before being output.
|
|
# If a column type is set to None, it won't be included in the output.
|
|
_data_types = {
|
|
'AutoField': 'integer AUTO_INCREMENT',
|
|
'BigAutoField': 'bigint AUTO_INCREMENT',
|
|
'BinaryField': 'longblob',
|
|
'BooleanField': 'bool',
|
|
'CharField': 'varchar(%(max_length)s)',
|
|
'CommaSeparatedIntegerField': 'varchar(%(max_length)s)',
|
|
'DateField': 'date',
|
|
'DateTimeField': 'datetime',
|
|
'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)',
|
|
'DurationField': 'bigint',
|
|
'FileField': 'varchar(%(max_length)s)',
|
|
'FilePathField': 'varchar(%(max_length)s)',
|
|
'FloatField': 'double precision',
|
|
'IntegerField': 'integer',
|
|
'BigIntegerField': 'bigint',
|
|
'IPAddressField': 'char(15)',
|
|
'GenericIPAddressField': 'char(39)',
|
|
'NullBooleanField': 'bool',
|
|
'OneToOneField': 'integer',
|
|
'PositiveIntegerField': 'integer UNSIGNED',
|
|
'PositiveSmallIntegerField': 'smallint UNSIGNED',
|
|
'SlugField': 'varchar(%(max_length)s)',
|
|
'SmallIntegerField': 'smallint',
|
|
'TextField': 'longtext',
|
|
'TimeField': 'time',
|
|
'UUIDField': 'char(32)',
|
|
}
|
|
|
|
@cached_property
|
|
def data_types(self):
|
|
if self.features.supports_microsecond_precision:
|
|
return dict(self._data_types, DateTimeField='datetime(6)', TimeField='time(6)')
|
|
else:
|
|
return self._data_types
|
|
|
|
operators = {
|
|
'exact': '= %s',
|
|
'iexact': 'LIKE %s',
|
|
'contains': 'LIKE BINARY %s',
|
|
'icontains': 'LIKE %s',
|
|
'regex': 'REGEXP BINARY %s',
|
|
'iregex': 'REGEXP %s',
|
|
'gt': '> %s',
|
|
'gte': '>= %s',
|
|
'lt': '< %s',
|
|
'lte': '<= %s',
|
|
'startswith': 'LIKE BINARY %s',
|
|
'endswith': 'LIKE BINARY %s',
|
|
'istartswith': 'LIKE %s',
|
|
'iendswith': 'LIKE %s',
|
|
}
|
|
|
|
# The patterns below are used to generate SQL pattern lookup clauses when
|
|
# the right-hand side of the lookup isn't a raw string (it might be an expression
|
|
# or the result of a bilateral transformation).
|
|
# In those cases, special characters for LIKE operators (e.g. \, *, _) should be
|
|
# escaped on database side.
|
|
#
|
|
# Note: we use str.format() here for readability as '%' is used as a wildcard for
|
|
# the LIKE operator.
|
|
pattern_esc = r"REPLACE(REPLACE(REPLACE({}, '\\', '\\\\'), '%%', '\%%'), '_', '\_')"
|
|
pattern_ops = {
|
|
'contains': "LIKE BINARY CONCAT('%%', {}, '%%')",
|
|
'icontains': "LIKE CONCAT('%%', {}, '%%')",
|
|
'startswith': "LIKE BINARY CONCAT({}, '%%')",
|
|
'istartswith': "LIKE CONCAT({}, '%%')",
|
|
'endswith': "LIKE BINARY CONCAT('%%', {})",
|
|
'iendswith': "LIKE CONCAT('%%', {})",
|
|
}
|
|
|
|
Database = Database
|
|
SchemaEditorClass = DatabaseSchemaEditor
|
|
# Classes instantiated in __init__().
|
|
client_class = DatabaseClient
|
|
creation_class = DatabaseCreation
|
|
features_class = DatabaseFeatures
|
|
introspection_class = DatabaseIntrospection
|
|
ops_class = DatabaseOperations
|
|
validation_class = DatabaseValidation
|
|
|
|
def get_connection_params(self):
|
|
kwargs = {
|
|
'conv': django_conversions,
|
|
'charset': 'utf8',
|
|
}
|
|
if six.PY2:
|
|
kwargs['use_unicode'] = True
|
|
settings_dict = self.settings_dict
|
|
if settings_dict['USER']:
|
|
kwargs['user'] = settings_dict['USER']
|
|
if settings_dict['NAME']:
|
|
kwargs['db'] = settings_dict['NAME']
|
|
if settings_dict['PASSWORD']:
|
|
kwargs['passwd'] = force_str(settings_dict['PASSWORD'])
|
|
if settings_dict['HOST'].startswith('/'):
|
|
kwargs['unix_socket'] = settings_dict['HOST']
|
|
elif settings_dict['HOST']:
|
|
kwargs['host'] = settings_dict['HOST']
|
|
if settings_dict['PORT']:
|
|
kwargs['port'] = int(settings_dict['PORT'])
|
|
# We need the number of potentially affected rows after an
|
|
# "UPDATE", not the number of changed rows.
|
|
kwargs['client_flag'] = CLIENT.FOUND_ROWS
|
|
kwargs.update(settings_dict['OPTIONS'])
|
|
return kwargs
|
|
|
|
def get_new_connection(self, conn_params):
|
|
conn = Database.connect(**conn_params)
|
|
conn.encoders[SafeText] = conn.encoders[six.text_type]
|
|
conn.encoders[SafeBytes] = conn.encoders[bytes]
|
|
return conn
|
|
|
|
def init_connection_state(self):
|
|
if self.features.is_sql_auto_is_null_enabled:
|
|
with self.cursor() as cursor:
|
|
# SQL_AUTO_IS_NULL controls whether an AUTO_INCREMENT column on
|
|
# a recently inserted row will return when the field is tested
|
|
# for NULL. Disabling this brings this aspect of MySQL in line
|
|
# with SQL standards.
|
|
cursor.execute('SET SQL_AUTO_IS_NULL = 0')
|
|
|
|
def create_cursor(self, name=None):
|
|
cursor = self.connection.cursor()
|
|
return CursorWrapper(cursor)
|
|
|
|
def _rollback(self):
|
|
try:
|
|
BaseDatabaseWrapper._rollback(self)
|
|
except Database.NotSupportedError:
|
|
pass
|
|
|
|
def _set_autocommit(self, autocommit):
|
|
with self.wrap_database_errors:
|
|
self.connection.autocommit(autocommit)
|
|
|
|
def disable_constraint_checking(self):
|
|
"""
|
|
Disables foreign key checks, primarily for use in adding rows with forward references. Always returns True,
|
|
to indicate constraint checks need to be re-enabled.
|
|
"""
|
|
self.cursor().execute('SET foreign_key_checks=0')
|
|
return True
|
|
|
|
def enable_constraint_checking(self):
|
|
"""
|
|
Re-enable foreign key checks after they have been disabled.
|
|
"""
|
|
# Override needs_rollback in case constraint_checks_disabled is
|
|
# nested inside transaction.atomic.
|
|
self.needs_rollback, needs_rollback = False, self.needs_rollback
|
|
try:
|
|
self.cursor().execute('SET foreign_key_checks=1')
|
|
finally:
|
|
self.needs_rollback = needs_rollback
|
|
|
|
def check_constraints(self, table_names=None):
|
|
"""
|
|
Checks each table name in `table_names` for rows with invalid foreign
|
|
key references. This method is intended to be used in conjunction with
|
|
`disable_constraint_checking()` and `enable_constraint_checking()`, to
|
|
determine if rows with invalid references were entered while constraint
|
|
checks were off.
|
|
|
|
Raises an IntegrityError on the first invalid foreign key reference
|
|
encountered (if any) and provides detailed information about the
|
|
invalid reference in the error message.
|
|
|
|
Backends can override this method if they can more directly apply
|
|
constraint checking (e.g. via "SET CONSTRAINTS ALL IMMEDIATE")
|
|
"""
|
|
cursor = self.cursor()
|
|
if table_names is None:
|
|
table_names = self.introspection.table_names(cursor)
|
|
for table_name in table_names:
|
|
primary_key_column_name = self.introspection.get_primary_key_column(cursor, table_name)
|
|
if not primary_key_column_name:
|
|
continue
|
|
key_columns = self.introspection.get_key_columns(cursor, table_name)
|
|
for column_name, referenced_table_name, referenced_column_name in key_columns:
|
|
cursor.execute(
|
|
"""
|
|
SELECT REFERRING.`%s`, REFERRING.`%s` FROM `%s` as REFERRING
|
|
LEFT JOIN `%s` as REFERRED
|
|
ON (REFERRING.`%s` = REFERRED.`%s`)
|
|
WHERE REFERRING.`%s` IS NOT NULL AND REFERRED.`%s` IS NULL
|
|
""" % (
|
|
primary_key_column_name, column_name, table_name,
|
|
referenced_table_name, column_name, referenced_column_name,
|
|
column_name, referenced_column_name,
|
|
)
|
|
)
|
|
for bad_row in cursor.fetchall():
|
|
raise utils.IntegrityError(
|
|
"The row in table '%s' with primary key '%s' has an invalid "
|
|
"foreign key: %s.%s contains a value '%s' that does not have a corresponding value in %s.%s."
|
|
% (
|
|
table_name, bad_row[0], table_name, column_name,
|
|
bad_row[1], referenced_table_name, referenced_column_name,
|
|
)
|
|
)
|
|
|
|
def is_usable(self):
|
|
try:
|
|
self.connection.ping()
|
|
except Database.Error:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
@cached_property
|
|
def mysql_version(self):
|
|
with self.temporary_connection() as cursor:
|
|
cursor.execute('SELECT VERSION()')
|
|
server_info = cursor.fetchone()[0]
|
|
match = server_version_re.match(server_info)
|
|
if not match:
|
|
raise Exception('Unable to determine MySQL version from version string %r' % server_info)
|
|
return tuple(int(x) for x in match.groups())
|