| 1 | """ |
|---|
| 2 | MySQL database backend for Django. |
|---|
| 3 | |
|---|
| 4 | Requires MySQLdb: http://sourceforge.net/projects/mysql-python |
|---|
| 5 | |
|---|
| 6 | Patched svn revision 7852: |
|---|
| 7 | http://code.djangoproject.com/changeset/7852 |
|---|
| 8 | http://code.djangoproject.com/browser/django/trunk/django/db/backends/mysql_old/base.py?rev=7852 |
|---|
| 9 | """ |
|---|
| 10 | |
|---|
| 11 | from django.db.backends import BaseDatabaseWrapper, BaseDatabaseFeatures, BaseDatabaseOperations, util |
|---|
| 12 | from django.utils.encoding import force_unicode |
|---|
| 13 | try: |
|---|
| 14 | import MySQLdb as Database |
|---|
| 15 | except ImportError, e: |
|---|
| 16 | from django.core.exceptions import ImproperlyConfigured |
|---|
| 17 | raise ImproperlyConfigured("Error loading MySQLdb module: %s" % e) |
|---|
| 18 | from MySQLdb.converters import conversions |
|---|
| 19 | from MySQLdb.constants import FIELD_TYPE |
|---|
| 20 | import types |
|---|
| 21 | import re |
|---|
| 22 | |
|---|
| 23 | DatabaseError = Database.DatabaseError |
|---|
| 24 | IntegrityError = Database.IntegrityError |
|---|
| 25 | |
|---|
| 26 | django_conversions = conversions.copy() |
|---|
| 27 | django_conversions.update({ |
|---|
| 28 | types.BooleanType: util.rev_typecast_boolean, |
|---|
| 29 | FIELD_TYPE.DATETIME: util.typecast_timestamp, |
|---|
| 30 | FIELD_TYPE.DATE: util.typecast_date, |
|---|
| 31 | FIELD_TYPE.TIME: util.typecast_time, |
|---|
| 32 | FIELD_TYPE.DECIMAL: util.typecast_decimal, |
|---|
| 33 | FIELD_TYPE.STRING: force_unicode, |
|---|
| 34 | FIELD_TYPE.VAR_STRING: force_unicode, |
|---|
| 35 | # Note: We don't add a convertor for BLOB here. Doesn't seem to be required. |
|---|
| 36 | }) |
|---|
| 37 | |
|---|
| 38 | # This should match the numerical portion of the version numbers (we can treat |
|---|
| 39 | # versions like 5.0.24 and 5.0.24a as the same). Based on the list of version |
|---|
| 40 | # at http://dev.mysql.com/doc/refman/4.1/en/news.html and |
|---|
| 41 | # http://dev.mysql.com/doc/refman/5.0/en/news.html . |
|---|
| 42 | server_version_re = re.compile(r'(\d{1,2})\.(\d{1,2})\.(\d{1,2})') |
|---|
| 43 | |
|---|
| 44 | # This is an extra debug layer over MySQL queries, to display warnings. |
|---|
| 45 | # It's only used when DEBUG=True. |
|---|
| 46 | class MysqlDebugWrapper: |
|---|
| 47 | def __init__(self, cursor): |
|---|
| 48 | self.cursor = cursor |
|---|
| 49 | |
|---|
| 50 | def execute(self, sql, params=()): |
|---|
| 51 | try: |
|---|
| 52 | return self.cursor.execute(sql, params) |
|---|
| 53 | except Database.Warning, w: |
|---|
| 54 | self.cursor.execute("SHOW WARNINGS") |
|---|
| 55 | raise Database.Warning("%s: %s" % (w, self.cursor.fetchall())) |
|---|
| 56 | |
|---|
| 57 | def executemany(self, sql, param_list): |
|---|
| 58 | try: |
|---|
| 59 | return self.cursor.executemany(sql, param_list) |
|---|
| 60 | except Database.Warning, w: |
|---|
| 61 | self.cursor.execute("SHOW WARNINGS") |
|---|
| 62 | raise Database.Warning("%s: %s" % (w, self.cursor.fetchall())) |
|---|
| 63 | |
|---|
| 64 | def __getattr__(self, attr): |
|---|
| 65 | if attr in self.__dict__: |
|---|
| 66 | return self.__dict__[attr] |
|---|
| 67 | else: |
|---|
| 68 | return getattr(self.cursor, attr) |
|---|
| 69 | |
|---|
| 70 | class MysqlUnicodeWrapper: |
|---|
| 71 | """ |
|---|
| 72 | A Wrapper who decode all byte strings to unicode. |
|---|
| 73 | """ |
|---|
| 74 | def __init__(self, cursor): |
|---|
| 75 | self.cursor = cursor |
|---|
| 76 | |
|---|
| 77 | def _decode_results(self, result_raw): |
|---|
| 78 | """ |
|---|
| 79 | decode all byte string to unicode with the server encoding. |
|---|
| 80 | """ |
|---|
| 81 | if not result_raw: |
|---|
| 82 | return result_raw |
|---|
| 83 | result = [] |
|---|
| 84 | for item in result_raw: |
|---|
| 85 | if isinstance(item, str): |
|---|
| 86 | item = item.decode("utf-8") |
|---|
| 87 | result.append(item) |
|---|
| 88 | |
|---|
| 89 | return tuple(result) |
|---|
| 90 | |
|---|
| 91 | def _decode_lines(self, result_raw): |
|---|
| 92 | """ |
|---|
| 93 | decode every line in a resultset. |
|---|
| 94 | """ |
|---|
| 95 | result = [] |
|---|
| 96 | for line in result_raw: |
|---|
| 97 | result.append(self._decode_results(line)) |
|---|
| 98 | return tuple(result) |
|---|
| 99 | |
|---|
| 100 | def fetchone(self): |
|---|
| 101 | result_raw = self.cursor.fetchone() |
|---|
| 102 | return self._decode_results(result_raw) |
|---|
| 103 | |
|---|
| 104 | def fetchall(self): |
|---|
| 105 | result_raw = self.cursor.fetchall() |
|---|
| 106 | return self._decode_lines(result_raw) |
|---|
| 107 | |
|---|
| 108 | def fetchmany(self, *args): |
|---|
| 109 | result_raw = self.cursor.fetchmany(*args) |
|---|
| 110 | return self._decode_lines(result_raw) |
|---|
| 111 | return result_raw |
|---|
| 112 | |
|---|
| 113 | def __getattr__(self, attr): |
|---|
| 114 | if attr in self.__dict__: |
|---|
| 115 | return self.__dict__[attr] |
|---|
| 116 | else: |
|---|
| 117 | return getattr(self.cursor, attr) |
|---|
| 118 | |
|---|
| 119 | class DatabaseFeatures(BaseDatabaseFeatures): |
|---|
| 120 | inline_fk_references = False |
|---|
| 121 | empty_fetchmany_value = () |
|---|
| 122 | update_can_self_select = False |
|---|
| 123 | supports_usecs = False |
|---|
| 124 | |
|---|
| 125 | class DatabaseOperations(BaseDatabaseOperations): |
|---|
| 126 | def date_extract_sql(self, lookup_type, field_name): |
|---|
| 127 | # http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html |
|---|
| 128 | return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) |
|---|
| 129 | |
|---|
| 130 | def date_trunc_sql(self, lookup_type, field_name): |
|---|
| 131 | fields = ['year', 'month', 'day', 'hour', 'minute', 'second'] |
|---|
| 132 | format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape. |
|---|
| 133 | format_def = ('0000-', '01', '-01', ' 00:', '00', ':00') |
|---|
| 134 | try: |
|---|
| 135 | i = fields.index(lookup_type) + 1 |
|---|
| 136 | except ValueError: |
|---|
| 137 | sql = field_name |
|---|
| 138 | else: |
|---|
| 139 | format_str = ''.join([f for f in format[:i]] + [f for f in format_def[i:]]) |
|---|
| 140 | sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str) |
|---|
| 141 | return sql |
|---|
| 142 | |
|---|
| 143 | def drop_foreignkey_sql(self): |
|---|
| 144 | return "DROP FOREIGN KEY" |
|---|
| 145 | |
|---|
| 146 | def fulltext_search_sql(self, field_name): |
|---|
| 147 | return 'MATCH (%s) AGAINST (%%s IN BOOLEAN MODE)' % field_name |
|---|
| 148 | |
|---|
| 149 | def no_limit_value(self): |
|---|
| 150 | # 2**64 - 1, as recommended by the MySQL documentation |
|---|
| 151 | return 18446744073709551615L |
|---|
| 152 | |
|---|
| 153 | def quote_name(self, name): |
|---|
| 154 | if name.startswith("`") and name.endswith("`"): |
|---|
| 155 | return name # Quoting once is enough. |
|---|
| 156 | return "`%s`" % name |
|---|
| 157 | |
|---|
| 158 | def random_function_sql(self): |
|---|
| 159 | return 'RAND()' |
|---|
| 160 | |
|---|
| 161 | def sql_flush(self, style, tables, sequences): |
|---|
| 162 | # NB: The generated SQL below is specific to MySQL |
|---|
| 163 | # 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements |
|---|
| 164 | # to clear all tables of all data |
|---|
| 165 | if tables: |
|---|
| 166 | sql = ['SET FOREIGN_KEY_CHECKS = 0;'] |
|---|
| 167 | for table in tables: |
|---|
| 168 | sql.append('%s %s;' % (style.SQL_KEYWORD('TRUNCATE'), style.SQL_FIELD(self.quote_name(table)))) |
|---|
| 169 | sql.append('SET FOREIGN_KEY_CHECKS = 1;') |
|---|
| 170 | |
|---|
| 171 | # 'ALTER TABLE table AUTO_INCREMENT = 1;'... style SQL statements |
|---|
| 172 | # to reset sequence indices |
|---|
| 173 | sql.extend(["%s %s %s %s %s;" % \ |
|---|
| 174 | (style.SQL_KEYWORD('ALTER'), |
|---|
| 175 | style.SQL_KEYWORD('TABLE'), |
|---|
| 176 | style.SQL_TABLE(self.quote_name(sequence['table'])), |
|---|
| 177 | style.SQL_KEYWORD('AUTO_INCREMENT'), |
|---|
| 178 | style.SQL_FIELD('= 1'), |
|---|
| 179 | ) for sequence in sequences]) |
|---|
| 180 | return sql |
|---|
| 181 | else: |
|---|
| 182 | return [] |
|---|
| 183 | |
|---|
| 184 | class DatabaseWrapper(BaseDatabaseWrapper): |
|---|
| 185 | features = DatabaseFeatures() |
|---|
| 186 | ops = DatabaseOperations() |
|---|
| 187 | operators = { |
|---|
| 188 | 'exact': '= BINARY %s', |
|---|
| 189 | 'iexact': 'LIKE %s', |
|---|
| 190 | 'contains': 'LIKE BINARY %s', |
|---|
| 191 | 'icontains': 'LIKE %s', |
|---|
| 192 | 'regex': 'REGEXP BINARY %s', |
|---|
| 193 | 'iregex': 'REGEXP %s', |
|---|
| 194 | 'gt': '> %s', |
|---|
| 195 | 'gte': '>= %s', |
|---|
| 196 | 'lt': '< %s', |
|---|
| 197 | 'lte': '<= %s', |
|---|
| 198 | 'startswith': 'LIKE BINARY %s', |
|---|
| 199 | 'endswith': 'LIKE BINARY %s', |
|---|
| 200 | 'istartswith': 'LIKE %s', |
|---|
| 201 | 'iendswith': 'LIKE %s', |
|---|
| 202 | } |
|---|
| 203 | |
|---|
| 204 | def __init__(self, **kwargs): |
|---|
| 205 | super(DatabaseWrapper, self).__init__(**kwargs) |
|---|
| 206 | self.server_version = None |
|---|
| 207 | |
|---|
| 208 | def _valid_connection(self): |
|---|
| 209 | if self.connection is not None: |
|---|
| 210 | try: |
|---|
| 211 | self.connection.ping() |
|---|
| 212 | return True |
|---|
| 213 | except DatabaseError: |
|---|
| 214 | self.connection.close() |
|---|
| 215 | self.connection = None |
|---|
| 216 | return False |
|---|
| 217 | |
|---|
| 218 | def _cursor(self, settings): |
|---|
| 219 | if not self._valid_connection(): |
|---|
| 220 | kwargs = { |
|---|
| 221 | # Note: use_unicode intentonally not set to work around some |
|---|
| 222 | # backwards-compat issues. We do it manually. |
|---|
| 223 | 'user': settings.DATABASE_USER, |
|---|
| 224 | 'db': settings.DATABASE_NAME, |
|---|
| 225 | 'passwd': settings.DATABASE_PASSWORD, |
|---|
| 226 | 'conv': django_conversions, |
|---|
| 227 | } |
|---|
| 228 | if settings.DATABASE_HOST.startswith('/'): |
|---|
| 229 | kwargs['unix_socket'] = settings.DATABASE_HOST |
|---|
| 230 | else: |
|---|
| 231 | kwargs['host'] = settings.DATABASE_HOST |
|---|
| 232 | if settings.DATABASE_PORT: |
|---|
| 233 | kwargs['port'] = int(settings.DATABASE_PORT) |
|---|
| 234 | kwargs.update(self.options) |
|---|
| 235 | self.connection = Database.connect(**kwargs) |
|---|
| 236 | cursor = self.connection.cursor() |
|---|
| 237 | if self.connection.get_server_info() >= '4.1' and not self.connection.character_set_name().startswith('utf8'): |
|---|
| 238 | if hasattr(self.connection, 'charset'): |
|---|
| 239 | # MySQLdb < 1.2.1 backwards-compat hacks. |
|---|
| 240 | conn = self.connection |
|---|
| 241 | cursor.execute("SET NAMES 'utf8'") |
|---|
| 242 | cursor.execute("SET CHARACTER SET 'utf8'") |
|---|
| 243 | to_str = lambda u, dummy=None, c=conn: c.literal(u.encode('utf-8')) |
|---|
| 244 | conn.converter[unicode] = to_str |
|---|
| 245 | else: |
|---|
| 246 | self.connection.set_character_set('utf8') |
|---|
| 247 | else: |
|---|
| 248 | cursor = self.connection.cursor() |
|---|
| 249 | |
|---|
| 250 | cursor = MysqlUnicodeWrapper(cursor) |
|---|
| 251 | |
|---|
| 252 | return cursor |
|---|
| 253 | |
|---|
| 254 | def make_debug_cursor(self, cursor): |
|---|
| 255 | return BaseDatabaseWrapper.make_debug_cursor(self, MysqlDebugWrapper(cursor)) |
|---|
| 256 | |
|---|
| 257 | def _rollback(self): |
|---|
| 258 | try: |
|---|
| 259 | BaseDatabaseWrapper._rollback(self) |
|---|
| 260 | except Database.NotSupportedError: |
|---|
| 261 | pass |
|---|
| 262 | |
|---|
| 263 | def get_server_version(self): |
|---|
| 264 | if not self.server_version: |
|---|
| 265 | if not self._valid_connection(): |
|---|
| 266 | self.cursor() |
|---|
| 267 | m = server_version_re.match(self.connection.get_server_info()) |
|---|
| 268 | if not m: |
|---|
| 269 | raise Exception('Unable to determine MySQL version from version string %r' % self.connection.get_server_info()) |
|---|
| 270 | self.server_version = tuple([int(x) for x in m.groups()]) |
|---|
| 271 | return self.server_version |
|---|