PageRenderTime 99ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/test/orm/test_naturalpks.py

https://bitbucket.org/danblack_ibm/sqlalchemy
Python | 1321 lines | 1181 code | 104 blank | 36 comment | 13 complexity | 0fe4fa7b42f8fcec0fd9c18717b6f768 MD5 | raw file
  1. """
  2. Primary key changing capabilities and passive/non-passive cascading updates.
  3. """
  4. from sqlalchemy.testing import fixtures, eq_, ne_, assert_raises
  5. import sqlalchemy as sa
  6. from sqlalchemy import testing, Integer, String, ForeignKey
  7. from sqlalchemy.testing.schema import Table, Column
  8. from sqlalchemy.orm import mapper, relationship, create_session, Session
  9. from sqlalchemy.orm.session import make_transient
  10. from test.orm import _fixtures
  11. def _backend_specific_fk_args():
  12. if testing.requires.deferrable_fks.enabled:
  13. fk_args = dict(deferrable=True, initially='deferred')
  14. elif not testing.requires.on_update_cascade.enabled:
  15. fk_args = dict()
  16. else:
  17. fk_args = dict(onupdate='cascade')
  18. return fk_args
  19. class NaturalPKTest(fixtures.MappedTest):
  20. # MySQL 5.5 on Windows crashes (the entire server, not the client)
  21. # if you screw around with ON UPDATE CASCADE type of stuff.
  22. __requires__ = 'skip_mysql_on_windows', 'on_update_or_deferrable_fks'
  23. __backend__ = True
  24. @classmethod
  25. def define_tables(cls, metadata):
  26. fk_args = _backend_specific_fk_args()
  27. Table('users', metadata,
  28. Column('username', String(50), primary_key=True),
  29. Column('fullname', String(100)),
  30. test_needs_fk=True)
  31. Table(
  32. 'addresses', metadata,
  33. Column('email', String(50), primary_key=True),
  34. Column(
  35. 'username', String(50),
  36. ForeignKey('users.username', **fk_args)),
  37. test_needs_fk=True)
  38. Table(
  39. 'items', metadata,
  40. Column('itemname', String(50), primary_key=True),
  41. Column('description', String(100)),
  42. test_needs_fk=True)
  43. Table(
  44. 'users_to_items', metadata,
  45. Column(
  46. 'username', String(50),
  47. ForeignKey('users.username', **fk_args), primary_key=True),
  48. Column(
  49. 'itemname', String(50),
  50. ForeignKey('items.itemname', **fk_args), primary_key=True),
  51. test_needs_fk=True)
  52. @classmethod
  53. def setup_classes(cls):
  54. class User(cls.Comparable):
  55. pass
  56. class Address(cls.Comparable):
  57. pass
  58. class Item(cls.Comparable):
  59. pass
  60. def test_entity(self):
  61. users, User = self.tables.users, self.classes.User
  62. mapper(User, users)
  63. sess = create_session()
  64. u1 = User(username='jack', fullname='jack')
  65. sess.add(u1)
  66. sess.flush()
  67. assert sess.query(User).get('jack') is u1
  68. u1.username = 'ed'
  69. sess.flush()
  70. def go():
  71. assert sess.query(User).get('ed') is u1
  72. self.assert_sql_count(testing.db, go, 0)
  73. assert sess.query(User).get('jack') is None
  74. sess.expunge_all()
  75. u1 = sess.query(User).get('ed')
  76. eq_(User(username='ed', fullname='jack'), u1)
  77. def test_load_after_expire(self):
  78. users, User = self.tables.users, self.classes.User
  79. mapper(User, users)
  80. sess = create_session()
  81. u1 = User(username='jack', fullname='jack')
  82. sess.add(u1)
  83. sess.flush()
  84. assert sess.query(User).get('jack') is u1
  85. users.update(values={User.username: 'jack'}).execute(username='ed')
  86. # expire/refresh works off of primary key. the PK is gone
  87. # in this case so there's no way to look it up. criterion-
  88. # based session invalidation could solve this [ticket:911]
  89. sess.expire(u1)
  90. assert_raises(sa.orm.exc.ObjectDeletedError, getattr, u1, 'username')
  91. sess.expunge_all()
  92. assert sess.query(User).get('jack') is None
  93. assert sess.query(User).get('ed').fullname == 'jack'
  94. @testing.requires.returning
  95. def test_update_to_sql_expr(self):
  96. users, User = self.tables.users, self.classes.User
  97. mapper(User, users)
  98. sess = create_session()
  99. u1 = User(username='jack', fullname='jack')
  100. sess.add(u1)
  101. sess.flush()
  102. u1.username = User.username + ' jones'
  103. sess.flush()
  104. eq_(u1.username, 'jack jones')
  105. def test_update_to_self_sql_expr(self):
  106. # SQL expression where the PK won't actually change,
  107. # such as to bump a server side trigger
  108. users, User = self.tables.users, self.classes.User
  109. mapper(User, users)
  110. sess = create_session()
  111. u1 = User(username='jack', fullname='jack')
  112. sess.add(u1)
  113. sess.flush()
  114. u1.username = User.username + ''
  115. sess.flush()
  116. eq_(u1.username, 'jack')
  117. def test_flush_new_pk_after_expire(self):
  118. User, users = self.classes.User, self.tables.users
  119. mapper(User, users)
  120. sess = create_session()
  121. u1 = User(username='jack', fullname='jack')
  122. sess.add(u1)
  123. sess.flush()
  124. assert sess.query(User).get('jack') is u1
  125. sess.expire(u1)
  126. u1.username = 'ed'
  127. sess.flush()
  128. sess.expunge_all()
  129. assert sess.query(User).get('ed').fullname == 'jack'
  130. @testing.requires.on_update_cascade
  131. def test_onetomany_passive(self):
  132. self._test_onetomany(True)
  133. def test_onetomany_nonpassive(self):
  134. self._test_onetomany(False)
  135. def _test_onetomany(self, passive_updates):
  136. users, Address, addresses, User = (self.tables.users,
  137. self.classes.Address,
  138. self.tables.addresses,
  139. self.classes.User)
  140. mapper(
  141. User, users, properties={
  142. 'addresses': relationship(
  143. Address, passive_updates=passive_updates)})
  144. mapper(Address, addresses)
  145. sess = create_session()
  146. u1 = User(username='jack', fullname='jack')
  147. u1.addresses.append(Address(email='jack1'))
  148. u1.addresses.append(Address(email='jack2'))
  149. sess.add(u1)
  150. sess.flush()
  151. assert sess.query(Address).get('jack1') is u1.addresses[0]
  152. u1.username = 'ed'
  153. sess.flush()
  154. assert u1.addresses[0].username == 'ed'
  155. sess.expunge_all()
  156. eq_(
  157. [Address(username='ed'), Address(username='ed')],
  158. sess.query(Address).all())
  159. u1 = sess.query(User).get('ed')
  160. u1.username = 'jack'
  161. def go():
  162. sess.flush()
  163. if not passive_updates:
  164. # test passive_updates=False;
  165. # load addresses, update user, update 2 addresses
  166. self.assert_sql_count(testing.db, go, 3)
  167. else:
  168. # test passive_updates=True; update user
  169. self.assert_sql_count(testing.db, go, 1)
  170. sess.expunge_all()
  171. assert User(
  172. username='jack', addresses=[
  173. Address(username='jack'),
  174. Address(username='jack')]) == sess.query(User).get('jack')
  175. u1 = sess.query(User).get('jack')
  176. u1.addresses = []
  177. u1.username = 'fred'
  178. sess.flush()
  179. sess.expunge_all()
  180. assert sess.query(Address).get('jack1').username is None
  181. u1 = sess.query(User).get('fred')
  182. eq_(User(username='fred', fullname='jack'), u1)
  183. @testing.requires.on_update_cascade
  184. def test_manytoone_passive(self):
  185. self._test_manytoone(True)
  186. def test_manytoone_nonpassive(self):
  187. self._test_manytoone(False)
  188. def test_manytoone_nonpassive_cold_mapping(self):
  189. """test that the mapper-level m2o dependency processor
  190. is set up even if the opposite side relationship
  191. hasn't yet been part of a flush.
  192. """
  193. users, Address, addresses, User = (self.tables.users,
  194. self.classes.Address,
  195. self.tables.addresses,
  196. self.classes.User)
  197. with testing.db.begin() as conn:
  198. conn.execute(users.insert(), username='jack', fullname='jack')
  199. conn.execute(addresses.insert(), email='jack1', username='jack')
  200. conn.execute(addresses.insert(), email='jack2', username='jack')
  201. mapper(User, users)
  202. mapper(Address, addresses, properties={
  203. 'user': relationship(User,
  204. passive_updates=False)
  205. })
  206. sess = create_session()
  207. u1 = sess.query(User).first()
  208. a1, a2 = sess.query(Address).all()
  209. u1.username = 'ed'
  210. def go():
  211. sess.flush()
  212. self.assert_sql_count(testing.db, go, 2)
  213. def _test_manytoone(self, passive_updates):
  214. users, Address, addresses, User = (self.tables.users,
  215. self.classes.Address,
  216. self.tables.addresses,
  217. self.classes.User)
  218. mapper(User, users)
  219. mapper(Address, addresses, properties={
  220. 'user': relationship(User, passive_updates=passive_updates)
  221. })
  222. sess = create_session()
  223. a1 = Address(email='jack1')
  224. a2 = Address(email='jack2')
  225. u1 = User(username='jack', fullname='jack')
  226. a1.user = u1
  227. a2.user = u1
  228. sess.add(a1)
  229. sess.add(a2)
  230. sess.flush()
  231. u1.username = 'ed'
  232. def go():
  233. sess.flush()
  234. if passive_updates:
  235. self.assert_sql_count(testing.db, go, 1)
  236. else:
  237. self.assert_sql_count(testing.db, go, 2)
  238. def go():
  239. sess.flush()
  240. self.assert_sql_count(testing.db, go, 0)
  241. assert a1.username == a2.username == 'ed'
  242. sess.expunge_all()
  243. eq_(
  244. [Address(username='ed'), Address(username='ed')],
  245. sess.query(Address).all())
  246. @testing.requires.on_update_cascade
  247. def test_onetoone_passive(self):
  248. self._test_onetoone(True)
  249. def test_onetoone_nonpassive(self):
  250. self._test_onetoone(False)
  251. def _test_onetoone(self, passive_updates):
  252. users, Address, addresses, User = (self.tables.users,
  253. self.classes.Address,
  254. self.tables.addresses,
  255. self.classes.User)
  256. mapper(
  257. User, users, properties={
  258. "address": relationship(
  259. Address, passive_updates=passive_updates, uselist=False)})
  260. mapper(Address, addresses)
  261. sess = create_session()
  262. u1 = User(username='jack', fullname='jack')
  263. sess.add(u1)
  264. sess.flush()
  265. a1 = Address(email='jack1')
  266. u1.address = a1
  267. sess.add(a1)
  268. sess.flush()
  269. u1.username = 'ed'
  270. def go():
  271. sess.flush()
  272. if passive_updates:
  273. sess.expire(u1, ['address'])
  274. self.assert_sql_count(testing.db, go, 1)
  275. else:
  276. self.assert_sql_count(testing.db, go, 2)
  277. def go():
  278. sess.flush()
  279. self.assert_sql_count(testing.db, go, 0)
  280. sess.expunge_all()
  281. eq_([Address(username='ed')], sess.query(Address).all())
  282. @testing.requires.on_update_cascade
  283. def test_bidirectional_passive(self):
  284. self._test_bidirectional(True)
  285. def test_bidirectional_nonpassive(self):
  286. self._test_bidirectional(False)
  287. def _test_bidirectional(self, passive_updates):
  288. users, Address, addresses, User = (self.tables.users,
  289. self.classes.Address,
  290. self.tables.addresses,
  291. self.classes.User)
  292. mapper(User, users)
  293. mapper(Address, addresses, properties={
  294. 'user': relationship(User, passive_updates=passive_updates,
  295. backref='addresses')})
  296. sess = create_session()
  297. a1 = Address(email='jack1')
  298. a2 = Address(email='jack2')
  299. u1 = User(username='jack', fullname='jack')
  300. a1.user = u1
  301. a2.user = u1
  302. sess.add(a1)
  303. sess.add(a2)
  304. sess.flush()
  305. u1.username = 'ed'
  306. (ad1, ad2) = sess.query(Address).all()
  307. eq_([Address(username='jack'), Address(username='jack')], [ad1, ad2])
  308. def go():
  309. sess.flush()
  310. if passive_updates:
  311. self.assert_sql_count(testing.db, go, 1)
  312. else:
  313. # two updates bundled
  314. self.assert_sql_count(testing.db, go, 2)
  315. eq_([Address(username='ed'), Address(username='ed')], [ad1, ad2])
  316. sess.expunge_all()
  317. eq_(
  318. [Address(username='ed'), Address(username='ed')],
  319. sess.query(Address).all())
  320. u1 = sess.query(User).get('ed')
  321. assert len(u1.addresses) == 2 # load addresses
  322. u1.username = 'fred'
  323. def go():
  324. sess.flush()
  325. # check that the passive_updates is on on the other side
  326. if passive_updates:
  327. self.assert_sql_count(testing.db, go, 1)
  328. else:
  329. # two updates bundled
  330. self.assert_sql_count(testing.db, go, 2)
  331. sess.expunge_all()
  332. eq_(
  333. [Address(username='fred'), Address(username='fred')],
  334. sess.query(Address).all())
  335. @testing.requires.on_update_cascade
  336. def test_manytomany_passive(self):
  337. self._test_manytomany(True)
  338. @testing.requires.non_updating_cascade
  339. @testing.requires.sane_multi_rowcount.not_()
  340. def test_manytomany_nonpassive(self):
  341. self._test_manytomany(False)
  342. def _test_manytomany(self, passive_updates):
  343. users, items, Item, User, users_to_items = (self.tables.users,
  344. self.tables.items,
  345. self.classes.Item,
  346. self.classes.User,
  347. self.tables.users_to_items)
  348. mapper(
  349. User, users, properties={
  350. 'items': relationship(
  351. Item, secondary=users_to_items, backref='users',
  352. passive_updates=passive_updates)})
  353. mapper(Item, items)
  354. sess = create_session()
  355. u1 = User(username='jack')
  356. u2 = User(username='fred')
  357. i1 = Item(itemname='item1')
  358. i2 = Item(itemname='item2')
  359. u1.items.append(i1)
  360. u1.items.append(i2)
  361. i2.users.append(u2)
  362. sess.add(u1)
  363. sess.add(u2)
  364. sess.flush()
  365. r = sess.query(Item).all()
  366. # ComparableEntity can't handle a comparison with the backrefs
  367. # involved....
  368. eq_(Item(itemname='item1'), r[0])
  369. eq_(['jack'], [u.username for u in r[0].users])
  370. eq_(Item(itemname='item2'), r[1])
  371. eq_(['jack', 'fred'], [u.username for u in r[1].users])
  372. u2.username = 'ed'
  373. def go():
  374. sess.flush()
  375. go()
  376. def go():
  377. sess.flush()
  378. self.assert_sql_count(testing.db, go, 0)
  379. sess.expunge_all()
  380. r = sess.query(Item).all()
  381. eq_(Item(itemname='item1'), r[0])
  382. eq_(['jack'], [u.username for u in r[0].users])
  383. eq_(Item(itemname='item2'), r[1])
  384. eq_(['ed', 'jack'], sorted([u.username for u in r[1].users]))
  385. sess.expunge_all()
  386. u2 = sess.query(User).get(u2.username)
  387. u2.username = 'wendy'
  388. sess.flush()
  389. r = sess.query(Item).with_parent(u2).all()
  390. eq_(Item(itemname='item2'), r[0])
  391. class TransientExceptionTesst(_fixtures.FixtureTest):
  392. run_inserts = None
  393. __backend__ = True
  394. def test_transient_exception(self):
  395. """An object that goes from a pk value to transient/pending
  396. doesn't count as a "pk" switch.
  397. """
  398. users, Address, addresses, User = (self.tables.users,
  399. self.classes.Address,
  400. self.tables.addresses,
  401. self.classes.User)
  402. mapper(User, users)
  403. mapper(Address, addresses, properties={'user': relationship(User)})
  404. sess = create_session()
  405. u1 = User(id=5, name='u1')
  406. ad1 = Address(email_address='e1', user=u1)
  407. sess.add_all([u1, ad1])
  408. sess.flush()
  409. make_transient(u1)
  410. u1.id = None
  411. u1.username = 'u2'
  412. sess.add(u1)
  413. sess.flush()
  414. eq_(ad1.user_id, 5)
  415. sess.expire_all()
  416. eq_(ad1.user_id, 5)
  417. ne_(u1.id, 5)
  418. ne_(u1.id, None)
  419. eq_(sess.query(User).count(), 2)
  420. class ReversePKsTest(fixtures.MappedTest):
  421. """reverse the primary keys of two entities and ensure bookkeeping
  422. succeeds."""
  423. __backend__ = True
  424. @classmethod
  425. def define_tables(cls, metadata):
  426. Table(
  427. 'user', metadata,
  428. Column('code', Integer, autoincrement=False, primary_key=True),
  429. Column('status', Integer, autoincrement=False, primary_key=True),
  430. Column('username', String(50), nullable=False),
  431. test_needs_acid=True
  432. )
  433. @classmethod
  434. def setup_classes(cls):
  435. class User(cls.Comparable):
  436. def __init__(self, code, status, username):
  437. self.code = code
  438. self.status = status
  439. self.username = username
  440. def test_reverse(self):
  441. user, User = self.tables.user, self.classes.User
  442. PUBLISHED, EDITABLE, ARCHIVED = 1, 2, 3
  443. mapper(User, user)
  444. session = sa.orm.sessionmaker()()
  445. a_published = User(1, PUBLISHED, 'a')
  446. session.add(a_published)
  447. session.commit()
  448. a_editable = User(1, EDITABLE, 'a')
  449. session.add(a_editable)
  450. session.commit()
  451. # do the switch in both directions -
  452. # one or the other should raise the error
  453. # based on platform dictionary ordering
  454. a_published.status = ARCHIVED
  455. a_editable.status = PUBLISHED
  456. session.commit()
  457. assert session.query(User).get([1, PUBLISHED]) is a_editable
  458. assert session.query(User).get([1, ARCHIVED]) is a_published
  459. a_published.status = PUBLISHED
  460. a_editable.status = EDITABLE
  461. session.commit()
  462. assert session.query(User).get([1, PUBLISHED]) is a_published
  463. assert session.query(User).get([1, EDITABLE]) is a_editable
  464. @testing.requires.savepoints
  465. def test_reverse_savepoint(self):
  466. user, User = self.tables.user, self.classes.User
  467. PUBLISHED, EDITABLE, ARCHIVED = 1, 2, 3
  468. mapper(User, user)
  469. session = sa.orm.sessionmaker()()
  470. a_published = User(1, PUBLISHED, 'a')
  471. session.add(a_published)
  472. session.commit()
  473. a_editable = User(1, EDITABLE, 'a')
  474. session.add(a_editable)
  475. session.commit()
  476. # testing #3108
  477. session.begin_nested()
  478. a_published.status = ARCHIVED
  479. a_editable.status = PUBLISHED
  480. session.commit()
  481. session.rollback()
  482. eq_(a_published.status, PUBLISHED)
  483. eq_(a_editable.status, EDITABLE)
  484. class SelfReferentialTest(fixtures.MappedTest):
  485. # mssql, mysql don't allow
  486. # ON UPDATE on self-referential keys
  487. __unsupported_on__ = ('mssql', 'mysql')
  488. __requires__ = 'on_update_or_deferrable_fks',
  489. __backend__ = True
  490. @classmethod
  491. def define_tables(cls, metadata):
  492. fk_args = _backend_specific_fk_args()
  493. Table(
  494. 'nodes', metadata,
  495. Column('name', String(50), primary_key=True),
  496. Column('parent', String(50), ForeignKey('nodes.name', **fk_args)),
  497. test_needs_fk=True)
  498. @classmethod
  499. def setup_classes(cls):
  500. class Node(cls.Comparable):
  501. pass
  502. def test_one_to_many_on_m2o(self):
  503. Node, nodes = self.classes.Node, self.tables.nodes
  504. mapper(
  505. Node, nodes, properties={
  506. 'children': relationship(
  507. Node,
  508. backref=sa.orm.backref(
  509. 'parentnode', remote_side=nodes.c.name,
  510. passive_updates=False),
  511. )})
  512. sess = Session()
  513. n1 = Node(name='n1')
  514. sess.add(n1)
  515. n2 = Node(name='n11', parentnode=n1)
  516. n3 = Node(name='n12', parentnode=n1)
  517. n4 = Node(name='n13', parentnode=n1)
  518. sess.add_all([n2, n3, n4])
  519. sess.commit()
  520. n1.name = 'new n1'
  521. sess.commit()
  522. eq_(['new n1', 'new n1', 'new n1'],
  523. [n.parent
  524. for n in sess.query(Node).filter(
  525. Node.name.in_(['n11', 'n12', 'n13']))])
  526. def test_one_to_many_on_o2m(self):
  527. Node, nodes = self.classes.Node, self.tables.nodes
  528. mapper(
  529. Node, nodes, properties={
  530. 'children': relationship(
  531. Node,
  532. backref=sa.orm.backref(
  533. 'parentnode', remote_side=nodes.c.name),
  534. passive_updates=False)})
  535. sess = Session()
  536. n1 = Node(name='n1')
  537. n1.children.append(Node(name='n11'))
  538. n1.children.append(Node(name='n12'))
  539. n1.children.append(Node(name='n13'))
  540. sess.add(n1)
  541. sess.commit()
  542. n1.name = 'new n1'
  543. sess.commit()
  544. eq_(n1.children[1].parent, 'new n1')
  545. eq_(['new n1', 'new n1', 'new n1'],
  546. [n.parent
  547. for n in sess.query(Node).filter(
  548. Node.name.in_(['n11', 'n12', 'n13']))])
  549. @testing.requires.on_update_cascade
  550. def test_many_to_one_passive(self):
  551. self._test_many_to_one(True)
  552. def test_many_to_one_nonpassive(self):
  553. self._test_many_to_one(False)
  554. def _test_many_to_one(self, passive):
  555. Node, nodes = self.classes.Node, self.tables.nodes
  556. mapper(
  557. Node, nodes, properties={
  558. 'parentnode': relationship(
  559. Node, remote_side=nodes.c.name, passive_updates=passive)}
  560. )
  561. sess = Session()
  562. n1 = Node(name='n1')
  563. n11 = Node(name='n11', parentnode=n1)
  564. n12 = Node(name='n12', parentnode=n1)
  565. n13 = Node(name='n13', parentnode=n1)
  566. sess.add_all([n1, n11, n12, n13])
  567. sess.commit()
  568. n1.name = 'new n1'
  569. sess.commit()
  570. eq_(
  571. ['new n1', 'new n1', 'new n1'],
  572. [
  573. n.parent for n in sess.query(Node).filter(
  574. Node.name.in_(['n11', 'n12', 'n13']))])
  575. class NonPKCascadeTest(fixtures.MappedTest):
  576. __requires__ = 'skip_mysql_on_windows', 'on_update_or_deferrable_fks'
  577. __backend__ = True
  578. @classmethod
  579. def define_tables(cls, metadata):
  580. fk_args = _backend_specific_fk_args()
  581. Table(
  582. 'users', metadata,
  583. Column(
  584. 'id', Integer, primary_key=True,
  585. test_needs_autoincrement=True),
  586. Column('username', String(50), unique=True),
  587. Column('fullname', String(100)),
  588. test_needs_fk=True)
  589. Table(
  590. 'addresses', metadata,
  591. Column(
  592. 'id', Integer, primary_key=True,
  593. test_needs_autoincrement=True),
  594. Column('email', String(50)),
  595. Column(
  596. 'username', String(50),
  597. ForeignKey('users.username', **fk_args)),
  598. test_needs_fk=True)
  599. @classmethod
  600. def setup_classes(cls):
  601. class User(cls.Comparable):
  602. pass
  603. class Address(cls.Comparable):
  604. pass
  605. @testing.requires.on_update_cascade
  606. def test_onetomany_passive(self):
  607. self._test_onetomany(True)
  608. def test_onetomany_nonpassive(self):
  609. self._test_onetomany(False)
  610. def _test_onetomany(self, passive_updates):
  611. User, Address, users, addresses = (self.classes.User,
  612. self.classes.Address,
  613. self.tables.users,
  614. self.tables.addresses)
  615. mapper(
  616. User, users, properties={
  617. 'addresses': relationship(
  618. Address, passive_updates=passive_updates)})
  619. mapper(Address, addresses)
  620. sess = create_session()
  621. u1 = User(username='jack', fullname='jack')
  622. u1.addresses.append(Address(email='jack1'))
  623. u1.addresses.append(Address(email='jack2'))
  624. sess.add(u1)
  625. sess.flush()
  626. a1 = u1.addresses[0]
  627. eq_(
  628. sa.select([addresses.c.username]).execute().fetchall(),
  629. [('jack',), ('jack',)])
  630. assert sess.query(Address).get(a1.id) is u1.addresses[0]
  631. u1.username = 'ed'
  632. sess.flush()
  633. assert u1.addresses[0].username == 'ed'
  634. eq_(
  635. sa.select([addresses.c.username]).execute().fetchall(),
  636. [('ed',), ('ed',)])
  637. sess.expunge_all()
  638. eq_(
  639. [Address(username='ed'), Address(username='ed')],
  640. sess.query(Address).all())
  641. u1 = sess.query(User).get(u1.id)
  642. u1.username = 'jack'
  643. def go():
  644. sess.flush()
  645. if not passive_updates:
  646. # test passive_updates=False; load addresses,
  647. # update user, update 2 addresses (in one executemany)
  648. self.assert_sql_count(testing.db, go, 3)
  649. else:
  650. # test passive_updates=True; update user
  651. self.assert_sql_count(testing.db, go, 1)
  652. sess.expunge_all()
  653. assert User(
  654. username='jack', addresses=[
  655. Address(username='jack'),
  656. Address(username='jack')]) == sess.query(User).get(u1.id)
  657. sess.expunge_all()
  658. u1 = sess.query(User).get(u1.id)
  659. u1.addresses = []
  660. u1.username = 'fred'
  661. sess.flush()
  662. sess.expunge_all()
  663. a1 = sess.query(Address).get(a1.id)
  664. eq_(a1.username, None)
  665. eq_(
  666. sa.select([addresses.c.username]).execute().fetchall(),
  667. [(None,), (None,)])
  668. u1 = sess.query(User).get(u1.id)
  669. eq_(User(username='fred', fullname='jack'), u1)
  670. class CascadeToFKPKTest(fixtures.MappedTest, testing.AssertsCompiledSQL):
  671. """A primary key mutation cascades onto a foreign key that is itself a
  672. primary key."""
  673. __backend__ = True
  674. @classmethod
  675. def define_tables(cls, metadata):
  676. fk_args = _backend_specific_fk_args()
  677. Table('users', metadata,
  678. Column('username', String(50), primary_key=True),
  679. test_needs_fk=True)
  680. Table(
  681. 'addresses', metadata,
  682. Column(
  683. 'username', String(50),
  684. ForeignKey('users.username', **fk_args),
  685. primary_key=True),
  686. Column('email', String(50), primary_key=True),
  687. Column('etc', String(50)),
  688. test_needs_fk=True)
  689. @classmethod
  690. def setup_classes(cls):
  691. class User(cls.Comparable):
  692. pass
  693. class Address(cls.Comparable):
  694. pass
  695. @testing.requires.on_update_cascade
  696. def test_onetomany_passive(self):
  697. self._test_onetomany(True)
  698. @testing.requires.non_updating_cascade
  699. def test_onetomany_nonpassive(self):
  700. self._test_onetomany(False)
  701. def test_o2m_change_passive(self):
  702. self._test_o2m_change(True)
  703. def test_o2m_change_nonpassive(self):
  704. self._test_o2m_change(False)
  705. def _test_o2m_change(self, passive_updates):
  706. """Change the PK of a related entity to another.
  707. "on update cascade" is not involved here, so the mapper has
  708. to do the UPDATE itself.
  709. """
  710. User, Address, users, addresses = (self.classes.User,
  711. self.classes.Address,
  712. self.tables.users,
  713. self.tables.addresses)
  714. mapper(
  715. User, users, properties={
  716. 'addresses': relationship(
  717. Address, passive_updates=passive_updates)})
  718. mapper(Address, addresses)
  719. sess = create_session()
  720. a1 = Address(username='ed', email='ed@host1')
  721. u1 = User(username='ed', addresses=[a1])
  722. u2 = User(username='jack')
  723. sess.add_all([a1, u1, u2])
  724. sess.flush()
  725. a1.username = 'jack'
  726. sess.flush()
  727. def test_o2m_move_passive(self):
  728. self._test_o2m_move(True)
  729. def test_o2m_move_nonpassive(self):
  730. self._test_o2m_move(False)
  731. def _test_o2m_move(self, passive_updates):
  732. """Move the related entity to a different collection,
  733. changing its PK.
  734. """
  735. User, Address, users, addresses = (self.classes.User,
  736. self.classes.Address,
  737. self.tables.users,
  738. self.tables.addresses)
  739. mapper(
  740. User, users, properties={
  741. 'addresses': relationship(
  742. Address, passive_updates=passive_updates)})
  743. mapper(Address, addresses)
  744. sess = create_session()
  745. a1 = Address(username='ed', email='ed@host1')
  746. u1 = User(username='ed', addresses=[a1])
  747. u2 = User(username='jack')
  748. sess.add_all([a1, u1, u2])
  749. sess.flush()
  750. u1.addresses.remove(a1)
  751. u2.addresses.append(a1)
  752. sess.flush()
  753. @testing.requires.on_update_cascade
  754. def test_change_m2o_passive(self):
  755. self._test_change_m2o(True)
  756. @testing.requires.non_updating_cascade
  757. def test_change_m2o_nonpassive(self):
  758. self._test_change_m2o(False)
  759. def _test_change_m2o(self, passive_updates):
  760. User, Address, users, addresses = (self.classes.User,
  761. self.classes.Address,
  762. self.tables.users,
  763. self.tables.addresses)
  764. mapper(User, users)
  765. mapper(Address, addresses, properties={
  766. 'user': relationship(User, passive_updates=passive_updates)
  767. })
  768. sess = create_session()
  769. u1 = User(username='jack')
  770. a1 = Address(user=u1, email='foo@bar')
  771. sess.add_all([u1, a1])
  772. sess.flush()
  773. u1.username = 'edmodified'
  774. sess.flush()
  775. eq_(a1.username, 'edmodified')
  776. sess.expire_all()
  777. eq_(a1.username, 'edmodified')
  778. def test_move_m2o_passive(self):
  779. self._test_move_m2o(True)
  780. def test_move_m2o_nonpassive(self):
  781. self._test_move_m2o(False)
  782. def _test_move_m2o(self, passive_updates):
  783. User, Address, users, addresses = (self.classes.User,
  784. self.classes.Address,
  785. self.tables.users,
  786. self.tables.addresses)
  787. # tests [ticket:1856]
  788. mapper(User, users)
  789. mapper(
  790. Address, addresses, properties={
  791. 'user': relationship(User, passive_updates=passive_updates)})
  792. sess = create_session()
  793. u1 = User(username='jack')
  794. u2 = User(username='ed')
  795. a1 = Address(user=u1, email='foo@bar')
  796. sess.add_all([u1, u2, a1])
  797. sess.flush()
  798. a1.user = u2
  799. sess.flush()
  800. def test_rowswitch_doesntfire(self):
  801. User, Address, users, addresses = (self.classes.User,
  802. self.classes.Address,
  803. self.tables.users,
  804. self.tables.addresses)
  805. mapper(User, users)
  806. mapper(Address, addresses, properties={
  807. 'user': relationship(User, passive_updates=True)
  808. })
  809. sess = create_session()
  810. u1 = User(username='ed')
  811. a1 = Address(user=u1, email='ed@host1')
  812. sess.add(u1)
  813. sess.add(a1)
  814. sess.flush()
  815. sess.delete(u1)
  816. sess.delete(a1)
  817. u2 = User(username='ed')
  818. a2 = Address(user=u2, email='ed@host1', etc='foo')
  819. sess.add(u2)
  820. sess.add(a2)
  821. from sqlalchemy.testing.assertsql import CompiledSQL
  822. # test that the primary key columns of addresses are not
  823. # being updated as well, since this is a row switch.
  824. self.assert_sql_execution(
  825. testing.db, sess.flush, CompiledSQL(
  826. "UPDATE addresses SET etc=:etc WHERE "
  827. "addresses.username = :addresses_username AND"
  828. " addresses.email = :addresses_email", {
  829. 'etc': 'foo', 'addresses_username': 'ed',
  830. 'addresses_email': 'ed@host1'}),
  831. )
  832. def _test_onetomany(self, passive_updates):
  833. """Change the PK of a related entity via foreign key cascade.
  834. For databases that require "on update cascade", the mapper
  835. has to identify the row by the new value, not the old, when
  836. it does the update.
  837. """
  838. User, Address, users, addresses = (self.classes.User,
  839. self.classes.Address,
  840. self.tables.users,
  841. self.tables.addresses)
  842. mapper(
  843. User, users, properties={
  844. 'addresses': relationship(
  845. Address, passive_updates=passive_updates)})
  846. mapper(Address, addresses)
  847. sess = create_session()
  848. a1, a2 = Address(username='ed', email='ed@host1'), \
  849. Address(username='ed', email='ed@host2')
  850. u1 = User(username='ed', addresses=[a1, a2])
  851. sess.add(u1)
  852. sess.flush()
  853. eq_(a1.username, 'ed')
  854. eq_(a2.username, 'ed')
  855. eq_(
  856. sa.select([addresses.c.username]).execute().fetchall(),
  857. [('ed',), ('ed',)])
  858. u1.username = 'jack'
  859. a2.email = 'ed@host3'
  860. sess.flush()
  861. eq_(a1.username, 'jack')
  862. eq_(a2.username, 'jack')
  863. eq_(
  864. sa.select([addresses.c.username]).execute().fetchall(),
  865. [('jack',), ('jack', )])
  866. class JoinedInheritanceTest(fixtures.MappedTest):
  867. """Test cascades of pk->pk/fk on joined table inh."""
  868. # mssql doesn't allow ON UPDATE on self-referential keys
  869. __unsupported_on__ = ('mssql',)
  870. __requires__ = 'skip_mysql_on_windows',
  871. __backend__ = True
  872. @classmethod
  873. def define_tables(cls, metadata):
  874. fk_args = _backend_specific_fk_args()
  875. Table(
  876. 'person', metadata,
  877. Column('name', String(50), primary_key=True),
  878. Column('type', String(50), nullable=False),
  879. test_needs_fk=True)
  880. Table(
  881. 'engineer', metadata,
  882. Column(
  883. 'name', String(50), ForeignKey('person.name', **fk_args),
  884. primary_key=True),
  885. Column('primary_language', String(50)),
  886. Column(
  887. 'boss_name', String(50),
  888. ForeignKey('manager.name', **fk_args)),
  889. test_needs_fk=True
  890. )
  891. Table(
  892. 'manager', metadata, Column('name', String(50),
  893. ForeignKey('person.name', **fk_args),
  894. primary_key=True),
  895. Column('paperwork', String(50)), test_needs_fk=True
  896. )
  897. @classmethod
  898. def setup_classes(cls):
  899. class Person(cls.Comparable):
  900. pass
  901. class Engineer(Person):
  902. pass
  903. class Manager(Person):
  904. pass
  905. @testing.requires.on_update_cascade
  906. def test_pk_passive(self):
  907. self._test_pk(True)
  908. @testing.requires.non_updating_cascade
  909. def test_pk_nonpassive(self):
  910. self._test_pk(False)
  911. @testing.requires.on_update_cascade
  912. def test_fk_passive(self):
  913. self._test_fk(True)
  914. # PG etc. need passive=True to allow PK->PK cascade
  915. @testing.requires.non_updating_cascade
  916. def test_fk_nonpassive(self):
  917. self._test_fk(False)
  918. def _test_pk(self, passive_updates):
  919. Person, Manager, person, manager, Engineer, engineer = (
  920. self.classes.Person, self.classes.Manager, self.tables.person,
  921. self.tables.manager, self.classes.Engineer, self.tables.engineer)
  922. mapper(
  923. Person, person, polymorphic_on=person.c.type,
  924. polymorphic_identity='person', passive_updates=passive_updates)
  925. mapper(
  926. Engineer, engineer, inherits=Person,
  927. polymorphic_identity='engineer', properties={
  928. 'boss': relationship(
  929. Manager,
  930. primaryjoin=manager.c.name == engineer.c.boss_name,
  931. passive_updates=passive_updates)})
  932. mapper(
  933. Manager, manager, inherits=Person, polymorphic_identity='manager')
  934. sess = sa.orm.sessionmaker()()
  935. e1 = Engineer(name='dilbert', primary_language='java')
  936. sess.add(e1)
  937. sess.commit()
  938. e1.name = 'wally'
  939. e1.primary_language = 'c++'
  940. sess.commit()
  941. def _test_fk(self, passive_updates):
  942. Person, Manager, person, manager, Engineer, engineer = (
  943. self.classes.Person, self.classes.Manager, self.tables.person,
  944. self.tables.manager, self.classes.Engineer, self.tables.engineer)
  945. mapper(
  946. Person, person, polymorphic_on=person.c.type,
  947. polymorphic_identity='person', passive_updates=passive_updates)
  948. mapper(
  949. Engineer, engineer, inherits=Person,
  950. polymorphic_identity='engineer', properties={
  951. 'boss': relationship(
  952. Manager,
  953. primaryjoin=manager.c.name == engineer.c.boss_name,
  954. passive_updates=passive_updates)})
  955. mapper(
  956. Manager, manager, inherits=Person, polymorphic_identity='manager')
  957. sess = sa.orm.sessionmaker()()
  958. m1 = Manager(name='dogbert', paperwork='lots')
  959. e1, e2 = Engineer(name='dilbert', primary_language='java', boss=m1),\
  960. Engineer(name='wally', primary_language='c++', boss=m1)
  961. sess.add_all([
  962. e1, e2, m1
  963. ])
  964. sess.commit()
  965. eq_(e1.boss_name, 'dogbert')
  966. eq_(e2.boss_name, 'dogbert')
  967. sess.expire_all()
  968. m1.name = 'pointy haired'
  969. e1.primary_language = 'scala'
  970. e2.primary_language = 'cobol'
  971. sess.commit()
  972. eq_(e1.boss_name, 'pointy haired')
  973. eq_(e2.boss_name, 'pointy haired')
  974. class JoinedInheritancePKOnFKTest(fixtures.MappedTest):
  975. """Test cascades of pk->non-pk/fk on joined table inh."""
  976. # mssql doesn't allow ON UPDATE on self-referential keys
  977. __unsupported_on__ = ('mssql',)
  978. __requires__ = 'skip_mysql_on_windows',
  979. __backend__ = True
  980. @classmethod
  981. def define_tables(cls, metadata):
  982. fk_args = _backend_specific_fk_args()
  983. Table(
  984. 'person', metadata,
  985. Column('name', String(50), primary_key=True),
  986. Column('type', String(50), nullable=False),
  987. test_needs_fk=True)
  988. Table(
  989. 'engineer', metadata,
  990. Column(
  991. 'id', Integer,
  992. primary_key=True, test_needs_autoincrement=True),
  993. Column(
  994. 'person_name', String(50),
  995. ForeignKey('person.name', **fk_args)),
  996. Column('primary_language', String(50)),
  997. test_needs_fk=True
  998. )
  999. @classmethod
  1000. def setup_classes(cls):
  1001. class Person(cls.Comparable):
  1002. pass
  1003. class Engineer(Person):
  1004. pass
  1005. def _test_pk(self, passive_updates):
  1006. Person, person, Engineer, engineer = (
  1007. self.classes.Person, self.tables.person,
  1008. self.classes.Engineer, self.tables.engineer)
  1009. mapper(
  1010. Person, person, polymorphic_on=person.c.type,
  1011. polymorphic_identity='person', passive_updates=passive_updates)
  1012. mapper(
  1013. Engineer, engineer, inherits=Person,
  1014. polymorphic_identity='engineer')
  1015. sess = sa.orm.sessionmaker()()
  1016. e1 = Engineer(name='dilbert', primary_language='java')
  1017. sess.add(e1)
  1018. sess.commit()
  1019. e1.name = 'wally'
  1020. e1.primary_language = 'c++'
  1021. sess.flush()
  1022. eq_(e1.person_name, 'wally')
  1023. sess.expire_all()
  1024. eq_(e1.primary_language, "c++")
  1025. @testing.requires.on_update_cascade
  1026. def test_pk_passive(self):
  1027. self._test_pk(True)
  1028. # @testing.requires.non_updating_cascade
  1029. def test_pk_nonpassive(self):
  1030. self._test_pk(False)