zope.sqlalchemy-0.6.1/000755 000765 000120 00000000000 11512115253 014536 5ustar00ldradmin000000 000000 zope.sqlalchemy-0.6.1/bootstrap.py000644 000765 000120 00000003366 11421133757 017145 0ustar00ldradmin000000 000000 ############################################################################## # # Copyright (c) 2006 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Bootstrap a buildout-based project Simply run this script in a directory containing a buildout.cfg. The script accepts buildout command-line options, so you can use the -c option to specify an alternate configuration file. $Id: bootstrap.py 71258 2006-11-21 22:22:48Z jim $ """ import os, shutil, sys, tempfile, urllib2 tmpeggs = tempfile.mkdtemp() ez = {} exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py' ).read() in ez ez['use_setuptools'](to_dir=tmpeggs, download_delay=0) import pkg_resources cmd = 'from setuptools.command.easy_install import main; main()' if sys.platform == 'win32': cmd = '"%s"' % cmd # work around spawn lamosity on windows ws = pkg_resources.working_set assert os.spawnle( os.P_WAIT, sys.executable, sys.executable, '-c', cmd, '-mqNxd', tmpeggs, 'zc.buildout', dict(os.environ, PYTHONPATH= ws.find(pkg_resources.Requirement.parse('setuptools')).location ), ) == 0 ws.add_entry(tmpeggs) ws.require('zc.buildout') import zc.buildout.buildout zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap']) shutil.rmtree(tmpeggs) zope.sqlalchemy-0.6.1/._buildout.cfg000644 000765 000120 00000000272 11403177554 017277 0ustar00ldradmin000000 000000 Mac OS X  2ˆºATTR–׺˜"˜"com.macromates.caret{ column = 0; line = 14; }zope.sqlalchemy-0.6.1/buildout.cfg000644 000765 000120 00000000416 11403177554 017062 0ustar00ldradmin000000 000000 [buildout] develop = . parts = test scripts allow-hosts = pypi.python.org [test] recipe = zc.recipe.testrunner eggs = zope.sqlalchemy [test] defaults = ['--auto-color'] [scripts] recipe = zc.recipe.egg eggs = ${test:eggs} collective.checkdocs interpreter = py zope.sqlalchemy-0.6.1/._CHANGES.txt000644 000765 000120 00000000271 11512115131 016557 0ustar00ldradmin000000 000000 Mac OS X  2‡¹ATTRf!¹˜!˜!com.macromates.caret{ column = 0; line = 5; }zope.sqlalchemy-0.6.1/CHANGES.txt000644 000765 000120 00000004472 11512115131 016351 0ustar00ldradmin000000 000000 Changes ======= 0.6.1 (2011-01-08) ------------------ * Update datamanager.mark_changed to handle sessions which have not yet logged a (ORM) query. 0.6 (2010-07-24) ---------------- * Implement should_retry for sqlalchemy.orm.exc.ConcurrentModificationError and serialization errors from PostgreSQL and Oracle. (Specify transaction>=1.1 to use this functionality.) * Include license files. * Add ``transaction_manager`` attribute to data managers for compliance with IDataManager interface. 0.5 (2010-06-07) ---------------- * Remove redundant session.flush() / session.clear() on savepoint operations. These were only needed with SQLAlchemy 0.4.x. * SQLAlchemy 0.6.x support. Require SQLAlchemy >= 0.5.1. * Add support for running ``python setup.py test``. * Pull in pysqlite explicitly as a test dependency. * Setup sqlalchemy mappers in test setup and clear them in tear down. This makes the tests more robust and clears up the global state after. It caused the tests to fail when other tests in the same run called clear_mappers. 0.4 (2009-01-20) ---------------- Bugs fixed: * Only raise errors in tpc_abort if we have committed. * Remove the session id from the SESSION_STATE just before we de-reference the session (i.e. all work is already successfuly completed). This fixes cases where the transaction commit failed but SESSION_STATE was already cleared. In those cases, the transaction was wedeged as abort would always error. This happened on PostgreSQL where invalid SQL was used and the error caught. * Call session.flush() unconditionally in tpc_begin. * Change error message on session.commit() to be friendlier to non zope users. Feature changes: * Support for bulk update and delete with SQLAlchemy 0.5.1 0.3 (2008-07-29) ---------------- Bugs fixed: * New objects added to a session did not cause a transaction join, so were not committed at the end of the transaction unless the database was accessed. SQLAlchemy 0.4.7 or 0.5beta3 now required. Feature changes: * For correctness and consistency with ZODB, renamed the function 'invalidate' to 'mark_changed' and the status 'invalidated' to 'changed'. 0.2 (2008-06-28) ---------------- Feature changes: * Updated to support SQLAlchemy 0.5. (0.4.6 is still supported). 0.1 (2008-05-15) ---------------- * Initial public release. zope.sqlalchemy-0.6.1/COPYRIGHT.txt000644 000765 000120 00000000040 11421133757 016651 0ustar00ldradmin000000 000000 Zope Foundation and Contributorszope.sqlalchemy-0.6.1/CREDITS.txt000644 000765 000120 00000000526 11120253246 016377 0ustar00ldradmin000000 000000 zope.sqlalchemy credits *********************** * Laurence Rowe - creator and main developer * Martijn Faassen - updated to work with SQLAlchemy 0.5 Also thanks to Michael Bayer for help with integration in SQLAlchemy and of course SQLAlchemy itself, as well as the many Zope developers who worked on Zope/SQLAlchemy integration projects. zope.sqlalchemy-0.6.1/LICENSE.txt000644 000765 000120 00000004026 11421133757 016373 0ustar00ldradmin000000 000000 Zope Public License (ZPL) Version 2.1 A copyright notice accompanies this license document that identifies the copyright holders. This license has been certified as open source. It has also been designated as GPL compatible by the Free Software Foundation (FSF). Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions in source code must retain the accompanying copyright notice, this list of conditions, and the following disclaimer. 2. Redistributions in binary form must reproduce the accompanying copyright notice, this list of conditions, and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Names of the copyright holders must not be used to endorse or promote products derived from this software without prior written permission from the copyright holders. 4. The right to distribute this software or to use it for any purpose does not give you the right to use Servicemarks (sm) or Trademarks (tm) of the copyright holders. Use of them is covered by separate agreement with the copyright holders. 5. If any files are modified, you must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. Disclaimer THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. zope.sqlalchemy-0.6.1/oracle.cfg000644 000765 000120 00000001557 11503171204 016472 0ustar00ldradmin000000 000000 # To run the oracle tests I use the oracle developer days VirtualBox image: # http://www.oracle.com/technology/software/products/virtualbox/appliances/index.html # For cx_Oracle to build, download instantclient basiclite and sdk from: # http://www.oracle.com/technology/software/tech/oci/instantclient/index.html [buildout] extends = buildout.cfg # extends = postgres.cfg parts += python-oracle cx_Oracle testora python = python-oracle allow-hosts += *.sourceforge.net [python-oracle] recipe = gocept.cxoracle instant-client = ${buildout:directory}/instantclient-basiclite-10.2.0.4.0-macosx-x64.zip instant-sdk = instantclient-sdk-10.2.0.4.0-macosx-x64.zip [cx_Oracle] recipe = zc.recipe.egg:custom egg = cx_Oracle [test] eggs += cx_Oracle [testora] <= test environment = oraenv [scripts] eggs += cx_Oracle [oraenv] TEST_DSN = oracle://system:oracle@192.168.56.101/orcl zope.sqlalchemy-0.6.1/PKG-INFO000644 000765 000120 00000025167 11512115253 015646 0ustar00ldradmin000000 000000 Metadata-Version: 1.0 Name: zope.sqlalchemy Version: 0.6.1 Summary: Minimal Zope/SQLAlchemy transaction integration Home-page: http://pypi.python.org/pypi/zope.sqlalchemy Author: Laurence Rowe Author-email: laurence@lrowe.co.uk License: ZPL 2.1 Description: *************** zope.sqlalchemy *************** .. contents:: :local: Introduction ============ The aim of this package is to unify the plethora of existing packages integrating SQLAlchemy with Zope's transaction management. As such it seeks only to provide a data manager and makes no attempt to define a `zopeish` way to configure engines. For WSGI applications, Zope style automatic transaction management is available with `repoze.tm2`_, a part of `Repoze BFG`_ and `Turbogears 2`_. You need to understand `SQLAlchemy`_ for this package and this README to make any sense. .. _repoze.tm2: http://docs.repoze.org/tm2/ .. _Repoze BFG: http://bfg.repoze.org/ .. _Turbogears 2: http://turbogears.org/ .. _SQLAlchemy: http://sqlalchemy.org/docs/ Running the tests ================= This package is distributed as a buildout. Using your desired python run: $ python bootstrap.py This will download the dependent packages and setup the test script, which may be run with: $ ./bin/test or with the standard setuptools test command: $ ./bin/py setup.py test To enable testing with your own database set the TEST_DSN environment variable to your sqlalchemy database dsn. Two-phase commit behaviour may be tested by setting the TEST_TWOPHASE variable to a non empty string. e.g: $ TEST_DSN=postgres://test:test@localhost/test TEST_TWOPHASE=True bin/test Example ======= This example is lifted directly from the SQLAlchemy declarative documentation. First the necessary imports. >>> from sqlalchemy import * >>> from sqlalchemy.ext.declarative import declarative_base >>> from sqlalchemy.orm import scoped_session, sessionmaker, relation >>> from zope.sqlalchemy import ZopeTransactionExtension >>> import transaction Now to define the mapper classes. >>> Base = declarative_base() >>> class User(Base): ... __tablename__ = 'test_users' ... id = Column('id', Integer, primary_key=True) ... name = Column('name', String(50)) ... addresses = relation("Address", backref="user") >>> class Address(Base): ... __tablename__ = 'test_addresses' ... id = Column('id', Integer, primary_key=True) ... email = Column('email', String(50)) ... user_id = Column('user_id', Integer, ForeignKey('test_users.id')) Create an engine and setup the tables. Note that for this example to work a recent version of sqlite/pysqlite is required. 3.4.0 seems to be sufficient. >>> engine = create_engine(TEST_DSN, convert_unicode=True) >>> Base.metadata.create_all(engine) Now to create the session itself. As zope is a threaded web server we must use scoped sessions. Zope and SQLAlchemy sessions are tied together by using the ZopeTransactionExtension from this package. >>> Session = scoped_session(sessionmaker(bind=engine, ... twophase=TEST_TWOPHASE, extension=ZopeTransactionExtension())) Call the scoped session factory to retrieve a session. You may call this as many times as you like within a transaction and you will always retrieve the same session. At present there are no users in the database. >>> session = Session() >>> session.query(User).all() [] We can now create a new user and commit the changes using Zope's transaction machinary, just as Zope's publisher would. >>> session.add(User(id=1, name='bob')) >>> transaction.commit() Engine level connections are outside the scope of the transaction integration. >>> engine.connect().execute('SELECT * FROM test_users').fetchall() [(1, ...'bob')] A new transaction requires a new session. Let's add an address. >>> session = Session() >>> bob = session.query(User).all()[0] >>> bob.name u'bob' >>> bob.addresses [] >>> bob.addresses.append(Address(id=1, email='bob@bob.bob')) >>> transaction.commit() >>> session = Session() >>> bob = session.query(User).all()[0] >>> bob.addresses [
] >>> bob.addresses[0].email u'bob@bob.bob' >>> bob.addresses[0].email = 'wrong@wrong' To rollback a transaction, use transaction.abort(). >>> transaction.abort() >>> session = Session() >>> bob = session.query(User).all()[0] >>> bob.addresses[0].email u'bob@bob.bob' >>> transaction.abort() By default, zope.sqlalchemy puts sessions in an 'active' state when they are first used. ORM write operations automatically move the session into a 'changed' state. This avoids unnecessary database commits. Sometimes it is necessary to interact with the database directly through SQL. It is not possible to guess whether such an operation is a read or a write. Therefore we must manually mark the session as changed when manual SQL statements write to the DB. >>> session = Session() >>> conn = session.connection() >>> users = Base.metadata.tables['test_users'] >>> conn.execute(users.update(users.c.name=='bob'), name='ben') >>> from zope.sqlalchemy import mark_changed >>> mark_changed(session) >>> transaction.commit() >>> session = Session() >>> session.query(User).all()[0].name u'ben' >>> transaction.abort() If this is a problem you may tell the extension to place the session in the 'changed' state initially. >>> Session.configure(extension=ZopeTransactionExtension('changed')) >>> Session.remove() >>> session = Session() >>> conn = session.connection() >>> conn.execute(users.update(users.c.name=='ben'), name='bob') >>> transaction.commit() >>> session = Session() >>> session.query(User).all()[0].name u'bob' >>> transaction.abort() Development version =================== `SVN version `_ Changes ======= 0.6.1 (2011-01-08) ------------------ * Update datamanager.mark_changed to handle sessions which have not yet logged a (ORM) query. 0.6 (2010-07-24) ---------------- * Implement should_retry for sqlalchemy.orm.exc.ConcurrentModificationError and serialization errors from PostgreSQL and Oracle. (Specify transaction>=1.1 to use this functionality.) * Include license files. * Add ``transaction_manager`` attribute to data managers for compliance with IDataManager interface. 0.5 (2010-06-07) ---------------- * Remove redundant session.flush() / session.clear() on savepoint operations. These were only needed with SQLAlchemy 0.4.x. * SQLAlchemy 0.6.x support. Require SQLAlchemy >= 0.5.1. * Add support for running ``python setup.py test``. * Pull in pysqlite explicitly as a test dependency. * Setup sqlalchemy mappers in test setup and clear them in tear down. This makes the tests more robust and clears up the global state after. It caused the tests to fail when other tests in the same run called clear_mappers. 0.4 (2009-01-20) ---------------- Bugs fixed: * Only raise errors in tpc_abort if we have committed. * Remove the session id from the SESSION_STATE just before we de-reference the session (i.e. all work is already successfuly completed). This fixes cases where the transaction commit failed but SESSION_STATE was already cleared. In those cases, the transaction was wedeged as abort would always error. This happened on PostgreSQL where invalid SQL was used and the error caught. * Call session.flush() unconditionally in tpc_begin. * Change error message on session.commit() to be friendlier to non zope users. Feature changes: * Support for bulk update and delete with SQLAlchemy 0.5.1 0.3 (2008-07-29) ---------------- Bugs fixed: * New objects added to a session did not cause a transaction join, so were not committed at the end of the transaction unless the database was accessed. SQLAlchemy 0.4.7 or 0.5beta3 now required. Feature changes: * For correctness and consistency with ZODB, renamed the function 'invalidate' to 'mark_changed' and the status 'invalidated' to 'changed'. 0.2 (2008-06-28) ---------------- Feature changes: * Updated to support SQLAlchemy 0.5. (0.4.6 is still supported). 0.1 (2008-05-15) ---------------- * Initial public release. Keywords: zope zope3 sqlalchemy Platform: UNKNOWN Classifier: Framework :: Zope3 Classifier: Programming Language :: Python Classifier: License :: OSI Approved :: Zope Public License Classifier: Topic :: Software Development :: Libraries :: Python Modules zope.sqlalchemy-0.6.1/postgres.cfg000644 000765 000120 00000001145 11503171204 017064 0ustar00ldradmin000000 000000 # PATH=/opt/local/lib/postgresql90/bin:$PATH bin/buildout -c postgres.cfg # sudo -u postgres /opt/local/lib/postgresql90/bin/createdb zope_sqlalchemy_tests # sudo -u postgres /opt/local/lib/postgresql90/bin/createuser -s # sudo -u postgres /opt/local/lib/postgresql90/bin/postgres -D /opt/local/var/db/postgresql90/defaultdb -d 1 [buildout] extends = buildout.cfg find-links = http://initd.org/pub/software/psycopg/ allow-hosts += initd.org parts += testpg [test] eggs += psycopg2 [testpg] <= test environment = pgenv [scripts] eggs += psycopg2 [pgenv] TEST_DSN = postgres:///zope_sqlalchemy_tests zope.sqlalchemy-0.6.1/._README.txt000644 000765 000120 00000000271 11403200443 016444 0ustar00ldradmin000000 000000 Mac OS X  2‡¹ATTR–ѹ˜!˜!com.macromates.caret{ column = 0; line = 1; }zope.sqlalchemy-0.6.1/README.txt000644 000765 000120 00000000043 11403200443 016224 0ustar00ldradmin000000 000000 See src/zope/sqlalchemy/README.txt zope.sqlalchemy-0.6.1/setup.cfg000644 000765 000120 00000000073 11512115253 016357 0ustar00ldradmin000000 000000 [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 zope.sqlalchemy-0.6.1/._setup.py000644 000765 000120 00000000272 11512115005 016461 0ustar00ldradmin000000 000000 Mac OS X  2ˆºATTRfº˜"˜"com.macromates.caret{ column = 18; line = 5; }zope.sqlalchemy-0.6.1/setup.py000644 000765 000120 00000002263 11512115005 016246 0ustar00ldradmin000000 000000 import os.path from setuptools import setup, find_packages setup( name='zope.sqlalchemy', version='0.6.1', # Remember to update __version__ in __init__.py packages=find_packages('src'), package_dir = {'':'src'}, include_package_data=True, zip_safe=False, namespace_packages=['zope'], test_suite='zope.sqlalchemy', author='Laurence Rowe', author_email='laurence@lrowe.co.uk', url='http://pypi.python.org/pypi/zope.sqlalchemy', description="Minimal Zope/SQLAlchemy transaction integration", long_description=open(os.path.join('src', 'zope', 'sqlalchemy', 'README.txt')).read() + "\n\n" + open('CHANGES.txt').read(), license='ZPL 2.1', keywords='zope zope3 sqlalchemy', classifiers=[ "Framework :: Zope3", "Programming Language :: Python", "License :: OSI Approved :: Zope Public License", "Topic :: Software Development :: Libraries :: Python Modules", ], install_requires=[ 'setuptools', 'SQLAlchemy>=0.5.1', 'transaction', 'zope.interface', ], extras_require={ 'test': [ 'pysqlite', ] }, ) zope.sqlalchemy-0.6.1/src/000755 000765 000120 00000000000 11512115253 015325 5ustar00ldradmin000000 000000 zope.sqlalchemy-0.6.1/src/zope/000755 000765 000120 00000000000 11512115253 016302 5ustar00ldradmin000000 000000 zope.sqlalchemy-0.6.1/src/zope.sqlalchemy.egg-info/000755 000765 000120 00000000000 11512115253 022135 5ustar00ldradmin000000 000000 zope.sqlalchemy-0.6.1/src/zope.sqlalchemy.egg-info/dependency_links.txt000644 000765 000120 00000000001 11512115252 026202 0ustar00ldradmin000000 000000 zope.sqlalchemy-0.6.1/src/zope.sqlalchemy.egg-info/namespace_packages.txt000644 000765 000120 00000000005 11512115252 026462 0ustar00ldradmin000000 000000 zope zope.sqlalchemy-0.6.1/src/zope.sqlalchemy.egg-info/not-zip-safe000644 000765 000120 00000000001 11216432702 024365 0ustar00ldradmin000000 000000 zope.sqlalchemy-0.6.1/src/zope.sqlalchemy.egg-info/PKG-INFO000644 000765 000120 00000025167 11512115252 023244 0ustar00ldradmin000000 000000 Metadata-Version: 1.0 Name: zope.sqlalchemy Version: 0.6.1 Summary: Minimal Zope/SQLAlchemy transaction integration Home-page: http://pypi.python.org/pypi/zope.sqlalchemy Author: Laurence Rowe Author-email: laurence@lrowe.co.uk License: ZPL 2.1 Description: *************** zope.sqlalchemy *************** .. contents:: :local: Introduction ============ The aim of this package is to unify the plethora of existing packages integrating SQLAlchemy with Zope's transaction management. As such it seeks only to provide a data manager and makes no attempt to define a `zopeish` way to configure engines. For WSGI applications, Zope style automatic transaction management is available with `repoze.tm2`_, a part of `Repoze BFG`_ and `Turbogears 2`_. You need to understand `SQLAlchemy`_ for this package and this README to make any sense. .. _repoze.tm2: http://docs.repoze.org/tm2/ .. _Repoze BFG: http://bfg.repoze.org/ .. _Turbogears 2: http://turbogears.org/ .. _SQLAlchemy: http://sqlalchemy.org/docs/ Running the tests ================= This package is distributed as a buildout. Using your desired python run: $ python bootstrap.py This will download the dependent packages and setup the test script, which may be run with: $ ./bin/test or with the standard setuptools test command: $ ./bin/py setup.py test To enable testing with your own database set the TEST_DSN environment variable to your sqlalchemy database dsn. Two-phase commit behaviour may be tested by setting the TEST_TWOPHASE variable to a non empty string. e.g: $ TEST_DSN=postgres://test:test@localhost/test TEST_TWOPHASE=True bin/test Example ======= This example is lifted directly from the SQLAlchemy declarative documentation. First the necessary imports. >>> from sqlalchemy import * >>> from sqlalchemy.ext.declarative import declarative_base >>> from sqlalchemy.orm import scoped_session, sessionmaker, relation >>> from zope.sqlalchemy import ZopeTransactionExtension >>> import transaction Now to define the mapper classes. >>> Base = declarative_base() >>> class User(Base): ... __tablename__ = 'test_users' ... id = Column('id', Integer, primary_key=True) ... name = Column('name', String(50)) ... addresses = relation("Address", backref="user") >>> class Address(Base): ... __tablename__ = 'test_addresses' ... id = Column('id', Integer, primary_key=True) ... email = Column('email', String(50)) ... user_id = Column('user_id', Integer, ForeignKey('test_users.id')) Create an engine and setup the tables. Note that for this example to work a recent version of sqlite/pysqlite is required. 3.4.0 seems to be sufficient. >>> engine = create_engine(TEST_DSN, convert_unicode=True) >>> Base.metadata.create_all(engine) Now to create the session itself. As zope is a threaded web server we must use scoped sessions. Zope and SQLAlchemy sessions are tied together by using the ZopeTransactionExtension from this package. >>> Session = scoped_session(sessionmaker(bind=engine, ... twophase=TEST_TWOPHASE, extension=ZopeTransactionExtension())) Call the scoped session factory to retrieve a session. You may call this as many times as you like within a transaction and you will always retrieve the same session. At present there are no users in the database. >>> session = Session() >>> session.query(User).all() [] We can now create a new user and commit the changes using Zope's transaction machinary, just as Zope's publisher would. >>> session.add(User(id=1, name='bob')) >>> transaction.commit() Engine level connections are outside the scope of the transaction integration. >>> engine.connect().execute('SELECT * FROM test_users').fetchall() [(1, ...'bob')] A new transaction requires a new session. Let's add an address. >>> session = Session() >>> bob = session.query(User).all()[0] >>> bob.name u'bob' >>> bob.addresses [] >>> bob.addresses.append(Address(id=1, email='bob@bob.bob')) >>> transaction.commit() >>> session = Session() >>> bob = session.query(User).all()[0] >>> bob.addresses [
] >>> bob.addresses[0].email u'bob@bob.bob' >>> bob.addresses[0].email = 'wrong@wrong' To rollback a transaction, use transaction.abort(). >>> transaction.abort() >>> session = Session() >>> bob = session.query(User).all()[0] >>> bob.addresses[0].email u'bob@bob.bob' >>> transaction.abort() By default, zope.sqlalchemy puts sessions in an 'active' state when they are first used. ORM write operations automatically move the session into a 'changed' state. This avoids unnecessary database commits. Sometimes it is necessary to interact with the database directly through SQL. It is not possible to guess whether such an operation is a read or a write. Therefore we must manually mark the session as changed when manual SQL statements write to the DB. >>> session = Session() >>> conn = session.connection() >>> users = Base.metadata.tables['test_users'] >>> conn.execute(users.update(users.c.name=='bob'), name='ben') >>> from zope.sqlalchemy import mark_changed >>> mark_changed(session) >>> transaction.commit() >>> session = Session() >>> session.query(User).all()[0].name u'ben' >>> transaction.abort() If this is a problem you may tell the extension to place the session in the 'changed' state initially. >>> Session.configure(extension=ZopeTransactionExtension('changed')) >>> Session.remove() >>> session = Session() >>> conn = session.connection() >>> conn.execute(users.update(users.c.name=='ben'), name='bob') >>> transaction.commit() >>> session = Session() >>> session.query(User).all()[0].name u'bob' >>> transaction.abort() Development version =================== `SVN version `_ Changes ======= 0.6.1 (2011-01-08) ------------------ * Update datamanager.mark_changed to handle sessions which have not yet logged a (ORM) query. 0.6 (2010-07-24) ---------------- * Implement should_retry for sqlalchemy.orm.exc.ConcurrentModificationError and serialization errors from PostgreSQL and Oracle. (Specify transaction>=1.1 to use this functionality.) * Include license files. * Add ``transaction_manager`` attribute to data managers for compliance with IDataManager interface. 0.5 (2010-06-07) ---------------- * Remove redundant session.flush() / session.clear() on savepoint operations. These were only needed with SQLAlchemy 0.4.x. * SQLAlchemy 0.6.x support. Require SQLAlchemy >= 0.5.1. * Add support for running ``python setup.py test``. * Pull in pysqlite explicitly as a test dependency. * Setup sqlalchemy mappers in test setup and clear them in tear down. This makes the tests more robust and clears up the global state after. It caused the tests to fail when other tests in the same run called clear_mappers. 0.4 (2009-01-20) ---------------- Bugs fixed: * Only raise errors in tpc_abort if we have committed. * Remove the session id from the SESSION_STATE just before we de-reference the session (i.e. all work is already successfuly completed). This fixes cases where the transaction commit failed but SESSION_STATE was already cleared. In those cases, the transaction was wedeged as abort would always error. This happened on PostgreSQL where invalid SQL was used and the error caught. * Call session.flush() unconditionally in tpc_begin. * Change error message on session.commit() to be friendlier to non zope users. Feature changes: * Support for bulk update and delete with SQLAlchemy 0.5.1 0.3 (2008-07-29) ---------------- Bugs fixed: * New objects added to a session did not cause a transaction join, so were not committed at the end of the transaction unless the database was accessed. SQLAlchemy 0.4.7 or 0.5beta3 now required. Feature changes: * For correctness and consistency with ZODB, renamed the function 'invalidate' to 'mark_changed' and the status 'invalidated' to 'changed'. 0.2 (2008-06-28) ---------------- Feature changes: * Updated to support SQLAlchemy 0.5. (0.4.6 is still supported). 0.1 (2008-05-15) ---------------- * Initial public release. Keywords: zope zope3 sqlalchemy Platform: UNKNOWN Classifier: Framework :: Zope3 Classifier: Programming Language :: Python Classifier: License :: OSI Approved :: Zope Public License Classifier: Topic :: Software Development :: Libraries :: Python Modules zope.sqlalchemy-0.6.1/src/zope.sqlalchemy.egg-info/requires.txt000644 000765 000120 00000000110 11512115252 024524 0ustar00ldradmin000000 000000 setuptools SQLAlchemy>=0.5.1 transaction zope.interface [test] pysqlitezope.sqlalchemy-0.6.1/src/zope.sqlalchemy.egg-info/SOURCES.txt000644 000765 000120 00000001077 11512115253 024026 0ustar00ldradmin000000 000000 CHANGES.txt COPYRIGHT.txt CREDITS.txt LICENSE.txt README.txt bootstrap.py buildout.cfg oracle.cfg postgres.cfg setup.py src/zope/__init__.py src/zope.sqlalchemy.egg-info/PKG-INFO src/zope.sqlalchemy.egg-info/SOURCES.txt src/zope.sqlalchemy.egg-info/dependency_links.txt src/zope.sqlalchemy.egg-info/namespace_packages.txt src/zope.sqlalchemy.egg-info/not-zip-safe src/zope.sqlalchemy.egg-info/requires.txt src/zope.sqlalchemy.egg-info/top_level.txt src/zope/sqlalchemy/README.txt src/zope/sqlalchemy/__init__.py src/zope/sqlalchemy/datamanager.py src/zope/sqlalchemy/tests.pyzope.sqlalchemy-0.6.1/src/zope.sqlalchemy.egg-info/top_level.txt000644 000765 000120 00000000005 11512115252 024661 0ustar00ldradmin000000 000000 zope zope.sqlalchemy-0.6.1/src/zope/__init__.py000644 000765 000120 00000000364 11120253246 020416 0ustar00ldradmin000000 000000 # See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages try: __import__('pkg_resources').declare_namespace(__name__) except ImportError: from pkgutil import extend_path __path__ = extend_path(__path__, __name__) zope.sqlalchemy-0.6.1/src/zope/sqlalchemy/000755 000765 000120 00000000000 11512115253 020444 5ustar00ldradmin000000 000000 zope.sqlalchemy-0.6.1/src/zope/sqlalchemy/.___init__.py000644 000765 000120 00000000273 11512115015 022770 0ustar00ldradmin000000 000000 Mac OS X  2‰»ATTRfø»˜#˜#com.macromates.caret{ column = 20; line = 14; }zope.sqlalchemy-0.6.1/src/zope/sqlalchemy/__init__.py000644 000765 000120 00000001354 11512115015 022554 0ustar00ldradmin000000 000000 ############################################################################## # # Copyright (c) 2008 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE # ############################################################################## __version__ = '0.6.1' from datamanager import ZopeTransactionExtension, mark_changed invalidate = mark_changed zope.sqlalchemy-0.6.1/src/zope/sqlalchemy/._datamanager.py000644 000765 000120 00000000273 11512114670 023503 0ustar00ldradmin000000 000000 Mac OS X  2‰»ATTRf»˜#˜#com.macromates.caret{ column = 8; line = 190; }zope.sqlalchemy-0.6.1/src/zope/sqlalchemy/datamanager.py000644 000765 000120 00000021210 11512114670 023260 0ustar00ldradmin000000 000000 ############################################################################## # # Copyright (c) 2008 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE # ############################################################################## import transaction as zope_transaction from zope.interface import implements from transaction.interfaces import ISavepointDataManager, IDataManagerSavepoint from transaction._transaction import Status as ZopeStatus from sqlalchemy.orm.exc import ConcurrentModificationError from sqlalchemy.exc import DBAPIError from sqlalchemy.orm.session import SessionExtension from sqlalchemy.engine.base import Engine _retryable_errors = [] try: import psycopg2.extensions except ImportError: pass else: _retryable_errors.append((psycopg2.extensions.TransactionRollbackError, None)) # ORA-08177: can't serialize access for this transaction try: import cx_Oracle except ImportError: pass else: _retryable_errors.append((cx_Oracle.DatabaseError, lambda e: e.args[0].code==8177)) # The status of the session is stored on the connection info STATUS_ACTIVE = 'active' # session joined to transaction, writes allowed. STATUS_CHANGED = 'changed' # data has been written STATUS_READONLY = 'readonly' # session joined to transaction, no writes allowed. STATUS_INVALIDATED = STATUS_CHANGED # BBB NO_SAVEPOINT_SUPPORT = set(['sqlite']) _SESSION_STATE = {} # a mapping of id(session) -> status # This is thread safe because you are using scoped sessions # # The two variants of the DataManager. # class SessionDataManager(object): """Integrate a top level sqlalchemy session transaction into a zope transaction One phase variant. """ implements(ISavepointDataManager) def __init__(self, session, status, transaction_manager): self.transaction_manager = transaction_manager self.tx = session.transaction._iterate_parents()[-1] self.session = session _SESSION_STATE[id(session)] = status self.state = 'init' def _finish(self, final_state): assert self.tx is not None session = self.session del _SESSION_STATE[id(self.session)] self.tx = self.session = None self.state = final_state # closing the session is the last thing we do. If it fails the # transactions don't get wedged and the error propagates session.close() def abort(self, trans): if self.tx is not None: # there may have been no work to do self._finish('aborted') def tpc_begin(self, trans): self.session.flush() def commit(self, trans): status = _SESSION_STATE[id(self.session)] if status is not STATUS_INVALIDATED: self._finish('no work') def tpc_vote(self, trans): # for a one phase data manager commit last in tpc_vote if self.tx is not None: # there may have been no work to do self.tx.commit() self._finish('committed') def tpc_finish(self, trans): pass def tpc_abort(self, trans): assert self.state is not 'committed' def sortKey(self): # Try to sort last, so that we vote last - we may commit in tpc_vote(), # which allows Zope to roll back its transaction if the RDBMS # threw a conflict error. return "~sqlalchemy:%d" % id(self.tx) @property def savepoint(self): """Savepoints are only supported when all connections support subtransactions """ # ATT: the following check is weak since the savepoint capability # of a RDBMS also depends on its version. E.g. Postgres 7.X does not # support savepoints but Postgres is whitelisted independent of its # version. Possibly additional version information should be taken # into account (ajung) if set(engine.url.drivername for engine in self.session.transaction._connections.keys() if isinstance(engine, Engine) ).intersection(NO_SAVEPOINT_SUPPORT): raise AttributeError('savepoint') return self._savepoint def _savepoint(self): return SessionSavepoint(self.session) def should_retry(self, error): if isinstance(error, ConcurrentModificationError): return True if isinstance(error, DBAPIError): orig = error.orig for error_type, test in _retryable_errors: if isinstance(orig, error_type): if test is None: return True if test(orig): return True class TwoPhaseSessionDataManager(SessionDataManager): """Two phase variant. """ def tpc_vote(self, trans): if self.tx is not None: # there may have been no work to do self.tx.prepare() self.state = 'voted' def tpc_finish(self, trans): if self.tx is not None: self.tx.commit() self._finish('committed') def tpc_abort(self, trans): if self.tx is not None: # we may not have voted, and been aborted already self.tx.rollback() self._finish('aborted commit') def sortKey(self): # Sort normally return "sqlalchemy.twophase:%d" % id(self.tx) class SessionSavepoint: implements(IDataManagerSavepoint) def __init__(self, session): self.session = session self.transaction = session.begin_nested() def rollback(self): # no need to check validity, sqlalchemy should raise an exception. I think. self.transaction.rollback() def join_transaction(session, initial_state=STATUS_ACTIVE, transaction_manager=zope_transaction.manager): """Join a session to a transaction using the appropriate datamanager. It is safe to call this multiple times, if the session is already joined then it just returns. `initial_state` is either STATUS_ACTIVE, STATUS_INVALIDATED or STATUS_READONLY If using the default initial status of STATUS_ACTIVE, you must ensure that mark_changed(session) is called when data is written to the database. The ZopeTransactionExtesion SessionExtension can be used to ensure that this is called automatically after session write operations. """ if _SESSION_STATE.get(id(session), None) is None: if session.twophase: DataManager = TwoPhaseSessionDataManager else: DataManager = SessionDataManager transaction_manager.get().join(DataManager(session, initial_state, transaction_manager)) def mark_changed(session, transaction_manager=zope_transaction.manager): """Mark a session as needing to be committed. """ session_id = id(session) assert _SESSION_STATE.get(session_id, None) is not STATUS_READONLY, "Session already registered as read only" join_transaction(session, STATUS_CHANGED, transaction_manager) _SESSION_STATE[session_id] = STATUS_CHANGED class ZopeTransactionExtension(SessionExtension): """Record that a flush has occurred on a session's connection. This allows the DataManager to rollback rather than commit on read only transactions. """ def __init__(self, initial_state=STATUS_ACTIVE, transaction_manager=zope_transaction.manager): if initial_state=='invalidated': initial_state = STATUS_CHANGED #BBB SessionExtension.__init__(self) self.initial_state = initial_state self.transaction_manager = transaction_manager def after_begin(self, session, transaction, connection): join_transaction(session, self.initial_state, self.transaction_manager) def after_attach(self, session, instance): join_transaction(session, self.initial_state, self.transaction_manager) def after_flush(self, session, flush_context): mark_changed(session, self.transaction_manager) def after_bulk_update(self, session, query, query_context, result): mark_changed(session, self.transaction_manager) def after_bulk_delete(self, session, query, query_context, result): mark_changed(session, self.transaction_manager) def before_commit(self, session): assert self.transaction_manager.get().status == ZopeStatus.COMMITTING, "Transaction must be committed using the transaction manager" zope.sqlalchemy-0.6.1/src/zope/sqlalchemy/README.txt000644 000765 000120 00000013370 11503171204 022144 0ustar00ldradmin000000 000000 *************** zope.sqlalchemy *************** .. contents:: :local: Introduction ============ The aim of this package is to unify the plethora of existing packages integrating SQLAlchemy with Zope's transaction management. As such it seeks only to provide a data manager and makes no attempt to define a `zopeish` way to configure engines. For WSGI applications, Zope style automatic transaction management is available with `repoze.tm2`_, a part of `Repoze BFG`_ and `Turbogears 2`_. You need to understand `SQLAlchemy`_ for this package and this README to make any sense. .. _repoze.tm2: http://docs.repoze.org/tm2/ .. _Repoze BFG: http://bfg.repoze.org/ .. _Turbogears 2: http://turbogears.org/ .. _SQLAlchemy: http://sqlalchemy.org/docs/ Running the tests ================= This package is distributed as a buildout. Using your desired python run: $ python bootstrap.py This will download the dependent packages and setup the test script, which may be run with: $ ./bin/test or with the standard setuptools test command: $ ./bin/py setup.py test To enable testing with your own database set the TEST_DSN environment variable to your sqlalchemy database dsn. Two-phase commit behaviour may be tested by setting the TEST_TWOPHASE variable to a non empty string. e.g: $ TEST_DSN=postgres://test:test@localhost/test TEST_TWOPHASE=True bin/test Example ======= This example is lifted directly from the SQLAlchemy declarative documentation. First the necessary imports. >>> from sqlalchemy import * >>> from sqlalchemy.ext.declarative import declarative_base >>> from sqlalchemy.orm import scoped_session, sessionmaker, relation >>> from zope.sqlalchemy import ZopeTransactionExtension >>> import transaction Now to define the mapper classes. >>> Base = declarative_base() >>> class User(Base): ... __tablename__ = 'test_users' ... id = Column('id', Integer, primary_key=True) ... name = Column('name', String(50)) ... addresses = relation("Address", backref="user") >>> class Address(Base): ... __tablename__ = 'test_addresses' ... id = Column('id', Integer, primary_key=True) ... email = Column('email', String(50)) ... user_id = Column('user_id', Integer, ForeignKey('test_users.id')) Create an engine and setup the tables. Note that for this example to work a recent version of sqlite/pysqlite is required. 3.4.0 seems to be sufficient. >>> engine = create_engine(TEST_DSN, convert_unicode=True) >>> Base.metadata.create_all(engine) Now to create the session itself. As zope is a threaded web server we must use scoped sessions. Zope and SQLAlchemy sessions are tied together by using the ZopeTransactionExtension from this package. >>> Session = scoped_session(sessionmaker(bind=engine, ... twophase=TEST_TWOPHASE, extension=ZopeTransactionExtension())) Call the scoped session factory to retrieve a session. You may call this as many times as you like within a transaction and you will always retrieve the same session. At present there are no users in the database. >>> session = Session() >>> session.query(User).all() [] We can now create a new user and commit the changes using Zope's transaction machinary, just as Zope's publisher would. >>> session.add(User(id=1, name='bob')) >>> transaction.commit() Engine level connections are outside the scope of the transaction integration. >>> engine.connect().execute('SELECT * FROM test_users').fetchall() [(1, ...'bob')] A new transaction requires a new session. Let's add an address. >>> session = Session() >>> bob = session.query(User).all()[0] >>> bob.name u'bob' >>> bob.addresses [] >>> bob.addresses.append(Address(id=1, email='bob@bob.bob')) >>> transaction.commit() >>> session = Session() >>> bob = session.query(User).all()[0] >>> bob.addresses [
] >>> bob.addresses[0].email u'bob@bob.bob' >>> bob.addresses[0].email = 'wrong@wrong' To rollback a transaction, use transaction.abort(). >>> transaction.abort() >>> session = Session() >>> bob = session.query(User).all()[0] >>> bob.addresses[0].email u'bob@bob.bob' >>> transaction.abort() By default, zope.sqlalchemy puts sessions in an 'active' state when they are first used. ORM write operations automatically move the session into a 'changed' state. This avoids unnecessary database commits. Sometimes it is necessary to interact with the database directly through SQL. It is not possible to guess whether such an operation is a read or a write. Therefore we must manually mark the session as changed when manual SQL statements write to the DB. >>> session = Session() >>> conn = session.connection() >>> users = Base.metadata.tables['test_users'] >>> conn.execute(users.update(users.c.name=='bob'), name='ben') >>> from zope.sqlalchemy import mark_changed >>> mark_changed(session) >>> transaction.commit() >>> session = Session() >>> session.query(User).all()[0].name u'ben' >>> transaction.abort() If this is a problem you may tell the extension to place the session in the 'changed' state initially. >>> Session.configure(extension=ZopeTransactionExtension('changed')) >>> Session.remove() >>> session = Session() >>> conn = session.connection() >>> conn.execute(users.update(users.c.name=='ben'), name='bob') >>> transaction.commit() >>> session = Session() >>> session.query(User).all()[0].name u'bob' >>> transaction.abort() Development version =================== `SVN version `_ zope.sqlalchemy-0.6.1/src/zope/sqlalchemy/tests.py000644 000765 000120 00000051252 11512077640 022174 0ustar00ldradmin000000 000000 ############################################################################## # # Copyright (c) 2008 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE # ############################################################################## # Much inspiration from z3c.sqlalchemy/trunk/src/z3c/sqlalchemy/tests/testSQLAlchemy.py # # You may want to run the tests with your database. To do so set the environment variable # TEST_DSN to the connection url. e.g.: # export TEST_DSN=postgres://plone:plone@localhost/test # export TEST_DSN=mssql://plone:plone@/test?dsn=mydsn # # To test in twophase commit mode export TEST_TWOPHASE=True # # NOTE: The sqlite that ships with Mac OS X 10.4 is buggy. Install a newer version (3.5.6) # and rebuild pysqlite2 against it. import os import unittest import transaction import threading import time import sqlalchemy as sa from sqlalchemy import orm, sql, exc from zope.sqlalchemy import datamanager as tx from zope.sqlalchemy import mark_changed TEST_TWOPHASE = bool(os.environ.get('TEST_TWOPHASE')) TEST_DSN = os.environ.get('TEST_DSN', 'sqlite:///:memory:') class SimpleModel(object): def __init__(self, **kw): for k, v in kw.items(): setattr(self, k, v) def asDict(self): return dict((k, v) for k, v in self.__dict__.items() if not k.startswith('_')) class User(SimpleModel): pass class Skill(SimpleModel): pass engine = sa.create_engine(TEST_DSN) engine2 = sa.create_engine(TEST_DSN) Session = orm.scoped_session(orm.sessionmaker( bind=engine, extension=tx.ZopeTransactionExtension(), twophase=TEST_TWOPHASE, )) UnboundSession = orm.scoped_session(orm.sessionmaker( extension=tx.ZopeTransactionExtension(), twophase=TEST_TWOPHASE, )) metadata = sa.MetaData() # best to use unbound metadata test_users = sa.Table('test_users', metadata, sa.Column('id', sa.Integer, primary_key=True), sa.Column('firstname', sa.VARCHAR(255)), # mssql cannot do equality on a text type sa.Column('lastname', sa.VARCHAR(255)), ) test_skills = sa.Table('test_skills', metadata, sa.Column('id', sa.Integer, primary_key=True), sa.Column('user_id', sa.Integer), sa.Column('name', sa.VARCHAR(255)), sa.ForeignKeyConstraint(('user_id',), ('test_users.id',)), ) bound_metadata1 = sa.MetaData(engine) bound_metadata2 = sa.MetaData(engine2) test_one = sa.Table('test_one', bound_metadata1, sa.Column('id', sa.Integer, primary_key=True)) test_two = sa.Table('test_two', bound_metadata2, sa.Column('id', sa.Integer, primary_key=True)) class TestOne(SimpleModel): pass class TestTwo(SimpleModel): pass def setup_mappers(): orm.clear_mappers() # Other tests can clear mappers by calling clear_mappers(), # be more robust by setting up mappers in the test setup. m1 = orm.mapper(User, test_users, properties = {'skills': orm.relation(Skill, primaryjoin=test_users.columns['id']==test_skills.columns['user_id']), }) m2 = orm.mapper(Skill, test_skills) m3 = orm.mapper(TestOne, test_one) m4 = orm.mapper(TestTwo, test_two) return [m1, m2, m3, m4] class DummyException(Exception): pass class DummyTargetRaised(DummyException): pass class DummyTargetResult(DummyException): pass class DummyDataManager(object): def __init__(self, key, target=None, args=(), kwargs={}): self.key = key self.target = target self.args = args self.kwargs = kwargs def abort(self, trans): pass def tpc_begin(self, trans): pass def commit(self, trans): pass def tpc_vote(self, trans): if self.target is not None: try: result = self.target(*self.args, **self.kwargs) except Exception, e: raise DummyTargetRaised(e) raise DummyTargetResult(result) else: raise DummyException('DummyDataManager cannot commit') def tpc_finish(self, trans): pass def tpc_abort(self, trans): pass def sortKey(self): return self.key class ZopeSQLAlchemyTests(unittest.TestCase): def setUp(self): self.mappers = setup_mappers() metadata.drop_all(engine) metadata.create_all(engine) def tearDown(self): transaction.abort() metadata.drop_all(engine) orm.clear_mappers() def testMarkUnknownSession(self): import zope.sqlalchemy.datamanager dummy = DummyDataManager(key='dummy.first') session = Session() mark_changed(session) self.assertTrue(id(session) in zope.sqlalchemy.datamanager._SESSION_STATE) def testAbortBeforeCommit(self): # Simulate what happens in a conflict error dummy = DummyDataManager(key='dummy.first') session = Session() conn = session.connection() mark_changed(session) try: # Thus we could fail in commit transaction.commit() except: # But abort must succed (and actually rollback the base connection) transaction.abort() pass # Or the next transaction the next transaction will not be able to start! transaction.begin() session = Session() conn = session.connection() conn.execute("SELECT 1 FROM test_users") mark_changed(session) transaction.commit() def testAbortAfterCommit(self): # This is a regression test which used to wedge the transaction # machinery when using PostgreSQL (and perhaps other) connections. # Basically, if a commit failed, there was no way to abort the # transaction. Leaving the transaction wedged. transaction.begin() session = Session() conn = session.connection() # At least PostgresSQL requires a rollback after invalid SQL is executed self.assertRaises(Exception, conn.execute, "BAD SQL SYNTAX") mark_changed(session) try: # Thus we could fail in commit transaction.commit() except: # But abort must succed (and actually rollback the base connection) transaction.abort() pass # Or the next transaction the next transaction will not be able to start! transaction.begin() session = Session() conn = session.connection() conn.execute("SELECT 1 FROM test_users") mark_changed(session) transaction.commit() def testSimplePopulation(self): session = Session() query = session.query(User) rows = query.all() self.assertEqual(len(rows), 0) session.add(User(id=1, firstname='udo', lastname='juergens')) session.add(User(id=2, firstname='heino', lastname='n/a')) session.flush() rows = query.order_by(User.id).all() self.assertEqual(len(rows), 2) row1 = rows[0] d = row1.asDict() self.assertEqual(d, {'firstname' : 'udo', 'lastname' : 'juergens', 'id' : 1}) # bypass the session machinary stmt = sql.select(test_users.columns).order_by('id') conn = session.connection() results = conn.execute(stmt) self.assertEqual(results.fetchall(), [(1, u'udo', u'juergens'), (2, u'heino', u'n/a')]) def testRelations(self): session = Session() session.add(User(id=1,firstname='foo', lastname='bar')) user = session.query(User).filter_by(firstname='foo')[0] user.skills.append(Skill(id=1, name='Zope')) session.flush() def testTransactionJoining(self): transaction.abort() # clean slate t = transaction.get() self.failIf([r for r in t._resources if isinstance(r, tx.SessionDataManager)], "Joined transaction too early") session = Session() session.add(User(id=1, firstname='udo', lastname='juergens')) t = transaction.get() # Expect this to fail with SQLAlchemy 0.4 self.failUnless([r for r in t._resources if isinstance(r, tx.SessionDataManager)], "Not joined transaction") transaction.abort() conn = Session().connection() self.failUnless([r for r in t._resources if isinstance(r, tx.SessionDataManager)], "Not joined transaction") def testSavepoint(self): use_savepoint = not engine.url.drivername in tx.NO_SAVEPOINT_SUPPORT t = transaction.get() session = Session() query = session.query(User) self.failIf(query.all(), "Users table should be empty") s0 = t.savepoint(optimistic=True) # this should always work if not use_savepoint: self.assertRaises(TypeError, t.savepoint) return # sqlite databases do not support savepoints s1 = t.savepoint() session.add(User(id=1, firstname='udo', lastname='juergens')) session.flush() self.failUnless(len(query.all())==1, "Users table should have one row") s2 = t.savepoint() session.add(User(id=2, firstname='heino', lastname='n/a')) session.flush() self.failUnless(len(query.all())==2, "Users table should have two rows") s2.rollback() self.failUnless(len(query.all())==1, "Users table should have one row") s1.rollback() self.failIf(query.all(), "Users table should be empty") def testRollbackAttributes(self): use_savepoint = not engine.url.drivername in tx.NO_SAVEPOINT_SUPPORT if not use_savepoint: return # sqlite databases do not support savepoints t = transaction.get() session = Session() query = session.query(User) self.failIf(query.all(), "Users table should be empty") s1 = t.savepoint() user = User(id=1, firstname='udo', lastname='juergens') session.add(user) session.flush() s2 = t.savepoint() user.firstname='heino' session.flush() s2.rollback() self.assertEqual(user.firstname, 'udo', "User firstname attribute should have been rolled back") def testCommit(self): session = Session() use_savepoint = not engine.url.drivername in tx.NO_SAVEPOINT_SUPPORT query = session.query(User) rows = query.all() self.assertEqual(len(rows), 0) transaction.commit() # test a none modifying transaction works session = Session() query = session.query(User) session.add(User(id=1, firstname='udo', lastname='juergens')) session.add(User(id=2, firstname='heino', lastname='n/a')) session.flush() rows = query.order_by(User.id).all() self.assertEqual(len(rows), 2) transaction.abort() # test that the abort really aborts session = Session() query = session.query(User) rows = query.order_by(User.id).all() self.assertEqual(len(rows), 0) session.add(User(id=1, firstname='udo', lastname='juergens')) session.add(User(id=2, firstname='heino', lastname='n/a')) session.flush() rows = query.order_by(User.id).all() row1 = rows[0] d = row1.asDict() self.assertEqual(d, {'firstname' : 'udo', 'lastname' : 'juergens', 'id' : 1}) transaction.commit() rows = query.order_by(User.id).all() self.assertEqual(len(rows), 2) row1 = rows[0] d = row1.asDict() self.assertEqual(d, {'firstname' : 'udo', 'lastname' : 'juergens', 'id' : 1}) # bypass the session (and transaction) machinary results = engine.connect().execute(test_users.select()) self.assertEqual(len(results.fetchall()), 2) def testCommitWithSavepoint(self): if engine.url.drivername in tx.NO_SAVEPOINT_SUPPORT: return session = Session() session.add(User(id=1, firstname='udo', lastname='juergens')) session.add(User(id=2, firstname='heino', lastname='n/a')) session.flush() transaction.commit() session = Session() query = session.query(User) # lets just test that savepoints don't affect commits t = transaction.get() rows = query.order_by(User.id).all() s1 = t.savepoint() session.delete(rows[1]) session.flush() transaction.commit() # bypass the session machinary results = engine.connect().execute(test_users.select()) self.assertEqual(len(results.fetchall()), 1) def testTwoPhase(self): session = Session() if not session.twophase: return session.add(User(id=1, firstname='udo', lastname='juergens')) session.add(User(id=2, firstname='heino', lastname='n/a')) session.flush() transaction.commit() # Test that we clean up after a tpc_abort t = transaction.get() def target(): return engine.connect().recover_twophase() dummy = DummyDataManager(key='~~~dummy.last', target=target) t.join(dummy) session = Session() query = session.query(User) rows = query.all() session.delete(rows[0]) session.flush() result = None try: t.commit() except DummyTargetResult, e: result = e.args[0] except DummyTargetRaised, e: raise e.args[0] self.assertEqual(len(result), 1, "Should have been one prepared transaction when dummy aborted") transaction.begin() self.assertEqual(len(engine.connect().recover_twophase()), 0, "Test no outstanding prepared transactions") def testThread(self): transaction.abort() global thread_error thread_error = None def target(): try: session = Session() metadata.drop_all(engine) metadata.create_all(engine) query = session.query(User) rows = query.all() self.assertEqual(len(rows), 0) session.add(User(id=1, firstname='udo', lastname='juergens')) session.add(User(id=2, firstname='heino', lastname='n/a')) session.flush() rows = query.order_by(User.id).all() self.assertEqual(len(rows), 2) row1 = rows[0] d = row1.asDict() self.assertEqual(d, {'firstname' : 'udo', 'lastname' : 'juergens', 'id' : 1}) except Exception, err: global thread_error thread_error = err transaction.abort() thread = threading.Thread(target=target) thread.start() thread.join() if thread_error is not None: raise thread_error # reraise in current thread def testBulkDelete(self): session = Session() session.add(User(id=1, firstname='udo', lastname='juergens')) session.add(User(id=2, firstname='heino', lastname='n/a')) transaction.commit() session = Session() session.query(User).delete() transaction.commit() results = engine.connect().execute(test_users.select()) self.assertEqual(len(results.fetchall()), 0) def testBulkUpdate(self): session = Session() session.add(User(id=1, firstname='udo', lastname='juergens')) session.add(User(id=2, firstname='heino', lastname='n/a')) transaction.commit() session = Session() session.query(User).update(dict(lastname="smith")) transaction.commit() results = engine.connect().execute(test_users.select(test_users.c.lastname=="smith")) self.assertEqual(len(results.fetchall()), 2) class RetryTests(unittest.TestCase): def setUp(self): self.mappers = setup_mappers() metadata.drop_all(engine) metadata.create_all(engine) self.tm1 = transaction.TransactionManager() self.tm2 = transaction.TransactionManager() # With psycopg2 you might supply isolation_level='SERIALIZABLE' here, # unfortunately that is not supported by cx_Oracle. e1 = sa.create_engine(TEST_DSN) e2 = sa.create_engine(TEST_DSN) self.s1 = orm.sessionmaker( bind=e1, extension=tx.ZopeTransactionExtension(transaction_manager=self.tm1), twophase=TEST_TWOPHASE, )() self.s2 = orm.sessionmaker( bind=e2, extension=tx.ZopeTransactionExtension(transaction_manager=self.tm2), twophase=TEST_TWOPHASE, )() self.tm1.begin() self.s1.add(User(id=1, firstname='udo', lastname='juergens')) self.tm1.commit() def tearDown(self): self.tm1.abort() self.tm2.abort() metadata.drop_all(engine) orm.clear_mappers() def testRetry(self): # sqlite is unable to run this test as the databse is locked tm1, tm2, s1, s2 = self.tm1, self.tm2, self.s1, self.s2 # make sure we actually start a session. tm1.begin() self.failUnless(len(s1.query(User).all())==1, "Users table should have one row") tm2.begin() self.failUnless(len(s2.query(User).all())==1, "Users table should have one row") s1.query(User).delete() user = s2.query(User).get(1) user.lastname = u'smith' tm1.commit() raised = False try: s2.flush() except orm.exc.ConcurrentModificationError, e: # This error is thrown when the number of updated rows is not as expected raised = True self.failUnless(raised, "Did not raise expected error") self.failUnless(tm2._retryable(type(e), e), "Error should be retryable") def testRetryThread(self): tm1, tm2, s1, s2 = self.tm1, self.tm2, self.s1, self.s2 # make sure we actually start a session. tm1.begin() self.failUnless(len(s1.query(User).all())==1, "Users table should have one row") tm2.begin() s2.connection().execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE") self.failUnless(len(s2.query(User).all())==1, "Users table should have one row") s1.query(User).delete() raised = False def target(): time.sleep(0.2) tm1.commit() thread = threading.Thread(target=target) thread.start() try: user = s2.query(User).with_lockmode('update').get(1) except exc.DBAPIError, e: # This error wraps the underlying DBAPI module error, some of which are retryable raised = True self.failUnless(raised, "Did not raise expected error") self.failUnless(tm2._retryable(type(e), e), "Error should be retryable") thread.join() # well, we must have joined by now class MultipleEngineTests(unittest.TestCase): def setUp(self): self.mappers = setup_mappers() bound_metadata1.drop_all() bound_metadata1.create_all() bound_metadata2.drop_all() bound_metadata2.create_all() def tearDown(self): transaction.abort() bound_metadata1.drop_all() bound_metadata2.drop_all() orm.clear_mappers() def testTwoEngines(self): session = UnboundSession() session.add(TestOne(id=1)) session.add(TestTwo(id=2)) session.flush() transaction.commit() session = UnboundSession() rows = session.query(TestOne).all() self.assertEqual(len(rows), 1) rows = session.query(TestTwo).all() self.assertEqual(len(rows), 1) def tearDownReadMe(test): Base = test.globs['Base'] engine = test.globs['engine'] Base.metadata.drop_all(engine) def test_suite(): from unittest import TestSuite, makeSuite import doctest optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS suite = TestSuite() suite.addTest(makeSuite(ZopeSQLAlchemyTests)) suite.addTest(makeSuite(MultipleEngineTests)) if TEST_DSN.startswith('postgres') or TEST_DSN.startswith('oracle'): suite.addTest(makeSuite(RetryTests)) suite.addTest(doctest.DocFileSuite('README.txt', optionflags=optionflags, tearDown=tearDownReadMe, globs={'TEST_DSN': TEST_DSN, 'TEST_TWOPHASE': TEST_TWOPHASE})) return suite