diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-03-28 16:32:11 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-03-28 16:32:11 -0400 |
| commit | c01558ae7f4af08acc523786e107ea5e2e214184 (patch) | |
| tree | a235ab825660b00c674f496354b364f58f8b464a /test | |
| parent | 9cdbed37f8c420db0b42fb959813d079622c3f3a (diff) | |
| download | sqlalchemy-c01558ae7f4af08acc523786e107ea5e2e214184.tar.gz | |
- Fixed ORM bug where changing the primary key of an object, then marking
it for DELETE would fail to target the correct row for DELETE.
Then to compound matters, basic "number of rows matched" checks were
not being performed. Both issues are fixed, however note that the
"rows matched" check requires so-called "sane multi-row count"
functionality; the DBAPI's executemany() method must count up the
rows matched by individual statements and SQLAlchemy's dialect must
mark this feature as supported, currently applies to some mysql dialects,
psycopg2, sqlite only. fixes #3006
- Enabled "sane multi-row count" checking for the psycopg2 DBAPI, as
this seems to be supported as of psycopg2 2.0.9.
Diffstat (limited to 'test')
| -rw-r--r-- | test/orm/test_naturalpks.py | 16 | ||||
| -rw-r--r-- | test/orm/test_unitofworkv2.py | 108 | ||||
| -rw-r--r-- | test/requirements.py | 9 |
3 files changed, 110 insertions, 23 deletions
diff --git a/test/orm/test_naturalpks.py b/test/orm/test_naturalpks.py index 1bb536d70..8c675b94d 100644 --- a/test/orm/test_naturalpks.py +++ b/test/orm/test_naturalpks.py @@ -389,10 +389,7 @@ class NaturalPKTest(fixtures.MappedTest): def test_manytomany_passive(self): self._test_manytomany(True) - # mysqldb executemany() of the association table fails to - # report the correct row count - @testing.fails_if(lambda: testing.against('mysql') - and not (testing.against('+zxjdbc') or testing.against('+cymysql'))) + @testing.requires.non_updating_cascade def test_manytomany_nonpassive(self): self._test_manytomany(False) @@ -798,8 +795,7 @@ class CascadeToFKPKTest(fixtures.MappedTest, testing.AssertsCompiledSQL): def test_onetomany_passive(self): self._test_onetomany(True) - # PG etc. need passive=True to allow PK->PK cascade - @testing.fails_on_everything_except('sqlite', 'oracle', '+zxjdbc') + @testing.requires.non_updating_cascade def test_onetomany_nonpassive(self): self._test_onetomany(False) @@ -876,7 +872,7 @@ class CascadeToFKPKTest(fixtures.MappedTest, testing.AssertsCompiledSQL): def test_change_m2o_passive(self): self._test_change_m2o(True) - @testing.fails_on_everything_except('sqlite', 'oracle', '+zxjdbc') + @testing.requires.non_updating_cascade def test_change_m2o_nonpassive(self): self._test_change_m2o(False) @@ -1063,8 +1059,7 @@ class JoinedInheritanceTest(fixtures.MappedTest): def test_pk_passive(self): self._test_pk(True) - # PG etc. need passive=True to allow PK->PK cascade - @testing.fails_on_everything_except('sqlite', 'oracle', '+zxjdbc') + @testing.requires.non_updating_cascade def test_pk_nonpassive(self): self._test_pk(False) @@ -1073,8 +1068,7 @@ class JoinedInheritanceTest(fixtures.MappedTest): self._test_fk(True) # PG etc. need passive=True to allow PK->PK cascade - @testing.fails_on_everything_except('sqlite', 'mysql+zxjdbc', 'oracle', - 'postgresql+zxjdbc') + @testing.requires.non_updating_cascade def test_fk_nonpassive(self): self._test_fk(False) diff --git a/test/orm/test_unitofworkv2.py b/test/orm/test_unitofworkv2.py index 34648c256..1dfc0401b 100644 --- a/test/orm/test_unitofworkv2.py +++ b/test/orm/test_unitofworkv2.py @@ -1203,20 +1203,20 @@ class RowswitchAccountingTest(fixtures.MappedTest): @classmethod def define_tables(cls, metadata): Table('parent', metadata, - Column('id', Integer, primary_key=True) + Column('id', Integer, primary_key=True), + Column('data', Integer) ) Table('child', metadata, - Column('id', Integer, ForeignKey('parent.id'), primary_key=True) + Column('id', Integer, ForeignKey('parent.id'), primary_key=True), + Column('data', Integer) ) - def test_accounting_for_rowswitch(self): + def _fixture(self): parent, child = self.tables.parent, self.tables.child - class Parent(object): - def __init__(self, id): - self.id = id - self.child = Child() - class Child(object): + class Parent(fixtures.BasicEntity): + pass + class Child(fixtures.BasicEntity): pass mapper(Parent, parent, properties={ @@ -1225,15 +1225,19 @@ class RowswitchAccountingTest(fixtures.MappedTest): backref="parent") }) mapper(Child, child) + return Parent, Child + + def test_switch_on_update(self): + Parent, Child = self._fixture() sess = create_session(autocommit=False) - p1 = Parent(1) + p1 = Parent(id=1, child=Child()) sess.add(p1) sess.commit() sess.close() - p2 = Parent(1) + p2 = Parent(id=1, child=Child()) p3 = sess.merge(p2) old = attributes.get_history(p3, 'child')[2][0] @@ -1244,7 +1248,7 @@ class RowswitchAccountingTest(fixtures.MappedTest): assert p3.child._sa_instance_state.session_id == sess.hash_key assert p3.child in sess - p4 = Parent(1) + p4 = Parent(id=1, child=Child()) p5 = sess.merge(p4) old = attributes.get_history(p5, 'child')[2][0] @@ -1252,6 +1256,88 @@ class RowswitchAccountingTest(fixtures.MappedTest): sess.flush() + def test_switch_on_delete(self): + Parent, Child = self._fixture() + + sess = Session() + p1 = Parent(id=1, data=2, child=None) + sess.add(p1) + sess.flush() + + p1.id = 5 + sess.delete(p1) + eq_(p1.id, 5) + sess.flush() + + eq_(sess.scalar(self.tables.parent.count()), 0) + + +class BasicStaleChecksTest(fixtures.MappedTest): + @classmethod + def define_tables(cls, metadata): + Table('parent', metadata, + Column('id', Integer, primary_key=True), + Column('data', Integer) + ) + Table('child', metadata, + Column('id', Integer, ForeignKey('parent.id'), primary_key=True), + Column('data', Integer) + ) + + def _fixture(self): + parent, child = self.tables.parent, self.tables.child + + class Parent(fixtures.BasicEntity): + pass + class Child(fixtures.BasicEntity): + pass + + mapper(Parent, parent, properties={ + 'child':relationship(Child, uselist=False, + cascade="all, delete-orphan", + backref="parent") + }) + mapper(Child, child) + return Parent, Child + + def test_update_single_missing(self): + Parent, Child = self._fixture() + sess = Session() + p1 = Parent(id=1, data=2) + sess.add(p1) + sess.flush() + + sess.execute(self.tables.parent.delete()) + + p1.data = 3 + assert_raises_message( + orm_exc.StaleDataError, + "UPDATE statement on table 'parent' expected to " + "update 1 row\(s\); 0 were matched.", + sess.flush + ) + + @testing.requires.sane_multi_rowcount + def test_delete_multi_missing(self): + Parent, Child = self._fixture() + sess = Session() + p1 = Parent(id=1, data=2, child=None) + p2 = Parent(id=2, data=3, child=None) + sess.add_all([p1, p2]) + sess.flush() + + sess.execute(self.tables.parent.delete()) + sess.delete(p1) + sess.delete(p2) + + assert_raises_message( + orm_exc.StaleDataError, + "DELETE statement on table 'parent' expected to " + "delete 2 row\(s\); 0 were matched.", + sess.flush + ) + + class BatchInsertsTest(fixtures.MappedTest, testing.AssertsExecutionResults): @classmethod def define_tables(cls, metadata): diff --git a/test/requirements.py b/test/requirements.py index 4184058b2..d6b1d3969 100644 --- a/test/requirements.py +++ b/test/requirements.py @@ -64,6 +64,13 @@ class DefaultRequirements(SuiteRequirements): ) @property + def non_updating_cascade(self): + """target database must *not* support ON UPDATE..CASCADE behavior in + foreign keys.""" + + return fails_on_everything_except('sqlite', 'oracle', '+zxjdbc') + skip_if('mssql') + + @property def deferrable_fks(self): """target database must support deferrable fks""" @@ -438,7 +445,7 @@ class DefaultRequirements(SuiteRequirements): @property def sane_multi_rowcount(self): - return skip_if( + return fails_if( lambda config: not config.db.dialect.supports_sane_multi_rowcount, "driver doesn't support 'sane' multi row count" ) |
