Ticket #12540: t12540-r12262.1.diff

File t12540-r12262.1.diff, 46.2 KB (added by russellm, 6 years ago)

First pass at improved cross-database support.

  • django/contrib/auth/models.py

    diff -r e165fea06e06 django/contrib/auth/models.py
    a b  
    33
    44from django.contrib import auth
    55from django.core.exceptions import ImproperlyConfigured
    6 from django.db import models, DEFAULT_DB_ALIAS
     6from django.db import models
    77from django.db.models.manager import EmptyManager
    88from django.contrib.contenttypes.models import ContentType
    99from django.utils.encoding import smart_str
  • django/contrib/contenttypes/generic.py

    diff -r e165fea06e06 django/contrib/contenttypes/generic.py
    a b  
    55from django.core.exceptions import ObjectDoesNotExist
    66from django.db import connection
    77from django.db.models import signals
    8 from django.db import models, DEFAULT_DB_ALIAS
     8from django.db import models
    99from django.db.models.fields.related import RelatedField, Field, ManyToManyRel
    1010from django.db.models.loading import get_model
    1111from django.forms import ModelForm
     
    255255                    raise TypeError("'%s' instance expected" % self.model._meta.object_name)
    256256                setattr(obj, self.content_type_field_name, self.content_type)
    257257                setattr(obj, self.object_id_field_name, self.pk_val)
    258                 obj.save(using=self.instance._state.db)
     258                obj.save()
    259259        add.alters_data = True
    260260
    261261        def remove(self, *objs):
  • django/contrib/contenttypes/models.py

    diff -r e165fea06e06 django/contrib/contenttypes/models.py
    a b  
    1 from django.db import models, DEFAULT_DB_ALIAS
     1from django.db import models
    22from django.utils.translation import ugettext_lazy as _
    33from django.utils.encoding import smart_unicode
    44
  • django/contrib/gis/db/models/sql/query.py

    diff -r e165fea06e06 django/contrib/gis/db/models/sql/query.py
    a b  
    1 from django.db import connections, DEFAULT_DB_ALIAS
     1from django.db import connections
    22from django.db.models.query import sql
    33
    44from django.contrib.gis.db.models.fields import GeometryField
  • django/db/models/base.py

    diff -r e165fea06e06 django/db/models/base.py
    a b  
    439439        need for overrides of save() to pass around internal-only parameters
    440440        ('raw', 'cls', and 'origin').
    441441        """
    442         using = using or self._state.db or DEFAULT_DB_ALIAS
     442        using = using or self._default_manager._db_for_write(self)
    443443        connection = connections[using]
    444444        assert not (force_insert and force_update)
    445445        if cls is None:
     
    592592            parent_obj._collect_sub_objects(seen_objs)
    593593
    594594    def delete(self, using=None):
    595         using = using or self._state.db or DEFAULT_DB_ALIAS
     595        using = using or self._default_manager._db_for_write(self)
    596596        connection = connections[using]
    597597        assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname)
    598598
     
    719719                    # no value, skip the lookup
    720720                    continue
    721721                if f.primary_key and not getattr(self, '_adding', False):
    722                     # no need to check for unique primary key when editting
     722                    # no need to check for unique primary key when editing
    723723                    continue
    724724                lookup_kwargs[str(field_name)] = lookup_value
    725725
  • django/db/models/fields/related.py

    diff -r e165fea06e06 django/db/models/fields/related.py
    a b  
     1from django.conf import settings
    12from django.db import connection, transaction, DEFAULT_DB_ALIAS
    23from django.db.backends import util
    34from django.db.models import signals, get_model
     
    218219            raise ValueError('Cannot assign "%r": "%s.%s" must be a "%s" instance.' %
    219220                                (value, instance._meta.object_name,
    220221                                 self.related.get_accessor_name(), self.related.opts.object_name))
     222        elif value is not None:
     223            if instance._state.db is None:
     224                instance._state.db = instance._default_manager._db_for_write(value)
     225            elif value._state.db is None:
     226                value._state.db = value._default_manager._db_for_write(instance)
     227            elif value._state.db is not None and instance._state.db is not None:
     228                if (settings.DATABASES[value._default_manager._db_for_write(instance)]['SOURCE']
     229                        != settings.DATABASES[value._state.db]['SOURCE']):
     230                    raise ValueError('Cannot assign "%r": instance is on database "%s", value is is on database "%s"' %
     231                                        (value, instance._state.db, value._state.db))
    221232
    222233        # Set the value of the related field to the value of the related object's related field
    223234        setattr(value, self.related.field.attname, getattr(instance, self.related.field.rel.get_related_field().attname))
     
    260271            # If the related manager indicates that it should be used for
    261272            # related fields, respect that.
    262273            rel_mgr = self.field.rel.to._default_manager
    263             using = instance._state.db or DEFAULT_DB_ALIAS
     274            using = rel_mgr._db_for_read(instance)
    264275            if getattr(rel_mgr, 'use_for_related_fields', False):
    265276                rel_obj = rel_mgr.using(using).get(**params)
    266277            else:
     
    281292            raise ValueError('Cannot assign "%r": "%s.%s" must be a "%s" instance.' %
    282293                                (value, instance._meta.object_name,
    283294                                 self.field.name, self.field.rel.to._meta.object_name))
    284         elif value is not None and value._state.db != instance._state.db:
     295        elif value is not None:
    285296            if instance._state.db is None:
    286                 instance._state.db = value._state.db
    287             else:#elif value._state.db is None:
    288                 value._state.db = instance._state.db
    289 #            elif value._state.db is not None and instance._state.db is not None:
    290 #                raise ValueError('Cannot assign "%r": instance is on database "%s", value is is on database "%s"' %
    291 #                                    (value, instance._state.db, value._state.db))
     297                instance._state.db = instance._default_manager._db_for_write(value)
     298            elif value._state.db is None:
     299                value._state.db = value._default_manager._db_for_write(instance)
     300            elif value._state.db is not None and instance._state.db is not None:
     301                if (settings.DATABASES[value._default_manager._db_for_write(instance)]['SOURCE']
     302                        != settings.DATABASES[value._state.db]['SOURCE']):
     303                    raise ValueError('Cannot assign "%r": instance is on database "%s", value is is on database "%s"' %
     304                                        (value, instance._state.db, value._state.db))
    292305
    293306        # If we're setting the value of a OneToOneField to None, we need to clear
    294307        # out the cache on any old related object. Otherwise, deleting the
     
    370383
    371384        class RelatedManager(superclass):
    372385            def get_query_set(self):
    373                 using = instance._state.db or DEFAULT_DB_ALIAS
     386                using = rel_model._default_manager._db_for_read(instance)
    374387                return superclass.get_query_set(self).using(using).filter(**(self.core_filters))
    375388
    376389            def add(self, *objs):
     
    378391                    if not isinstance(obj, self.model):
    379392                        raise TypeError("'%s' instance expected" % self.model._meta.object_name)
    380393                    setattr(obj, rel_field.name, instance)
    381                     obj.save(using=instance._state.db)
     394                    obj.save()
    382395            add.alters_data = True
    383396
    384397            def create(self, **kwargs):
     
    390403                # Update kwargs with the related object that this
    391404                # ForeignRelatedObjectsDescriptor knows about.
    392405                kwargs.update({rel_field.name: instance})
    393                 using = instance._state.db or DEFAULT_DB_ALIAS
     406                using = rel_model._default_manager._db_for_write(instance)
    394407                return super(RelatedManager, self).using(using).get_or_create(**kwargs)
    395408            get_or_create.alters_data = True
    396409
     
    402415                        # Is obj actually part of this descriptor set?
    403416                        if getattr(obj, rel_field.attname) == val:
    404417                            setattr(obj, rel_field.name, None)
    405                             obj.save(using=instance._state.db)
     418                            obj.save()
    406419                        else:
    407420                            raise rel_field.rel.to.DoesNotExist("%r is not related to %r." % (obj, instance))
    408421                remove.alters_data = True
     
    410423                def clear(self):
    411424                    for obj in self.all():
    412425                        setattr(obj, rel_field.name, None)
    413                         obj.save(using=instance._state.db)
     426                        obj.save()
    414427                clear.alters_data = True
    415428
    416429        manager = RelatedManager()
     
    505518                new_ids = set()
    506519                for obj in objs:
    507520                    if isinstance(obj, self.model):
    508 #                        if obj._state.db != self.instance._state.db:
    509 #                            raise ValueError('Cannot add "%r": instance is on database "%s", value is is on database "%s"' %
    510 #                                                (obj, self.instance._state.db, obj._state.db))
     521                        if (settings.DATABASES[obj._default_manager._db_for_write(self.instance)]['SOURCE']
     522                               != settings.DATABASES[obj._state.db]['SOURCE']):
     523                           raise ValueError('Cannot add "%r": instance is on database "%s", value is is on database "%s"' %
     524                                               (obj, self.instance._state.db, obj._state.db))
    511525                        new_ids.add(obj.pk)
    512526                    elif isinstance(obj, Model):
    513527                        raise TypeError("'%s' instance expected" % self.model._meta.object_name)
    514528                    else:
    515529                        new_ids.add(obj)
    516                 vals = self.through._default_manager.using(self.instance._state.db).values_list(target_field_name, flat=True)
     530                db = self.instance._default_manager._db_for_read(self.instance)
     531                vals = self.through._default_manager.using(db).values_list(target_field_name, flat=True)
    517532                vals = vals.filter(**{
    518533                    source_field_name: self._pk_val,
    519534                    '%s__in' % target_field_name: new_ids,
    520535                })
    521536                new_ids = new_ids - set(vals)
    522537                # Add the ones that aren't there already
     538                db = self.instance._default_manager._db_for_write(self.instance)
    523539                for obj_id in new_ids:
    524                     self.through._default_manager.using(self.instance._state.db).create(**{
     540                    self.through._default_manager.using(db).create(**{
    525541                        '%s_id' % source_field_name: self._pk_val,
    526542                        '%s_id' % target_field_name: obj_id,
    527543                    })
  • django/db/models/manager.py

    diff -r e165fea06e06 django/db/models/manager.py
    a b  
    11from django.utils import copycompat as copy
    2 
     2from django.conf import settings
    33from django.db import DEFAULT_DB_ALIAS
    44from django.db.models.query import QuerySet, EmptyQuerySet, insert_query, RawQuerySet
    55from django.db.models import signals
     
    8787        mgr._inherited = True
    8888        return mgr
    8989
    90     def db_manager(self, alias):
     90    def db_manager(self, using):
    9191        obj = copy.copy(self)
    92         obj._db = alias
     92        obj._db = using
    9393        return obj
    9494
    9595    @property
    9696    def db(self):
    97         return self._db or DEFAULT_DB_ALIAS
     97        return self._db
     98
     99    def _db_for_write(self, instance):
     100        if instance._state.db:
     101            return settings.DATABASES[instance._state.db]['SOURCE'] or DEFAULT_DB_ALIAS
     102        return DEFAULT_DB_ALIAS
     103
     104    def _db_for_read(self, instance):
     105        return instance._state.db or DEFAULT_DB_ALIAS
    98106
    99107    #######################
    100108    # PROXIES TO QUERYSET #
    101109    #######################
    102110
    103111    def get_empty_query_set(self):
    104         return EmptyQuerySet(self.model)
     112        return EmptyQuerySet(self.model, using=self.db)
    105113
    106114    def get_query_set(self):
    107115        """Returns a new QuerySet object.  Subclasses can override this method
    108116        to easily customize the behavior of the Manager.
    109117        """
    110         qs = QuerySet(self.model)
    111         if self._db is not None:
    112             qs = qs.using(self._db)
    113         return qs
     118        return QuerySet(self.model, using=self.db)
    114119
    115120    def none(self):
    116121        return self.get_empty_query_set()
  • django/db/models/query.py

    diff -r e165fea06e06 django/db/models/query.py
    a b  
    988988
    989989
    990990class EmptyQuerySet(QuerySet):
    991     def __init__(self, model=None, query=None):
    992         super(EmptyQuerySet, self).__init__(model, query)
     991    def __init__(self, model=None, query=None, using=None):
     992        super(EmptyQuerySet, self).__init__(model, query, using)
    993993        self._result_cache = []
    994994
    995995    def __and__(self, other):
  • django/db/utils.py

    diff -r e165fea06e06 django/db/utils.py
    a b  
    5555            conn = self.databases[alias]
    5656        except KeyError:
    5757            raise ConnectionDoesNotExist("The connection %s doesn't exist" % alias)
     58
    5859        conn.setdefault('ENGINE', 'django.db.backends.dummy')
    5960        if conn['ENGINE'] == 'django.db.backends.' or not conn['ENGINE']:
    6061            conn['ENGINE'] = 'django.db.backends.dummy'
     
    6566        conn.setdefault('TIME_ZONE', settings.TIME_ZONE)
    6667        for setting in ('NAME', 'USER', 'PASSWORD', 'HOST', 'PORT'):
    6768            conn.setdefault(setting, '')
     69        conn.setdefault('SOURCE', alias)
    6870
    6971    def __getitem__(self, alias):
    7072        if alias in self._connections:
  • tests/regressiontests/multiple_database/tests.py

    diff -r e165fea06e06 tests/regressiontests/multiple_database/tests.py
    a b  
    259259        self.assertEquals(list(Person.objects.using('other').filter(book__title='Dive into HTML5').values_list('name', flat=True)),
    260260                          [u'Mark Pilgrim'])
    261261
    262 #    def test_m2m_cross_database_protection(self):
    263 #        "Operations that involve sharing M2M objects across databases raise an error"
    264 #        # Create a book and author on the default database
    265 #        pro = Book.objects.create(title="Pro Django",
    266 #                                  published=datetime.date(2008, 12, 16))
     262    def test_m2m_cross_database_protection(self):
     263        "Operations that involve sharing M2M objects across databases raise an error"
     264        # Create a book and author on the default database
     265        pro = Book.objects.create(title="Pro Django",
     266                                  published=datetime.date(2008, 12, 16))
    267267
    268 #        marty = Person.objects.create(name="Marty Alchin")
     268        marty = Person.objects.create(name="Marty Alchin")
    269269
    270 #        # Create a book and author on the other database
    271 #        dive = Book.objects.using('other').create(title="Dive into Python",
    272 #                                                  published=datetime.date(2009, 5, 4))
     270        # Create a book and author on the other database
     271        dive = Book.objects.using('other').create(title="Dive into Python",
     272                                                  published=datetime.date(2009, 5, 4))
    273273
    274 #        mark = Person.objects.using('other').create(name="Mark Pilgrim")
    275 #        # Set a foreign key set with an object from a different database
    276 #        try:
    277 #            marty.book_set = [pro, dive]
    278 #            self.fail("Shouldn't be able to assign across databases")
    279 #        except ValueError:
    280 #            pass
     274        mark = Person.objects.using('other').create(name="Mark Pilgrim")
     275        # Set a foreign key set with an object from a different database
     276        try:
     277            marty.book_set = [pro, dive]
     278            self.fail("Shouldn't be able to assign across databases")
     279        except ValueError:
     280            pass
    281281
    282 #        # Add to an m2m with an object from a different database
    283 #        try:
    284 #            marty.book_set.add(dive)
    285 #            self.fail("Shouldn't be able to assign across databases")
    286 #        except ValueError:
    287 #            pass
     282        # Add to an m2m with an object from a different database
     283        try:
     284            marty.book_set.add(dive)
     285            self.fail("Shouldn't be able to assign across databases")
     286        except ValueError:
     287            pass
    288288
    289 #        # Set a m2m with an object from a different database
    290 #        try:
    291 #            marty.book_set = [pro, dive]
    292 #            self.fail("Shouldn't be able to assign across databases")
    293 #        except ValueError:
    294 #            pass
     289        # Set a m2m with an object from a different database
     290        try:
     291            marty.book_set = [pro, dive]
     292            self.fail("Shouldn't be able to assign across databases")
     293        except ValueError:
     294            pass
    295295
    296 #        # Add to a reverse m2m with an object from a different database
    297 #        try:
    298 #            dive.authors.add(marty)
    299 #            self.fail("Shouldn't be able to assign across databases")
    300 #        except ValueError:
    301 #            pass
     296        # Add to a reverse m2m with an object from a different database
     297        try:
     298            dive.authors.add(marty)
     299            self.fail("Shouldn't be able to assign across databases")
     300        except ValueError:
     301            pass
    302302
    303 #        # Set a reverse m2m with an object from a different database
    304 #        try:
    305 #            dive.authors = [mark, marty]
    306 #            self.fail("Shouldn't be able to assign across databases")
    307 #        except ValueError:
    308 #            pass
     303        # Set a reverse m2m with an object from a different database
     304        try:
     305            dive.authors = [mark, marty]
     306            self.fail("Shouldn't be able to assign across databases")
     307        except ValueError:
     308            pass
    309309
    310310    def test_foreign_key_separation(self):
    311311        "FK fields are constrained to a single database"
     
    401401        self.assertEquals(list(Person.objects.using('other').filter(edited__title='Dive into Python').values_list('name', flat=True)),
    402402                          [])
    403403
    404 #    def test_foreign_key_cross_database_protection(self):
    405 #        "Operations that involve sharing FK objects across databases raise an error"
    406 #        # Create a book and author on the default database
    407 #        pro = Book.objects.create(title="Pro Django",
    408 #                                  published=datetime.date(2008, 12, 16))
     404    def test_foreign_key_cross_database_protection(self):
     405        "Operations that involve sharing FK objects across databases raise an error"
     406        # Create a book and author on the default database
     407        pro = Book.objects.create(title="Pro Django",
     408                                  published=datetime.date(2008, 12, 16))
    409409
    410 #        marty = Person.objects.create(name="Marty Alchin")
     410        marty = Person.objects.create(name="Marty Alchin")
    411411
    412 #        # Create a book and author on the other database
    413 #        dive = Book.objects.using('other').create(title="Dive into Python",
    414 #                                                  published=datetime.date(2009, 5, 4))
     412        # Create a book and author on the other database
     413        dive = Book.objects.using('other').create(title="Dive into Python",
     414                                                  published=datetime.date(2009, 5, 4))
    415415
    416 #        mark = Person.objects.using('other').create(name="Mark Pilgrim")
     416        mark = Person.objects.using('other').create(name="Mark Pilgrim")
    417417
    418 #        # Set a foreign key with an object from a different database
    419 #        try:
    420 #            dive.editor = marty
    421 #            self.fail("Shouldn't be able to assign across databases")
    422 #        except ValueError:
    423 #            pass
     418        # Set a foreign key with an object from a different database
     419        try:
     420            dive.editor = marty
     421            self.fail("Shouldn't be able to assign across databases")
     422        except ValueError:
     423            pass
    424424
    425 #        # Set a foreign key set with an object from a different database
    426 #        try:
    427 #            marty.edited = [pro, dive]
    428 #            self.fail("Shouldn't be able to assign across databases")
    429 #        except ValueError:
    430 #            pass
     425        # Set a foreign key set with an object from a different database
     426        try:
     427            marty.edited = [pro, dive]
     428            self.fail("Shouldn't be able to assign across databases")
     429        except ValueError:
     430            pass
    431431
    432 #        # Add to a foreign key set with an object from a different database
    433 #        try:
    434 #            marty.edited.add(dive)
    435 #            self.fail("Shouldn't be able to assign across databases")
    436 #        except ValueError:
    437 #            pass
     432        # Add to a foreign key set with an object from a different database
     433        try:
     434            marty.edited.add(dive)
     435            self.fail("Shouldn't be able to assign across databases")
     436        except ValueError:
     437            pass
    438438
    439 #        # BUT! if you assign a FK object when the base object hasn't
    440 #        # been saved yet, you implicitly assign the database for the
    441 #        # base object.
    442 #        chris = Person(name="Chris Mills")
    443 #        html5 = Book(title="Dive into HTML5", published=datetime.date(2010, 3, 15))
    444 #        # initially, no db assigned
    445 #        self.assertEquals(chris._state.db, None)
    446 #        self.assertEquals(html5._state.db, None)
     439        # BUT! if you assign a FK object when the base object hasn't
     440        # been saved yet, you implicitly assign the database for the
     441        # base object.
     442        chris = Person(name="Chris Mills")
     443        html5 = Book(title="Dive into HTML5", published=datetime.date(2010, 3, 15))
     444        # initially, no db assigned
     445        self.assertEquals(chris._state.db, None)
     446        self.assertEquals(html5._state.db, None)
    447447
    448 #        # old object comes from 'other', so the new object is set to use 'other'...
    449 #        dive.editor = chris
    450 #        html5.editor = mark
    451 #        # self.assertEquals(chris._state.db, 'other')
    452 #        self.assertEquals(html5._state.db, 'other')
    453 #        # ... but it isn't saved yet
    454 #        self.assertEquals(list(Person.objects.using('other').values_list('name',flat=True)),
    455 #                          [u'Mark Pilgrim'])
    456 #        self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
    457 #                           [u'Dive into Python'])
     448        # old object comes from 'other', so the new object is set to use 'other'...
     449        dive.editor = chris
     450        html5.editor = mark
     451        self.assertEquals(chris._state.db, 'other')
     452        self.assertEquals(html5._state.db, 'other')
     453        # ... but it isn't saved yet
     454        self.assertEquals(list(Person.objects.using('other').values_list('name',flat=True)),
     455                          [u'Mark Pilgrim'])
     456        self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
     457                           [u'Dive into Python'])
    458458
    459 #        # When saved (no using required), new objects goes to 'other'
    460 #        chris.save()
    461 #        html5.save()
    462 #        self.assertEquals(list(Person.objects.using('default').values_list('name',flat=True)),
    463 #                          [u'Marty Alchin'])
    464 #        self.assertEquals(list(Person.objects.using('other').values_list('name',flat=True)),
    465 #                          [u'Chris Mills', u'Mark Pilgrim'])
    466 #        self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)),
    467 #                          [u'Pro Django'])
    468 #        self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
    469 #                          [u'Dive into HTML5', u'Dive into Python'])
     459        # When saved (no using required), new objects goes to 'other'
     460        chris.save()
     461        html5.save()
     462        self.assertEquals(list(Person.objects.using('default').values_list('name',flat=True)),
     463                          [u'Marty Alchin'])
     464        self.assertEquals(list(Person.objects.using('other').values_list('name',flat=True)),
     465                          [u'Chris Mills', u'Mark Pilgrim'])
     466        self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)),
     467                          [u'Pro Django'])
     468        self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
     469                          [u'Dive into HTML5', u'Dive into Python'])
    470470
    471 #        # This also works if you assign the FK in the constructor
    472 #        water = Book(title="Dive into Water", published=datetime.date(2001, 1, 1), editor=mark)
    473 #        self.assertEquals(water._state.db, 'other')
    474 #        # ... but it isn't saved yet
    475 #        self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)),
    476 #                          [u'Pro Django'])
    477 #        self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
    478 #                          [u'Dive into HTML5', u'Dive into Python'])
     471        # This also works if you assign the FK in the constructor
     472        water = Book(title="Dive into Water", published=datetime.date(2001, 1, 1), editor=mark)
     473        self.assertEquals(water._state.db, 'other')
     474        # ... but it isn't saved yet
     475        self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)),
     476                          [u'Pro Django'])
     477        self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
     478                          [u'Dive into HTML5', u'Dive into Python'])
    479479
    480 #        # When saved, the new book goes to 'other'
    481 #        water.save()
    482 #        self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)),
    483 #                          [u'Pro Django'])
    484 #        self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
    485 #                          [u'Dive into HTML5', u'Dive into Python', u'Dive into Water'])
     480        # When saved, the new book goes to 'other'
     481        water.save()
     482        self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)),
     483                          [u'Pro Django'])
     484        self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)),
     485                          [u'Dive into HTML5', u'Dive into Python', u'Dive into Water'])
    486486
    487487    def test_generic_key_separation(self):
    488488        "Generic fields are constrained to a single database"
     
    555555        self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source', flat=True)),
    556556                          [u'Python Daily'])
    557557
    558 #    def test_generic_key_cross_database_protection(self):
    559 ##        "Operations that involve sharing FK objects across databases raise an error"
    560 ##        # Create a book and author on the default database
    561 ##        pro = Book.objects.create(title="Pro Django",
    562 ##                                  published=datetime.date(2008, 12, 16))
     558    def test_generic_key_cross_database_protection(self):
     559        "Operations that involve sharing generic key objects across databases raise an error"
     560        # Create a book and author on the default database
     561        pro = Book.objects.create(title="Pro Django",
     562                                  published=datetime.date(2008, 12, 16))
    563563
    564 ##        review1 = Review.objects.create(source="Python Monthly", content_object=pro)
     564        review1 = Review.objects.create(source="Python Monthly", content_object=pro)
    565565
    566 ##        # Create a book and author on the other database
    567 ##        dive = Book.objects.using('other').create(title="Dive into Python",
    568 ##                                                  published=datetime.date(2009, 5, 4))
     566        # Create a book and author on the other database
     567        dive = Book.objects.using('other').create(title="Dive into Python",
     568                                                  published=datetime.date(2009, 5, 4))
    569569
    570 ##        review2 = Review.objects.using('other').create(source="Python Weekly", content_object=dive)
     570        review2 = Review.objects.using('other').create(source="Python Weekly", content_object=dive)
    571571
    572 ##        # Set a foreign key with an object from a different database
    573 ##        try:
    574 ##            review1.content_object = dive
    575 ##            self.fail("Shouldn't be able to assign across databases")
    576 ##        except ValueError:
    577 ##            pass
     572        # Set a foreign key with an object from a different database
     573        try:
     574            review1.content_object = dive
     575            self.fail("Shouldn't be able to assign across databases")
     576        except ValueError:
     577            pass
    578578
    579 #        # Add to a foreign key set with an object from a different database
    580 #        try:
    581 #            dive.reviews.add(review1)
    582 #            self.fail("Shouldn't be able to assign across databases")
    583 #        except ValueError:
    584 #            pass
     579        # Add to a foreign key set with an object from a different database
     580        try:
     581            dive.reviews.add(review1)
     582            self.fail("Shouldn't be able to assign across databases")
     583        except ValueError:
     584            pass
    585585
    586 #        # BUT! if you assign a FK object when the base object hasn't
    587 #        # been saved yet, you implicitly assign the database for the
    588 #        # base object.
    589 #        review3 = Review(source="Python Daily")
    590 #        # initially, no db assigned
    591 #        self.assertEquals(review3._state.db, None)
     586        # BUT! if you assign a FK object when the base object hasn't
     587        # been saved yet, you implicitly assign the database for the
     588        # base object.
     589        review3 = Review(source="Python Daily")
     590        # initially, no db assigned
     591        self.assertEquals(review3._state.db, None)
    592592
    593 #        # Dive comes from 'other', so review3 is set to use 'other'...
    594 #        review3.content_object = dive
    595 #        self.assertEquals(review3._state.db, 'other')
    596 #        # ... but it isn't saved yet
    597 #        self.assertEquals(list(Review.objects.using('default').filter(object_id=pro.pk).values_list('source', flat=True)),
    598 #                          [u'Python Monthly'])
    599 #        self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source',flat=True)),
    600 #                          [u'Python Weekly'])
     593        # Dive comes from 'other', so review3 is set to use 'other'...
     594        review3.content_object = dive
     595        self.assertEquals(review3._state.db, 'other')
     596        # ... but it isn't saved yet
     597        self.assertEquals(list(Review.objects.using('default').filter(object_id=pro.pk).values_list('source', flat=True)),
     598                          [u'Python Monthly'])
     599        self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source',flat=True)),
     600                          [u'Python Weekly'])
    601601
    602 #        # When saved, John goes to 'other'
    603 #        review3.save()
    604 #        self.assertEquals(list(Review.objects.using('default').filter(object_id=pro.pk).values_list('source', flat=True)),
    605 #                          [u'Python Monthly'])
    606 #        self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source',flat=True)),
    607 #                          [u'Python Daily', u'Python Weekly'])
     602        # When saved, John goes to 'other'
     603        review3.save()
     604        self.assertEquals(list(Review.objects.using('default').filter(object_id=pro.pk).values_list('source', flat=True)),
     605                          [u'Python Monthly'])
     606        self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source',flat=True)),
     607                          [u'Python Daily', u'Python Weekly'])
    608608
    609609    def test_ordering(self):
    610610        "get_next_by_XXX commands stick to a single database"
     
    630630        val = Book.objects.raw('SELECT id FROM "multiple_database_book"').using('other')
    631631        self.assertEqual(map(lambda o: o.pk, val), [dive.pk])
    632632
     633class MasterSlaveTestCase(TestCase):
     634    multi_db = True
     635
     636    def setUp(self):
     637        # Make the 'other' database appear to be a slave of the 'default'
     638        self.old_source = settings.DATABASES['other']['SOURCE']
     639        settings.DATABASES['other']['SOURCE'] = 'default'
     640
     641    def tearDown(self):
     642        # Restore the 'other' database as an independent database
     643        settings.DATABASES['other']['SOURCE'] = self.old_source
     644
     645    def test_foreign_key_cross_database_protection(self):
     646        "Foreign keys can cross databases if they two databases have a common source"
     647        # Create a book and author on the default database
     648        pro = Book.objects.create(title="Pro Django",
     649                                  published=datetime.date(2008, 12, 16))
     650
     651        marty = Person.objects.create(name="Marty Alchin")
     652
     653        # Create a book and author on the other database
     654        dive = Book.objects.using('other').create(title="Dive into Python",
     655                                                  published=datetime.date(2009, 5, 4))
     656
     657        mark = Person.objects.using('other').create(name="Mark Pilgrim")
     658
     659        # Set a foreign key with an object from a different database
     660        try:
     661            dive.editor = marty
     662        except ValueError:
     663            self.fail("Assignment across master/slave databases with a common source should be ok")
     664
     665        # Database assignments of original objects haven't changed...
     666        self.assertEquals(marty._state.db, 'default')
     667        self.assertEquals(pro._state.db, 'default')
     668        self.assertEquals(dive._state.db, 'other')
     669        self.assertEquals(mark._state.db, 'other')
     670
     671        # ... but they will when the affected object is saved.
     672        dive.save()
     673        self.assertEquals(dive._state.db, 'default')
     674
     675        # ...and the source database now has a copy of any object saved
     676        try:
     677            Book.objects.using('default').get(title='Dive into Python').delete()
     678        except Book.DoesNotExist:
     679            self.fail('Source database should have a copy of saved object')
     680
     681        # This isn't a real master-slave database, so restore the original from other
     682        dive = Book.objects.using('other').get(title='Dive into Python')
     683        self.assertEquals(dive._state.db, 'other')
     684
     685        # Set a foreign key set with an object from a different database
     686        try:
     687            marty.edited = [pro, dive]
     688        except ValueError:
     689            self.fail("Assignment across master/slave databases with a common source should be ok")
     690
     691        # Assignment implies a save, so database assignments of original objects have changed...
     692        self.assertEquals(marty._state.db, 'default')
     693        self.assertEquals(pro._state.db, 'default')
     694        self.assertEquals(dive._state.db, 'default')
     695        self.assertEquals(mark._state.db, 'other')
     696
     697        # ...and the source database now has a copy of any object saved
     698        try:
     699            Book.objects.using('default').get(title='Dive into Python').delete()
     700        except Book.DoesNotExist:
     701            self.fail('Source database should have a copy of saved object')
     702
     703        # This isn't a real master-slave database, so restore the original from other
     704        dive = Book.objects.using('other').get(title='Dive into Python')
     705        self.assertEquals(dive._state.db, 'other')
     706
     707        # Add to a foreign key set with an object from a different database
     708        try:
     709            marty.edited.add(dive)
     710        except ValueError:
     711            self.fail("Assignment across master/slave databases with a common source should be ok")
     712
     713        # Add implies a save, so database assignments of original objects have changed...
     714        self.assertEquals(marty._state.db, 'default')
     715        self.assertEquals(pro._state.db, 'default')
     716        self.assertEquals(dive._state.db, 'default')
     717        self.assertEquals(mark._state.db, 'other')
     718
     719        # ...and the source database now has a copy of any object saved
     720        try:
     721            Book.objects.using('default').get(title='Dive into Python').delete()
     722        except Book.DoesNotExist:
     723            self.fail('Source database should have a copy of saved object')
     724
     725        # This isn't a real master-slave database, so restore the original from other
     726        dive = Book.objects.using('other').get(title='Dive into Python')
     727
     728        # If you assign a FK object when the base object hasn't
     729        # been saved yet, you implicitly assign the database for the
     730        # base object.
     731        chris = Person(name="Chris Mills")
     732        html5 = Book(title="Dive into HTML5", published=datetime.date(2010, 3, 15))
     733        # initially, no db assigned
     734        self.assertEquals(chris._state.db, None)
     735        self.assertEquals(html5._state.db, None)
     736
     737        # old object comes from 'other', so the new object is set to use the
     738        # source of 'other'...
     739        self.assertEquals(dive._state.db, 'other')
     740        dive.editor = chris
     741        html5.editor = mark
     742
     743        self.assertEquals(dive._state.db, 'other')
     744        self.assertEquals(mark._state.db, 'other')
     745        self.assertEquals(chris._state.db, 'default')
     746        self.assertEquals(html5._state.db, 'default')
     747
     748        # This also works if you assign the FK in the constructor
     749        water = Book(title="Dive into Water", published=datetime.date(2001, 1, 1), editor=mark)
     750        self.assertEquals(water._state.db, 'default')
     751
     752    def test_m2m_cross_database_protection(self):
     753        "M2M relations can cross databases if the database share a source"
     754        # Create books and authors on the inverse to the usual database
     755        pro = Book.objects.using('other').create(pk=1, title="Pro Django",
     756                                                 published=datetime.date(2008, 12, 16))
     757
     758        marty = Person.objects.using('other').create(pk=1, name="Marty Alchin")
     759
     760        dive = Book.objects.using('default').create(pk=2, title="Dive into Python",
     761                                                    published=datetime.date(2009, 5, 4))
     762
     763        mark = Person.objects.using('default').create(pk=2, name="Mark Pilgrim")
     764
     765        # Now save back onto the usual databse.
     766        # This simulates master/slave - the objects exist on both database,
     767        # but the _state.db is as it is for all other tests.
     768        pro.save(using='default')
     769        marty.save(using='default')
     770        dive.save(using='other')
     771        mark.save(using='other')
     772
     773        # Check that we have 2 of both types of object on both databases
     774        self.assertEquals(Book.objects.using('default').count(), 2)
     775        self.assertEquals(Book.objects.using('other').count(), 2)
     776        self.assertEquals(Person.objects.using('default').count(), 2)
     777        self.assertEquals(Person.objects.using('other').count(), 2)
     778
     779        # Set a m2m set with an object from a different database
     780        try:
     781            marty.book_set = [pro, dive]
     782        except ValueError:
     783            self.fail("Assignment across master/slave databases with a common source should be ok")
     784
     785        # Database assignments don't change
     786        self.assertEquals(marty._state.db, 'default')
     787        self.assertEquals(pro._state.db, 'default')
     788        self.assertEquals(dive._state.db, 'other')
     789        self.assertEquals(mark._state.db, 'other')
     790
     791        # All m2m relations should be saved on the default database
     792        self.assertEquals(Book.authors.through.objects.using('default').count(), 2)
     793        self.assertEquals(Book.authors.through.objects.using('other').count(), 0)
     794
     795        # Reset relations
     796        Book.authors.through.objects.using('default').delete()
     797
     798        # Add to an m2m with an object from a different database
     799        try:
     800            marty.book_set.add(dive)
     801        except ValueError:
     802            self.fail("Assignment across master/slave databases with a common source should be ok")
     803
     804        # Database assignments don't change
     805        self.assertEquals(marty._state.db, 'default')
     806        self.assertEquals(pro._state.db, 'default')
     807        self.assertEquals(dive._state.db, 'other')
     808        self.assertEquals(mark._state.db, 'other')
     809
     810        # All m2m relations should be saved on the default database
     811        self.assertEquals(Book.authors.through.objects.using('default').count(), 1)
     812        self.assertEquals(Book.authors.through.objects.using('other').count(), 0)
     813
     814        # Reset relations
     815        Book.authors.through.objects.using('default').delete()
     816
     817        # Set a reverse m2m with an object from a different database
     818        try:
     819            dive.authors = [mark, marty]
     820        except ValueError:
     821            self.fail("Assignment across master/slave databases with a common source should be ok")
     822
     823        # Database assignments don't change
     824        self.assertEquals(marty._state.db, 'default')
     825        self.assertEquals(pro._state.db, 'default')
     826        self.assertEquals(dive._state.db, 'other')
     827        self.assertEquals(mark._state.db, 'other')
     828
     829        # All m2m relations should be saved on the default database
     830        self.assertEquals(Book.authors.through.objects.using('default').count(), 2)
     831        self.assertEquals(Book.authors.through.objects.using('other').count(), 0)
     832
     833        # Reset relations
     834        Book.authors.through.objects.using('default').delete()
     835
     836        self.assertEquals(Book.authors.through.objects.using('default').count(), 0)
     837        self.assertEquals(Book.authors.through.objects.using('other').count(), 0)
     838
     839        # Add to a reverse m2m with an object from a different database
     840        try:
     841            dive.authors.add(marty)
     842        except ValueError:
     843            self.fail("Assignment across master/slave databases with a common source should be ok")
     844
     845        # Database assignments don't change
     846        self.assertEquals(marty._state.db, 'default')
     847        self.assertEquals(pro._state.db, 'default')
     848        self.assertEquals(dive._state.db, 'other')
     849        self.assertEquals(mark._state.db, 'other')
     850
     851        # All m2m relations should be saved on the default database
     852        self.assertEquals(Book.authors.through.objects.using('default').count(), 1)
     853        self.assertEquals(Book.authors.through.objects.using('other').count(), 0)
     854
     855    def test_generic_key_cross_database_protection(self):
     856        "Generic Key operations can span databases if they share a source"
     857        # Create a book and author on the default database
     858        pro = Book.objects.create(title="Pro Django",
     859                                  published=datetime.date(2008, 12, 16))
     860
     861        review1 = Review.objects.create(source="Python Monthly", content_object=pro)
     862
     863        # Create a book and author on the other database
     864        dive = Book.objects.using('other').create(title="Dive into Python",
     865                                                  published=datetime.date(2009, 5, 4))
     866
     867        review2 = Review.objects.using('other').create(source="Python Weekly", content_object=dive)
     868
     869        # Set a generic foreign key with an object from a different database
     870        try:
     871            review1.content_object = dive
     872        except ValueError:
     873            self.fail("Assignment across master/slave databases with a common source should be ok")
     874
     875        # Database assignments of original objects haven't changed...
     876        self.assertEquals(pro._state.db, 'default')
     877        self.assertEquals(review1._state.db, 'default')
     878        self.assertEquals(dive._state.db, 'other')
     879        self.assertEquals(review2._state.db, 'other')
     880
     881        # ... but they will when the affected object is saved.
     882        dive.save()
     883        self.assertEquals(review1._state.db, 'default')
     884        self.assertEquals(dive._state.db, 'default')
     885
     886        # ...and the source database now has a copy of any object saved
     887        try:
     888            Book.objects.using('default').get(title='Dive into Python').delete()
     889        except Book.DoesNotExist:
     890            self.fail('Source database should have a copy of saved object')
     891
     892        # This isn't a real master-slave database, so restore the original from other
     893        dive = Book.objects.using('other').get(title='Dive into Python')
     894        self.assertEquals(dive._state.db, 'other')
     895
     896        # Add to a generic foreign key set with an object from a different database
     897        try:
     898            dive.reviews.add(review1)
     899        except ValueError:
     900            self.fail("Assignment across master/slave databases with a common source should be ok")
     901
     902        # Database assignments of original objects haven't changed...
     903        self.assertEquals(pro._state.db, 'default')
     904        self.assertEquals(review1._state.db, 'default')
     905        self.assertEquals(dive._state.db, 'other')
     906        self.assertEquals(review2._state.db, 'other')
     907
     908        # ... but they will when the affected object is saved.
     909        dive.save()
     910        self.assertEquals(dive._state.db, 'default')
     911
     912        # ...and the source database now has a copy of any object saved
     913        try:
     914            Book.objects.using('default').get(title='Dive into Python').delete()
     915        except Book.DoesNotExist:
     916            self.fail('Source database should have a copy of saved object')
     917
     918        # BUT! if you assign a FK object when the base object hasn't
     919        # been saved yet, you implicitly assign the database for the
     920        # base object.
     921        review3 = Review(source="Python Daily")
     922        # initially, no db assigned
     923        self.assertEquals(review3._state.db, None)
     924
     925        # Dive comes from 'other', so review3 is set to use the source of 'other'...
     926        review3.content_object = dive
     927        self.assertEquals(review3._state.db, 'default')
     928
    633929
    634930class UserProfileTestCase(TestCase):
    635931    def setUp(self):
Back to Top