1
0
mirror of https://github.com/django/django.git synced 2025-07-05 10:19:20 +00:00

[soc2009/multidb] Updated the test runner to support syncing all the databases django knows about so that tests can operate against more than one database

git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/multidb@10895 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Alex Gaynor 2009-06-03 02:13:01 +00:00
parent f280c325cd
commit 9286db5145
14 changed files with 110 additions and 53 deletions

View File

@ -7,7 +7,6 @@ that need to be done. I'm trying to be as granular as possible.
2) Update all old references to ``settings.DATABASE_*`` to reference 2) Update all old references to ``settings.DATABASE_*`` to reference
``settings.DATABASES``. This includes the following locations ``settings.DATABASES``. This includes the following locations
* howto/custom-model-fields -- defered since it refers to custom model fields
* internals/contributing -- still needs an update on TEST_* * internals/contributing -- still needs an update on TEST_*
* ref/settings -- needs to be upddated for TEST_* * ref/settings -- needs to be upddated for TEST_*
* topics/testing -- needs update for the TEST_* settings, plus test refactor * topics/testing -- needs update for the TEST_* settings, plus test refactor
@ -24,22 +23,17 @@ that need to be done. I'm trying to be as granular as possible.
``--database`` flag to overide that? ``--database`` flag to overide that?
These items will be fixed pending both community consensus, and the API These items will be fixed pending both community consensus, and the API
that will go in that's actually necessary for these to happen. that will go in that's actually necessary for these to happen. Due to
internal APIs loaddata probably will need an update to load stuff into a
specific DB.
4) Rig up the test harness to work with multiple databases. This includes: 4) Rig up the test harness to work with multiple databases. This includes:
* Figure out how we can actually test multiple databases. If the user has * The current strategy is to test on N dbs, where N is however many the
more than one database in ``settings.DATABASES`` we can just use the test user defines and ensuring the data all stays seperate and no exceptions
database for each of them. Otherwise we are going to have to make some are raised. Practically speaking this means we're only going to have
assumptions. Either just go for SQLite, since that's going to be easiest good coverage if we write a lot of tests that can break. That's life.
(and going forward it will be included in all versions of Python we work
with), or we can try to create a database with ``test_2_`` prefix.
Similar to how we use a ``test_`` prefix by default.
5) Add the ``using`` Meta option. Tests and docs(these are to be assumed at
each stage from here on out).
6) Add the ``using`` method to ``QuerySet``. This will more or less "just
work" across multiple databases that use the same backend. However, it
will fail gratuitously when trying to use 2 different backends.
7) Remove any references to the global ``django.db.connection`` object in the 7) Remove any references to the global ``django.db.connection`` object in the
SQL creation process. This includes(but is probably not limited to): SQL creation process. This includes(but is probably not limited to):
@ -57,6 +51,13 @@ that need to be done. I'm trying to be as granular as possible.
need to be totally refactored. There's a ticket to at least move that need to be totally refactored. There's a ticket to at least move that
raw SQL and execution to ``Query``/``QuerySet`` so hopefully that makes raw SQL and execution to ``Query``/``QuerySet`` so hopefully that makes
it in before I need to tackle this. it in before I need to tackle this.
5) Add the ``using`` Meta option. Tests and docs(these are to be assumed at
each stage from here on out).
6) Add the ``using`` method to ``QuerySet``. This will more or less "just
work" across multiple databases that use the same backend. However, it
will fail gratuitously when trying to use 2 different backends.
8) Implement some way to create a new ``Query`` for a different backend when 8) Implement some way to create a new ``Query`` for a different backend when
we switch. There are several checks against ``self.connection`` prior to we switch. There are several checks against ``self.connection`` prior to
SQL construction, so we either need to defer all these(which will be SQL construction, so we either need to defer all these(which will be

View File

@ -33,7 +33,7 @@ class Command(NoArgsCommand):
if not options['database']: if not options['database']:
dbs = connections.all() dbs = connections.all()
else: else:
dbs = [options['database']] dbs = [connections[options['database']]]
for connection in dbs: for connection in dbs:
# Import the 'management' module within each installed app, to register # Import the 'management' module within each installed app, to register
@ -154,6 +154,15 @@ class Command(NoArgsCommand):
else: else:
transaction.commit_unless_managed() transaction.commit_unless_managed()
<<<<<<< HEAD:django/core/management/commands/syncdb.py
# Install the 'initial_data' fixture, using format discovery # Install the 'initial_data' fixture, using format discovery
from django.core.management import call_command from django.core.management import call_command
call_command('loaddata', 'initial_data', verbosity=verbosity) call_command('loaddata', 'initial_data', verbosity=verbosity)
=======
# Install the 'initial_data' fixture, using format discovery
# FIXME we only load the fixture data for one DB right now, since we
# can't control what DB it does into, once we can control this we
# should move it back into the DB loop
from django.core.management import call_command
call_command('loaddata', 'initial_data', verbosity=verbosity)
>>>>>>> 2c764d3ff7cb665ec919d1f3e2977587752c6f2c:django/core/management/commands/syncdb.py

View File

@ -40,7 +40,7 @@ class BaseDatabaseCreation(object):
pending_references = {} pending_references = {}
qn = self.connection.ops.quote_name qn = self.connection.ops.quote_name
for f in opts.local_fields: for f in opts.local_fields:
col_type = f.db_type() col_type = f.db_type(self.connection)
tablespace = f.db_tablespace or opts.db_tablespace tablespace = f.db_tablespace or opts.db_tablespace
if col_type is None: if col_type is None:
# Skip ManyToManyFields, because they're not represented as # Skip ManyToManyFields, because they're not represented as
@ -68,7 +68,7 @@ class BaseDatabaseCreation(object):
table_output.append(' '.join(field_output)) table_output.append(' '.join(field_output))
if opts.order_with_respect_to: if opts.order_with_respect_to:
table_output.append(style.SQL_FIELD(qn('_order')) + ' ' + \ table_output.append(style.SQL_FIELD(qn('_order')) + ' ' + \
style.SQL_COLTYPE(models.IntegerField().db_type())) style.SQL_COLTYPE(models.IntegerField().db_type(self.connection)))
for field_constraints in opts.unique_together: for field_constraints in opts.unique_together:
table_output.append(style.SQL_KEYWORD('UNIQUE') + ' (%s)' % \ table_output.append(style.SQL_KEYWORD('UNIQUE') + ' (%s)' % \
", ".join([style.SQL_FIELD(qn(opts.get_field(f).column)) for f in field_constraints])) ", ".join([style.SQL_FIELD(qn(opts.get_field(f).column)) for f in field_constraints]))
@ -166,7 +166,7 @@ class BaseDatabaseCreation(object):
style.SQL_TABLE(qn(f.m2m_db_table())) + ' ('] style.SQL_TABLE(qn(f.m2m_db_table())) + ' (']
table_output.append(' %s %s %s%s,' % table_output.append(' %s %s %s%s,' %
(style.SQL_FIELD(qn('id')), (style.SQL_FIELD(qn('id')),
style.SQL_COLTYPE(models.AutoField(primary_key=True).db_type()), style.SQL_COLTYPE(models.AutoField(primary_key=True).db_type(self.connection)),
style.SQL_KEYWORD('NOT NULL PRIMARY KEY'), style.SQL_KEYWORD('NOT NULL PRIMARY KEY'),
tablespace_sql)) tablespace_sql))
@ -211,14 +211,14 @@ class BaseDatabaseCreation(object):
table_output = [ table_output = [
' %s %s %s %s (%s)%s,' % ' %s %s %s %s (%s)%s,' %
(style.SQL_FIELD(qn(field.m2m_column_name())), (style.SQL_FIELD(qn(field.m2m_column_name())),
style.SQL_COLTYPE(models.ForeignKey(model).db_type()), style.SQL_COLTYPE(models.ForeignKey(model).db_type(self.connection)),
style.SQL_KEYWORD('NOT NULL REFERENCES'), style.SQL_KEYWORD('NOT NULL REFERENCES'),
style.SQL_TABLE(qn(opts.db_table)), style.SQL_TABLE(qn(opts.db_table)),
style.SQL_FIELD(qn(opts.pk.column)), style.SQL_FIELD(qn(opts.pk.column)),
self.connection.ops.deferrable_sql()), self.connection.ops.deferrable_sql()),
' %s %s %s %s (%s)%s,' % ' %s %s %s %s (%s)%s,' %
(style.SQL_FIELD(qn(field.m2m_reverse_name())), (style.SQL_FIELD(qn(field.m2m_reverse_name())),
style.SQL_COLTYPE(models.ForeignKey(field.rel.to).db_type()), style.SQL_COLTYPE(models.ForeignKey(field.rel.to).db_type(self.connection)),
style.SQL_KEYWORD('NOT NULL REFERENCES'), style.SQL_KEYWORD('NOT NULL REFERENCES'),
style.SQL_TABLE(qn(field.rel.to._meta.db_table)), style.SQL_TABLE(qn(field.rel.to._meta.db_table)),
style.SQL_FIELD(qn(field.rel.to._meta.pk.column)), style.SQL_FIELD(qn(field.rel.to._meta.pk.column)),
@ -310,7 +310,7 @@ class BaseDatabaseCreation(object):
output.append(ds) output.append(ds)
return output return output
def create_test_db(self, verbosity=1, autoclobber=False): def create_test_db(self, verbosity=1, autoclobber=False, alias=''):
""" """
Creates a test database, prompting the user for confirmation if the Creates a test database, prompting the user for confirmation if the
database already exists. Returns the name of the test database created. database already exists. Returns the name of the test database created.
@ -325,7 +325,10 @@ class BaseDatabaseCreation(object):
can_rollback = self._rollback_works() can_rollback = self._rollback_works()
self.connection.settings_dict["DATABASE_SUPPORTS_TRANSACTIONS"] = can_rollback self.connection.settings_dict["DATABASE_SUPPORTS_TRANSACTIONS"] = can_rollback
call_command('syncdb', verbosity=verbosity, interactive=False) # FIXME we end up loading the same fixture into the default DB for each
# DB we have, this causes various test failures, but can't really be
# fixed until we have an API for saving to a specific DB
call_command('syncdb', verbosity=verbosity, interactive=False, database=alias)
if settings.CACHE_BACKEND.startswith('db://'): if settings.CACHE_BACKEND.startswith('db://'):
from django.core.cache import parse_backend_uri from django.core.cache import parse_backend_uri

View File

@ -48,11 +48,11 @@ class DatabaseCreation(BaseDatabaseCreation):
table_output = [ table_output = [
' %s %s %s,' % ' %s %s %s,' %
(style.SQL_FIELD(qn(field.m2m_column_name())), (style.SQL_FIELD(qn(field.m2m_column_name())),
style.SQL_COLTYPE(models.ForeignKey(model).db_type()), style.SQL_COLTYPE(models.ForeignKey(model).db_type(self.connection)),
style.SQL_KEYWORD('NOT NULL')), style.SQL_KEYWORD('NOT NULL')),
' %s %s %s,' % ' %s %s %s,' %
(style.SQL_FIELD(qn(field.m2m_reverse_name())), (style.SQL_FIELD(qn(field.m2m_reverse_name())),
style.SQL_COLTYPE(models.ForeignKey(field.rel.to).db_type()), style.SQL_COLTYPE(models.ForeignKey(field.rel.to).db_type(self.connection)),
style.SQL_KEYWORD('NOT NULL')) style.SQL_KEYWORD('NOT NULL'))
] ]
deferred = [ deferred = [

View File

@ -118,10 +118,10 @@ class Field(object):
""" """
return value return value
def db_type(self): def db_type(self, connection):
""" """
Returns the database column data type for this field, taking into Returns the database column data type for this field, for the provided
account the DATABASE_ENGINE setting. connection.
""" """
# The default implementation of this method looks at the # The default implementation of this method looks at the
# backend-specific DATA_TYPES dictionary, looking up the field by its # backend-specific DATA_TYPES dictionary, looking up the field by its

View File

@ -731,7 +731,7 @@ class ForeignKey(RelatedField, Field):
defaults.update(kwargs) defaults.update(kwargs)
return super(ForeignKey, self).formfield(**defaults) return super(ForeignKey, self).formfield(**defaults)
def db_type(self): def db_type(self, connection):
# The database column type of a ForeignKey is the column type # The database column type of a ForeignKey is the column type
# of the field to which it points. An exception is if the ForeignKey # of the field to which it points. An exception is if the ForeignKey
# points to an AutoField/PositiveIntegerField/PositiveSmallIntegerField, # points to an AutoField/PositiveIntegerField/PositiveSmallIntegerField,
@ -743,8 +743,8 @@ class ForeignKey(RelatedField, Field):
(not connection.features.related_fields_match_type and (not connection.features.related_fields_match_type and
isinstance(rel_field, (PositiveIntegerField, isinstance(rel_field, (PositiveIntegerField,
PositiveSmallIntegerField)))): PositiveSmallIntegerField)))):
return IntegerField().db_type() return IntegerField().db_type(connection)
return rel_field.db_type() return rel_field.db_type(connection)
class OneToOneField(ForeignKey): class OneToOneField(ForeignKey):
""" """
@ -954,8 +954,7 @@ class ManyToManyField(RelatedField, Field):
defaults['initial'] = [i._get_pk_val() for i in initial] defaults['initial'] = [i._get_pk_val() for i in initial]
return super(ManyToManyField, self).formfield(**defaults) return super(ManyToManyField, self).formfield(**defaults)
def db_type(self): def db_type(self, connection):
# A ManyToManyField is not represented by a single column, # A ManyToManyField is not represented by a single column,
# so return None. # so return None.
return None return None

View File

@ -267,7 +267,9 @@ class Constraint(object):
try: try:
if self.field: if self.field:
params = self.field.get_db_prep_lookup(lookup_type, value) params = self.field.get_db_prep_lookup(lookup_type, value)
db_type = self.field.db_type() # FIXME, we're using the global connection object here, once a
# WhereNode know's it's connection we should pass that through
db_type = self.field.db_type(connection)
else: else:
# This branch is used at times when we add a comparison to NULL # This branch is used at times when we add a comparison to NULL
# (we don't really want to waste time looking up the associated # (we don't really want to waste time looking up the associated
@ -278,4 +280,3 @@ class Constraint(object):
raise EmptyShortCircuit raise EmptyShortCircuit
return (self.alias, self.col, db_type), params return (self.alias, self.col, db_type), params

View File

@ -1,5 +1,6 @@
import os import os
from django.conf import settings
from django.utils.importlib import import_module from django.utils.importlib import import_module
def load_backend(backend_name): def load_backend(backend_name):
@ -39,6 +40,10 @@ class ConnectionHandler(object):
conn = self.databases[alias] conn = self.databases[alias]
conn.setdefault('DATABASE_ENGINE', 'dummy') conn.setdefault('DATABASE_ENGINE', 'dummy')
conn.setdefault('DATABASE_OPTIONS', {}) conn.setdefault('DATABASE_OPTIONS', {})
conn.setdefault('TEST_DATABASE_CHARSET', None)
conn.setdefault('TEST_DATABASE_COLLATION', None)
conn.setdefault('TEST_DATABASE_NAME', None)
conn.setdefault('TIME_ZONE', settings.TIME_ZONE)
for setting in ('DATABASE_NAME', 'DATABASE_USER', 'DATABASE_PASSWORD', for setting in ('DATABASE_NAME', 'DATABASE_USER', 'DATABASE_PASSWORD',
'DATABASE_HOST', 'DATABASE_PORT'): 'DATABASE_HOST', 'DATABASE_PORT'):
conn.setdefault(setting, '') conn.setdefault(setting, '')
@ -54,5 +59,8 @@ class ConnectionHandler(object):
self._connections[alias] = conn self._connections[alias] = conn
return conn return conn
def __iter__(self):
return iter(self.databases)
def all(self): def all(self):
return [self[alias] for alias in self.databases] return [self[alias] for alias in self.databases]

View File

@ -186,11 +186,15 @@ def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[]):
suite = reorder_suite(suite, (TestCase,)) suite = reorder_suite(suite, (TestCase,))
old_name = settings.DATABASE_NAME old_names = []
from django.db import connection from django.db import connections
connection.creation.create_test_db(verbosity, autoclobber=not interactive) for alias in connections:
connection = connections[alias]
old_names.append((connection, connection.settings_dict['DATABASE_NAME']))
connection.creation.create_test_db(verbosity, autoclobber=not interactive, alias=alias)
result = unittest.TextTestRunner(verbosity=verbosity).run(suite) result = unittest.TextTestRunner(verbosity=verbosity).run(suite)
connection.creation.destroy_test_db(old_name, verbosity) for connection, old_name in old_names:
connection.creation.destroy_test_db(old_name, verbosity)
teardown_test_environment() teardown_test_environment()

View File

@ -7,7 +7,7 @@ from django.conf import settings
from django.core import mail from django.core import mail
from django.core.management import call_command from django.core.management import call_command
from django.core.urlresolvers import clear_url_caches from django.core.urlresolvers import clear_url_caches
from django.db import transaction, connection from django.db import transaction, connections
from django.http import QueryDict from django.http import QueryDict
from django.test import _doctest as doctest from django.test import _doctest as doctest
from django.test.client import Client from django.test.client import Client
@ -427,6 +427,13 @@ class TransactionTestCase(unittest.TestCase):
(u"Template '%s' was used unexpectedly in rendering the" (u"Template '%s' was used unexpectedly in rendering the"
u" response") % template_name) u" response") % template_name)
def connections_support_transactions():
"""
Returns True if all connections support transactions. This is messy
because 2.4 doesn't support any or all.
"""
return len([None for conn in connections.all() if conn.settings_dict['DATABASE_SUPPORTS_TRANSACTIONS']]) == len(connections.all())
class TestCase(TransactionTestCase): class TestCase(TransactionTestCase):
""" """
Does basically the same as TransactionTestCase, but surrounds every test Does basically the same as TransactionTestCase, but surrounds every test
@ -436,7 +443,7 @@ class TestCase(TransactionTestCase):
""" """
def _fixture_setup(self): def _fixture_setup(self):
if not connection.settings_dict['DATABASE_SUPPORTS_TRANSACTIONS']: if not connections_support_transactions():
return super(TestCase, self)._fixture_setup() return super(TestCase, self)._fixture_setup()
transaction.enter_transaction_management() transaction.enter_transaction_management()
@ -453,10 +460,11 @@ class TestCase(TransactionTestCase):
}) })
def _fixture_teardown(self): def _fixture_teardown(self):
if not connection.settings_dict['DATABASE_SUPPORTS_TRANSACTIONS']: if not connections_support_transactions():
return super(TestCase, self)._fixture_teardown() return super(TestCase, self)._fixture_teardown()
restore_transaction_methods() restore_transaction_methods()
transaction.rollback() transaction.rollback()
transaction.leave_transaction_management() transaction.leave_transaction_management()
connection.close() for connection in connections.all():
connection.close()

View File

@ -1,6 +1,5 @@
import sys, time, os import sys, time, os
from django.conf import settings from django.conf import settings
from django.db import connection
from django.core import mail from django.core import mail
from django.test import signals from django.test import signals
from django.template import Template from django.template import Template

View File

@ -263,10 +263,10 @@ approximately decreasing order of importance, so start from the top.
Custom database types Custom database types
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
.. method:: db_type(self) .. method:: db_type(self, connection)
Returns the database column data type for the :class:`~django.db.models.Field`, Returns the database column data type for the :class:`~django.db.models.Field`,
taking into account the current :setting:`DATABASE_ENGINE` setting. taking into account the connection object, and the settings associated with it.
Say you've created a PostgreSQL custom type called ``mytype``. You can use this Say you've created a PostgreSQL custom type called ``mytype``. You can use this
field with Django by subclassing ``Field`` and implementing the :meth:`db_type` field with Django by subclassing ``Field`` and implementing the :meth:`db_type`
@ -275,7 +275,7 @@ method, like so::
from django.db import models from django.db import models
class MytypeField(models.Field): class MytypeField(models.Field):
def db_type(self): def db_type(self, connection):
return 'mytype' return 'mytype'
Once you have ``MytypeField``, you can use it in any model, just like any other Once you have ``MytypeField``, you can use it in any model, just like any other
@ -290,13 +290,13 @@ If you aim to build a database-agnostic application, you should account for
differences in database column types. For example, the date/time column type differences in database column types. For example, the date/time column type
in PostgreSQL is called ``timestamp``, while the same column in MySQL is called in PostgreSQL is called ``timestamp``, while the same column in MySQL is called
``datetime``. The simplest way to handle this in a ``db_type()`` method is to ``datetime``. The simplest way to handle this in a ``db_type()`` method is to
import the Django settings module and check the :setting:`DATABASE_ENGINE` setting. check the ``connection.settings_dict['DATABASE_ENGINE']`` attribute.
For example:: For example::
class MyDateField(models.Field): class MyDateField(models.Field):
def db_type(self): def db_type(self, connection):
from django.conf import settings if connection.settings_dict['DATABASE_ENGINE'] == 'mysql':
if settings.DATABASE_ENGINE == 'mysql':
return 'datetime' return 'datetime'
else: else:
return 'timestamp' return 'timestamp'
@ -304,7 +304,7 @@ For example::
The :meth:`db_type` method is only called by Django when the framework The :meth:`db_type` method is only called by Django when the framework
constructs the ``CREATE TABLE`` statements for your application -- that is, when constructs the ``CREATE TABLE`` statements for your application -- that is, when
you first create your tables. It's not called at any other time, so it can you first create your tables. It's not called at any other time, so it can
afford to execute slightly complex code, such as the :setting:`DATABASE_ENGINE` afford to execute slightly complex code, such as the ``connection.settings_dict``
check in the above example. check in the above example.
Some database column types accept parameters, such as ``CHAR(25)``, where the Some database column types accept parameters, such as ``CHAR(25)``, where the
@ -315,7 +315,7 @@ sense to have a ``CharMaxlength25Field``, shown here::
# This is a silly example of hard-coded parameters. # This is a silly example of hard-coded parameters.
class CharMaxlength25Field(models.Field): class CharMaxlength25Field(models.Field):
def db_type(self): def db_type(self, connection):
return 'char(25)' return 'char(25)'
# In the model: # In the model:
@ -333,7 +333,7 @@ time -- i.e., when the class is instantiated. To do that, just implement
self.max_length = max_length self.max_length = max_length
super(BetterCharField, self).__init__(*args, **kwargs) super(BetterCharField, self).__init__(*args, **kwargs)
def db_type(self): def db_type(self, connection):
return 'char(%s)' % self.max_length return 'char(%s)' % self.max_length
# In the model: # In the model:

View File

@ -1 +1,7 @@
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=100)
def __unicode__(self):
return self.title

View File

@ -2,6 +2,8 @@ from django.conf import settings
from django.db import connections from django.db import connections
from django.test import TestCase from django.test import TestCase
from models import Book
class DatabaseSettingTestCase(TestCase): class DatabaseSettingTestCase(TestCase):
def setUp(self): def setUp(self):
settings.DATABASES['__test_db'] = { settings.DATABASES['__test_db'] = {
@ -15,3 +17,20 @@ class DatabaseSettingTestCase(TestCase):
def test_db_connection(self): def test_db_connection(self):
connections['default'].cursor() connections['default'].cursor()
connections['__test_db'].cursor() connections['__test_db'].cursor()
class ConnectionTestCase(TestCase):
def test_queries(self):
for connection in connections.all():
qn = connection.ops.quote_name
cursor = connection.cursor()
cursor.execute("""INSERT INTO %(table)s (%(col)s) VALUES (%%s)""" % {
'table': qn(Book._meta.db_table),
'col': qn(Book._meta.get_field_by_name('title')[0].column),
}, ('Dive Into Python',))
for connection in connections.all():
qn = connection.ops.quote_name
cursor = connection.cursor()
cursor.execute("""SELECT * FROM %(table)s""" % {'table': qn(Book._meta.db_table)})
data = cursor.fetchall()
self.assertEqual('Dive Into Python', data[0][1])