diff --git a/django/db/backends/mysql_old/__init__.py b/django/db/backends/mysql_old/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/db/backends/mysql_old/base.py b/django/db/backends/mysql_old/base.py new file mode 100644 index 0000000000..4bd87518e8 --- /dev/null +++ b/django/db/backends/mysql_old/base.py @@ -0,0 +1,233 @@ +""" +MySQL database backend for Django. + +Requires MySQLdb: http://sourceforge.net/projects/mysql-python +""" + +from django.db.backends import util +try: + import MySQLdb as Database +except ImportError, e: + from django.core.exceptions import ImproperlyConfigured + raise ImproperlyConfigured, "Error loading MySQLdb module: %s" % e +from MySQLdb.converters import conversions +from MySQLdb.constants import FIELD_TYPE +import types +import re + +DatabaseError = Database.DatabaseError + +django_conversions = conversions.copy() +django_conversions.update({ + types.BooleanType: util.rev_typecast_boolean, + FIELD_TYPE.DATETIME: util.typecast_timestamp, + FIELD_TYPE.DATE: util.typecast_date, + FIELD_TYPE.TIME: util.typecast_time, +}) + +# 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). Based on the list of version +# at http://dev.mysql.com/doc/refman/4.1/en/news.html and +# http://dev.mysql.com/doc/refman/5.0/en/news.html . +server_version_re = re.compile(r'(\d{1,2})\.(\d{1,2})\.(\d{1,2})') + +# This is an extra debug layer over MySQL queries, to display warnings. +# It's only used when DEBUG=True. +class MysqlDebugWrapper: + def __init__(self, cursor): + self.cursor = cursor + + def execute(self, sql, params=()): + try: + return self.cursor.execute(sql, params) + except Database.Warning, w: + self.cursor.execute("SHOW WARNINGS") + raise Database.Warning, "%s: %s" % (w, self.cursor.fetchall()) + + def executemany(self, sql, param_list): + try: + return self.cursor.executemany(sql, param_list) + except Database.Warning, w: + self.cursor.execute("SHOW WARNINGS") + raise Database.Warning, "%s: %s" % (w, self.cursor.fetchall()) + + def __getattr__(self, attr): + if self.__dict__.has_key(attr): + return self.__dict__[attr] + else: + return getattr(self.cursor, attr) + +try: + # Only exists in Python 2.4+ + from threading import local +except ImportError: + # Import copy of _thread_local.py from Python 2.4 + from django.utils._threading_local import local + +class DatabaseWrapper(local): + def __init__(self, **kwargs): + self.connection = None + self.queries = [] + self.server_version = None + self.options = kwargs + + def _valid_connection(self): + if self.connection is not None: + try: + self.connection.ping() + return True + except DatabaseError: + self.connection.close() + self.connection = None + return False + + def cursor(self): + from django.conf import settings + if not self._valid_connection(): + kwargs = { + 'user': settings.DATABASE_USER, + 'db': settings.DATABASE_NAME, + 'passwd': settings.DATABASE_PASSWORD, + 'conv': django_conversions, + } + if settings.DATABASE_HOST.startswith('/'): + kwargs['unix_socket'] = settings.DATABASE_HOST + else: + kwargs['host'] = settings.DATABASE_HOST + if settings.DATABASE_PORT: + kwargs['port'] = int(settings.DATABASE_PORT) + kwargs.update(self.options) + self.connection = Database.connect(**kwargs) + cursor = self.connection.cursor() + if self.connection.get_server_info() >= '4.1': + cursor.execute("SET NAMES 'utf8'") + else: + cursor = self.connection.cursor() + if settings.DEBUG: + return util.CursorDebugWrapper(MysqlDebugWrapper(cursor), self) + return cursor + + def _commit(self): + if self.connection is not None: + self.connection.commit() + + def _rollback(self): + if self.connection is not None: + try: + self.connection.rollback() + except Database.NotSupportedError: + pass + + def close(self): + if self.connection is not None: + self.connection.close() + self.connection = None + + def get_server_version(self): + if not self.server_version: + if not self._valid_connection(): + self.cursor() + m = server_version_re.match(self.connection.get_server_info()) + if not m: + raise Exception('Unable to determine MySQL version from version string %r' % self.connection.get_server_info()) + self.server_version = tuple([int(x) for x in m.groups()]) + return self.server_version + +supports_constraints = True + +def quote_name(name): + if name.startswith("`") and name.endswith("`"): + return name # Quoting once is enough. + return "`%s`" % name + +dictfetchone = util.dictfetchone +dictfetchmany = util.dictfetchmany +dictfetchall = util.dictfetchall + +def get_last_insert_id(cursor, table_name, pk_name): + return cursor.lastrowid + +def get_date_extract_sql(lookup_type, table_name): + # lookup_type is 'year', 'month', 'day' + # http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html + return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), table_name) + +def get_date_trunc_sql(lookup_type, field_name): + # lookup_type is 'year', 'month', 'day' + fields = ['year', 'month', 'day', 'hour', 'minute', 'second'] + format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape. + format_def = ('0000-', '01', '-01', ' 00:', '00', ':00') + try: + i = fields.index(lookup_type) + 1 + except ValueError: + sql = field_name + else: + format_str = ''.join([f for f in format[:i]] + [f for f in format_def[i:]]) + sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str) + return sql + +def get_limit_offset_sql(limit, offset=None): + sql = "LIMIT " + if offset and offset != 0: + sql += "%s," % offset + return sql + str(limit) + +def get_random_function_sql(): + return "RAND()" + +def get_deferrable_sql(): + return "" + +def get_fulltext_search_sql(field_name): + return 'MATCH (%s) AGAINST (%%s IN BOOLEAN MODE)' % field_name + +def get_drop_foreignkey_sql(): + return "DROP FOREIGN KEY" + +def get_pk_default_value(): + return "DEFAULT" + +def get_sql_flush(style, tables, sequences): + """Return a list of SQL statements required to remove all data from + all tables in the database (without actually removing the tables + themselves) and put the database in an empty 'initial' state + + """ + # NB: The generated SQL below is specific to MySQL + # 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements + # to clear all tables of all data + if tables: + sql = ['SET FOREIGN_KEY_CHECKS = 0;'] + \ + ['%s %s;' % \ + (style.SQL_KEYWORD('TRUNCATE'), + style.SQL_FIELD(quote_name(table)) + ) for table in tables] + \ + ['SET FOREIGN_KEY_CHECKS = 1;'] + + # 'ALTER TABLE table AUTO_INCREMENT = 1;'... style SQL statements + # to reset sequence indices + sql.extend(["%s %s %s %s %s;" % \ + (style.SQL_KEYWORD('ALTER'), + style.SQL_KEYWORD('TABLE'), + style.SQL_TABLE(quote_name(sequence['table'])), + style.SQL_KEYWORD('AUTO_INCREMENT'), + style.SQL_FIELD('= 1'), + ) for sequence in sequences]) + return sql + else: + return [] + +OPERATOR_MAPPING = { + 'exact': '= %s', + 'iexact': 'LIKE %s', + 'contains': 'LIKE BINARY %s', + 'icontains': 'LIKE %s', + 'gt': '> %s', + 'gte': '>= %s', + 'lt': '< %s', + 'lte': '<= %s', + 'startswith': 'LIKE BINARY %s', + 'endswith': 'LIKE BINARY %s', + 'istartswith': 'LIKE %s', + 'iendswith': 'LIKE %s', +} diff --git a/django/db/backends/mysql_old/client.py b/django/db/backends/mysql_old/client.py new file mode 100644 index 0000000000..f9d6297b8e --- /dev/null +++ b/django/db/backends/mysql_old/client.py @@ -0,0 +1,14 @@ +from django.conf import settings +import os + +def runshell(): + args = [''] + args += ["--user=%s" % settings.DATABASE_USER] + if settings.DATABASE_PASSWORD: + args += ["--password=%s" % settings.DATABASE_PASSWORD] + if settings.DATABASE_HOST: + args += ["--host=%s" % settings.DATABASE_HOST] + if settings.DATABASE_PORT: + args += ["--port=%s" % settings.DATABASE_PORT] + args += [settings.DATABASE_NAME] + os.execvp('mysql', args) diff --git a/django/db/backends/mysql_old/creation.py b/django/db/backends/mysql_old/creation.py new file mode 100644 index 0000000000..22ed901653 --- /dev/null +++ b/django/db/backends/mysql_old/creation.py @@ -0,0 +1,29 @@ +# 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', + 'BooleanField': 'bool', + 'CharField': 'varchar(%(maxlength)s)', + 'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)', + 'DateField': 'date', + 'DateTimeField': 'datetime', + 'FileField': 'varchar(100)', + 'FilePathField': 'varchar(100)', + 'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)', + 'ImageField': 'varchar(100)', + 'IntegerField': 'integer', + 'IPAddressField': 'char(15)', + 'ManyToManyField': None, + 'NullBooleanField': 'bool', + 'OneToOneField': 'integer', + 'PhoneNumberField': 'varchar(20)', + 'PositiveIntegerField': 'integer UNSIGNED', + 'PositiveSmallIntegerField': 'smallint UNSIGNED', + 'SlugField': 'varchar(%(maxlength)s)', + 'SmallIntegerField': 'smallint', + 'TextField': 'longtext', + 'TimeField': 'time', + 'USStateField': 'varchar(2)', +} diff --git a/django/db/backends/mysql_old/introspection.py b/django/db/backends/mysql_old/introspection.py new file mode 100644 index 0000000000..7829457fa9 --- /dev/null +++ b/django/db/backends/mysql_old/introspection.py @@ -0,0 +1,95 @@ +from django.db.backends.mysql.base import quote_name +from MySQLdb import ProgrammingError, OperationalError +from MySQLdb.constants import FIELD_TYPE +import re + +foreign_key_re = re.compile(r"\sCONSTRAINT `[^`]*` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)` \(`([^`]*)`\)") + +def get_table_list(cursor): + "Returns a list of table names in the current database." + cursor.execute("SHOW TABLES") + return [row[0] for row in cursor.fetchall()] + +def get_table_description(cursor, table_name): + "Returns a description of the table, with the DB-API cursor.description interface." + cursor.execute("SELECT * FROM %s LIMIT 1" % quote_name(table_name)) + return cursor.description + +def _name_to_index(cursor, table_name): + """ + Returns a dictionary of {field_name: field_index} for the given table. + Indexes are 0-based. + """ + return dict([(d[0], i) for i, d in enumerate(get_table_description(cursor, table_name))]) + +def get_relations(cursor, table_name): + """ + Returns a dictionary of {field_index: (field_index_other_table, other_table)} + representing all relationships to the given table. Indexes are 0-based. + """ + my_field_dict = _name_to_index(cursor, table_name) + constraints = [] + relations = {} + try: + # This should work for MySQL 5.0. + cursor.execute(""" + SELECT column_name, referenced_table_name, referenced_column_name + FROM information_schema.key_column_usage + WHERE table_name = %s + AND table_schema = DATABASE() + AND referenced_table_name IS NOT NULL + AND referenced_column_name IS NOT NULL""", [table_name]) + constraints.extend(cursor.fetchall()) + except (ProgrammingError, OperationalError): + # Fall back to "SHOW CREATE TABLE", for previous MySQL versions. + # Go through all constraints and save the equal matches. + cursor.execute("SHOW CREATE TABLE %s" % quote_name(table_name)) + for row in cursor.fetchall(): + pos = 0 + while True: + match = foreign_key_re.search(row[1], pos) + if match == None: + break + pos = match.end() + constraints.append(match.groups()) + + for my_fieldname, other_table, other_field in constraints: + other_field_index = _name_to_index(cursor, other_table)[other_field] + my_field_index = my_field_dict[my_fieldname] + relations[my_field_index] = (other_field_index, other_table) + + return relations + +def get_indexes(cursor, table_name): + """ + Returns a dictionary of fieldname -> infodict for the given table, + where each infodict is in the format: + {'primary_key': boolean representing whether it's the primary key, + 'unique': boolean representing whether it's a unique index} + """ + cursor.execute("SHOW INDEX FROM %s" % quote_name(table_name)) + indexes = {} + for row in cursor.fetchall(): + indexes[row[4]] = {'primary_key': (row[2] == 'PRIMARY'), 'unique': not bool(row[1])} + return indexes + +DATA_TYPES_REVERSE = { + FIELD_TYPE.BLOB: 'TextField', + FIELD_TYPE.CHAR: 'CharField', + FIELD_TYPE.DECIMAL: 'FloatField', + FIELD_TYPE.DATE: 'DateField', + FIELD_TYPE.DATETIME: 'DateTimeField', + FIELD_TYPE.DOUBLE: 'FloatField', + FIELD_TYPE.FLOAT: 'FloatField', + FIELD_TYPE.INT24: 'IntegerField', + FIELD_TYPE.LONG: 'IntegerField', + FIELD_TYPE.LONGLONG: 'IntegerField', + FIELD_TYPE.SHORT: 'IntegerField', + FIELD_TYPE.STRING: 'TextField', + FIELD_TYPE.TIMESTAMP: 'DateTimeField', + FIELD_TYPE.TINY: 'IntegerField', + FIELD_TYPE.TINY_BLOB: 'TextField', + FIELD_TYPE.MEDIUM_BLOB: 'TextField', + FIELD_TYPE.LONG_BLOB: 'TextField', + FIELD_TYPE.VAR_STRING: 'CharField', +} diff --git a/docs/settings.txt b/docs/settings.txt index b41281ee49..6f85e312c0 100644 --- a/docs/settings.txt +++ b/docs/settings.txt @@ -245,7 +245,8 @@ DATABASE_ENGINE Default: ``''`` (Empty string) Which database backend to use. Either ``'postgresql_psycopg2'``, -``'postgresql'``, ``'mysql'``, ``'sqlite3'`` or ``'ado_mssql'``. +``'postgresql'``, ``'mysql'``, ``'mysql_old'``, ``'sqlite3'`` or +``'ado_mssql'``. DATABASE_HOST -------------