├── src └── mysql_pymysql │ ├── __init__.py │ ├── py3.py │ ├── introspection.py │ └── base.py ├── README.md ├── LICENSE └── setup.py /src/mysql_pymysql/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/mysql_pymysql/py3.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info[0] < 3: 4 | text_type = unicode 5 | long_type = long 6 | iteritems = lambda o: o.iteritems() 7 | def exec_(code, globs=None, locs=None): 8 | """Execute code in a namespace.""" 9 | if globs is None: 10 | frame = sys._getframe(1) 11 | globs = frame.f_globals 12 | if locs is None: 13 | locs = frame.f_locals 14 | del frame 15 | elif locs is None: 16 | locs = globs 17 | exec("""exec code in globs, locs""") 18 | exec_("""def reraise(tp, value, tb=None): 19 | raise tp, value, tb 20 | """) 21 | else: 22 | text_type = str 23 | long_type = int 24 | iteritems = lambda o: o.items() 25 | def reraise(tp, value, tb=None): 26 | if value.__traceback__ is not tb: 27 | raise value.with_traceback(tb) 28 | raise value 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | django-mysql-pymysql 2 | ==================== 3 | 4 | This is a Django database backend for MySQL, using the PyMySQL database adapter. It is intended to be a drop-in replacement for the built-in MySQLdb backend, and leverages quite a bit of its code. 5 | 6 | It is currently experimental, and has only been tested against Django trunk (1.4-pre-alpha), and Vinay Sajip's Py3k branch on BitBucket (https://bitbucket.org/vinay.sajip/django). At the moment, it won't work with Django 1.3, as it uses Aymeric Augustin's timezone-aware datetime patch. 7 | 8 | 9 | Requirements 10 | ------------ 11 | 12 | * Django trunk or Py3k Branch 13 | * PyMySQL (patches here: https://github.com/clelland/PyMySQL) 14 | 15 | Installation 16 | ------------ 17 | 18 | 1. Clone and install into your site-packages directory: 19 | 20 | $ git clone https://github.com/clelland/django-mysql-pymysql 21 | $ cd django-mysql-pymysql 22 | $ python setup.py install 23 | 24 | 2. Edit your settings file: 25 | 26 | DATABASES = { 27 | 'default': { 28 | 'ENGINE': 'mysql_pymysql', 29 | 'HOST': ..., 30 | 'USER': ..., 31 | 'PASSWORD': ..., 32 | } 33 | } 34 | 35 | 36 | 3. You're done. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Django Software Foundation and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Django nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | setup( 3 | name = "django-mysql-pymysql", 4 | version = "0.1", 5 | packages = find_packages('src'), 6 | package_dir = {'':'src'}, 7 | 8 | # metadata for upload to PyPI, one day 9 | author = "Ian Clelland", 10 | author_email = "clelland@gmail.com", 11 | description = "Django MySQL backend for PyMySQL adapter", 12 | license = "BSD", 13 | keywords = "django mysql pymysql", 14 | url = "https://github.com/clelland/django-mysql-pymysql", 15 | 16 | classifiers = [ 17 | "Development Status :: 4 - Beta", 18 | "Framework :: Django", 19 | "Intended Audience :: Developers", 20 | "License :: OSI Approved :: BSD License", 21 | "Operating System :: OS Independent", 22 | "Programming Language :: Python", 23 | "Programming Language :: Python :: 2", 24 | "Programming Language :: Python :: 2.5", 25 | "Programming Language :: Python :: 2.6", 26 | "Programming Language :: Python :: 2.7", 27 | "Programming Language :: Python :: 3", 28 | "Programming Language :: Python :: 3.2", 29 | "Programming Language :: Python :: 3.3", 30 | "Topic :: Database :: Front-Ends", 31 | ], 32 | 33 | long_description = """ 34 | django-mysql-pymysql 35 | ==================== 36 | 37 | This is a Django database backend for MySQL, using the PyMySQL database adapter. It is intended to be a drop-in replacement for the built-in MySQLdb backend, and leverages quite a bit of its code. 38 | 39 | It is currently experimental, and has only been tested against Django trunk (1.4-pre-alpha), and Vinay Sajip's Py3k branch on BitBucket (https://bitbucket.org/vinay.sajip/django). At the moment, it won't work with Django 1.3, as it uses Aymeric Augustin's timezone-aware datetime patch. 40 | 41 | 42 | Requirements 43 | ------------ 44 | 45 | * Django trunk or Py3k Branch 46 | * PyMySQL (patches here: https://github.com/clelland/PyMySQL) 47 | 48 | Installation 49 | ------------ 50 | 51 | :: 52 | 53 | 1. Clone and install into your site-packages directory: 54 | 55 | $ git clone https://github.com/clelland/django-mysql-pymysql 56 | $ cd django-mysql-pymysql 57 | $ python setup.py install 58 | 59 | 2. Edit your settings file: 60 | 61 | :: 62 | 63 | DATABASES = { 64 | 'default': { 65 | 'ENGINE': 'mysql_pymysql', 66 | 'HOST': ..., 67 | 'USER': ..., 68 | 'PASSWORD': ..., 69 | } 70 | } 71 | 72 | 73 | 3. You're done. 74 | """ 75 | ) 76 | -------------------------------------------------------------------------------- /src/mysql_pymysql/introspection.py: -------------------------------------------------------------------------------- 1 | from django.db.backends import BaseDatabaseIntrospection 2 | from pymysql import ProgrammingError, OperationalError 3 | from pymysql.constants import FIELD_TYPE 4 | import re 5 | from .py3 import iteritems 6 | 7 | foreign_key_re = re.compile(r"\sCONSTRAINT `[^`]*` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)` \(`([^`]*)`\)") 8 | 9 | class DatabaseIntrospection(BaseDatabaseIntrospection): 10 | data_types_reverse = { 11 | FIELD_TYPE.BLOB: 'TextField', 12 | FIELD_TYPE.CHAR: 'CharField', 13 | FIELD_TYPE.DECIMAL: 'DecimalField', 14 | FIELD_TYPE.NEWDECIMAL: 'DecimalField', 15 | FIELD_TYPE.DATE: 'DateField', 16 | FIELD_TYPE.DATETIME: 'DateTimeField', 17 | FIELD_TYPE.DOUBLE: 'FloatField', 18 | FIELD_TYPE.FLOAT: 'FloatField', 19 | FIELD_TYPE.INT24: 'IntegerField', 20 | FIELD_TYPE.LONG: 'IntegerField', 21 | FIELD_TYPE.LONGLONG: 'BigIntegerField', 22 | FIELD_TYPE.SHORT: 'IntegerField', 23 | FIELD_TYPE.STRING: 'CharField', 24 | FIELD_TYPE.TIMESTAMP: 'DateTimeField', 25 | FIELD_TYPE.TINY: 'IntegerField', 26 | FIELD_TYPE.TINY_BLOB: 'TextField', 27 | FIELD_TYPE.MEDIUM_BLOB: 'TextField', 28 | FIELD_TYPE.LONG_BLOB: 'TextField', 29 | FIELD_TYPE.VAR_STRING: 'CharField', 30 | } 31 | 32 | def get_table_list(self, cursor): 33 | "Returns a list of table names in the current database." 34 | cursor.execute("SHOW TABLES") 35 | return [row[0] for row in cursor.fetchall()] 36 | 37 | def get_table_description(self, cursor, table_name): 38 | "Returns a description of the table, with the DB-API cursor.description interface." 39 | cursor.execute("SELECT * FROM %s LIMIT 1" % self.connection.ops.quote_name(table_name)) 40 | return cursor.description 41 | 42 | def _name_to_index(self, cursor, table_name): 43 | """ 44 | Returns a dictionary of {field_name: field_index} for the given table. 45 | Indexes are 0-based. 46 | """ 47 | return dict([(d[0], i) for i, d in enumerate(self.get_table_description(cursor, table_name))]) 48 | 49 | def get_relations(self, cursor, table_name): 50 | """ 51 | Returns a dictionary of {field_index: (field_index_other_table, other_table)} 52 | representing all relationships to the given table. Indexes are 0-based. 53 | """ 54 | my_field_dict = self._name_to_index(cursor, table_name) 55 | constraints = self.get_key_columns(cursor, table_name) 56 | relations = {} 57 | for my_fieldname, other_table, other_field in constraints: 58 | other_field_index = self._name_to_index(cursor, other_table)[other_field] 59 | my_field_index = my_field_dict[my_fieldname] 60 | relations[my_field_index] = (other_field_index, other_table) 61 | return relations 62 | 63 | def get_key_columns(self, cursor, table_name): 64 | """ 65 | Returns a list of (column_name, referenced_table_name, referenced_column_name) for all 66 | key columns in given table. 67 | """ 68 | key_columns = [] 69 | try: 70 | cursor.execute(""" 71 | SELECT column_name, referenced_table_name, referenced_column_name 72 | FROM information_schema.key_column_usage 73 | WHERE table_name = %s 74 | AND table_schema = DATABASE() 75 | AND referenced_table_name IS NOT NULL 76 | AND referenced_column_name IS NOT NULL""", [table_name]) 77 | key_columns.extend(cursor.fetchall()) 78 | except (ProgrammingError, OperationalError): 79 | # Fall back to "SHOW CREATE TABLE", for previous MySQL versions. 80 | # Go through all constraints and save the equal matches. 81 | cursor.execute("SHOW CREATE TABLE %s" % self.connection.ops.quote_name(table_name)) 82 | for row in cursor.fetchall(): 83 | pos = 0 84 | while True: 85 | match = foreign_key_re.search(row[1], pos) 86 | if match == None: 87 | break 88 | pos = match.end() 89 | key_columns.append(match.groups()) 90 | return key_columns 91 | 92 | def get_primary_key_column(self, cursor, table_name): 93 | """ 94 | Returns the name of the primary key column for the given table 95 | """ 96 | for column in iteritems(self.get_indexes(cursor, table_name)): 97 | if column[1]['primary_key']: 98 | return column[0] 99 | return None 100 | 101 | def get_indexes(self, cursor, table_name): 102 | """ 103 | Returns a dictionary of fieldname -> infodict for the given table, 104 | where each infodict is in the format: 105 | {'primary_key': boolean representing whether it's the primary key, 106 | 'unique': boolean representing whether it's a unique index} 107 | """ 108 | cursor.execute("SHOW INDEX FROM %s" % self.connection.ops.quote_name(table_name)) 109 | indexes = {} 110 | for row in cursor.fetchall(): 111 | indexes[row[4]] = {'primary_key': (row[2] == 'PRIMARY'), 'unique': not bool(row[1])} 112 | return indexes 113 | 114 | -------------------------------------------------------------------------------- /src/mysql_pymysql/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | PyMySQL database backend for Django. 3 | 4 | Requires PyMySQL: https://github.com/clelland/pymysql 5 | """ 6 | from .py3 import text_type, long_type, reraise 7 | 8 | import re 9 | import sys 10 | 11 | try: 12 | import pymysql as Database 13 | from pymysql.converters import conversions 14 | from pymysql.constants import FIELD_TYPE, CLIENT 15 | backend = 'pymysql' 16 | 17 | except ImportError: 18 | e = sys.exc_info()[1] 19 | from django.core.exceptions import ImproperlyConfigured 20 | raise ImproperlyConfigured("Error loading PyMySQL module: %s" % e) 21 | 22 | from django.db import utils 23 | from django.db.backends import * 24 | from django.db.backends.signals import connection_created 25 | from django.db.backends.mysql.client import DatabaseClient 26 | from django.db.backends.mysql.creation import DatabaseCreation 27 | from .introspection import DatabaseIntrospection 28 | from django.db.backends.mysql.validation import DatabaseValidation 29 | from django.utils.safestring import SafeString, SafeUnicode 30 | from django.utils.timezone import is_aware, is_naive, utc 31 | 32 | # Raise exceptions for database warnings if DEBUG is on 33 | from django.conf import settings 34 | if settings.DEBUG: 35 | from warnings import filterwarnings 36 | filterwarnings("error", category=Database.Warning) 37 | 38 | DatabaseError = Database.DatabaseError 39 | IntegrityError = Database.IntegrityError 40 | 41 | # PyMySQL raises an InternalError with error 1048 (BAD_NULL) -- here we patch 42 | # the error map to force an IntegrityError instead. 43 | from pymysql.err import error_map 44 | error_map[1048] = IntegrityError 45 | 46 | django_conversions = conversions.copy() 47 | # It's impossible to import datetime_or_None directly from MySQLdb.times 48 | datetime_or_None = conversions[FIELD_TYPE.DATETIME] 49 | 50 | # As with the MySQLdb adapter, PyMySQL returns TIME columns as timedelta -- 51 | # so we add a conversion here 52 | def datetime_or_None_with_timezone_support(connection, field, obj): 53 | dt = datetime_or_None(connection, field, obj) 54 | # Confirm that dt is naive before overwriting its tzinfo. 55 | if dt is not None and settings.USE_TZ and is_naive(dt): 56 | dt = dt.replace(tzinfo=utc) 57 | return dt 58 | 59 | # The conversion functions in django.db.util only accept one argument, while 60 | # the PyMySQL converters require three. This adapter function produces a 61 | # PyMySQL-compabtible conversion function, given the Django function. 62 | def conversion_adapter(fn): 63 | def converter(connection, field, obj): 64 | return fn(obj) 65 | return converter 66 | 67 | django_conversions.update({ 68 | FIELD_TYPE.TIME: conversion_adapter(util.typecast_time), 69 | FIELD_TYPE.DECIMAL: conversion_adapter(util.typecast_decimal), 70 | FIELD_TYPE.NEWDECIMAL: conversion_adapter(util.typecast_decimal), 71 | FIELD_TYPE.DATETIME: datetime_or_None_with_timezone_support, 72 | }) 73 | 74 | 75 | # ----------------------------------------------------------------------------- 76 | # The rest of this file is taken verbatim from django/db/backends/mysql/base/py 77 | # which, unfortunately, cannot be imported directly in an environment without 78 | # MySQLdb installed. 79 | # ----------------------------------------------------------------------------- 80 | 81 | 82 | # This should match the numerical portion of the version numbers (we can treat 83 | # versions like 5.0.24 and 5.0.24a as the same). Based on the list of version 84 | # at http://dev.mysql.com/doc/refman/4.1/en/news.html and 85 | # http://dev.mysql.com/doc/refman/5.0/en/news.html . 86 | server_version_re = re.compile(r'(\d{1,2})\.(\d{1,2})\.(\d{1,2})') 87 | 88 | # MySQLdb-1.2.1 and newer automatically makes use of SHOW WARNINGS on 89 | # MySQL-4.1 and newer, so the MysqlDebugWrapper is unnecessary. Since the 90 | # point is to raise Warnings as exceptions, this can be done with the Python 91 | # warning module, and this is setup when the connection is created, and the 92 | # standard util.CursorDebugWrapper can be used. Also, using sql_mode 93 | # TRADITIONAL will automatically cause most warnings to be treated as errors. 94 | 95 | class CursorWrapper(object): 96 | """ 97 | A thin wrapper around MySQLdb's normal cursor class so that we can catch 98 | particular exception instances and reraise them with the right types. 99 | 100 | Implemented as a wrapper, rather than a subclass, so that we aren't stuck 101 | to the particular underlying representation returned by Connection.cursor(). 102 | """ 103 | codes_for_integrityerror = (1048,) 104 | 105 | def __init__(self, cursor): 106 | self.cursor = cursor 107 | 108 | def execute(self, query, args=None): 109 | try: 110 | return self.cursor.execute(query, args) 111 | except Database.IntegrityError: 112 | e = sys.exc_info() 113 | reraise(utils.IntegrityError, utils.IntegrityError(*e[1].args), e[2]) 114 | except Database.OperationalError: 115 | e = sys.exc_info() 116 | # Map some error codes to IntegrityError, since they seem to be 117 | # misclassified and Django would prefer the more logical place. 118 | if e[1].code in self.codes_for_integrityerror: 119 | reraise(utils.IntegrityError, utils.IntegrityError(*e[1].args), e[2]) 120 | raise 121 | except Database.DatabaseError: 122 | e = sys.exc_info() 123 | reraise(utils.DatabaseError, utils.DatabaseError(*e[1].args), e[2]) 124 | 125 | def executemany(self, query, args): 126 | try: 127 | return self.cursor.executemany(query, args) 128 | except Database.IntegrityError: 129 | e = sys.exc_info() 130 | reraise(utils.IntegrityError, utils.IntegrityError(*e[1].args), e[2]) 131 | except Database.OperationalError: 132 | e = sys.exc_info() 133 | # Map some error codes to IntegrityError, since they seem to be 134 | # misclassified and Django would prefer the more logical place. 135 | if e[1].code in self.codes_for_integrityerror: 136 | reraise(utils.IntegrityError, utils.IntegrityError(*e[1].args), e[2]) 137 | raise 138 | except Database.DatabaseError: 139 | e = sys.exc_info() 140 | reraise(utils.DatabaseError, utils.DatabaseError(*e[1].args), e[2]) 141 | 142 | def __getattr__(self, attr): 143 | if attr in self.__dict__: 144 | return self.__dict__[attr] 145 | else: 146 | return getattr(self.cursor, attr) 147 | 148 | def __iter__(self): 149 | return iter(self.cursor) 150 | 151 | class DatabaseFeatures(BaseDatabaseFeatures): 152 | empty_fetchmany_value = () 153 | update_can_self_select = False 154 | allows_group_by_pk = True 155 | related_fields_match_type = True 156 | allow_sliced_subqueries = False 157 | has_bulk_insert = True 158 | has_select_for_update = True 159 | has_select_for_update_nowait = False 160 | supports_forward_references = False 161 | supports_long_model_names = False 162 | supports_microsecond_precision = False 163 | supports_regex_backreferencing = False 164 | supports_date_lookup_using_string = False 165 | supports_timezones = False 166 | requires_explicit_null_ordering_when_grouping = True 167 | allows_primary_key_0 = False 168 | 169 | def _can_introspect_foreign_keys(self): 170 | "Confirm support for introspected foreign keys" 171 | cursor = self.connection.cursor() 172 | cursor.execute('CREATE TABLE INTROSPECT_TEST (X INT)') 173 | # This command is MySQL specific; the second column 174 | # will tell you the default table type of the created 175 | # table. Since all Django's test tables will have the same 176 | # table type, that's enough to evaluate the feature. 177 | cursor.execute("SHOW TABLE STATUS WHERE Name='INTROSPECT_TEST'") 178 | result = cursor.fetchone() 179 | cursor.execute('DROP TABLE INTROSPECT_TEST') 180 | return result[1] != 'MyISAM' 181 | 182 | class DatabaseOperations(BaseDatabaseOperations): 183 | compiler_module = "django.db.backends.mysql.compiler" 184 | 185 | def date_extract_sql(self, lookup_type, field_name): 186 | # http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html 187 | if lookup_type == 'week_day': 188 | # DAYOFWEEK() returns an integer, 1-7, Sunday=1. 189 | # Note: WEEKDAY() returns 0-6, Monday=0. 190 | return "DAYOFWEEK(%s)" % field_name 191 | else: 192 | return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) 193 | 194 | def date_trunc_sql(self, lookup_type, field_name): 195 | fields = ['year', 'month', 'day', 'hour', 'minute', 'second'] 196 | format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape. 197 | format_def = ('0000-', '01', '-01', ' 00:', '00', ':00') 198 | try: 199 | i = fields.index(lookup_type) + 1 200 | except ValueError: 201 | sql = field_name 202 | else: 203 | format_str = ''.join([f for f in format[:i]] + [f for f in format_def[i:]]) 204 | sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str) 205 | return sql 206 | 207 | def date_interval_sql(self, sql, connector, timedelta): 208 | return "(%s %s INTERVAL '%d 0:0:%d:%d' DAY_MICROSECOND)" % (sql, connector, 209 | timedelta.days, timedelta.seconds, timedelta.microseconds) 210 | 211 | def drop_foreignkey_sql(self): 212 | return "DROP FOREIGN KEY" 213 | 214 | def force_no_ordering(self): 215 | """ 216 | "ORDER BY NULL" prevents MySQL from implicitly ordering by grouped 217 | columns. If no ordering would otherwise be applied, we don't want any 218 | implicit sorting going on. 219 | """ 220 | return ["NULL"] 221 | 222 | def fulltext_search_sql(self, field_name): 223 | return 'MATCH (%s) AGAINST (%%s IN BOOLEAN MODE)' % field_name 224 | 225 | def last_executed_query(self, cursor, sql, params): 226 | # With MySQLdb, cursor objects have an (undocumented) "_last_executed" 227 | # attribute where the exact query sent to the database is saved. 228 | # See MySQLdb/cursors.py in the source distribution. 229 | return cursor._last_executed 230 | 231 | def no_limit_value(self): 232 | # 2**64 - 1, as recommended by the MySQL documentation 233 | return long_type(18446744073709551615) 234 | 235 | def quote_name(self, name): 236 | if name.startswith("`") and name.endswith("`"): 237 | return name # Quoting once is enough. 238 | return "`%s`" % name 239 | 240 | def random_function_sql(self): 241 | return 'RAND()' 242 | 243 | def sql_flush(self, style, tables, sequences): 244 | # NB: The generated SQL below is specific to MySQL 245 | # 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements 246 | # to clear all tables of all data 247 | if tables: 248 | sql = ['SET FOREIGN_KEY_CHECKS = 0;'] 249 | for table in tables: 250 | sql.append('%s %s;' % (style.SQL_KEYWORD('TRUNCATE'), style.SQL_FIELD(self.quote_name(table)))) 251 | sql.append('SET FOREIGN_KEY_CHECKS = 1;') 252 | 253 | # 'ALTER TABLE table AUTO_INCREMENT = 1;'... style SQL statements 254 | # to reset sequence indices 255 | sql.extend(["%s %s %s %s %s;" % \ 256 | (style.SQL_KEYWORD('ALTER'), 257 | style.SQL_KEYWORD('TABLE'), 258 | style.SQL_TABLE(self.quote_name(sequence['table'])), 259 | style.SQL_KEYWORD('AUTO_INCREMENT'), 260 | style.SQL_FIELD('= 1'), 261 | ) for sequence in sequences]) 262 | return sql 263 | else: 264 | return [] 265 | 266 | def value_to_db_datetime(self, value): 267 | if value is None: 268 | return None 269 | 270 | # MySQL doesn't support tz-aware datetimes 271 | if is_aware(value): 272 | if settings.USE_TZ: 273 | value = value.astimezone(utc).replace(tzinfo=None) 274 | else: 275 | raise ValueError("MySQL backend does not support timezone-aware datetimes when USE_TZ is False.") 276 | 277 | # MySQL doesn't support microseconds 278 | return text_type(value.replace(microsecond=0)) 279 | 280 | def value_to_db_time(self, value): 281 | if value is None: 282 | return None 283 | 284 | # MySQL doesn't support tz-aware times 285 | if is_aware(value): 286 | raise ValueError("MySQL backend does not support timezone-aware times.") 287 | 288 | # MySQL doesn't support microseconds 289 | return text_type(value.replace(microsecond=0)) 290 | 291 | def year_lookup_bounds(self, value): 292 | # Again, no microseconds 293 | first = '%s-01-01 00:00:00' 294 | second = '%s-12-31 23:59:59.99' 295 | return [first % value, second % value] 296 | 297 | def max_name_length(self): 298 | return 64 299 | 300 | def bulk_insert_sql(self, fields, num_values): 301 | items_sql = "(%s)" % ", ".join(["%s"] * len(fields)) 302 | return "VALUES " + ", ".join([items_sql] * num_values) 303 | 304 | class DatabaseWrapper(BaseDatabaseWrapper): 305 | vendor = 'mysql' 306 | operators = { 307 | 'exact': '= %s', 308 | 'iexact': 'LIKE %s', 309 | 'contains': 'LIKE BINARY %s', 310 | 'icontains': 'LIKE %s', 311 | 'regex': 'REGEXP BINARY %s', 312 | 'iregex': 'REGEXP %s', 313 | 'gt': '> %s', 314 | 'gte': '>= %s', 315 | 'lt': '< %s', 316 | 'lte': '<= %s', 317 | 'startswith': 'LIKE BINARY %s', 318 | 'endswith': 'LIKE BINARY %s', 319 | 'istartswith': 'LIKE %s', 320 | 'iendswith': 'LIKE %s', 321 | } 322 | 323 | def __init__(self, *args, **kwargs): 324 | super(DatabaseWrapper, self).__init__(*args, **kwargs) 325 | 326 | self.server_version = None 327 | self.features = DatabaseFeatures(self) 328 | self.ops = DatabaseOperations(self) 329 | self.client = DatabaseClient(self) 330 | self.creation = DatabaseCreation(self) 331 | self.introspection = DatabaseIntrospection(self) 332 | self.validation = DatabaseValidation(self) 333 | 334 | def _valid_connection(self): 335 | if self.connection is not None: 336 | try: 337 | self.connection.ping() 338 | return True 339 | except DatabaseError: 340 | self.connection.close() 341 | self.connection = None 342 | return False 343 | 344 | def _cursor(self): 345 | new_connection = False 346 | if not self._valid_connection(): 347 | new_connection = True 348 | kwargs = { 349 | 'conv': django_conversions, 350 | 'charset': 'utf8', 351 | 'use_unicode': True, 352 | } 353 | settings_dict = self.settings_dict 354 | if settings_dict['USER']: 355 | kwargs['user'] = settings_dict['USER'] 356 | if settings_dict['NAME']: 357 | kwargs['db'] = settings_dict['NAME'] 358 | if settings_dict['PASSWORD']: 359 | kwargs['passwd'] = settings_dict['PASSWORD'] 360 | if settings_dict['HOST'].startswith('/'): 361 | kwargs['unix_socket'] = settings_dict['HOST'] 362 | elif settings_dict['HOST']: 363 | kwargs['host'] = settings_dict['HOST'] 364 | if settings_dict['PORT']: 365 | kwargs['port'] = int(settings_dict['PORT']) 366 | # We need the number of potentially affected rows after an 367 | # "UPDATE", not the number of changed rows. 368 | kwargs['client_flag'] = CLIENT.FOUND_ROWS 369 | kwargs.update(settings_dict['OPTIONS']) 370 | self.connection = Database.connect(**kwargs) 371 | self.connection.encoders[SafeUnicode] = self.connection.encoders[text_type] 372 | self.connection.encoders[SafeString] = self.connection.encoders[str] 373 | connection_created.send(sender=self.__class__, connection=self) 374 | cursor = self.connection.cursor() 375 | if new_connection: 376 | # SQL_AUTO_IS_NULL in MySQL controls whether an AUTO_INCREMENT column 377 | # on a recently-inserted row will return when the field is tested for 378 | # NULL. Disabling this value brings this aspect of MySQL in line with 379 | # SQL standards. 380 | cursor.execute('SET SQL_AUTO_IS_NULL = 0') 381 | return CursorWrapper(cursor) 382 | 383 | def _rollback(self): 384 | try: 385 | BaseDatabaseWrapper._rollback(self) 386 | except Database.NotSupportedError: 387 | pass 388 | 389 | def get_server_version(self): 390 | if not self.server_version: 391 | if not self._valid_connection(): 392 | self.cursor() 393 | m = server_version_re.match(self.connection.get_server_info()) 394 | if not m: 395 | raise Exception('Unable to determine MySQL version from version string %r' % self.connection.get_server_info()) 396 | self.server_version = tuple([int(x) for x in m.groups()]) 397 | return self.server_version 398 | 399 | def disable_constraint_checking(self): 400 | """ 401 | Disables foreign key checks, primarily for use in adding rows with forward references. Always returns True, 402 | to indicate constraint checks need to be re-enabled. 403 | """ 404 | self.cursor().execute('SET foreign_key_checks=0') 405 | return True 406 | 407 | def enable_constraint_checking(self): 408 | """ 409 | Re-enable foreign key checks after they have been disabled. 410 | """ 411 | self.cursor().execute('SET foreign_key_checks=1') 412 | 413 | def check_constraints(self, table_names=None): 414 | """ 415 | Checks each table name in `table_names` for rows with invalid foreign key references. This method is 416 | intended to be used in conjunction with `disable_constraint_checking()` and `enable_constraint_checking()`, to 417 | determine if rows with invalid references were entered while constraint checks were off. 418 | 419 | Raises an IntegrityError on the first invalid foreign key reference encountered (if any) and provides 420 | detailed information about the invalid reference in the error message. 421 | 422 | Backends can override this method if they can more directly apply constraint checking (e.g. via "SET CONSTRAINTS 423 | ALL IMMEDIATE") 424 | """ 425 | cursor = self.cursor() 426 | if table_names is None: 427 | table_names = self.introspection.get_table_list(cursor) 428 | for table_name in table_names: 429 | primary_key_column_name = self.introspection.get_primary_key_column(cursor, table_name) 430 | if not primary_key_column_name: 431 | continue 432 | key_columns = self.introspection.get_key_columns(cursor, table_name) 433 | for column_name, referenced_table_name, referenced_column_name in key_columns: 434 | cursor.execute(""" 435 | SELECT REFERRING.`%s`, REFERRING.`%s` FROM `%s` as REFERRING 436 | LEFT JOIN `%s` as REFERRED 437 | ON (REFERRING.`%s` = REFERRED.`%s`) 438 | WHERE REFERRING.`%s` IS NOT NULL AND REFERRED.`%s` IS NULL""" 439 | % (primary_key_column_name, column_name, table_name, referenced_table_name, 440 | column_name, referenced_column_name, column_name, referenced_column_name)) 441 | for bad_row in cursor.fetchall(): 442 | raise utils.IntegrityError("The row in table '%s' with primary key '%s' has an invalid " 443 | "foreign key: %s.%s contains a value '%s' that does not have a corresponding value in %s.%s." 444 | % (table_name, bad_row[0], 445 | table_name, column_name, bad_row[1], 446 | referenced_table_name, referenced_column_name)) 447 | --------------------------------------------------------------------------------