From c59faac144a45fcb8ec56f37dac346b6da899792 Mon Sep 17 00:00:00 2001 From: Geert Vanderkelen Date: Fri, 2 Oct 2009 11:06:51 +0200 Subject: [PATCH] Initial upload --- README | 53 ++++++ build/lib/mysql/__init__.py | 0 build/lib/mysql/django/__init__.py | 0 build/lib/mysql/django/_version.py | 20 ++ build/lib/mysql/django/base.py | 238 ++++++++++++++++++++++++ build/lib/mysql/django/client.py | 27 +++ build/lib/mysql/django/creation.py | 29 +++ build/lib/mysql/django/introspection.py | 96 ++++++++++ mysql/__init__.py | 0 mysql/__init__.pyc | Bin 0 -> 150 bytes mysql/django/.base.py.swp | Bin 0 -> 16384 bytes mysql/django/__init__.py | 0 mysql/django/__init__.pyc | Bin 0 -> 157 bytes mysql/django/_version.py | 20 ++ mysql/django/_version.pyc | Bin 0 -> 1086 bytes mysql/django/base.py | 238 ++++++++++++++++++++++++ mysql/django/client.py | 27 +++ mysql/django/creation.py | 29 +++ mysql/django/introspection.py | 96 ++++++++++ setup.py | 39 ++++ 20 files changed, 912 insertions(+) create mode 100644 README create mode 100644 build/lib/mysql/__init__.py create mode 100644 build/lib/mysql/django/__init__.py create mode 100644 build/lib/mysql/django/_version.py create mode 100644 build/lib/mysql/django/base.py create mode 100644 build/lib/mysql/django/client.py create mode 100644 build/lib/mysql/django/creation.py create mode 100644 build/lib/mysql/django/introspection.py create mode 100644 mysql/__init__.py create mode 100644 mysql/__init__.pyc create mode 100644 mysql/django/.base.py.swp create mode 100644 mysql/django/__init__.py create mode 100644 mysql/django/__init__.pyc create mode 100644 mysql/django/_version.py create mode 100644 mysql/django/_version.pyc create mode 100644 mysql/django/base.py create mode 100644 mysql/django/client.py create mode 100644 mysql/django/creation.py create mode 100644 mysql/django/introspection.py create mode 100755 setup.py 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 0000000000000000000000000000000000000000..da7decf014ad8c9bd2fb967d1216adb928dae35a GIT binary patch literal 150 zcmcckiI=Ojw8lG`0SXv_v;zPO2TqreYvw00901BXa-% literal 0 HcmV?d00001 diff --git a/mysql/django/.base.py.swp b/mysql/django/.base.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..7aad2aa25d0de8610630ab7a37567b38eec93797 GIT binary patch literal 16384 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 gazcl+lB5`WgATsrn_AIjKeZ z#rdU0$*KBDRYm&A`FVM%$tC$kx+z(SdFlDOxs}C*IrAGvjY&zM1*`=Vkb-`uQabcE2NhKjUv-LF7Sj3wa#e9t8POkT#xs z%co~S5T0wLc;>W;#DK|}24_PLMmWb62}N7C79Or^BbKG3({}qk zrIn(g$c(nNb$n%;)K^)HVkv3kWyx^M%{|XsG_{}#W!hONA7VK^tN3-`bb?^3NR%9sLVF03QHM}eDo6IN+#!=L zHkZrMlrFjAhDjP%3q+^EE(3-qHw20-?sU2j zutRH-HerJ~pRggl_K)5m%bFxZsrz^TDEP6i+>`FJDD@g;lt4pa4+6w4I8~Mxm28G! zfNuJetLy26;?az5;v|VjlUWy@Xe+Gz9{uw;Vzrh6#equ0l&g_*IJ`*Cuizh_^#}dQ z3~5uZKN(%5DfO-sifJ4tlm7X15GORACgbb$qD7Q~79fUC&{=HSVL0X7F(G|#XK0>9 zN;0RCVbBcEIEE}IMj2*mPvH}wOlq|ZePIayI4E5b1u5;Cn{h>IdQ(stXY2DgKKu)E+EJJQ literal 0 HcmV?d00001 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 +)