commit c59faac144a45fcb8ec56f37dac346b6da899792 Author: Geert Vanderkelen Date: Fri Oct 2 11:06:51 2009 +0200 Initial upload diff --git a/README b/README new file mode 100644 index 0000000..56c73dc --- /dev/null +++ b/README @@ -0,0 +1,53 @@ + +Experimental Django backend using MySQL Connector/Python +============================================================================== + +Disclaimer +===================== + +!!!!!!!!!!! THIS IS STILL IN DEVELOPMENT !!!!!!!!!!!!!!!!!!! +!!!!!!!! EXPECT THING TO NOT WORK OR GO WRONG !!!!!!!!!!!!!! +!!!! DO NOT USE IN PRODUCTION etc.. etc... !!!!!!!!!!!!!!!!! + +Ah, and make backups! :-) + +Dependencies +===================== + +* Python 2.3 or greater. +* Django 1.2: http://www.djangoproject.com +* MySQL Connector/Python (currently in development) + shell> bzr checkout lp:~mysql/myconnpy/main myconnpy + shell> cd myconnpy + shell> python setup.py install + +Installation +===================== + +To install the Django backend, do the following: + shell> python ./setup.py install + +It will install it in site-packages/mysql/django + +Usage +===================== + +To configure your Django project to use the backend, set the engine in +your settings.py like this: + +DATABASE_ENGINE='mysql.django' + +The above assumes you installed the mysql.django module somewhere where +Python can find it (see Installation). + +Some caveats though: +* When you were using the DATABASE_OPTIONS and stored settings in an + option file, that will not work anymore. Do it the normal Django way. +* You can't use UNIX Sockets (yet). + +Report problems +===================== + +Report problems to Geert Vanderkelen + + diff --git a/build/lib/mysql/__init__.py b/build/lib/mysql/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/mysql/django/__init__.py b/build/lib/mysql/django/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/mysql/django/_version.py b/build/lib/mysql/django/_version.py new file mode 100644 index 0000000..92bb623 --- /dev/null +++ b/build/lib/mysql/django/_version.py @@ -0,0 +1,20 @@ +""" +Connector/Python, native MySQL driver written in Python. +Copyright 2009 Sun Microsystems, Inc. All rights reserved. Use is subject to license terms. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +# This file should be automatically generated on each release +version = (0, 0, 1, 'alpha', '') diff --git a/build/lib/mysql/django/base.py b/build/lib/mysql/django/base.py new file mode 100644 index 0000000..ee29622 --- /dev/null +++ b/build/lib/mysql/django/base.py @@ -0,0 +1,238 @@ +""" +MySQL database backend for Django using MySQL Connector/Python. + +""" + +from django.db.backends import BaseDatabaseWrapper, BaseDatabaseFeatures, BaseDatabaseOperations, util +try: + import mysql.connector as Database +except ImportError, e: + from django.core.exceptions import ImproperlyConfigured + raise ImproperlyConfigured("Error loading MySQLdb module: %s" % e) + +# 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__ +if ( version[:3] < (0, 0, 2) ): + from django.core.exceptions import ImproperlyConfigured + raise ImproperlyConfigured("MySQL Connector/Python 0.0.2 or newer is required; you have %s" % Database.__version__) + +import mysql.connector.conversion +import re + +from django.db.backends import * +from django.db.backends.mysql.client import DatabaseClient +from django.db.backends.mysql.creation import DatabaseCreation +from django.db.backends.mysql.introspection import DatabaseIntrospection +from django.db.backends.mysql.validation import DatabaseValidation +from django.utils.safestring import SafeString, SafeUnicode + +# Raise exceptions for database warnings if DEBUG is on +from django.conf import settings +if settings.DEBUG: + from warnings import filterwarnings + filterwarnings("error", category=Database.Warning) + +DatabaseError = Database.DatabaseError +IntegrityError = Database.IntegrityError + + +class DjangoMySQLConverter(Database.conversion.MySQLConverter): + pass + """ + def _TIME_to_python(self, v, dsc=None): + return util.typecast_time(v) + + def _decimal(self, v, desc=None): + return util.typecast_decimal(v) + """ +# 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})') + +# 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 util.CursorDebugWrapper can be used. Also, using sql_mode +# TRADITIONAL will automatically cause most warnings to be treated as errors. + +class DatabaseFeatures(BaseDatabaseFeatures): + autoindexes_primary_keys = False + inline_fk_references = False + +class DatabaseOperations(BaseDatabaseOperations): + def date_extract_sql(self, lookup_type, field_name): + # http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html + return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) + + def date_trunc_sql(self, lookup_type, field_name): + 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 drop_foreignkey_sql(self): + return "DROP FOREIGN KEY" + + def fulltext_search_sql(self, field_name): + return 'MATCH (%s) AGAINST (%%s IN BOOLEAN MODE)' % field_name + + def limit_offset_sql(self, limit, offset=None): + # 'LIMIT 20,40' + sql = "LIMIT " + if offset and offset != 0: + sql += "%s," % offset + return sql + str(limit) + + def quote_name(self, name): + if name.startswith("`") and name.endswith("`"): + return name # Quoting once is enough. + return "`%s`" % name + + def random_function_sql(self): + return 'RAND()' + + def sql_flush(self, style, tables, sequences): + # 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;'] + for table in tables: + sql.append('%s %s;' % (style.SQL_KEYWORD('TRUNCATE'), style.SQL_FIELD(self.quote_name(table)))) + sql.append('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(self.quote_name(sequence['table'])), + style.SQL_KEYWORD('AUTO_INCREMENT'), + style.SQL_FIELD('= 1'), + ) for sequence in sequences]) + return sql + else: + return [] + + def value_to_db_datetime(self, value): + if value is None: + return None + + # MySQL doesn't support tz-aware datetimes + if value.tzinfo is not None: + raise ValueError("MySQL backend does not support timezone-aware datetimes.") + + # MySQL doesn't support microseconds + return unicode(value.replace(microsecond=0)) + + def value_to_db_time(self, value): + if value is None: + return None + + # MySQL doesn't support tz-aware datetimes + if value.tzinfo is not None: + raise ValueError("MySQL backend does not support timezone-aware datetimes.") + + # MySQL doesn't support microseconds + return unicode(value.replace(microsecond=0)) + + def year_lookup_bounds(self, value): + # Again, no microseconds + first = '%s-01-01 00:00:00' + second = '%s-12-31 23:59:59.99' + return [first % value, second % value] + + +class DatabaseWrapper(BaseDatabaseWrapper): + + 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', + } + + def __init__(self, **kwargs): + super(DatabaseWrapper, self).__init__(**kwargs) + self.server_version = None + + self.features = DatabaseFeatures() + self.ops = DatabaseOperations() + self.client = DatabaseClient() + self.creation = DatabaseCreation(self) + self.introspection = DatabaseIntrospection(self) + self.validation = DatabaseValidation() + + 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, settings): + if not self._valid_connection(): + kwargs = { + #'conv': django_conversions, + 'charset': 'utf8', + 'use_unicode': True, + } + if settings.DATABASE_USER: + kwargs['user'] = settings.DATABASE_USER + if settings.DATABASE_NAME: + kwargs['db'] = settings.DATABASE_NAME + if settings.DATABASE_PASSWORD: + kwargs['passwd'] = settings.DATABASE_PASSWORD + if settings.DATABASE_HOST.startswith('/'): + kwargs['unix_socket'] = settings.DATABASE_HOST + elif settings.DATABASE_HOST: + kwargs['host'] = settings.DATABASE_HOST + if settings.DATABASE_PORT: + kwargs['port'] = int(settings.DATABASE_PORT) + kwargs.update(self.options) + self.connection = Database.connect(**kwargs) + self.connection.set_converter_class(DjangoMySQLConverter) + cursor = self.connection.cursor() + return cursor + + def _rollback(self): + try: + BaseDatabaseWrapper._rollback(self) + except Database.NotSupportedError: + pass + + def get_server_version(self): + if not self.server_version: + if not self._valid_connection(): + self.cursor() + self.server_version = self.connection.get_server_version() + #m = server_version_re.match(self.connection.get_server_version()) + #if not m: + # raise Exception('Unable to determine MySQL version from version string %r' % self.connection.get_server_version()) + #self.server_version = tuple([int(x) for x in m.groups()]) + return self.server_version diff --git a/build/lib/mysql/django/client.py b/build/lib/mysql/django/client.py new file mode 100644 index 0000000..116074a --- /dev/null +++ b/build/lib/mysql/django/client.py @@ -0,0 +1,27 @@ +from django.conf import settings +import os + +def runshell(): + args = [''] + db = settings.DATABASE_OPTIONS.get('db', settings.DATABASE_NAME) + user = settings.DATABASE_OPTIONS.get('user', settings.DATABASE_USER) + passwd = settings.DATABASE_OPTIONS.get('passwd', settings.DATABASE_PASSWORD) + host = settings.DATABASE_OPTIONS.get('host', settings.DATABASE_HOST) + port = settings.DATABASE_OPTIONS.get('port', settings.DATABASE_PORT) + defaults_file = settings.DATABASE_OPTIONS.get('read_default_file') + # Seems to be no good way to set sql_mode with CLI + + if defaults_file: + args += ["--defaults-file=%s" % defaults_file] + if user: + args += ["--user=%s" % user] + if passwd: + args += ["--password=%s" % passwd] + if host: + args += ["--host=%s" % host] + if port: + args += ["--port=%s" % port] + if db: + args += [db] + + os.execvp('mysql', args) diff --git a/build/lib/mysql/django/creation.py b/build/lib/mysql/django/creation.py new file mode 100644 index 0000000..efb351c --- /dev/null +++ b/build/lib/mysql/django/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(%(max_length)s)', + 'CommaSeparatedIntegerField': 'varchar(%(max_length)s)', + 'DateField': 'date', + 'DateTimeField': 'datetime', + 'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)', + 'FileField': 'varchar(%(max_length)s)', + 'FilePathField': 'varchar(%(max_length)s)', + 'FloatField': 'double precision', + 'ImageField': 'varchar(%(max_length)s)', + 'IntegerField': 'integer', + 'IPAddressField': 'char(15)', + 'NullBooleanField': 'bool', + 'OneToOneField': 'integer', + 'PhoneNumberField': 'varchar(20)', + 'PositiveIntegerField': 'integer UNSIGNED', + 'PositiveSmallIntegerField': 'smallint UNSIGNED', + 'SlugField': 'varchar(%(max_length)s)', + 'SmallIntegerField': 'smallint', + 'TextField': 'longtext', + 'TimeField': 'time', + 'USStateField': 'varchar(2)', +} diff --git a/build/lib/mysql/django/introspection.py b/build/lib/mysql/django/introspection.py new file mode 100644 index 0000000..2f1dc7a --- /dev/null +++ b/build/lib/mysql/django/introspection.py @@ -0,0 +1,96 @@ +from base import DatabaseOperations +from mysql.connector.errors import ProgrammingError, OperationalError +from mysql.connector.constants import FieldType +import re + +quote_name = DatabaseOperations().quote_name +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 = { + FieldType.BLOB: 'TextField', + FieldType.STRING: 'CharField', + FieldType.DECIMAL: 'DecimalField', + FieldType.DATE: 'DateField', + FieldType.DATETIME: 'DateTimeField', + FieldType.DOUBLE: 'FloatField', + FieldType.FLOAT: 'FloatField', + FieldType.INT24: 'IntegerField', + FieldType.LONG: 'IntegerField', + FieldType.LONGLONG: 'IntegerField', + FieldType.SHORT: 'IntegerField', + FieldType.STRING: 'CharField', + FieldType.TIMESTAMP: 'DateTimeField', + FieldType.TINY: 'IntegerField', + FieldType.TINY_BLOB: 'TextField', + FieldType.MEDIUM_BLOB: 'TextField', + FieldType.LONG_BLOB: 'TextField', + FieldType.VAR_STRING: 'CharField', +} diff --git a/mysql/__init__.py b/mysql/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mysql/__init__.pyc b/mysql/__init__.pyc new file mode 100644 index 0000000..da7decf Binary files /dev/null and b/mysql/__init__.pyc differ diff --git a/mysql/django/.base.py.swp b/mysql/django/.base.py.swp new file mode 100644 index 0000000..7aad2aa Binary files /dev/null and b/mysql/django/.base.py.swp differ diff --git a/mysql/django/__init__.py b/mysql/django/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mysql/django/__init__.pyc b/mysql/django/__init__.pyc new file mode 100644 index 0000000..a01bb78 Binary files /dev/null and b/mysql/django/__init__.pyc differ diff --git a/mysql/django/_version.py b/mysql/django/_version.py new file mode 100644 index 0000000..92bb623 --- /dev/null +++ b/mysql/django/_version.py @@ -0,0 +1,20 @@ +""" +Connector/Python, native MySQL driver written in Python. +Copyright 2009 Sun Microsystems, Inc. All rights reserved. Use is subject to license terms. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +# This file should be automatically generated on each release +version = (0, 0, 1, 'alpha', '') diff --git a/mysql/django/_version.pyc b/mysql/django/_version.pyc new file mode 100644 index 0000000..388560a Binary files /dev/null and b/mysql/django/_version.pyc differ diff --git a/mysql/django/base.py b/mysql/django/base.py new file mode 100644 index 0000000..ee29622 --- /dev/null +++ b/mysql/django/base.py @@ -0,0 +1,238 @@ +""" +MySQL database backend for Django using MySQL Connector/Python. + +""" + +from django.db.backends import BaseDatabaseWrapper, BaseDatabaseFeatures, BaseDatabaseOperations, util +try: + import mysql.connector as Database +except ImportError, e: + from django.core.exceptions import ImproperlyConfigured + raise ImproperlyConfigured("Error loading MySQLdb module: %s" % e) + +# 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__ +if ( version[:3] < (0, 0, 2) ): + from django.core.exceptions import ImproperlyConfigured + raise ImproperlyConfigured("MySQL Connector/Python 0.0.2 or newer is required; you have %s" % Database.__version__) + +import mysql.connector.conversion +import re + +from django.db.backends import * +from django.db.backends.mysql.client import DatabaseClient +from django.db.backends.mysql.creation import DatabaseCreation +from django.db.backends.mysql.introspection import DatabaseIntrospection +from django.db.backends.mysql.validation import DatabaseValidation +from django.utils.safestring import SafeString, SafeUnicode + +# Raise exceptions for database warnings if DEBUG is on +from django.conf import settings +if settings.DEBUG: + from warnings import filterwarnings + filterwarnings("error", category=Database.Warning) + +DatabaseError = Database.DatabaseError +IntegrityError = Database.IntegrityError + + +class DjangoMySQLConverter(Database.conversion.MySQLConverter): + pass + """ + def _TIME_to_python(self, v, dsc=None): + return util.typecast_time(v) + + def _decimal(self, v, desc=None): + return util.typecast_decimal(v) + """ +# 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})') + +# 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 util.CursorDebugWrapper can be used. Also, using sql_mode +# TRADITIONAL will automatically cause most warnings to be treated as errors. + +class DatabaseFeatures(BaseDatabaseFeatures): + autoindexes_primary_keys = False + inline_fk_references = False + +class DatabaseOperations(BaseDatabaseOperations): + def date_extract_sql(self, lookup_type, field_name): + # http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html + return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) + + def date_trunc_sql(self, lookup_type, field_name): + 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 drop_foreignkey_sql(self): + return "DROP FOREIGN KEY" + + def fulltext_search_sql(self, field_name): + return 'MATCH (%s) AGAINST (%%s IN BOOLEAN MODE)' % field_name + + def limit_offset_sql(self, limit, offset=None): + # 'LIMIT 20,40' + sql = "LIMIT " + if offset and offset != 0: + sql += "%s," % offset + return sql + str(limit) + + def quote_name(self, name): + if name.startswith("`") and name.endswith("`"): + return name # Quoting once is enough. + return "`%s`" % name + + def random_function_sql(self): + return 'RAND()' + + def sql_flush(self, style, tables, sequences): + # 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;'] + for table in tables: + sql.append('%s %s;' % (style.SQL_KEYWORD('TRUNCATE'), style.SQL_FIELD(self.quote_name(table)))) + sql.append('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(self.quote_name(sequence['table'])), + style.SQL_KEYWORD('AUTO_INCREMENT'), + style.SQL_FIELD('= 1'), + ) for sequence in sequences]) + return sql + else: + return [] + + def value_to_db_datetime(self, value): + if value is None: + return None + + # MySQL doesn't support tz-aware datetimes + if value.tzinfo is not None: + raise ValueError("MySQL backend does not support timezone-aware datetimes.") + + # MySQL doesn't support microseconds + return unicode(value.replace(microsecond=0)) + + def value_to_db_time(self, value): + if value is None: + return None + + # MySQL doesn't support tz-aware datetimes + if value.tzinfo is not None: + raise ValueError("MySQL backend does not support timezone-aware datetimes.") + + # MySQL doesn't support microseconds + return unicode(value.replace(microsecond=0)) + + def year_lookup_bounds(self, value): + # Again, no microseconds + first = '%s-01-01 00:00:00' + second = '%s-12-31 23:59:59.99' + return [first % value, second % value] + + +class DatabaseWrapper(BaseDatabaseWrapper): + + 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', + } + + def __init__(self, **kwargs): + super(DatabaseWrapper, self).__init__(**kwargs) + self.server_version = None + + self.features = DatabaseFeatures() + self.ops = DatabaseOperations() + self.client = DatabaseClient() + self.creation = DatabaseCreation(self) + self.introspection = DatabaseIntrospection(self) + self.validation = DatabaseValidation() + + 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, settings): + if not self._valid_connection(): + kwargs = { + #'conv': django_conversions, + 'charset': 'utf8', + 'use_unicode': True, + } + if settings.DATABASE_USER: + kwargs['user'] = settings.DATABASE_USER + if settings.DATABASE_NAME: + kwargs['db'] = settings.DATABASE_NAME + if settings.DATABASE_PASSWORD: + kwargs['passwd'] = settings.DATABASE_PASSWORD + if settings.DATABASE_HOST.startswith('/'): + kwargs['unix_socket'] = settings.DATABASE_HOST + elif settings.DATABASE_HOST: + kwargs['host'] = settings.DATABASE_HOST + if settings.DATABASE_PORT: + kwargs['port'] = int(settings.DATABASE_PORT) + kwargs.update(self.options) + self.connection = Database.connect(**kwargs) + self.connection.set_converter_class(DjangoMySQLConverter) + cursor = self.connection.cursor() + return cursor + + def _rollback(self): + try: + BaseDatabaseWrapper._rollback(self) + except Database.NotSupportedError: + pass + + def get_server_version(self): + if not self.server_version: + if not self._valid_connection(): + self.cursor() + self.server_version = self.connection.get_server_version() + #m = server_version_re.match(self.connection.get_server_version()) + #if not m: + # raise Exception('Unable to determine MySQL version from version string %r' % self.connection.get_server_version()) + #self.server_version = tuple([int(x) for x in m.groups()]) + return self.server_version diff --git a/mysql/django/client.py b/mysql/django/client.py new file mode 100644 index 0000000..116074a --- /dev/null +++ b/mysql/django/client.py @@ -0,0 +1,27 @@ +from django.conf import settings +import os + +def runshell(): + args = [''] + db = settings.DATABASE_OPTIONS.get('db', settings.DATABASE_NAME) + user = settings.DATABASE_OPTIONS.get('user', settings.DATABASE_USER) + passwd = settings.DATABASE_OPTIONS.get('passwd', settings.DATABASE_PASSWORD) + host = settings.DATABASE_OPTIONS.get('host', settings.DATABASE_HOST) + port = settings.DATABASE_OPTIONS.get('port', settings.DATABASE_PORT) + defaults_file = settings.DATABASE_OPTIONS.get('read_default_file') + # Seems to be no good way to set sql_mode with CLI + + if defaults_file: + args += ["--defaults-file=%s" % defaults_file] + if user: + args += ["--user=%s" % user] + if passwd: + args += ["--password=%s" % passwd] + if host: + args += ["--host=%s" % host] + if port: + args += ["--port=%s" % port] + if db: + args += [db] + + os.execvp('mysql', args) diff --git a/mysql/django/creation.py b/mysql/django/creation.py new file mode 100644 index 0000000..efb351c --- /dev/null +++ b/mysql/django/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(%(max_length)s)', + 'CommaSeparatedIntegerField': 'varchar(%(max_length)s)', + 'DateField': 'date', + 'DateTimeField': 'datetime', + 'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)', + 'FileField': 'varchar(%(max_length)s)', + 'FilePathField': 'varchar(%(max_length)s)', + 'FloatField': 'double precision', + 'ImageField': 'varchar(%(max_length)s)', + 'IntegerField': 'integer', + 'IPAddressField': 'char(15)', + 'NullBooleanField': 'bool', + 'OneToOneField': 'integer', + 'PhoneNumberField': 'varchar(20)', + 'PositiveIntegerField': 'integer UNSIGNED', + 'PositiveSmallIntegerField': 'smallint UNSIGNED', + 'SlugField': 'varchar(%(max_length)s)', + 'SmallIntegerField': 'smallint', + 'TextField': 'longtext', + 'TimeField': 'time', + 'USStateField': 'varchar(2)', +} diff --git a/mysql/django/introspection.py b/mysql/django/introspection.py new file mode 100644 index 0000000..2f1dc7a --- /dev/null +++ b/mysql/django/introspection.py @@ -0,0 +1,96 @@ +from base import DatabaseOperations +from mysql.connector.errors import ProgrammingError, OperationalError +from mysql.connector.constants import FieldType +import re + +quote_name = DatabaseOperations().quote_name +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 = { + FieldType.BLOB: 'TextField', + FieldType.STRING: 'CharField', + FieldType.DECIMAL: 'DecimalField', + FieldType.DATE: 'DateField', + FieldType.DATETIME: 'DateTimeField', + FieldType.DOUBLE: 'FloatField', + FieldType.FLOAT: 'FloatField', + FieldType.INT24: 'IntegerField', + FieldType.LONG: 'IntegerField', + FieldType.LONGLONG: 'IntegerField', + FieldType.SHORT: 'IntegerField', + FieldType.STRING: 'CharField', + FieldType.TIMESTAMP: 'DateTimeField', + FieldType.TINY: 'IntegerField', + FieldType.TINY_BLOB: 'TextField', + FieldType.MEDIUM_BLOB: 'TextField', + FieldType.LONG_BLOB: 'TextField', + FieldType.VAR_STRING: 'CharField', +} diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..8db0d9c --- /dev/null +++ b/setup.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Connector/Python, native MySQL driver written in Python. +Copyright 2009 Sun Microsystems, Inc. All rights reserved. Use is subject to license terms. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + +import sys + +from distutils.core import setup +from mysql.django._version import version as mysql_django_version + +_name = 'Django Database Backend using MySQL Connector/Python' +_version = '%d.%d.%d' % mysql_django_version[0:3] +_packages = ['mysql','mysql.django'] + +setup( + name = _name, + version = _version, + author = 'Geert Vanderkelen', + author_email = 'geert.vanderkelen@sun.com', + url = 'http://dev.mysql.com/usingmysql/python/', + download_url = 'http://dev.mysql.com/downloads/connector/python/', + packages = _packages +)