mirror of
https://github.com/sqlalchemy/sqlalchemy.git
synced 2026-05-28 11:35:19 -04:00
3feea4503f
Fixed bug in :meth:`.Session.merge` where objects in a collection that had
the primary key attribute set to ``None`` for a key that is typically
autoincrementing would be considered to be a database-persisted key for
part of the internal deduplication process, causing only one object to
actually be inserted in the database.
Change-Id: I0a6e00043be0b2979cda33740e1be3b430ecf8c7
Fixes: #4056
(cherry picked from commit 5243341ed8)
1682 lines
54 KiB
Python
1682 lines
54 KiB
Python
from sqlalchemy.testing import assert_raises_message
|
|
import sqlalchemy as sa
|
|
from sqlalchemy import Integer, PickleType, String, ForeignKey, Text
|
|
import operator
|
|
from sqlalchemy import testing
|
|
from sqlalchemy.util import OrderedSet
|
|
from sqlalchemy.orm import mapper, relationship, create_session, \
|
|
PropComparator, synonym, comparable_property, sessionmaker, \
|
|
attributes, Session, backref, configure_mappers, foreign, deferred, defer
|
|
from sqlalchemy.orm.collections import attribute_mapped_collection
|
|
from sqlalchemy.orm.interfaces import MapperOption
|
|
from sqlalchemy.testing import eq_, in_, not_in_
|
|
from sqlalchemy.testing import fixtures
|
|
from test.orm import _fixtures
|
|
from sqlalchemy import event, and_, case
|
|
from sqlalchemy.testing.schema import Table, Column
|
|
|
|
|
|
class MergeTest(_fixtures.FixtureTest):
|
|
"""Session.merge() functionality"""
|
|
|
|
run_inserts = None
|
|
|
|
def load_tracker(self, cls, canary=None):
|
|
if canary is None:
|
|
def canary(instance, *args):
|
|
canary.called += 1
|
|
canary.called = 0
|
|
|
|
event.listen(cls, 'load', canary)
|
|
|
|
return canary
|
|
|
|
def test_transient_to_pending(self):
|
|
User, users = self.classes.User, self.tables.users
|
|
|
|
mapper(User, users)
|
|
sess = create_session()
|
|
load = self.load_tracker(User)
|
|
|
|
u = User(id=7, name='fred')
|
|
eq_(load.called, 0)
|
|
u2 = sess.merge(u)
|
|
eq_(load.called, 1)
|
|
assert u2 in sess
|
|
eq_(u2, User(id=7, name='fred'))
|
|
sess.flush()
|
|
sess.expunge_all()
|
|
eq_(sess.query(User).first(), User(id=7, name='fred'))
|
|
|
|
def test_transient_to_pending_no_pk(self):
|
|
"""test that a transient object with no PK attribute
|
|
doesn't trigger a needless load."""
|
|
|
|
User, users = self.classes.User, self.tables.users
|
|
|
|
mapper(User, users)
|
|
sess = create_session()
|
|
u = User(name='fred')
|
|
|
|
def go():
|
|
sess.merge(u)
|
|
self.assert_sql_count(testing.db, go, 0)
|
|
|
|
def test_transient_to_pending_collection(self):
|
|
User, Address, addresses, users = (self.classes.User,
|
|
self.classes.Address,
|
|
self.tables.addresses,
|
|
self.tables.users)
|
|
|
|
mapper(User, users, properties={
|
|
'addresses': relationship(Address, backref='user',
|
|
collection_class=OrderedSet)})
|
|
mapper(Address, addresses)
|
|
load = self.load_tracker(User)
|
|
self.load_tracker(Address, load)
|
|
|
|
u = User(id=7, name='fred', addresses=OrderedSet([
|
|
Address(id=1, email_address='fred1'),
|
|
Address(id=2, email_address='fred2'),
|
|
]))
|
|
eq_(load.called, 0)
|
|
|
|
sess = create_session()
|
|
sess.merge(u)
|
|
eq_(load.called, 3)
|
|
|
|
merged_users = [e for e in sess if isinstance(e, User)]
|
|
eq_(len(merged_users), 1)
|
|
assert merged_users[0] is not u
|
|
|
|
sess.flush()
|
|
sess.expunge_all()
|
|
|
|
eq_(sess.query(User).one(),
|
|
User(id=7, name='fred', addresses=OrderedSet([
|
|
Address(id=1, email_address='fred1'),
|
|
Address(id=2, email_address='fred2'),
|
|
])))
|
|
|
|
def test_transient_to_pending_collection_pk_none(self):
|
|
User, Address, addresses, users = (self.classes.User,
|
|
self.classes.Address,
|
|
self.tables.addresses,
|
|
self.tables.users)
|
|
|
|
mapper(User, users, properties={
|
|
'addresses': relationship(Address, backref='user',
|
|
collection_class=OrderedSet)})
|
|
mapper(Address, addresses)
|
|
load = self.load_tracker(User)
|
|
self.load_tracker(Address, load)
|
|
|
|
u = User(id=None, name='fred', addresses=OrderedSet([
|
|
Address(id=None, email_address='fred1'),
|
|
Address(id=None, email_address='fred2'),
|
|
]))
|
|
eq_(load.called, 0)
|
|
|
|
sess = create_session()
|
|
sess.merge(u)
|
|
eq_(load.called, 3)
|
|
|
|
merged_users = [e for e in sess if isinstance(e, User)]
|
|
eq_(len(merged_users), 1)
|
|
assert merged_users[0] is not u
|
|
|
|
sess.flush()
|
|
sess.expunge_all()
|
|
|
|
eq_(sess.query(User).one(),
|
|
User(name='fred', addresses=OrderedSet([
|
|
Address(email_address='fred1'),
|
|
Address(email_address='fred2'),
|
|
])))
|
|
|
|
def test_transient_to_persistent(self):
|
|
User, users = self.classes.User, self.tables.users
|
|
|
|
mapper(User, users)
|
|
load = self.load_tracker(User)
|
|
|
|
sess = create_session()
|
|
u = User(id=7, name='fred')
|
|
sess.add(u)
|
|
sess.flush()
|
|
sess.expunge_all()
|
|
|
|
eq_(load.called, 0)
|
|
|
|
_u2 = u2 = User(id=7, name='fred jones')
|
|
eq_(load.called, 0)
|
|
u2 = sess.merge(u2)
|
|
assert u2 is not _u2
|
|
eq_(load.called, 1)
|
|
sess.flush()
|
|
sess.expunge_all()
|
|
eq_(sess.query(User).first(), User(id=7, name='fred jones'))
|
|
eq_(load.called, 2)
|
|
|
|
def test_transient_to_persistent_collection(self):
|
|
User, Address, addresses, users = (self.classes.User,
|
|
self.classes.Address,
|
|
self.tables.addresses,
|
|
self.tables.users)
|
|
|
|
mapper(User, users, properties={
|
|
'addresses': relationship(Address,
|
|
backref='user',
|
|
collection_class=OrderedSet,
|
|
order_by=addresses.c.id,
|
|
cascade="all, delete-orphan")
|
|
})
|
|
mapper(Address, addresses)
|
|
|
|
load = self.load_tracker(User)
|
|
self.load_tracker(Address, load)
|
|
|
|
u = User(id=7, name='fred', addresses=OrderedSet([
|
|
Address(id=1, email_address='fred1'),
|
|
Address(id=2, email_address='fred2'),
|
|
]))
|
|
sess = create_session()
|
|
sess.add(u)
|
|
sess.flush()
|
|
sess.expunge_all()
|
|
|
|
eq_(load.called, 0)
|
|
|
|
u = User(id=7, name='fred', addresses=OrderedSet([
|
|
Address(id=3, email_address='fred3'),
|
|
Address(id=4, email_address='fred4'),
|
|
]))
|
|
|
|
u = sess.merge(u)
|
|
|
|
# 1. merges User object. updates into session.
|
|
# 2.,3. merges Address ids 3 & 4, saves into session.
|
|
# 4.,5. loads pre-existing elements in "addresses" collection,
|
|
# marks as deleted, Address ids 1 and 2.
|
|
eq_(load.called, 5)
|
|
|
|
eq_(u,
|
|
User(id=7, name='fred', addresses=OrderedSet([
|
|
Address(id=3, email_address='fred3'),
|
|
Address(id=4, email_address='fred4'),
|
|
])))
|
|
sess.flush()
|
|
sess.expunge_all()
|
|
eq_(sess.query(User).one(),
|
|
User(id=7, name='fred', addresses=OrderedSet([
|
|
Address(id=3, email_address='fred3'),
|
|
Address(id=4, email_address='fred4'),
|
|
])))
|
|
|
|
def test_detached_to_persistent_collection(self):
|
|
users, Address, addresses, User = (self.tables.users,
|
|
self.classes.Address,
|
|
self.tables.addresses,
|
|
self.classes.User)
|
|
|
|
mapper(User, users, properties={
|
|
'addresses': relationship(Address,
|
|
backref='user',
|
|
order_by=addresses.c.id,
|
|
collection_class=OrderedSet)})
|
|
mapper(Address, addresses)
|
|
load = self.load_tracker(User)
|
|
self.load_tracker(Address, load)
|
|
|
|
a = Address(id=1, email_address='fred1')
|
|
u = User(id=7, name='fred', addresses=OrderedSet([
|
|
a,
|
|
Address(id=2, email_address='fred2'),
|
|
]))
|
|
sess = create_session()
|
|
sess.add(u)
|
|
sess.flush()
|
|
sess.expunge_all()
|
|
|
|
u.name = 'fred jones'
|
|
u.addresses.add(Address(id=3, email_address='fred3'))
|
|
u.addresses.remove(a)
|
|
|
|
eq_(load.called, 0)
|
|
u = sess.merge(u)
|
|
eq_(load.called, 4)
|
|
sess.flush()
|
|
sess.expunge_all()
|
|
|
|
eq_(sess.query(User).first(),
|
|
User(id=7, name='fred jones', addresses=OrderedSet([
|
|
Address(id=2, email_address='fred2'),
|
|
Address(id=3, email_address='fred3')])))
|
|
|
|
def test_unsaved_cascade(self):
|
|
"""Merge of a transient entity with two child transient
|
|
entities, with a bidirectional relationship."""
|
|
|
|
users, Address, addresses, User = (self.tables.users,
|
|
self.classes.Address,
|
|
self.tables.addresses,
|
|
self.classes.User)
|
|
|
|
mapper(User, users, properties={
|
|
'addresses': relationship(mapper(Address, addresses),
|
|
cascade="all", backref="user")
|
|
})
|
|
load = self.load_tracker(User)
|
|
self.load_tracker(Address, load)
|
|
sess = create_session()
|
|
|
|
u = User(id=7, name='fred')
|
|
a1 = Address(email_address='foo@bar.com')
|
|
a2 = Address(email_address='hoho@bar.com')
|
|
u.addresses.append(a1)
|
|
u.addresses.append(a2)
|
|
|
|
u2 = sess.merge(u)
|
|
eq_(load.called, 3)
|
|
|
|
eq_(u,
|
|
User(id=7, name='fred', addresses=[
|
|
Address(email_address='foo@bar.com'),
|
|
Address(email_address='hoho@bar.com')]))
|
|
eq_(u2,
|
|
User(id=7, name='fred', addresses=[
|
|
Address(email_address='foo@bar.com'),
|
|
Address(email_address='hoho@bar.com')]))
|
|
|
|
sess.flush()
|
|
sess.expunge_all()
|
|
u2 = sess.query(User).get(7)
|
|
|
|
eq_(u2, User(id=7, name='fred', addresses=[
|
|
Address(email_address='foo@bar.com'),
|
|
Address(email_address='hoho@bar.com')]))
|
|
eq_(load.called, 6)
|
|
|
|
def test_merge_empty_attributes(self):
|
|
User, dingalings = self.classes.User, self.tables.dingalings
|
|
|
|
mapper(User, dingalings)
|
|
|
|
sess = create_session()
|
|
|
|
# merge empty stuff. goes in as NULL.
|
|
# not sure what this was originally trying to
|
|
# test.
|
|
u1 = sess.merge(User(id=1))
|
|
sess.flush()
|
|
assert u1.data is None
|
|
|
|
# save another user with "data"
|
|
u2 = User(id=2, data="foo")
|
|
sess.add(u2)
|
|
sess.flush()
|
|
|
|
# merge User on u2's pk with
|
|
# no "data".
|
|
# value isn't whacked from the destination
|
|
# dict.
|
|
u3 = sess.merge(User(id=2))
|
|
eq_(u3.__dict__['data'], "foo")
|
|
|
|
# make a change.
|
|
u3.data = 'bar'
|
|
|
|
# merge another no-"data" user.
|
|
# attribute maintains modified state.
|
|
# (usually autoflush would have happened
|
|
# here anyway).
|
|
u4 = sess.merge(User(id=2))
|
|
eq_(u3.__dict__['data'], "bar")
|
|
|
|
sess.flush()
|
|
# and after the flush.
|
|
eq_(u3.data, "bar")
|
|
|
|
# new row.
|
|
u5 = User(id=3, data="foo")
|
|
sess.add(u5)
|
|
sess.flush()
|
|
|
|
# blow it away from u5, but don't
|
|
# mark as expired. so it would just
|
|
# be blank.
|
|
del u5.data
|
|
|
|
# the merge adds expiry to the
|
|
# attribute so that it loads.
|
|
# not sure if I like this - it currently is needed
|
|
# for test_pickled:PickleTest.test_instance_deferred_cols
|
|
u6 = sess.merge(User(id=3))
|
|
assert 'data' not in u6.__dict__
|
|
assert u6.data == "foo"
|
|
|
|
# set it to None. this is actually
|
|
# a change so gets preserved.
|
|
u6.data = None
|
|
u7 = sess.merge(User(id=3))
|
|
assert u6.__dict__['data'] is None
|
|
|
|
def test_merge_irregular_collection(self):
|
|
users, Address, addresses, User = (self.tables.users,
|
|
self.classes.Address,
|
|
self.tables.addresses,
|
|
self.classes.User)
|
|
|
|
mapper(User, users, properties={
|
|
'addresses': relationship(
|
|
mapper(Address, addresses),
|
|
backref='user',
|
|
collection_class=attribute_mapped_collection('email_address')),
|
|
})
|
|
u1 = User(id=7, name='fred')
|
|
u1.addresses['foo@bar.com'] = Address(email_address='foo@bar.com')
|
|
sess = create_session()
|
|
sess.merge(u1)
|
|
sess.flush()
|
|
assert list(u1.addresses.keys()) == ['foo@bar.com']
|
|
|
|
def test_attribute_cascade(self):
|
|
"""Merge of a persistent entity with two child
|
|
persistent entities."""
|
|
|
|
users, Address, addresses, User = (self.tables.users,
|
|
self.classes.Address,
|
|
self.tables.addresses,
|
|
self.classes.User)
|
|
|
|
mapper(User, users, properties={
|
|
'addresses': relationship(mapper(Address, addresses),
|
|
backref='user')
|
|
})
|
|
load = self.load_tracker(User)
|
|
self.load_tracker(Address, load)
|
|
|
|
sess = create_session()
|
|
|
|
# set up data and save
|
|
u = User(id=7, name='fred', addresses=[
|
|
Address(email_address='foo@bar.com'),
|
|
Address(email_address='hoho@la.com')])
|
|
sess.add(u)
|
|
sess.flush()
|
|
|
|
# assert data was saved
|
|
sess2 = create_session()
|
|
u2 = sess2.query(User).get(7)
|
|
eq_(u2,
|
|
User(id=7, name='fred', addresses=[
|
|
Address(email_address='foo@bar.com'),
|
|
Address(email_address='hoho@la.com')]))
|
|
|
|
# make local changes to data
|
|
u.name = 'fred2'
|
|
u.addresses[1].email_address = 'hoho@lalala.com'
|
|
|
|
eq_(load.called, 3)
|
|
|
|
# new session, merge modified data into session
|
|
sess3 = create_session()
|
|
u3 = sess3.merge(u)
|
|
eq_(load.called, 6)
|
|
|
|
# ensure local changes are pending
|
|
eq_(u3, User(id=7, name='fred2', addresses=[
|
|
Address(email_address='foo@bar.com'),
|
|
Address(email_address='hoho@lalala.com')]))
|
|
|
|
# save merged data
|
|
sess3.flush()
|
|
|
|
# assert modified/merged data was saved
|
|
sess.expunge_all()
|
|
u = sess.query(User).get(7)
|
|
eq_(u, User(id=7, name='fred2', addresses=[
|
|
Address(email_address='foo@bar.com'),
|
|
Address(email_address='hoho@lalala.com')]))
|
|
eq_(load.called, 9)
|
|
|
|
# merge persistent object into another session
|
|
sess4 = create_session()
|
|
u = sess4.merge(u)
|
|
assert len(u.addresses)
|
|
for a in u.addresses:
|
|
assert a.user is u
|
|
|
|
def go():
|
|
sess4.flush()
|
|
# no changes; therefore flush should do nothing
|
|
self.assert_sql_count(testing.db, go, 0)
|
|
eq_(load.called, 12)
|
|
|
|
# test with "dontload" merge
|
|
sess5 = create_session()
|
|
u = sess5.merge(u, load=False)
|
|
assert len(u.addresses)
|
|
for a in u.addresses:
|
|
assert a.user is u
|
|
|
|
def go():
|
|
sess5.flush()
|
|
# no changes; therefore flush should do nothing
|
|
# but also, load=False wipes out any difference in committed state,
|
|
# so no flush at all
|
|
self.assert_sql_count(testing.db, go, 0)
|
|
eq_(load.called, 15)
|
|
|
|
sess4 = create_session()
|
|
u = sess4.merge(u, load=False)
|
|
# post merge change
|
|
u.addresses[1].email_address = 'afafds'
|
|
|
|
def go():
|
|
sess4.flush()
|
|
# afafds change flushes
|
|
self.assert_sql_count(testing.db, go, 1)
|
|
eq_(load.called, 18)
|
|
|
|
sess5 = create_session()
|
|
u2 = sess5.query(User).get(u.id)
|
|
eq_(u2.name, 'fred2')
|
|
eq_(u2.addresses[1].email_address, 'afafds')
|
|
eq_(load.called, 21)
|
|
|
|
def test_dont_send_neverset_to_get(self):
|
|
# test issue #3647
|
|
CompositePk, composite_pk_table = (
|
|
self.classes.CompositePk, self.tables.composite_pk_table
|
|
)
|
|
mapper(CompositePk, composite_pk_table)
|
|
cp1 = CompositePk(j=1, k=1)
|
|
|
|
sess = Session()
|
|
|
|
rec = []
|
|
|
|
def go():
|
|
rec.append(sess.merge(cp1))
|
|
self.assert_sql_count(testing.db, go, 0)
|
|
rec[0].i = 5
|
|
sess.commit()
|
|
eq_(rec[0].i, 5)
|
|
|
|
def test_dont_send_neverset_to_get_w_relationship(self):
|
|
# test issue #3647
|
|
CompositePk, composite_pk_table = (
|
|
self.classes.CompositePk, self.tables.composite_pk_table
|
|
)
|
|
User, users = (
|
|
self.classes.User, self.tables.users
|
|
)
|
|
mapper(User, users, properties={
|
|
'elements': relationship(
|
|
CompositePk,
|
|
primaryjoin=users.c.id == foreign(composite_pk_table.c.i))
|
|
})
|
|
mapper(CompositePk, composite_pk_table)
|
|
|
|
u1 = User(id=5, name='some user')
|
|
cp1 = CompositePk(j=1, k=1)
|
|
u1.elements.append(cp1)
|
|
sess = Session()
|
|
|
|
rec = []
|
|
|
|
def go():
|
|
rec.append(sess.merge(u1))
|
|
self.assert_sql_count(testing.db, go, 1)
|
|
u2 = rec[0]
|
|
sess.commit()
|
|
eq_(u2.elements[0].i, 5)
|
|
eq_(u2.id, 5)
|
|
|
|
def test_no_relationship_cascade(self):
|
|
"""test that merge doesn't interfere with a relationship()
|
|
target that specifically doesn't include 'merge' cascade.
|
|
"""
|
|
|
|
Address, addresses, users, User = (self.classes.Address,
|
|
self.tables.addresses,
|
|
self.tables.users,
|
|
self.classes.User)
|
|
|
|
mapper(Address, addresses, properties={
|
|
'user': relationship(User, cascade="save-update")
|
|
})
|
|
mapper(User, users)
|
|
sess = create_session()
|
|
u1 = User(name="fred")
|
|
a1 = Address(email_address="asdf", user=u1)
|
|
sess.add(a1)
|
|
sess.flush()
|
|
|
|
a2 = Address(id=a1.id, email_address="bar", user=User(name="hoho"))
|
|
a2 = sess.merge(a2)
|
|
sess.flush()
|
|
|
|
# no expire of the attribute
|
|
|
|
assert a2.__dict__['user'] is u1
|
|
|
|
# merge succeeded
|
|
eq_(
|
|
sess.query(Address).all(),
|
|
[Address(id=a1.id, email_address="bar")]
|
|
)
|
|
|
|
# didn't touch user
|
|
eq_(
|
|
sess.query(User).all(),
|
|
[User(name="fred")]
|
|
)
|
|
|
|
def test_one_to_many_cascade(self):
|
|
users, Address, addresses, User = (self.tables.users,
|
|
self.classes.Address,
|
|
self.tables.addresses,
|
|
self.classes.User)
|
|
|
|
mapper(User, users, properties={
|
|
'addresses': relationship(mapper(Address, addresses))})
|
|
|
|
load = self.load_tracker(User)
|
|
self.load_tracker(Address, load)
|
|
|
|
sess = create_session()
|
|
u = User(name='fred')
|
|
a1 = Address(email_address='foo@bar')
|
|
a2 = Address(email_address='foo@quux')
|
|
u.addresses.extend([a1, a2])
|
|
|
|
sess.add(u)
|
|
sess.flush()
|
|
|
|
eq_(load.called, 0)
|
|
|
|
sess2 = create_session()
|
|
u2 = sess2.query(User).get(u.id)
|
|
eq_(load.called, 1)
|
|
|
|
u.addresses[1].email_address = 'addr 2 modified'
|
|
sess2.merge(u)
|
|
eq_(u2.addresses[1].email_address, 'addr 2 modified')
|
|
eq_(load.called, 3)
|
|
|
|
sess3 = create_session()
|
|
u3 = sess3.query(User).get(u.id)
|
|
eq_(load.called, 4)
|
|
|
|
u.name = 'also fred'
|
|
sess3.merge(u)
|
|
eq_(load.called, 6)
|
|
eq_(u3.name, 'also fred')
|
|
|
|
def test_many_to_one_cascade(self):
|
|
Address, addresses, users, User = (self.classes.Address,
|
|
self.tables.addresses,
|
|
self.tables.users,
|
|
self.classes.User)
|
|
|
|
mapper(Address, addresses, properties={
|
|
'user': relationship(User)
|
|
})
|
|
mapper(User, users)
|
|
|
|
u1 = User(id=1, name="u1")
|
|
a1 = Address(id=1, email_address="a1", user=u1)
|
|
u2 = User(id=2, name="u2")
|
|
|
|
sess = create_session()
|
|
sess.add_all([a1, u2])
|
|
sess.flush()
|
|
|
|
a1.user = u2
|
|
|
|
sess2 = create_session()
|
|
a2 = sess2.merge(a1)
|
|
eq_(
|
|
attributes.get_history(a2, 'user'),
|
|
([u2], (), ())
|
|
)
|
|
assert a2 in sess2.dirty
|
|
|
|
sess.refresh(a1)
|
|
|
|
sess2 = create_session()
|
|
a2 = sess2.merge(a1, load=False)
|
|
eq_(
|
|
attributes.get_history(a2, 'user'),
|
|
((), [u1], ())
|
|
)
|
|
assert a2 not in sess2.dirty
|
|
|
|
def test_many_to_many_cascade(self):
|
|
items, Order, orders, order_items, Item = (self.tables.items,
|
|
self.classes.Order,
|
|
self.tables.orders,
|
|
self.tables.order_items,
|
|
self.classes.Item)
|
|
|
|
mapper(Order, orders, properties={
|
|
'items': relationship(mapper(Item, items),
|
|
secondary=order_items)})
|
|
|
|
load = self.load_tracker(Order)
|
|
self.load_tracker(Item, load)
|
|
|
|
sess = create_session()
|
|
|
|
i1 = Item()
|
|
i1.description = 'item 1'
|
|
|
|
i2 = Item()
|
|
i2.description = 'item 2'
|
|
|
|
o = Order()
|
|
o.description = 'order description'
|
|
o.items.append(i1)
|
|
o.items.append(i2)
|
|
|
|
sess.add(o)
|
|
sess.flush()
|
|
|
|
eq_(load.called, 0)
|
|
|
|
sess2 = create_session()
|
|
o2 = sess2.query(Order).get(o.id)
|
|
eq_(load.called, 1)
|
|
|
|
o.items[1].description = 'item 2 modified'
|
|
sess2.merge(o)
|
|
eq_(o2.items[1].description, 'item 2 modified')
|
|
eq_(load.called, 3)
|
|
|
|
sess3 = create_session()
|
|
o3 = sess3.query(Order).get(o.id)
|
|
eq_(load.called, 4)
|
|
|
|
o.description = 'desc modified'
|
|
sess3.merge(o)
|
|
eq_(load.called, 6)
|
|
eq_(o3.description, 'desc modified')
|
|
|
|
def test_one_to_one_cascade(self):
|
|
users, Address, addresses, User = (self.tables.users,
|
|
self.classes.Address,
|
|
self.tables.addresses,
|
|
self.classes.User)
|
|
|
|
mapper(User, users, properties={
|
|
'address': relationship(mapper(Address, addresses),
|
|
uselist=False)
|
|
})
|
|
load = self.load_tracker(User)
|
|
self.load_tracker(Address, load)
|
|
sess = create_session()
|
|
|
|
u = User()
|
|
u.id = 7
|
|
u.name = "fred"
|
|
a1 = Address()
|
|
a1.email_address = 'foo@bar.com'
|
|
u.address = a1
|
|
|
|
sess.add(u)
|
|
sess.flush()
|
|
|
|
eq_(load.called, 0)
|
|
|
|
sess2 = create_session()
|
|
u2 = sess2.query(User).get(7)
|
|
eq_(load.called, 1)
|
|
u2.name = 'fred2'
|
|
u2.address.email_address = 'hoho@lalala.com'
|
|
eq_(load.called, 2)
|
|
|
|
u3 = sess.merge(u2)
|
|
eq_(load.called, 2)
|
|
assert u3 is u
|
|
|
|
def test_value_to_none(self):
|
|
users, Address, addresses, User = (self.tables.users,
|
|
self.classes.Address,
|
|
self.tables.addresses,
|
|
self.classes.User)
|
|
|
|
mapper(User, users, properties={
|
|
'address': relationship(mapper(Address, addresses),
|
|
uselist=False, backref='user')
|
|
})
|
|
sess = sessionmaker()()
|
|
u = User(id=7, name="fred",
|
|
address=Address(id=1, email_address='foo@bar.com'))
|
|
sess.add(u)
|
|
sess.commit()
|
|
sess.close()
|
|
|
|
u2 = User(id=7, name=None, address=None)
|
|
u3 = sess.merge(u2)
|
|
assert u3.name is None
|
|
assert u3.address is None
|
|
|
|
sess.close()
|
|
|
|
a1 = Address(id=1, user=None)
|
|
a2 = sess.merge(a1)
|
|
assert a2.user is None
|
|
|
|
def test_transient_no_load(self):
|
|
users, User = self.tables.users, self.classes.User
|
|
|
|
mapper(User, users)
|
|
|
|
sess = create_session()
|
|
u = User()
|
|
assert_raises_message(sa.exc.InvalidRequestError,
|
|
"load=False option does not support",
|
|
sess.merge, u, load=False)
|
|
|
|
def test_no_load_with_backrefs(self):
|
|
"""load=False populates relationships in both
|
|
directions without requiring a load"""
|
|
|
|
users, Address, addresses, User = (self.tables.users,
|
|
self.classes.Address,
|
|
self.tables.addresses,
|
|
self.classes.User)
|
|
|
|
mapper(User, users, properties={
|
|
'addresses': relationship(mapper(Address, addresses),
|
|
backref='user')
|
|
})
|
|
|
|
u = User(id=7, name='fred', addresses=[
|
|
Address(email_address='ad1'),
|
|
Address(email_address='ad2')])
|
|
sess = create_session()
|
|
sess.add(u)
|
|
sess.flush()
|
|
sess.close()
|
|
assert 'user' in u.addresses[1].__dict__
|
|
|
|
sess = create_session()
|
|
u2 = sess.merge(u, load=False)
|
|
assert 'user' in u2.addresses[1].__dict__
|
|
eq_(u2.addresses[1].user, User(id=7, name='fred'))
|
|
|
|
sess.expire(u2.addresses[1], ['user'])
|
|
assert 'user' not in u2.addresses[1].__dict__
|
|
sess.close()
|
|
|
|
sess = create_session()
|
|
u = sess.merge(u2, load=False)
|
|
assert 'user' not in u.addresses[1].__dict__
|
|
eq_(u.addresses[1].user, User(id=7, name='fred'))
|
|
|
|
def test_dontload_with_eager(self):
|
|
"""
|
|
|
|
This test illustrates that with load=False, we can't just copy
|
|
the committed_state of the merged instance over; since it
|
|
references collection objects which themselves are to be merged.
|
|
This committed_state would instead need to be piecemeal
|
|
'converted' to represent the correct objects. However, at the
|
|
moment I'd rather not support this use case; if you are merging
|
|
with load=False, you're typically dealing with caching and the
|
|
merged objects shouldn't be 'dirty'.
|
|
|
|
"""
|
|
|
|
users, Address, addresses, User = (self.tables.users,
|
|
self.classes.Address,
|
|
self.tables.addresses,
|
|
self.classes.User)
|
|
|
|
mapper(User, users, properties={
|
|
'addresses': relationship(mapper(Address, addresses))
|
|
})
|
|
sess = create_session()
|
|
u = User()
|
|
u.id = 7
|
|
u.name = "fred"
|
|
a1 = Address()
|
|
a1.email_address = 'foo@bar.com'
|
|
u.addresses.append(a1)
|
|
|
|
sess.add(u)
|
|
sess.flush()
|
|
|
|
sess2 = create_session()
|
|
u2 = sess2.query(User).\
|
|
options(sa.orm.joinedload('addresses')).get(7)
|
|
|
|
sess3 = create_session()
|
|
u3 = sess3.merge(u2, load=False)
|
|
|
|
def go():
|
|
sess3.flush()
|
|
self.assert_sql_count(testing.db, go, 0)
|
|
|
|
def test_no_load_disallows_dirty(self):
|
|
"""load=False doesn't support 'dirty' objects right now
|
|
|
|
(see test_no_load_with_eager()). Therefore lets assert it.
|
|
|
|
"""
|
|
|
|
users, User = self.tables.users, self.classes.User
|
|
|
|
mapper(User, users)
|
|
sess = create_session()
|
|
u = User()
|
|
u.id = 7
|
|
u.name = "fred"
|
|
sess.add(u)
|
|
sess.flush()
|
|
|
|
u.name = 'ed'
|
|
sess2 = create_session()
|
|
try:
|
|
sess2.merge(u, load=False)
|
|
assert False
|
|
except sa.exc.InvalidRequestError as e:
|
|
assert "merge() with load=False option does not support "\
|
|
"objects marked as 'dirty'. flush() all changes on "\
|
|
"mapped instances before merging with load=False." \
|
|
in str(e)
|
|
|
|
u2 = sess2.query(User).get(7)
|
|
|
|
sess3 = create_session()
|
|
u3 = sess3.merge(u2, load=False)
|
|
assert not sess3.dirty
|
|
|
|
def go():
|
|
sess3.flush()
|
|
self.assert_sql_count(testing.db, go, 0)
|
|
|
|
def test_no_load_sets_backrefs(self):
|
|
users, Address, addresses, User = (self.tables.users,
|
|
self.classes.Address,
|
|
self.tables.addresses,
|
|
self.classes.User)
|
|
|
|
mapper(User, users, properties={
|
|
'addresses': relationship(mapper(Address, addresses),
|
|
backref='user')})
|
|
|
|
sess = create_session()
|
|
u = User()
|
|
u.id = 7
|
|
u.name = "fred"
|
|
a1 = Address()
|
|
a1.email_address = 'foo@bar.com'
|
|
u.addresses.append(a1)
|
|
|
|
sess.add(u)
|
|
sess.flush()
|
|
|
|
assert u.addresses[0].user is u
|
|
|
|
sess2 = create_session()
|
|
u2 = sess2.merge(u, load=False)
|
|
assert not sess2.dirty
|
|
|
|
def go():
|
|
assert u2.addresses[0].user is u2
|
|
self.assert_sql_count(testing.db, go, 0)
|
|
|
|
def test_no_load_preserves_parents(self):
|
|
"""Merge with load=False does not trigger a 'delete-orphan'
|
|
operation.
|
|
|
|
merge with load=False sets attributes without using events.
|
|
this means the 'hasparent' flag is not propagated to the newly
|
|
merged instance. in fact this works out OK, because the
|
|
'_state.parents' collection on the newly merged instance is
|
|
empty; since the mapper doesn't see an active 'False' setting in
|
|
this collection when _is_orphan() is called, it does not count
|
|
as an orphan (i.e. this is the 'optimistic' logic in
|
|
mapper._is_orphan().)
|
|
|
|
"""
|
|
|
|
users, Address, addresses, User = (self.tables.users,
|
|
self.classes.Address,
|
|
self.tables.addresses,
|
|
self.classes.User)
|
|
|
|
mapper(User, users, properties={
|
|
'addresses': relationship(mapper(Address, addresses),
|
|
backref='user',
|
|
cascade="all, delete-orphan")})
|
|
sess = create_session()
|
|
u = User()
|
|
u.id = 7
|
|
u.name = "fred"
|
|
a1 = Address()
|
|
a1.email_address = 'foo@bar.com'
|
|
u.addresses.append(a1)
|
|
sess.add(u)
|
|
sess.flush()
|
|
|
|
assert u.addresses[0].user is u
|
|
|
|
sess2 = create_session()
|
|
u2 = sess2.merge(u, load=False)
|
|
assert not sess2.dirty
|
|
a2 = u2.addresses[0]
|
|
a2.email_address = 'somenewaddress'
|
|
assert not sa.orm.object_mapper(a2)._is_orphan(
|
|
sa.orm.attributes.instance_state(a2))
|
|
sess2.flush()
|
|
sess2.expunge_all()
|
|
|
|
eq_(sess2.query(User).get(u2.id).addresses[0].email_address,
|
|
'somenewaddress')
|
|
|
|
# this use case is not supported; this is with a pending Address
|
|
# on the pre-merged object, and we currently don't support
|
|
# 'dirty' objects being merged with load=False. in this case,
|
|
# the empty '_state.parents' collection would be an issue, since
|
|
# the optimistic flag is False in _is_orphan() for pending
|
|
# instances. so if we start supporting 'dirty' with load=False,
|
|
# this test will need to pass
|
|
|
|
sess = create_session()
|
|
u = sess.query(User).get(7)
|
|
u.addresses.append(Address())
|
|
sess2 = create_session()
|
|
try:
|
|
u2 = sess2.merge(u, load=False)
|
|
assert False
|
|
|
|
# if load=False is changed to support dirty objects, this code
|
|
# needs to pass
|
|
a2 = u2.addresses[0]
|
|
a2.email_address = 'somenewaddress'
|
|
assert not sa.orm.object_mapper(a2)._is_orphan(
|
|
sa.orm.attributes.instance_state(a2))
|
|
sess2.flush()
|
|
sess2.expunge_all()
|
|
eq_(sess2.query(User).get(u2.id).addresses[0].email_address,
|
|
'somenewaddress')
|
|
except sa.exc.InvalidRequestError as e:
|
|
assert "load=False option does not support" in str(e)
|
|
|
|
def test_synonym_comparable(self):
|
|
users = self.tables.users
|
|
|
|
class User(object):
|
|
|
|
class Comparator(PropComparator):
|
|
pass
|
|
|
|
def _getValue(self):
|
|
return self._value
|
|
|
|
def _setValue(self, value):
|
|
setattr(self, '_value', value)
|
|
|
|
value = property(_getValue, _setValue)
|
|
|
|
mapper(User, users, properties={
|
|
'uid': synonym('id'),
|
|
'foobar': comparable_property(User.Comparator, User.value),
|
|
})
|
|
|
|
sess = create_session()
|
|
u = User()
|
|
u.name = 'ed'
|
|
sess.add(u)
|
|
sess.flush()
|
|
sess.expunge(u)
|
|
sess.merge(u)
|
|
|
|
def test_cascade_doesnt_blowaway_manytoone(self):
|
|
"""a merge test that was fixed by [ticket:1202]"""
|
|
|
|
User, Address, addresses, users = (self.classes.User,
|
|
self.classes.Address,
|
|
self.tables.addresses,
|
|
self.tables.users)
|
|
|
|
s = create_session(autoflush=True, autocommit=False)
|
|
mapper(User, users, properties={
|
|
'addresses': relationship(mapper(Address, addresses),
|
|
backref='user')})
|
|
|
|
a1 = Address(user=s.merge(User(id=1, name='ed')), email_address='x')
|
|
before_id = id(a1.user)
|
|
a2 = Address(user=s.merge(User(id=1, name='jack')),
|
|
email_address='x')
|
|
after_id = id(a1.user)
|
|
other_id = id(a2.user)
|
|
eq_(before_id, other_id)
|
|
eq_(after_id, other_id)
|
|
eq_(before_id, after_id)
|
|
eq_(a1.user, a2.user)
|
|
|
|
def test_cascades_dont_autoflush(self):
|
|
User, Address, addresses, users = (self.classes.User,
|
|
self.classes.Address,
|
|
self.tables.addresses,
|
|
self.tables.users)
|
|
|
|
sess = create_session(autoflush=True, autocommit=False)
|
|
m = mapper(User, users, properties={
|
|
'addresses': relationship(mapper(Address, addresses),
|
|
backref='user')})
|
|
user = User(id=8, name='fred',
|
|
addresses=[Address(email_address='user')])
|
|
merged_user = sess.merge(user)
|
|
assert merged_user in sess.new
|
|
sess.flush()
|
|
assert merged_user not in sess.new
|
|
|
|
def test_cascades_dont_autoflush_2(self):
|
|
users, Address, addresses, User = (self.tables.users,
|
|
self.classes.Address,
|
|
self.tables.addresses,
|
|
self.classes.User)
|
|
|
|
mapper(User, users, properties={
|
|
'addresses': relationship(Address,
|
|
backref='user',
|
|
cascade="all, delete-orphan")
|
|
})
|
|
mapper(Address, addresses)
|
|
|
|
u = User(id=7, name='fred', addresses=[
|
|
Address(id=1, email_address='fred1'),
|
|
])
|
|
sess = create_session(autoflush=True, autocommit=False)
|
|
sess.add(u)
|
|
sess.commit()
|
|
|
|
sess.expunge_all()
|
|
|
|
u = User(id=7, name='fred', addresses=[
|
|
Address(id=1, email_address='fred1'),
|
|
Address(id=2, email_address='fred2'),
|
|
])
|
|
sess.merge(u)
|
|
assert sess.autoflush
|
|
sess.commit()
|
|
|
|
def test_dont_expire_pending(self):
|
|
"""test that pending instances aren't expired during a merge."""
|
|
|
|
users, User = self.tables.users, self.classes.User
|
|
|
|
mapper(User, users)
|
|
u = User(id=7)
|
|
sess = create_session(autoflush=True, autocommit=False)
|
|
u = sess.merge(u)
|
|
assert not bool(attributes.instance_state(u).expired_attributes)
|
|
|
|
def go():
|
|
eq_(u.name, None)
|
|
self.assert_sql_count(testing.db, go, 0)
|
|
|
|
def test_option_state(self):
|
|
"""test that the merged takes on the MapperOption characteristics
|
|
of that which is merged.
|
|
|
|
"""
|
|
|
|
users, User = self.tables.users, self.classes.User
|
|
|
|
class Option(MapperOption):
|
|
propagate_to_loaders = True
|
|
|
|
opt1, opt2 = Option(), Option()
|
|
|
|
sess = sessionmaker()()
|
|
|
|
umapper = mapper(User, users)
|
|
|
|
sess.add_all([
|
|
User(id=1, name='u1'),
|
|
User(id=2, name='u2'),
|
|
])
|
|
sess.commit()
|
|
|
|
sess2 = sessionmaker()()
|
|
s2_users = sess2.query(User).options(opt2).all()
|
|
|
|
# test 1. no options are replaced by merge options
|
|
sess = sessionmaker()()
|
|
s1_users = sess.query(User).all()
|
|
|
|
for u in s1_users:
|
|
ustate = attributes.instance_state(u)
|
|
eq_(ustate.load_path.path, (umapper, ))
|
|
eq_(ustate.load_options, set())
|
|
|
|
for u in s2_users:
|
|
sess.merge(u)
|
|
|
|
for u in s1_users:
|
|
ustate = attributes.instance_state(u)
|
|
eq_(ustate.load_path.path, (umapper, ))
|
|
eq_(ustate.load_options, set([opt2]))
|
|
|
|
# test 2. present options are replaced by merge options
|
|
sess = sessionmaker()()
|
|
s1_users = sess.query(User).options(opt1).all()
|
|
for u in s1_users:
|
|
ustate = attributes.instance_state(u)
|
|
eq_(ustate.load_path.path, (umapper, ))
|
|
eq_(ustate.load_options, set([opt1]))
|
|
|
|
for u in s2_users:
|
|
sess.merge(u)
|
|
|
|
for u in s1_users:
|
|
ustate = attributes.instance_state(u)
|
|
eq_(ustate.load_path.path, (umapper, ))
|
|
eq_(ustate.load_options, set([opt2]))
|
|
|
|
def test_resolve_conflicts_pending_doesnt_interfere_no_ident(self):
|
|
User, Address, Order = (
|
|
self.classes.User, self.classes.Address, self.classes.Order)
|
|
users, addresses, orders = (
|
|
self.tables.users, self.tables.addresses, self.tables.orders)
|
|
|
|
mapper(User, users, properties={
|
|
'orders': relationship(Order)
|
|
})
|
|
mapper(Order, orders, properties={
|
|
'address': relationship(Address)
|
|
})
|
|
mapper(Address, addresses)
|
|
|
|
u1 = User(id=7, name='x')
|
|
u1.orders = [
|
|
Order(description='o1', address=Address(email_address='a')),
|
|
Order(description='o2', address=Address(email_address='b')),
|
|
Order(description='o3', address=Address(email_address='c'))
|
|
]
|
|
|
|
sess = Session()
|
|
sess.merge(u1)
|
|
sess.flush()
|
|
|
|
eq_(
|
|
sess.query(Address.email_address).order_by(
|
|
Address.email_address).all(),
|
|
[('a', ), ('b', ), ('c', )]
|
|
)
|
|
|
|
def test_resolve_conflicts_pending(self):
|
|
User, Address, Order = (
|
|
self.classes.User, self.classes.Address, self.classes.Order)
|
|
users, addresses, orders = (
|
|
self.tables.users, self.tables.addresses, self.tables.orders)
|
|
|
|
mapper(User, users, properties={
|
|
'orders': relationship(Order)
|
|
})
|
|
mapper(Order, orders, properties={
|
|
'address': relationship(Address)
|
|
})
|
|
mapper(Address, addresses)
|
|
|
|
u1 = User(id=7, name='x')
|
|
u1.orders = [
|
|
Order(description='o1', address=Address(id=1, email_address='a')),
|
|
Order(description='o2', address=Address(id=1, email_address='b')),
|
|
Order(description='o3', address=Address(id=1, email_address='c'))
|
|
]
|
|
|
|
sess = Session()
|
|
sess.merge(u1)
|
|
sess.flush()
|
|
|
|
eq_(
|
|
sess.query(Address).one(),
|
|
Address(id=1, email_address='c')
|
|
)
|
|
|
|
def test_resolve_conflicts_persistent(self):
|
|
User, Address, Order = (
|
|
self.classes.User, self.classes.Address, self.classes.Order)
|
|
users, addresses, orders = (
|
|
self.tables.users, self.tables.addresses, self.tables.orders)
|
|
|
|
mapper(User, users, properties={
|
|
'orders': relationship(Order)
|
|
})
|
|
mapper(Order, orders, properties={
|
|
'address': relationship(Address)
|
|
})
|
|
mapper(Address, addresses)
|
|
|
|
sess = Session()
|
|
sess.add(Address(id=1, email_address='z'))
|
|
sess.commit()
|
|
|
|
u1 = User(id=7, name='x')
|
|
u1.orders = [
|
|
Order(description='o1', address=Address(id=1, email_address='a')),
|
|
Order(description='o2', address=Address(id=1, email_address='b')),
|
|
Order(description='o3', address=Address(id=1, email_address='c'))
|
|
]
|
|
|
|
sess = Session()
|
|
sess.merge(u1)
|
|
sess.flush()
|
|
|
|
eq_(
|
|
sess.query(Address).one(),
|
|
Address(id=1, email_address='c')
|
|
)
|
|
|
|
|
|
class M2ONoUseGetLoadingTest(fixtures.MappedTest):
|
|
"""Merge a one-to-many. The many-to-one on the other side is set up
|
|
so that use_get is False. See if skipping the "m2o" merge
|
|
vs. doing it saves on SQL calls.
|
|
|
|
"""
|
|
|
|
@classmethod
|
|
def define_tables(cls, metadata):
|
|
Table('user', metadata,
|
|
Column('id', Integer, primary_key=True,
|
|
test_needs_autoincrement=True),
|
|
Column('name', String(50)))
|
|
Table('address', metadata,
|
|
Column('id', Integer, primary_key=True,
|
|
test_needs_autoincrement=True),
|
|
Column('user_id', Integer, ForeignKey('user.id')),
|
|
Column('email', String(50)))
|
|
|
|
@classmethod
|
|
def setup_classes(cls):
|
|
class User(cls.Comparable):
|
|
pass
|
|
|
|
class Address(cls.Comparable):
|
|
pass
|
|
|
|
@classmethod
|
|
def setup_mappers(cls):
|
|
User, Address = cls.classes.User, cls.classes.Address
|
|
user, address = cls.tables.user, cls.tables.address
|
|
mapper(User, user, properties={
|
|
'addresses': relationship(Address,
|
|
backref=backref(
|
|
'user',
|
|
# needlessly complex primaryjoin so
|
|
# that the use_get flag is False
|
|
primaryjoin=and_(
|
|
user.c.id == address.c.user_id,
|
|
user.c.id == user.c.id
|
|
)))
|
|
})
|
|
mapper(Address, address)
|
|
configure_mappers()
|
|
assert Address.user.property._use_get is False
|
|
|
|
@classmethod
|
|
def insert_data(cls):
|
|
User, Address = cls.classes.User, cls.classes.Address
|
|
s = Session()
|
|
s.add_all([
|
|
User(id=1, name='u1', addresses=[Address(id=1, email='a1'),
|
|
Address(id=2, email='a2')])
|
|
])
|
|
s.commit()
|
|
|
|
# "persistent" - we get at an Address that was already present.
|
|
# With the "skip bidirectional" check removed, the "set" emits SQL
|
|
# for the "previous" version in any case,
|
|
# address.user_id is 1, you get a load.
|
|
def test_persistent_access_none(self):
|
|
User, Address = self.classes.User, self.classes.Address
|
|
s = Session()
|
|
|
|
def go():
|
|
u1 = User(id=1, addresses=[Address(id=1), Address(id=2)])
|
|
u2 = s.merge(u1)
|
|
self.assert_sql_count(testing.db, go, 2)
|
|
|
|
def test_persistent_access_one(self):
|
|
User, Address = self.classes.User, self.classes.Address
|
|
s = Session()
|
|
|
|
def go():
|
|
u1 = User(id=1, addresses=[Address(id=1), Address(id=2)])
|
|
u2 = s.merge(u1)
|
|
a1 = u2.addresses[0]
|
|
assert a1.user is u2
|
|
self.assert_sql_count(testing.db, go, 3)
|
|
|
|
def test_persistent_access_two(self):
|
|
User, Address = self.classes.User, self.classes.Address
|
|
s = Session()
|
|
|
|
def go():
|
|
u1 = User(id=1, addresses=[Address(id=1), Address(id=2)])
|
|
u2 = s.merge(u1)
|
|
a1 = u2.addresses[0]
|
|
assert a1.user is u2
|
|
a2 = u2.addresses[1]
|
|
assert a2.user is u2
|
|
self.assert_sql_count(testing.db, go, 4)
|
|
|
|
# "pending" - we get at an Address that is new- user_id should be
|
|
# None. But in this case the set attribute on the forward side
|
|
# already sets the backref. commenting out the "skip bidirectional"
|
|
# check emits SQL again for the other two Address objects already
|
|
# persistent.
|
|
def test_pending_access_one(self):
|
|
User, Address = self.classes.User, self.classes.Address
|
|
s = Session()
|
|
|
|
def go():
|
|
u1 = User(id=1,
|
|
addresses=[Address(id=1), Address(id=2),
|
|
Address(id=3, email='a3')])
|
|
u2 = s.merge(u1)
|
|
a3 = u2.addresses[2]
|
|
assert a3.user is u2
|
|
self.assert_sql_count(testing.db, go, 3)
|
|
|
|
def test_pending_access_two(self):
|
|
User, Address = self.classes.User, self.classes.Address
|
|
s = Session()
|
|
|
|
def go():
|
|
u1 = User(id=1,
|
|
addresses=[Address(id=1), Address(id=2),
|
|
Address(id=3, email='a3')])
|
|
u2 = s.merge(u1)
|
|
a3 = u2.addresses[2]
|
|
assert a3.user is u2
|
|
a2 = u2.addresses[1]
|
|
assert a2.user is u2
|
|
self.assert_sql_count(testing.db, go, 5)
|
|
|
|
|
|
class DeferredMergeTest(fixtures.MappedTest):
|
|
@classmethod
|
|
def define_tables(cls, metadata):
|
|
Table(
|
|
'book', metadata,
|
|
Column('id', Integer, primary_key=True),
|
|
Column('title', String(200), nullable=False),
|
|
Column('summary', String(2000)),
|
|
Column('excerpt', Text),
|
|
)
|
|
|
|
@classmethod
|
|
def setup_classes(cls):
|
|
class Book(cls.Basic):
|
|
pass
|
|
|
|
def test_deferred_column_mapping(self):
|
|
# defer 'excerpt' at mapping level instead of query level
|
|
Book, book = self.classes.Book, self.tables.book
|
|
mapper(Book, book, properties={'excerpt': deferred(book.c.excerpt)})
|
|
sess = sessionmaker()()
|
|
|
|
b = Book(
|
|
id=1,
|
|
title='Essential SQLAlchemy',
|
|
summary='some summary',
|
|
excerpt='some excerpt',
|
|
)
|
|
sess.add(b)
|
|
sess.commit()
|
|
|
|
b1 = sess.query(Book).first()
|
|
sess.expire(b1, ['summary'])
|
|
sess.close()
|
|
|
|
def go():
|
|
b2 = sess.merge(b1, load=False)
|
|
|
|
# should not emit load for deferred 'excerpt'
|
|
eq_(b2.summary, 'some summary')
|
|
not_in_('excerpt', b2.__dict__)
|
|
|
|
# now it should emit load for deferred 'excerpt'
|
|
eq_(b2.excerpt, 'some excerpt')
|
|
in_('excerpt', b2.__dict__)
|
|
|
|
self.sql_eq_(go, [
|
|
("SELECT book.summary AS book_summary "
|
|
"FROM book WHERE book.id = :param_1",
|
|
{'param_1': 1}),
|
|
("SELECT book.excerpt AS book_excerpt "
|
|
"FROM book WHERE book.id = :param_1",
|
|
{'param_1': 1})
|
|
])
|
|
|
|
def test_deferred_column_query(self):
|
|
Book, book = self.classes.Book, self.tables.book
|
|
mapper(Book, book)
|
|
sess = sessionmaker()()
|
|
|
|
b = Book(
|
|
id=1,
|
|
title='Essential SQLAlchemy',
|
|
summary='some summary',
|
|
excerpt='some excerpt',
|
|
)
|
|
sess.add(b)
|
|
sess.commit()
|
|
|
|
# defer 'excerpt' at query level instead of mapping level
|
|
b1 = sess.query(Book).options(defer(Book.excerpt)).first()
|
|
sess.expire(b1, ['summary'])
|
|
sess.close()
|
|
|
|
def go():
|
|
b2 = sess.merge(b1, load=False)
|
|
|
|
# should not emit load for deferred 'excerpt'
|
|
eq_(b2.summary, 'some summary')
|
|
not_in_('excerpt', b2.__dict__)
|
|
|
|
# now it should emit load for deferred 'excerpt'
|
|
eq_(b2.excerpt, 'some excerpt')
|
|
in_('excerpt', b2.__dict__)
|
|
|
|
self.sql_eq_(go, [
|
|
("SELECT book.summary AS book_summary "
|
|
"FROM book WHERE book.id = :param_1",
|
|
{'param_1': 1}),
|
|
("SELECT book.excerpt AS book_excerpt "
|
|
"FROM book WHERE book.id = :param_1",
|
|
{'param_1': 1})
|
|
])
|
|
|
|
|
|
class MutableMergeTest(fixtures.MappedTest):
|
|
@classmethod
|
|
def define_tables(cls, metadata):
|
|
Table("data", metadata,
|
|
Column('id', Integer, primary_key=True,
|
|
test_needs_autoincrement=True),
|
|
Column('data', PickleType(comparator=operator.eq)))
|
|
|
|
@classmethod
|
|
def setup_classes(cls):
|
|
class Data(cls.Basic):
|
|
pass
|
|
|
|
def test_list(self):
|
|
Data, data = self.classes.Data, self.tables.data
|
|
|
|
mapper(Data, data)
|
|
sess = sessionmaker()()
|
|
d = Data(data=["this", "is", "a", "list"])
|
|
|
|
sess.add(d)
|
|
sess.commit()
|
|
|
|
d2 = Data(id=d.id, data=["this", "is", "another", "list"])
|
|
d3 = sess.merge(d2)
|
|
eq_(d3.data, ["this", "is", "another", "list"])
|
|
|
|
|
|
class CompositeNullPksTest(fixtures.MappedTest):
|
|
@classmethod
|
|
def define_tables(cls, metadata):
|
|
Table("data", metadata,
|
|
Column('pk1', String(10), primary_key=True),
|
|
Column('pk2', String(10), primary_key=True))
|
|
|
|
@classmethod
|
|
def setup_classes(cls):
|
|
class Data(cls.Basic):
|
|
pass
|
|
|
|
def test_merge_allow_partial(self):
|
|
Data, data = self.classes.Data, self.tables.data
|
|
|
|
mapper(Data, data)
|
|
sess = sessionmaker()()
|
|
|
|
d1 = Data(pk1="someval", pk2=None)
|
|
|
|
def go():
|
|
return sess.merge(d1)
|
|
self.assert_sql_count(testing.db, go, 1)
|
|
|
|
def test_merge_disallow_partial(self):
|
|
Data, data = self.classes.Data, self.tables.data
|
|
|
|
mapper(Data, data, allow_partial_pks=False)
|
|
sess = sessionmaker()()
|
|
|
|
d1 = Data(pk1="someval", pk2=None)
|
|
|
|
def go():
|
|
return sess.merge(d1)
|
|
self.assert_sql_count(testing.db, go, 0)
|
|
|
|
|
|
class LoadOnPendingTest(fixtures.MappedTest):
|
|
"""Test interaction of merge() with load_on_pending relationships"""
|
|
@classmethod
|
|
def define_tables(cls, metadata):
|
|
rocks_table = Table("rocks", metadata,
|
|
Column("id", Integer, primary_key=True),
|
|
Column("description", String(10)))
|
|
bugs_table = Table("bugs", metadata,
|
|
Column("id", Integer, primary_key=True),
|
|
Column("rockid", Integer, ForeignKey('rocks.id')))
|
|
|
|
@classmethod
|
|
def setup_classes(cls):
|
|
class Rock(cls.Basic, fixtures.ComparableEntity):
|
|
pass
|
|
|
|
class Bug(cls.Basic, fixtures.ComparableEntity):
|
|
pass
|
|
|
|
def _setup_delete_orphan_o2o(self):
|
|
mapper(self.classes.Rock, self.tables.rocks,
|
|
properties={'bug': relationship(self.classes.Bug,
|
|
cascade='all,delete-orphan',
|
|
load_on_pending=True,
|
|
uselist=False)
|
|
})
|
|
mapper(self.classes.Bug, self.tables.bugs)
|
|
self.sess = sessionmaker()()
|
|
|
|
def _merge_delete_orphan_o2o_with(self, bug):
|
|
# create a transient rock with passed bug
|
|
r = self.classes.Rock(id=0, description='moldy')
|
|
r.bug = bug
|
|
m = self.sess.merge(r)
|
|
# we've already passed ticket #2374 problem since merge() returned,
|
|
# but for good measure:
|
|
assert m is not r
|
|
eq_(m, r)
|
|
|
|
def test_merge_delete_orphan_o2o_none(self):
|
|
"""one to one delete_orphan relationships marked load_on_pending
|
|
should be able to merge() with attribute None"""
|
|
|
|
self._setup_delete_orphan_o2o()
|
|
self._merge_delete_orphan_o2o_with(None)
|
|
|
|
def test_merge_delete_orphan_o2o(self):
|
|
"""one to one delete_orphan relationships marked load_on_pending
|
|
should be able to merge()"""
|
|
|
|
self._setup_delete_orphan_o2o()
|
|
self._merge_delete_orphan_o2o_with(self.classes.Bug(id=1))
|
|
|
|
|
|
class PolymorphicOnTest(fixtures.MappedTest):
|
|
"""Test merge() of polymorphic object when polymorphic_on
|
|
isn't a Column"""
|
|
|
|
@classmethod
|
|
def define_tables(cls, metadata):
|
|
Table('employees', metadata,
|
|
Column('employee_id', Integer, primary_key=True,
|
|
test_needs_autoincrement=True),
|
|
Column('type', String(1), nullable=False),
|
|
Column('data', String(50)))
|
|
|
|
@classmethod
|
|
def setup_classes(cls):
|
|
class Employee(cls.Basic, fixtures.ComparableEntity):
|
|
pass
|
|
|
|
class Manager(Employee):
|
|
pass
|
|
|
|
class Engineer(Employee):
|
|
pass
|
|
|
|
def _setup_polymorphic_on_mappers(self):
|
|
employee_mapper = mapper(self.classes.Employee,
|
|
self.tables.employees,
|
|
polymorphic_on=case(
|
|
value=self.tables.employees.c.type,
|
|
whens={
|
|
'E': 'employee',
|
|
'M': 'manager',
|
|
'G': 'engineer',
|
|
'R': 'engineer',
|
|
}),
|
|
polymorphic_identity='employee')
|
|
mapper(self.classes.Manager, inherits=employee_mapper,
|
|
polymorphic_identity='manager')
|
|
mapper(self.classes.Engineer, inherits=employee_mapper,
|
|
polymorphic_identity='engineer')
|
|
self.sess = sessionmaker()()
|
|
|
|
def test_merge_polymorphic_on(self):
|
|
"""merge() should succeed with a polymorphic object even when
|
|
polymorphic_on is not a Column
|
|
"""
|
|
self._setup_polymorphic_on_mappers()
|
|
|
|
m = self.classes.Manager(employee_id=55, type='M',
|
|
data='original data')
|
|
self.sess.add(m)
|
|
self.sess.commit()
|
|
self.sess.expunge_all()
|
|
|
|
m = self.classes.Manager(employee_id=55, data='updated data')
|
|
merged = self.sess.merge(m)
|
|
|
|
# we've already passed ticket #2449 problem since
|
|
# merge() returned, but for good measure:
|
|
assert m is not merged
|
|
eq_(m, merged)
|