diff --git a/tests/modeltests/multiple_databases/__init__.py b/tests/modeltests/multiple_databases/__init__.py new file mode 100644 index 0000000000..2ae28399f5 --- /dev/null +++ b/tests/modeltests/multiple_databases/__init__.py @@ -0,0 +1 @@ +pass diff --git a/tests/modeltests/multiple_databases/models.py b/tests/modeltests/multiple_databases/models.py new file mode 100644 index 0000000000..3c89c83e75 --- /dev/null +++ b/tests/modeltests/multiple_databases/models.py @@ -0,0 +1,96 @@ +""" +XXX. Using multiple database connections + +Django normally uses only a single database connection. However, +support is available for using any number of different, named +connections. Multiple database support is entirely optional and has +no impact on your application if you don't use it. + +Named connections are defined in your settings module. Create a +`DATABASES` variable that is a dict, mapping connection names to their +particulars. The particulars are defined in a dict with the same keys +as the variable names as are used to define the default connection. + +Access to named connections is through `django.db.connections`, which +behaves like a dict: you access connections by name. Connections are +established lazily, when accessed. `django.db.connections[database]` +holds a `ConnectionInfo` instance, with the attributes: +`DatabaseError`, `backend`, `get_introspection_module`, +`get_creation_module`, and `runshell`. + +Models can define which connection to use, by name. To use a named +connection, set the `db_connection` property in the model's Meta class +to the name of the connection. The name used must be a key in +settings.DATABASES, of course. + +To access a model's connection, use `model._meta.connection`. To find +the backend or other connection metadata, use +`model._meta.connection_info`. +""" + +from django.db import models + +class Artist(models.Model): + name = models.CharField(maxlength=100) + alive = models.BooleanField(default=True) + + def __str__(self): + return self.name + + class Meta: + db_connection = 'django_test_db_a' + +class Widget(models.Model): + code = models.CharField(maxlength=10, unique=True) + weight = models.IntegerField() + + def __str__(self): + return self.code + + class Meta: + db_connection = 'django_test_db_b' + +class Vehicle(models.Model): + make = models.CharField(maxlength=20) + model = models.CharField(maxlength=20) + year = models.IntegerField() + + def __str__(self): + return "%d %s %s" % (self.year, self.make, self.model) + + +API_TESTS = """ + +# See what connections are defined. django.db.connections acts like a dict. +>>> from django.db import connection, connections +>>> from django.conf import settings +>>> connections.keys() +['django_test_db_a', 'django_test_db_b'] + +# Each connection references its settings +>>> connections['django_test_db_a'].settings.DATABASE_NAME == settings.DATABASES['django_test_db_a']['DATABASE_NAME'] +True +>>> connections['django_test_db_b'].settings.DATABASE_NAME == settings.DATABASES['django_test_db_b']['DATABASE_NAME'] +True +>>> connections['django_test_db_b'].settings.DATABASE_NAME == settings.DATABASES['django_test_db_a']['DATABASE_NAME'] +False + +# Invalid connection names raise ImproperlyConfigured +>>> connections['bad'] +Traceback (most recent call last): + ... +ImproperlyConfigured: No database connection 'bad' has been configured + +# Models can access their connections through their _meta properties +>>> Artist._meta.connection.settings == connections['django_test_db_a'].settings +True +>>> Widget._meta.connection.settings == connections['django_test_db_b'].settings +True +>>> Vehicle._meta.connection.settings == connection.settings +True +>>> Artist._meta.connection.settings == Widget._meta.connection.settings +False +>>> Artist._meta.connection.settings == Vehicle._meta.connection.settings +False + +""" diff --git a/tests/othertests/multiple_databases.py b/tests/othertests/multiple_databases.py deleted file mode 100644 index be52757406..0000000000 --- a/tests/othertests/multiple_databases.py +++ /dev/null @@ -1,175 +0,0 @@ -import copy -import os -import sys -import tempfile -from django.conf import settings -from django import db -from runtests import doctest, DjangoDoctestRunner, error_list - -# database files -fh, db_a = tempfile.mkstemp() -os.close(fh) -fh, db_b = tempfile.mkstemp() -os.close(fh) - -# patches -tmp_settings = None -tmp_connections = None - -# tests -test = r""" -XXX. Using multiple database connections - -Django normally uses only a single database connection. However, support is -available for connecting to any number of different, named databases. - -Named connections are defined in your settings module. Create a -`DATABASES` variable that is a dict, mapping connection names to their -particulars. The particulars are defined in a dict with the same keys -as the variable names as are used to define the default connection. - -.. note:: - - Please note that this uses the sqlite3 backend and writes temporary - database files to disk. This test will fail unless sqlite3 is - installed and your temp directory is writable. - - >>> from django.conf import settings - >>> settings.DATABASES = { - ... 'a': { 'DATABASE_ENGINE': 'sqlite3', - ... 'DATABASE_NAME': db_a - ... }, - ... 'b': { 'DATABASE_ENGINE': 'sqlite3', - ... 'DATABASE_NAME': db_b - ... }} - -Connections are established lazily, when requested by name. When -accessed, `connections[database]` holds a `ConnectionInfo` instance, -with the attributes: `DatabaseError`, `backend`, -`get_introspection_module`, `get_creation_module`, and -`runshell`. Access connections through the `connections` property of -the `django.db` module: - - >>> from django.db import connection, connections - >>> connections['a'].settings.DATABASE_NAME == db_a - True - >>> connections['b'].settings.DATABASE_NAME == db_b - True - -Invalid connection names raise ImproperlyConfigured: - - >>> connections['bad'] - Traceback (most recent call last): - ... - ImproperlyConfigured: No database connection 'bad' has been configured - -Models can define which connection to use, by name. To use a named -connection, set the `db_connection` property in the model's Meta class -to the name of the connection. The name used must be a key in -settings.DATABASES, of course. - - >>> from django.db import models - >>> class Artist(models.Model): - ... name = models.CharField(maxlength=100) - ... alive = models.BooleanField(default=True) - ... - ... def __str__(self): - ... return self.name - ... - ... class Meta: - ... app_label = 'mdb' - ... db_connection = 'a' - ... - >>> class Widget(models.Model): - ... code = models.CharField(maxlength=10, unique=True) - ... weight = models.IntegerField() - ... - ... def __str__(self): - ... return self.code - ... - ... class Meta: - ... app_label = 'mdb' - ... db_connection = 'b' - -But they don't have to. Multiple database support is entirely optional -and has no impact on your application if you don't use it. - - >>> class Vehicle(models.Model): - ... make = models.CharField(maxlength=20) - ... model = models.CharField(maxlength=20) - ... year = models.IntegerField() - ... - ... def __str__(self): - ... return "%d %s %s" % (self.year, self.make, self.model) - ... - ... class Meta: - ... app_label = 'mdb' - - >>> Artist._meta.connection.settings.DATABASE_NAME == \ - ... connections['a'].connection.settings.DATABASE_NAME - True - >>> Widget._meta.connection.settings.DATABASE_NAME == \ - ... connections['b'].connection.settings.DATABASE_NAME - True - >>> Vehicle._meta.connection.settings.DATABASE_NAME == \ - ... connection.settings.DATABASE_NAME - True - >>> Artist._meta.connection.settings.DATABASE_NAME == \ - ... Widget._meta.connection.settings.DATABASE_NAME - False - >>> Artist._meta.connection.settings.DATABASE_NAME == \ - ... Vehicle._meta.connection.settings.DATABASE_NAME - False - -""" - -def cleanup(): - if os.path.exists(db_a): - os.unlink(db_a) - if os.path.exists(db_b): - os.unlink(db_b) - -def setup(): - global tmp_settings, tmp_connections - try: - tmp_connections = db.connections - db.connections = db.LazyConnectionManager() - tmp_settings = copy.copy(settings.DATABASES) - except AttributeError: - pass - -def teardown(): - try: - db.connections = tmp_connections - settings.DATABASES = tmp_settings - except AttributeError: - pass - cleanup() - -def run_tests(verbosity_level): - setup() - try: - main(verbosity_level) - finally: - teardown() - -def main(verbosity_level): - mod = sys.modules[__name__] - p = doctest.DocTestParser() - dtest = p.get_doctest(mod.test, mod.__dict__, __name__, None, None) - runner = DjangoDoctestRunner(verbosity_level=verbosity_level, - verbose=False) - runner.run(dtest, clear_globs=True, out=sys.stdout.write) - if error_list: - out = [] - for d in error_list: - out.extend([d['title'], "=" * len(d['title']), - d['description']]) - - raise Exception, "%s multiple_databases test%s failed:\n\n %s" \ - % (len(error_list), - len(error_list) != 1 and 's' or '', - '\n'.join(out)) - -if __name__ == '__main__': - run_tests(1) diff --git a/tests/runtests.py b/tests/runtests.py index 2f06081749..a4622ef9d5 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -85,39 +85,25 @@ class TestRunner: self.verbosity_level = verbosity_level self.which_tests = which_tests self.created_dbs = [] - self.sqlite_memory_db_used = False self.cleanup_files = [] + self.old_database_name = None + self.old_databases = None + self.sqlite_memory_db_used = False def output(self, required_level, message): if self.verbosity_level > required_level - 1: print message - def create_test_db(self, connection): + def create_test_db(self, db_name, connection): """Create a test db, returning a ConnectionInfo object holding a connection to that db. """ from django.db import connect + # settings may be a dict or settings object settings = connection.settings - - # If we're using SQLite, it's more convenient to test against an - # in-memory database. But we can only do this for the first one; after - # that we have to use temp files. - if settings.DATABASE_ENGINE == "sqlite3": - if self.sqlite_memory_db_used: - import tempfile - fd, filename = tempfile.mkstemp() - os.close(fd) - db_name = filename - self.cleanup_files.append(filename) - else: - db_name = ":memory:" - self.sqlite_memory_db_used = True - else: - db_name = TEST_DATABASE_NAME - if self.created_dbs: - db_name += "_%s" % (len(self.created_dbs)) - + + if settings.DATABASE_ENGINE != "sqlite3": # Create the test database and connect to it. We need to autocommit # if the database supports it because PostgreSQL doesn't allow # CREATE/DROP DATABASE statements within transactions. @@ -136,6 +122,7 @@ class TestRunner: else: raise Exception("Tests cancelled.") + settings.DATABASE_NAME = db_name connection.close() @@ -153,6 +140,7 @@ class TestRunner: self.teardown() def setup(self): + global TEST_DATABASE_NAME from django.conf import settings from django.db import connection, connections from django.core import management @@ -170,17 +158,38 @@ class TestRunner: self.output(0, "Running tests with database %r" % settings.DATABASE_ENGINE) - # Create test dbs for the default connection and any named connections - # in settings. Remeber the original default db name so we can connect - # there to drop the created test dbs. - self._old_database_name = settings.DATABASE_NAME - self.create_test_db(connection) + # Create test dbs for the default connection and two named connections, + # replacing any named connections defined in settings. All connections + # will use the default DATABASE_ENGINE + self.old_database_name = settings.DATABASE_NAME + self.old_databases = settings.DATABASES + + db_a = TEST_DATABASE_NAME + '_a' + db_b = TEST_DATABASE_NAME + '_b' + if settings.DATABASE_ENGINE == 'sqlite3': + # If we're using SQLite, it's more convenient to test against an + # in-memory database. But we can only do this for the default; + # after that we have to use temp files. + TEST_DATABASE_NAME = ':memory:' + db_a_name = self._tempfile() + db_b_name = self._tempfile() + self.cleanup_files.append(db_a_name) + self.cleanup_files.append(db_b_name) + else: + db_a_name = db_a + db_b_name = db_b - if hasattr(settings, 'DATABASES'): - for name, info in settings.DATABASES.items(): - cx = connections[name] - test_connection = self.create_test_db(cx.connection) - connections[name] = test_connection + settings.DATABASES = { + db_a: { 'DATABASE_NAME': db_a_name }, + db_b: { 'DATABASE_NAME': db_b_name } + } + + self.create_test_db(TEST_DATABASE_NAME, connection) + for name, info in settings.DATABASES.items(): + cx = connections[name] + test_connection = self.create_test_db(info['DATABASE_NAME'], + cx.connection) + connections[name] = test_connection # Install the core always installed apps for app in ALWAYS_INSTALLED_APPS: @@ -196,7 +205,8 @@ class TestRunner: from django.db import connection from django.conf import settings connection.close() - settings.DATABASE_NAME = self._old_database_name + settings.DATABASE_NAME = self.old_database_name + settings.DATABASES = self.old_databases for db_name, cx in self.created_dbs: settings = cx.settings cx.close() @@ -334,6 +344,12 @@ class TestRunner: elif hasattr(connection.connection, "set_isolation_level"): connection.connection.set_isolation_level(0) + def _tempfile(self): + import tempfile + fd, filename = tempfile.mkstemp() + os.close(fd) + return filename + if __name__ == "__main__": from optparse import OptionParser usage = "%prog [options] [model model model ...]"