diff options
| -rw-r--r-- | CHANGES | 7 | ||||
| -rw-r--r-- | lib/sqlalchemy/dialects/mysql/base.py | 6 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/base.py | 33 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/default.py | 2 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/strategies.py | 7 | ||||
| -rw-r--r-- | lib/sqlalchemy/exc.py | 77 | ||||
| -rw-r--r-- | test/base/test_except.py | 31 | ||||
| -rw-r--r-- | test/dialect/test_mysql.py | 4 | ||||
| -rw-r--r-- | test/dialect/test_postgresql.py | 2 | ||||
| -rw-r--r-- | test/dialect/test_sqlite.py | 2 | ||||
| -rw-r--r-- | test/engine/test_execute.py | 35 | ||||
| -rw-r--r-- | test/engine/test_reconnect.py | 27 | ||||
| -rw-r--r-- | test/sql/test_constraints.py | 8 | ||||
| -rw-r--r-- | test/sql/test_defaults.py | 4 | ||||
| -rw-r--r-- | test/sql/test_query.py | 8 |
15 files changed, 159 insertions, 94 deletions
@@ -175,6 +175,13 @@ CHANGES - TypeDecorator is present in the "sqlalchemy" import space. + - Non-DBAPI errors which occur in the scope of an `execute()` + call are now wrapped in sqlalchemy.exc.StatementError, + and the text of the SQL statement and repr() of params + is included. This makes it easier to identify statement + executions which fail before the DBAPI becomes + involved. [ticket:2015] + -sqlite - SQLite dialect now uses `NullPool` for file-based databases [ticket:1921] diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index 882e13d2e..271218dba 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -1767,7 +1767,7 @@ class MySQLDialect(default.DefaultDialect): have = rs.rowcount > 0 rs.close() return have - except exc.SQLError, e: + except exc.DBAPIError, e: if self._extract_error_code(e.orig) == 1146: return False raise @@ -2055,7 +2055,7 @@ class MySQLDialect(default.DefaultDialect): rp = None try: rp = connection.execute(st) - except exc.SQLError, e: + except exc.DBAPIError, e: if self._extract_error_code(e.orig) == 1146: raise exc.NoSuchTableError(full_name) else: @@ -2079,7 +2079,7 @@ class MySQLDialect(default.DefaultDialect): try: try: rp = connection.execute(st) - except exc.SQLError, e: + except exc.DBAPIError, e: if self._extract_error_code(e.orig) == 1146: raise exc.NoSuchTableError(full_name) else: diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index f6c974136..cf6c6ad49 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1286,12 +1286,14 @@ class Connection(Connectable): """Execute a schema.DDL object.""" dialect = self.dialect + + compiled = ddl.compile(dialect=dialect) return self._execute_context( dialect, dialect.execution_ctx_cls._init_ddl, - None, + compiled, None, - ddl.compile(dialect=dialect) + compiled ) def _execute_clauseelement(self, elem, multiparams, params): @@ -1322,7 +1324,7 @@ class Connection(Connectable): return self._execute_context( dialect, dialect.execution_ctx_cls._init_compiled, - None, + compiled_sql, params, compiled_sql, params ) @@ -1335,7 +1337,7 @@ class Connection(Connectable): return self._execute_context( dialect, dialect.execution_ctx_cls._init_compiled, - None, + compiled, parameters, compiled, parameters ) @@ -1357,7 +1359,8 @@ class Connection(Connectable): _after_cursor_execute = None def _execute_context(self, dialect, constructor, - statement, parameters, *args): + statement, parameters, + *args): """Create an :class:`.ExecutionContext` and execute, returning a :class:`.ResultProxy`.""" @@ -1370,7 +1373,7 @@ class Connection(Connectable): context = constructor(dialect, self, conn, *args) except Exception, e: self._handle_dbapi_exception(e, - statement, parameters, + str(statement), parameters, None, None) raise @@ -1505,20 +1508,28 @@ class Connection(Connectable): context): if getattr(self, '_reentrant_error', False): # Py3K - #raise exc.DBAPIError.instance(statement, parameters, e) from e + #raise exc.DBAPIError.instance(statement, parameters, e, + # self.dialect.dbapi.Error) from e # Py2K - raise exc.DBAPIError.instance(statement, parameters, e), \ + raise exc.DBAPIError.instance(statement, + parameters, + e, + self.dialect.dbapi.Error), \ None, sys.exc_info()[2] # end Py2K self._reentrant_error = True try: - if not isinstance(e, self.dialect.dbapi.Error): + # non-DBAPI error - if we already got a context, + # or theres no string statement, don't wrap it + if not isinstance(e, self.dialect.dbapi.Error) and \ + (statement is None or context is not None): return if context: context.handle_dbapi_exception(e) - is_disconnect = self.dialect.is_disconnect(e, self.__connection, cursor) + is_disconnect = isinstance(e, self.dialect.dbapi.Error) and \ + self.dialect.is_disconnect(e, self.__connection, cursor) if is_disconnect: self.invalidate(e) self.engine.dispose() @@ -1533,6 +1544,7 @@ class Connection(Connectable): # statement, # parameters, # e, + # self.dialect.dbapi.Error, # connection_invalidated=is_disconnect) \ # from e # Py2K @@ -1540,6 +1552,7 @@ class Connection(Connectable): statement, parameters, e, + self.dialect.dbapi.Error, connection_invalidated=is_disconnect), \ None, sys.exc_info()[2] # end Py2K diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index e669b305e..75a686475 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -392,7 +392,7 @@ class DefaultExecutionContext(base.ExecutionContext): self.compiled = compiled if not compiled.can_execute: - raise exc.ArgumentError("Not an executable clause: %s" % compiled) + raise exc.ArgumentError("Not an executable clause") self.execution_options = compiled.statement._execution_options if connection._execution_options: diff --git a/lib/sqlalchemy/engine/strategies.py b/lib/sqlalchemy/engine/strategies.py index e49d0e99e..06bf1126f 100644 --- a/lib/sqlalchemy/engine/strategies.py +++ b/lib/sqlalchemy/engine/strategies.py @@ -80,10 +80,13 @@ class DefaultEngineStrategy(EngineStrategy): return dialect.connect(*cargs, **cparams) except Exception, e: # Py3K - #raise exc.DBAPIError.instance(None, None, e) from e + #raise exc.DBAPIError.instance(None, None, + # dialect.dbapi.Error, e) from e # Py2K import sys - raise exc.DBAPIError.instance(None, None, e), None, sys.exc_info()[2] + raise exc.DBAPIError.instance( + None, None, e, dialect.dbapi.Error), \ + None, sys.exc_info()[2] # end Py2K creator = kwargs.pop('creator', connect) diff --git a/lib/sqlalchemy/exc.py b/lib/sqlalchemy/exc.py index b50e000a2..dd3f5a9f8 100644 --- a/lib/sqlalchemy/exc.py +++ b/lib/sqlalchemy/exc.py @@ -92,7 +92,37 @@ class UnboundExecutionError(InvalidRequestError): # Moved to orm.exc; compatability definition installed by orm import until 0.6 UnmappedColumnError = None -class DBAPIError(SQLAlchemyError): +class StatementError(SQLAlchemyError): + """An error occured during execution of a SQL statement. + + :class:`.StatementError` wraps the exception raised + during execution, and features :attr:`.statement` + and :attr:`.params` attributes which supply context regarding + the specifics of the statement which had an issue. + + The wrapped exception object is available in + the :attr:`.orig` attribute. + + """ + + def __init__(self, message, statement, params, orig): + SQLAlchemyError.__init__(self, message) + self.statement = statement + self.params = params + self.orig = orig + + def __str__(self): + if isinstance(self.params, (list, tuple)) and \ + len(self.params) > 10 and \ + isinstance(self.params[0], (list, dict, tuple)): + return ' '.join((SQLAlchemyError.__str__(self), + repr(self.statement), + repr(self.params[:2]), + '... and a total of %i bound parameter sets' % len(self.params))) + return ' '.join((SQLAlchemyError.__str__(self), + repr(self.statement), repr(self.params))) + +class DBAPIError(StatementError): """Raised when the execution of a database operation fails. ``DBAPIError`` wraps exceptions raised by the DB-API underlying the @@ -103,23 +133,33 @@ class DBAPIError(SQLAlchemyError): that there is no guarantee that different DB-API implementations will raise the same exception type for any given error condition. - If the error-raising operation occured in the execution of a SQL - statement, that statement and its parameters will be available on - the exception object in the ``statement`` and ``params`` attributes. + :class:`.DBAPIError` features :attr:`.statement` + and :attr:`.params` attributes which supply context regarding + the specifics of the statement which had an issue, for the + typical case when the error was raised within the context of + emitting a SQL statement. - The wrapped exception object is available in the ``orig`` attribute. + The wrapped exception object is available in the :attr:`.orig` attribute. Its type and properties are DB-API implementation specific. """ @classmethod - def instance(cls, statement, params, orig, connection_invalidated=False): + def instance(cls, statement, params, + orig, + dbapi_base_err, + connection_invalidated=False): # Don't ever wrap these, just return them directly as if # DBAPIError didn't exist. if isinstance(orig, (KeyboardInterrupt, SystemExit)): return orig if orig is not None: + # not a DBAPI error, statement is present. + # raise a StatementError + if not isinstance(orig, dbapi_base_err) and statement: + return StatementError(str(orig), statement, params, orig) + name, glob = orig.__class__.__name__, globals() if name in glob and issubclass(glob[name], DBAPIError): cls = glob[name] @@ -133,26 +173,15 @@ class DBAPIError(SQLAlchemyError): raise except Exception, e: text = 'Error in str() of DB-API-generated exception: ' + str(e) - SQLAlchemyError.__init__( - self, '(%s) %s' % (orig.__class__.__name__, text)) - self.statement = statement - self.params = params - self.orig = orig + StatementError.__init__( + self, + '(%s) %s' % (orig.__class__.__name__, text), + statement, + params, + orig + ) self.connection_invalidated = connection_invalidated - def __str__(self): - if isinstance(self.params, (list, tuple)) and len(self.params) > 10 and isinstance(self.params[0], (list, dict, tuple)): - return ' '.join((SQLAlchemyError.__str__(self), - repr(self.statement), - repr(self.params[:2]), - '... and a total of %i bound parameter sets' % len(self.params))) - return ' '.join((SQLAlchemyError.__str__(self), - repr(self.statement), repr(self.params))) - - -# As of 0.4, SQLError is now DBAPIError. -# SQLError alias will be removed in 0.6. -SQLError = DBAPIError class InterfaceError(DBAPIError): """Wraps a DB-API InterfaceError.""" diff --git a/test/base/test_except.py b/test/base/test_except.py index f02ca988b..044e7c244 100644 --- a/test/base/test_except.py +++ b/test/base/test_except.py @@ -38,14 +38,14 @@ class WrapTest(TestBase): def test_db_error_normal(self): try: raise sa_exceptions.DBAPIError.instance('', [], - OperationalError()) + OperationalError(), DatabaseError) except sa_exceptions.DBAPIError: self.assert_(True) def test_tostring(self): try: raise sa_exceptions.DBAPIError.instance('this is a message' - , None, OperationalError()) + , None, OperationalError(), DatabaseError) except sa_exceptions.DBAPIError, exc: assert str(exc) \ == "(OperationalError) 'this is a message' None" @@ -56,7 +56,7 @@ class WrapTest(TestBase): , {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6, 'g': 7, 'h': 8, 'i': 9, 'j': 10, 'k': 11, - }, OperationalError()) + }, OperationalError(), DatabaseError) except sa_exceptions.DBAPIError, exc: assert str(exc).startswith("(OperationalError) 'this is a " "message' {") @@ -64,7 +64,8 @@ class WrapTest(TestBase): def test_tostring_large_list(self): try: raise sa_exceptions.DBAPIError.instance('this is a message', - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,], OperationalError()) + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,], + OperationalError(), DatabaseError) except sa_exceptions.DBAPIError, exc: assert str(exc).startswith("(OperationalError) 'this is a " "message' [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]") @@ -74,7 +75,7 @@ class WrapTest(TestBase): raise sa_exceptions.DBAPIError.instance('this is a message', [{1: 1}, {1: 1}, {1: 1}, {1: 1}, {1: 1}, {1: 1}, {1: 1}, {1:1}, {1: 1}, {1: 1},], - OperationalError()) + OperationalError(), DatabaseError) except sa_exceptions.DBAPIError, exc: assert str(exc) \ == "(OperationalError) 'this is a message' [{1: 1}, "\ @@ -84,7 +85,7 @@ class WrapTest(TestBase): raise sa_exceptions.DBAPIError.instance('this is a message', [ {1: 1}, {1: 1}, {1: 1}, {1: 1}, {1: 1}, {1: 1}, {1: 1}, {1:1}, {1: 1}, {1: 1}, {1: 1}, - ], OperationalError()) + ], OperationalError(), DatabaseError) except sa_exceptions.DBAPIError, exc: assert str(exc) \ == "(OperationalError) 'this is a message' [{1: 1}, "\ @@ -94,7 +95,7 @@ class WrapTest(TestBase): [ (1, ), (1, ), (1, ), (1, ), (1, ), (1, ), (1, ), (1, ), (1, ), (1, ), - ], OperationalError()) + ], OperationalError(), DatabaseError) except sa_exceptions.DBAPIError, exc: assert str(exc) \ == "(OperationalError) 'this is a message' [(1,), "\ @@ -103,7 +104,7 @@ class WrapTest(TestBase): raise sa_exceptions.DBAPIError.instance('this is a message', [ (1, ), (1, ), (1, ), (1, ), (1, ), (1, ), (1, ), (1, ), (1, ), (1, ), (1, ), - ], OperationalError()) + ], OperationalError(), DatabaseError) except sa_exceptions.DBAPIError, exc: assert str(exc) \ == "(OperationalError) 'this is a message' [(1,), "\ @@ -112,25 +113,23 @@ class WrapTest(TestBase): def test_db_error_busted_dbapi(self): try: raise sa_exceptions.DBAPIError.instance('', [], - ProgrammingError()) + ProgrammingError(), DatabaseError) except sa_exceptions.DBAPIError, e: self.assert_(True) self.assert_('Error in str() of DB-API' in e.args[0]) def test_db_error_noncompliant_dbapi(self): try: - raise sa_exceptions.DBAPIError.instance('', [], OutOfSpec()) + raise sa_exceptions.DBAPIError.instance('', [], OutOfSpec(), + DatabaseError) except sa_exceptions.DBAPIError, e: self.assert_(e.__class__ is sa_exceptions.DBAPIError) except OutOfSpec: self.assert_(False) - # Make sure the DatabaseError recognition logic is limited to - # subclasses of sqlalchemy.exceptions.DBAPIError - try: raise sa_exceptions.DBAPIError.instance('', [], - sa_exceptions.ArgumentError()) + sa_exceptions.ArgumentError(), DatabaseError) except sa_exceptions.DBAPIError, e: self.assert_(e.__class__ is sa_exceptions.DBAPIError) except sa_exceptions.ArgumentError: @@ -139,7 +138,7 @@ class WrapTest(TestBase): def test_db_error_keyboard_interrupt(self): try: raise sa_exceptions.DBAPIError.instance('', [], - KeyboardInterrupt()) + KeyboardInterrupt(), DatabaseError) except sa_exceptions.DBAPIError: self.assert_(False) except KeyboardInterrupt: @@ -148,7 +147,7 @@ class WrapTest(TestBase): def test_db_error_system_exit(self): try: raise sa_exceptions.DBAPIError.instance('', [], - SystemExit()) + SystemExit(), DatabaseError) except sa_exceptions.DBAPIError: self.assert_(False) except SystemExit: diff --git a/test/dialect/test_mysql.py b/test/dialect/test_mysql.py index 183f227c3..fd7a15e5b 100644 --- a/test/dialect/test_mysql.py +++ b/test/dialect/test_mysql.py @@ -621,10 +621,10 @@ class TypesTest(TestBase, AssertsExecutionResults, AssertsCompiledSQL): enum_table.drop(checkfirst=True) enum_table.create() - assert_raises(exc.SQLError, enum_table.insert().execute, + assert_raises(exc.DBAPIError, enum_table.insert().execute, e1=None, e2=None, e3=None, e4=None) - assert_raises(exc.InvalidRequestError, enum_table.insert().execute, + assert_raises(exc.StatementError, enum_table.insert().execute, e1='c', e2='c', e2generic='c', e3='c', e4='c', e5='c', e5generic='c', e6='c') diff --git a/test/dialect/test_postgresql.py b/test/dialect/test_postgresql.py index 9aa281979..10067a669 100644 --- a/test/dialect/test_postgresql.py +++ b/test/dialect/test_postgresql.py @@ -993,7 +993,7 @@ class DomainReflectionTest(TestBase, AssertsExecutionResults): : try: con.execute(ddl) - except exc.SQLError, e: + except exc.DBAPIError, e: if not 'already exists' in str(e): raise e con.execute('CREATE TABLE testtable (question integer, answer ' diff --git a/test/dialect/test_sqlite.py b/test/dialect/test_sqlite.py index b99f58bd2..efd616c3d 100644 --- a/test/dialect/test_sqlite.py +++ b/test/dialect/test_sqlite.py @@ -43,7 +43,7 @@ class TestTypes(TestBase, AssertsExecutionResults): meta.drop_all() def test_string_dates_raise(self): - assert_raises(TypeError, testing.db.execute, + assert_raises(exc.StatementError, testing.db.execute, select([1]).where(bindparam('date', type_=Date)), date=str(datetime.date(2007, 10, 30))) diff --git a/test/engine/test_execute.py b/test/engine/test_execute.py index bb4e7de42..01a0100ab 100644 --- a/test/engine/test_execute.py +++ b/test/engine/test_execute.py @@ -1,8 +1,9 @@ -from test.lib.testing import eq_, assert_raises +from test.lib.testing import eq_, assert_raises, assert_raises_message import re from sqlalchemy.interfaces import ConnectionProxy from sqlalchemy import MetaData, Integer, String, INT, VARCHAR, func, \ - bindparam, select, event + bindparam, select, event, TypeDecorator +from sqlalchemy.sql import column, literal from test.lib.schema import Table, Column import sqlalchemy as tsa from test.lib import TestBase, testing, engines @@ -122,14 +123,30 @@ class ExecuteTest(TestBase): 'horse'), (4, 'sally')] conn.execute('delete from users') - def test_exception_wrapping(self): + def test_exception_wrapping_dbapi(self): for conn in testing.db, testing.db.connect(): - try: - conn.execute('osdjafioajwoejoasfjdoifjowejfoawejqoijwef' - ) - assert False - except tsa.exc.DBAPIError: - assert True + assert_raises_message( + tsa.exc.DBAPIError, + r"not_a_valid_statement", + conn.execute, 'not_a_valid_statement' + ) + + def test_exception_wrapping_non_dbapi_statement(self): + class MyType(TypeDecorator): + impl = Integer + def process_bind_param(self, value, dialect): + raise Exception("nope") + + for conn in testing.db, testing.db.connect(): + assert_raises_message( + tsa.exc.StatementError, + "nope 'SELECT 1 ", + conn.execute, + select([1]).\ + where( + column('foo') == literal('bar', MyType()) + ) + ) def test_empty_insert(self): """test that execute() interprets [] as a list with no params""" diff --git a/test/engine/test_reconnect.py b/test/engine/test_reconnect.py index 9b3a1b4db..74986c178 100644 --- a/test/engine/test_reconnect.py +++ b/test/engine/test_reconnect.py @@ -1,4 +1,4 @@ -from test.lib.testing import eq_, assert_raises +from test.lib.testing import eq_, assert_raises, assert_raises_message import time import weakref from sqlalchemy import select, MetaData, Integer, String, pool @@ -130,13 +130,11 @@ class MockReconnectTest(TestBase): assert not conn.closed assert conn.invalidated assert trans.is_active - try: - conn.execute(select([1])) - assert False - except tsa.exc.InvalidRequestError, e: - assert str(e) \ - == "Can't reconnect until invalid transaction is "\ - "rolled back" + assert_raises_message( + tsa.exc.StatementError, + "Can't reconnect until invalid transaction is rolled back", + conn.execute, select([1]) + ) assert trans.is_active try: trans.commit() @@ -364,13 +362,12 @@ class RealReconnectTest(TestBase): assert not conn.closed assert conn.invalidated assert trans.is_active - try: - conn.execute(select([1])) - assert False - except tsa.exc.InvalidRequestError, e: - assert str(e) \ - == "Can't reconnect until invalid transaction is "\ - "rolled back" + assert_raises_message( + tsa.exc.StatementError, + "Can't reconnect until invalid transaction is "\ + "rolled back", + conn.execute, select([1]) + ) assert trans.is_active try: trans.commit() diff --git a/test/sql/test_constraints.py b/test/sql/test_constraints.py index 4bacbd271..f4791c0bd 100644 --- a/test/sql/test_constraints.py +++ b/test/sql/test_constraints.py @@ -79,9 +79,9 @@ class ConstraintTest(TestBase, AssertsExecutionResults, AssertsCompiledSQL): metadata.create_all() foo.insert().execute(id=1,x=9,y=5) - assert_raises(exc.SQLError, foo.insert().execute, id=2,x=5,y=9) + assert_raises(exc.DBAPIError, foo.insert().execute, id=2,x=5,y=9) bar.insert().execute(id=1,x=10) - assert_raises(exc.SQLError, bar.insert().execute, id=2,x=5) + assert_raises(exc.DBAPIError, bar.insert().execute, id=2,x=5) def test_unique_constraint(self): foo = Table('foo', metadata, @@ -98,8 +98,8 @@ class ConstraintTest(TestBase, AssertsExecutionResults, AssertsCompiledSQL): foo.insert().execute(id=2, value='value2') bar.insert().execute(id=1, value='a', value2='a') bar.insert().execute(id=2, value='a', value2='b') - assert_raises(exc.SQLError, foo.insert().execute, id=3, value='value1') - assert_raises(exc.SQLError, bar.insert().execute, id=3, value='a', value2='b') + assert_raises(exc.DBAPIError, foo.insert().execute, id=3, value='value1') + assert_raises(exc.DBAPIError, bar.insert().execute, id=3, value='a', value2='b') def test_index_create(self): employees = Table('employees', metadata, diff --git a/test/sql/test_defaults.py b/test/sql/test_defaults.py index 1f2226842..cbecdbe18 100644 --- a/test/sql/test_defaults.py +++ b/test/sql/test_defaults.py @@ -304,7 +304,7 @@ class DefaultTest(testing.TestBase): 12, today, 'py')]) def test_missing_many_param(self): - assert_raises_message(exc.InvalidRequestError, + assert_raises_message(exc.StatementError, "A value is required for bind parameter 'col7', in parameter group 1", t.insert().execute, {'col4':7, 'col7':12, 'col8':19}, @@ -531,7 +531,7 @@ class AutoIncrementTest(_base.TablesTest): nonai.insert().execute(data='row 1') nonai.insert().execute(data='row 2') assert False - except sa.exc.SQLError, e: + except sa.exc.DBAPIError, e: assert True nonai.insert().execute(id=1, data='row 1') diff --git a/test/sql/test_query.py b/test/sql/test_query.py index cbf6e6e58..359084cd8 100644 --- a/test/sql/test_query.py +++ b/test/sql/test_query.py @@ -48,8 +48,8 @@ class QueryTest(TestBase): def test_insert_heterogeneous_params(self): """test that executemany parameters are asserted to match the parameter set of the first.""" - assert_raises_message(exc.InvalidRequestError, - "A value is required for bind parameter 'user_name', in parameter group 2", + assert_raises_message(exc.StatementError, + "A value is required for bind parameter 'user_name', in parameter group 2 'INSERT INTO query_users", users.insert().execute, {'user_id':7, 'user_name':'jack'}, {'user_id':8, 'user_name':'ed'}, @@ -852,8 +852,8 @@ class QueryTest(TestBase): def test_cant_execute_join(self): try: users.join(addresses).execute() - except exc.ArgumentError, e: - assert str(e).startswith('Not an executable clause: ') + except exc.StatementError, e: + assert str(e).startswith('Not an executable clause ') |
