diff options
-rw-r--r-- | doc/build/changelog/changelog_08.rst | 110 | ||||
-rw-r--r-- | doc/build/dialects/mysql.rst | 5 | ||||
-rw-r--r-- | examples/dogpile_caching/caching_query.py | 37 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/mssql/information_schema.py | 4 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/mysql/gaerdbms.py | 4 | ||||
-rw-r--r-- | lib/sqlalchemy/engine/base.py | 66 | ||||
-rw-r--r-- | lib/sqlalchemy/engine/default.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/engine/result.py | 6 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/instrumentation.py | 7 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/mapper.py | 3 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/session.py | 23 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/state.py | 27 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 9 | ||||
-rw-r--r-- | lib/sqlalchemy/util/__init__.py | 5 | ||||
-rw-r--r-- | lib/sqlalchemy/util/compat.py | 25 | ||||
-rw-r--r-- | lib/sqlalchemy/util/langhelpers.py | 32 | ||||
-rw-r--r-- | test/dialect/test_mssql.py | 2 | ||||
-rw-r--r-- | test/engine/test_reconnect.py | 24 | ||||
-rw-r--r-- | test/orm/test_instrumentation.py | 14 | ||||
-rw-r--r-- | test/orm/test_session.py | 144 | ||||
-rw-r--r-- | test/orm/test_subquery_relations.py | 53 |
21 files changed, 466 insertions, 136 deletions
diff --git a/doc/build/changelog/changelog_08.rst b/doc/build/changelog/changelog_08.rst index 60d61cb8d..2a2925726 100644 --- a/doc/build/changelog/changelog_08.rst +++ b/doc/build/changelog/changelog_08.rst @@ -8,6 +8,96 @@ .. change:: :tags: bug, orm + :tickets: 2708 + + Improved the behavior of instance management regarding + the creation of strong references within the Session; + an object will no longer have an internal reference cycle + created if it's in the transient state or moves into the + detached state - the strong ref is created only when the + object is attached to a Session and is removed when the + object is detached. This makes it somewhat safer for an + object to have a `__del__()` method, even though this is + not recommended, as relationships with backrefs produce + cycles too. A warning has been added when a class with + a `__del__()` method is mapped. + + .. change:: + :tags: bug, sql + :tickets: 2702 + + A major fix to the way in which a select() object produces + labeled columns when apply_labels() is used; this mode + produces a SELECT where each column is labeled as in + <tablename>_<columnname>, to remove column name collisions + for a multiple table select. The fix is that if two labels + collide when combined with the table name, i.e. + "foo.bar_id" and "foo_bar.id", anonymous aliasing will be + applied to one of the dupes. This allows the ORM to handle + both columns independently; previously, 0.7 + would in some cases silently emit a second SELECT for the + column that was "duped", and in 0.8 an ambiguous column error + would be emitted. The "keys" applied to the .c. collection + of the select() will also be deduped, so that the "column + being replaced" warning will no longer emit for any select() + that specifies use_labels, though the dupe key will be given + an anonymous label which isn't generally user-friendly. + + .. change:: + :tags: bug, mysql + :pullreq: 54 + + Updated a regexp to correctly extract error code on + google app engine v1.7.5 and newer. Courtesy + Dan Ring. + + .. change:: + :tags: bug, examples + + Fixed a long-standing bug in the caching example, where + the limit/offset parameter values wouldn't be taken into + account when computing the cache key. The + _key_from_query() function has been simplified to work + directly from the final compiled statement in order to get + at both the full statement as well as the fully processed + parameter list. + + .. change:: + :tags: bug, mssql + :tickets: 2355 + + Part of a longer series of fixes needed for pyodbc+ + mssql, a CAST to NVARCHAR(max) has been added to the bound + parameter for the table name and schema name in all information schema + queries to avoid the issue of comparing NVARCHAR to NTEXT, + which seems to be rejected by the ODBC driver in some cases, + such as FreeTDS (0.91 only?) plus unicode bound parameters being passed. + The issue seems to be specific to the SQL Server information + schema tables and the workaround is harmless for those cases + where the problem doesn't exist in the first place. + + .. change:: + :tags: bug, sql + :tickets: 2691 + + Fixed bug where disconnect detect on error would + raise an attribute error if the error were being + raised after the Connection object had already + been closed. + + .. change:: + :tags: bug, sql + :tickets: 2703 + + Reworked internal exception raises that emit + a rollback() before re-raising, so that the stack + trace is preserved from sys.exc_info() before entering + the rollback. This so that the traceback is preserved + when using coroutine frameworks which may have switched + contexts before the rollback function returns. + + .. change:: + :tags: bug, orm :tickets: 2697 Fixed bug whereby ORM would run the wrong kind of @@ -49,26 +139,6 @@ handling routine fails and regardless of whether the condition is a disconnect or not. - .. change:: - :tags: bug, sql - :tickets: 2702 - - A major fix to the way in which a select() object produces - labeled columns when apply_labels() is used; this mode - produces a SELECT where each column is labeled as in - <tablename>_<columnname>, to remove column name collisions - for a multiple table select. The fix is that if two labels - collide when combined with the table name, i.e. - "foo.bar_id" and "foo_bar.id", anonymous aliasing will be - applied to one of the dupes. This allows the ORM to handle - both columns independently; previously, 0.7 - would in some cases silently emit a second SELECT for the - column that was "duped", and in 0.8 an ambiguous column error - would be emitted. The "keys" applied to the .c. collection - of the select() will also be deduped, so that the "column - being replaced" warning will no longer emit for any select() - that specifies use_labels, though the dupe key will be given - an anonymous label which isn't generally user-friendly. .. change:: :tags: bug, orm, declarative diff --git a/doc/build/dialects/mysql.rst b/doc/build/dialects/mysql.rst index b5119f23f..1e2784554 100644 --- a/doc/build/dialects/mysql.rst +++ b/doc/build/dialects/mysql.rst @@ -175,6 +175,11 @@ MySQL-Connector .. automodule:: sqlalchemy.dialects.mysql.mysqlconnector +cymysql +------------ + +.. automodule:: sqlalchemy.dialects.mysql.cymysql + Google App Engine ----------------------- diff --git a/examples/dogpile_caching/caching_query.py b/examples/dogpile_caching/caching_query.py index 9a705cf31..f4724fb0b 100644 --- a/examples/dogpile_caching/caching_query.py +++ b/examples/dogpile_caching/caching_query.py @@ -136,24 +136,15 @@ def _key_from_query(query, qualifier=None): """ - v = [] - def visit_bindparam(bind): - - if bind.key in query._params: - value = query._params[bind.key] - elif bind.callable: - value = bind.callable() - else: - value = bind.value - - v.append(unicode(value)) - stmt = query.statement - visitors.traverse(stmt, {}, {'bindparam': visit_bindparam}) + compiled = stmt.compile() + params = compiled.params # here we return the key as a long string. our "key mangler" # set up with the region will boil it down to an md5. - return " ".join([unicode(stmt)] + v) + return " ".join( + [unicode(compiled)] + + [unicode(params[k]) for k in sorted(params)]) class FromCache(MapperOption): """Specifies that a Query should load results from a cache.""" @@ -187,24 +178,6 @@ class RelationshipCache(MapperOption): propagate_to_loaders = True - def __init__(self, attribute, region="default"): - self.region = region - self.cls_ = attribute.property.parent.class_ - self.key = attribute.property.key - - def process_query_conditionally(self, query): - if query._current_path: - mapper, key = query._current_path[-2:] - if issubclass(mapper.class_, self.cls_) and \ - key == self.key: - query._cache_region = self - -class RelationshipCache(MapperOption): - """Specifies that a Query as called within a "lazy load" - should load results from a cache.""" - - propagate_to_loaders = True - def __init__(self, attribute, region="default", cache_key=None): """Construct a new RelationshipCache. diff --git a/lib/sqlalchemy/dialects/mssql/information_schema.py b/lib/sqlalchemy/dialects/mssql/information_schema.py index 35ce2450e..80e59d323 100644 --- a/lib/sqlalchemy/dialects/mssql/information_schema.py +++ b/lib/sqlalchemy/dialects/mssql/information_schema.py @@ -8,6 +8,7 @@ from ... import Table, MetaData, Column from ...types import String, Unicode, Integer, TypeDecorator +from ... import cast ischema = MetaData() @@ -22,6 +23,9 @@ class CoerceUnicode(TypeDecorator): # end Py2K return value + def bind_expression(self, bindvalue): + return cast(bindvalue, Unicode) + schemata = Table("SCHEMATA", ischema, Column("CATALOG_NAME", CoerceUnicode, key="catalog_name"), Column("SCHEMA_NAME", CoerceUnicode, key="schema_name"), diff --git a/lib/sqlalchemy/dialects/mysql/gaerdbms.py b/lib/sqlalchemy/dialects/mysql/gaerdbms.py index a93a78b73..ad0ce7638 100644 --- a/lib/sqlalchemy/dialects/mysql/gaerdbms.py +++ b/lib/sqlalchemy/dialects/mysql/gaerdbms.py @@ -65,10 +65,10 @@ class MySQLDialect_gaerdbms(MySQLDialect_mysqldb): return [], opts def _extract_error_code(self, exception): - match = re.compile(r"^(\d+):").match(str(exception)) + match = re.compile(r"^(\d+):|^\((\d+),").match(str(exception)) # The rdbms api will wrap then re-raise some types of errors # making this regex return no matches. - code = match.group(1) if match else None + code = match.group(1) or match.group(2) if match else None if code: return int(code) diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index e40af6219..b4c9b1e1c 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -462,7 +462,6 @@ class Connection(Connectable): self.engine.dialect.do_begin(self.connection) except Exception, e: self._handle_dbapi_exception(e, None, None, None, None) - raise def _rollback_impl(self): if self._has_events: @@ -476,7 +475,6 @@ class Connection(Connectable): self.__transaction = None except Exception, e: self._handle_dbapi_exception(e, None, None, None, None) - raise else: self.__transaction = None @@ -491,7 +489,6 @@ class Connection(Connectable): self.__transaction = None except Exception, e: self._handle_dbapi_exception(e, None, None, None, None) - raise def _savepoint_impl(self, name=None): if self._has_events: @@ -693,7 +690,6 @@ class Connection(Connectable): dialect, self, conn) except Exception, e: self._handle_dbapi_exception(e, None, None, None, None) - raise ret = ctx._exec_default(default, None) if self.should_close_with_result: @@ -830,7 +826,6 @@ class Connection(Connectable): self._handle_dbapi_exception(e, str(statement), parameters, None, None) - raise if context.compiled: context.pre_exec() @@ -877,7 +872,6 @@ class Connection(Connectable): parameters, cursor, context) - raise if self._has_events: self.dispatch.after_cursor_execute(self, cursor, @@ -952,7 +946,6 @@ class Connection(Connectable): parameters, cursor, None) - raise def _safe_close_cursor(self, cursor): """Close the given cursor, catching exceptions @@ -983,22 +976,21 @@ class Connection(Connectable): cursor, context): + exc_info = sys.exc_info() + if not self._is_disconnect: self._is_disconnect = isinstance(e, self.dialect.dbapi.Error) and \ not self.closed and \ self.dialect.is_disconnect(e, self.__connection, cursor) if self._reentrant_error: - # Py3K - #raise exc.DBAPIError.instance(statement, parameters, e, - # self.dialect.dbapi.Error) from e - # Py2K - raise exc.DBAPIError.instance(statement, + util.raise_from_cause( + exc.DBAPIError.instance(statement, parameters, e, - self.dialect.dbapi.Error), \ - None, sys.exc_info()[2] - # end Py2K + self.dialect.dbapi.Error), + exc_info + ) self._reentrant_error = True try: # non-DBAPI error - if we already got a context, @@ -1021,26 +1013,18 @@ class Connection(Connectable): self._safe_close_cursor(cursor) self._autorollback() - if not should_wrap: - return - - # Py3K - #raise exc.DBAPIError.instance( - # statement, - # parameters, - # e, - # self.dialect.dbapi.Error, - # connection_invalidated=self._is_disconnect) \ - # from e - # Py2K - raise exc.DBAPIError.instance( - statement, - parameters, - e, - self.dialect.dbapi.Error, - connection_invalidated=self._is_disconnect), \ - None, sys.exc_info()[2] - # end Py2K + if should_wrap: + util.raise_from_cause( + exc.DBAPIError.instance( + statement, + parameters, + e, + self.dialect.dbapi.Error, + connection_invalidated=self._is_disconnect), + exc_info + ) + + util.reraise(*exc_info) finally: del self._reentrant_error @@ -1115,8 +1099,8 @@ class Connection(Connectable): trans.commit() return ret except: - trans.rollback() - raise + with util.safe_reraise(): + trans.rollback() def run_callable(self, callable_, *args, **kwargs): """Given a callable object or function, execute it, passing @@ -1222,8 +1206,8 @@ class Transaction(object): try: self.commit() except: - self.rollback() - raise + with util.safe_reraise(): + self.rollback() else: self.rollback() @@ -1548,8 +1532,8 @@ class Engine(Connectable, log.Identified): try: trans = conn.begin() except: - conn.close() - raise + with util.safe_reraise(): + conn.close() return Engine._trans_ctx(conn, trans, close_with_result) def transaction(self, callable_, *args, **kwargs): diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index abb9f0fc3..daa9fe085 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -737,7 +737,6 @@ class DefaultExecutionContext(interfaces.ExecutionContext): except Exception, e: self.root_connection._handle_dbapi_exception( e, None, None, None, self) - raise else: inputsizes = {} for key in self.compiled.bind_names.values(): @@ -756,7 +755,6 @@ class DefaultExecutionContext(interfaces.ExecutionContext): except Exception, e: self.root_connection._handle_dbapi_exception( e, None, None, None, self) - raise def _exec_default(self, default, type_): if default.is_sequence: diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 1c148e1f0..88930081e 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -443,7 +443,6 @@ class ResultProxy(object): except Exception, e: self.connection._handle_dbapi_exception( e, None, None, self.cursor, self.context) - raise @property def lastrowid(self): @@ -467,7 +466,6 @@ class ResultProxy(object): self.connection._handle_dbapi_exception( e, None, None, self._saved_cursor, self.context) - raise @property def returns_rows(self): @@ -752,7 +750,6 @@ class ResultProxy(object): self.connection._handle_dbapi_exception( e, None, None, self.cursor, self.context) - raise def fetchmany(self, size=None): """Fetch many rows, just like DB-API @@ -772,7 +769,6 @@ class ResultProxy(object): self.connection._handle_dbapi_exception( e, None, None, self.cursor, self.context) - raise def fetchone(self): """Fetch one row, just like DB-API ``cursor.fetchone()``. @@ -792,7 +788,6 @@ class ResultProxy(object): self.connection._handle_dbapi_exception( e, None, None, self.cursor, self.context) - raise def first(self): """Fetch the first row and then close the result set unconditionally. @@ -809,7 +804,6 @@ class ResultProxy(object): self.connection._handle_dbapi_exception( e, None, None, self.cursor, self.context) - raise try: if row is not None: diff --git a/lib/sqlalchemy/orm/instrumentation.py b/lib/sqlalchemy/orm/instrumentation.py index 51cf9edeb..0e71494c4 100644 --- a/lib/sqlalchemy/orm/instrumentation.py +++ b/lib/sqlalchemy/orm/instrumentation.py @@ -72,6 +72,13 @@ class ClassManager(dict): self.manage() self._instrument_init() + if '__del__' in class_.__dict__: + util.warn("__del__() method on class %s will " + "cause unreachable cycles and memory leaks, " + "as SQLAlchemy instrumentation often creates " + "reference cycles. Please remove this method." % + class_) + dispatch = event.dispatcher(events.InstanceEvents) @property diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 914c29b7f..c08d91b57 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -761,8 +761,9 @@ class Mapper(_InspectionAttr): del self._configure_failed if not self.non_primary and \ + self.class_manager is not None and \ self.class_manager.is_mapped and \ - self.class_manager.mapper is self: + self.class_manager.mapper is self: instrumentation.unregister_class(self.class_) def _configure_pks(self): diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 71e617e36..f7a5558f1 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -3,9 +3,10 @@ # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php - """Provides the Session class and related utilities.""" +from __future__ import with_statement + import weakref from .. import util, sql, engine, exc as sa_exc, event from ..sql import util as sql_util, expression @@ -341,8 +342,8 @@ class SessionTransaction(object): for t in set(self._connections.values()): t[1].prepare() except: - self.rollback() - raise + with util.safe_reraise(): + self.rollback() self._state = PREPARED @@ -441,8 +442,8 @@ class SessionTransaction(object): try: self.commit() except: - self.rollback() - raise + with util.safe_reraise(): + self.rollback() else: self.rollback() @@ -1726,13 +1727,13 @@ class Session(_SessionClassMethods): def _before_attach(self, state): if state.session_id != self.hash_key and \ - self.dispatch.before_attach: + self.dispatch.before_attach: self.dispatch.before_attach(self, state.obj()) def _attach(self, state, include_before=False): if state.key and \ state.key in self.identity_map and \ - not self.identity_map.contains_state(state): + not self.identity_map.contains_state(state): raise sa_exc.InvalidRequestError("Can't attach instance " "%s; another instance with key %s is already " "present in this session." @@ -1748,9 +1749,11 @@ class Session(_SessionClassMethods): if state.session_id != self.hash_key: if include_before and \ - self.dispatch.before_attach: + self.dispatch.before_attach: self.dispatch.before_attach(self, state.obj()) state.session_id = self.hash_key + if state.modified and state._strong_obj is None: + state._strong_obj = state.obj() if self.dispatch.after_attach: self.dispatch.after_attach(self, state.obj()) @@ -1928,8 +1931,8 @@ class Session(_SessionClassMethods): transaction.commit() except: - transaction.rollback(_capture_exception=True) - raise + with util.safe_reraise(): + transaction.rollback(_capture_exception=True) def is_modified(self, instance, include_collections=True, passive=True): diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py index 4bc689e94..193678c2f 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -164,7 +164,7 @@ class InstanceState(interfaces._InspectionAttr): return bool(self.key) def _detach(self): - self.session_id = None + self.session_id = self._strong_obj = None def _dispose(self): self._detach() @@ -176,7 +176,7 @@ class InstanceState(interfaces._InspectionAttr): instance_dict.discard(self) self.callables = {} - self.session_id = None + self.session_id = self._strong_obj = None del self.obj def obj(self): @@ -259,9 +259,6 @@ class InstanceState(interfaces._InspectionAttr): self.expired = state.get('expired', False) self.callables = state.get('callables', {}) - if self.modified: - self._strong_obj = inst - self.__dict__.update([ (k, state[k]) for k in ( 'key', 'load_options', @@ -322,6 +319,7 @@ class InstanceState(interfaces._InspectionAttr): modified_set.discard(self) self.modified = False + self._strong_obj = None self.committed_state.clear() @@ -335,7 +333,7 @@ class InstanceState(interfaces._InspectionAttr): for key in self.manager: impl = self.manager[key].impl if impl.accepts_scalar_loader and \ - (impl.expire_missing or key in dict_): + (impl.expire_missing or key in dict_): self.callables[key] = self old = dict_.pop(key, None) if impl.collection and old is not None: @@ -435,18 +433,22 @@ class InstanceState(interfaces._InspectionAttr): self.committed_state[attr.key] = previous - # the "or not self.modified" is defensive at - # this point. The assertion below is expected - # to be True: # assert self._strong_obj is None or self.modified - if self._strong_obj is None or not self.modified: + if (self.session_id and self._strong_obj is None) \ + or not self.modified: instance_dict = self._instance_dict() if instance_dict: instance_dict._modified.add(self) - self._strong_obj = self.obj() - if self._strong_obj is None: + # only create _strong_obj link if attached + # to a session + + inst = self.obj() + if self.session_id: + self._strong_obj = inst + + if inst is None: raise orm_exc.ObjectDereferencedError( "Can't emit change event for attribute '%s' - " "parent object of type %s has been garbage " @@ -467,7 +469,6 @@ class InstanceState(interfaces._InspectionAttr): this step if a value was not populated in state.dict. """ - class_manager = self.manager for key in keys: self.committed_state.pop(key, None) diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index e08bb40cb..0eed50ea4 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -778,11 +778,12 @@ class SubqueryLoader(AbstractRelationshipLoader): # to look only for significant columns q = orig_query._clone().correlate(None) - # TODO: why does polymporphic etc. require hardcoding - # into _adapt_col_list ? Does query.add_columns(...) work - # with polymorphic loading ? - if entity_mapper.isa(leftmost_mapper): + # set a real "from" if not present, as this is more + # accurate than just going off of the column expression + if not q._from_obj and entity_mapper.isa(leftmost_mapper): q._set_select_from(entity_mapper) + + # select from the identity columns of the outer q._set_entities(q._adapt_col_list(leftmost_attr)) if q._order_by is False: diff --git a/lib/sqlalchemy/util/__init__.py b/lib/sqlalchemy/util/__init__.py index 57bbdca85..3fa06c793 100644 --- a/lib/sqlalchemy/util/__init__.py +++ b/lib/sqlalchemy/util/__init__.py @@ -6,7 +6,8 @@ from .compat import callable, cmp, reduce, \ threading, py3k, py3k_warning, jython, pypy, cpython, win32, set_types, \ - pickle, dottedgetter, parse_qsl, namedtuple, next, WeakSet + pickle, dottedgetter, parse_qsl, namedtuple, next, WeakSet, reraise, \ + raise_from_cause from ._collections import KeyedTuple, ImmutableContainer, immutabledict, \ Properties, OrderedProperties, ImmutableProperties, OrderedDict, \ @@ -26,7 +27,7 @@ from .langhelpers import iterate_attributes, class_hierarchy, \ duck_type_collection, assert_arg_type, symbol, dictlike_iteritems,\ classproperty, set_creation_order, warn_exception, warn, NoneType,\ constructor_copy, methods_equivalent, chop_traceback, asint,\ - generic_repr, counter, PluginLoader, hybridmethod + generic_repr, counter, PluginLoader, hybridmethod, safe_reraise from .deprecations import warn_deprecated, warn_pending_deprecation, \ deprecated, pending_deprecation diff --git a/lib/sqlalchemy/util/compat.py b/lib/sqlalchemy/util/compat.py index 2a0f06f8e..033a87cc7 100644 --- a/lib/sqlalchemy/util/compat.py +++ b/lib/sqlalchemy/util/compat.py @@ -140,3 +140,28 @@ else: def b(s): return s + +if py3k: + def reraise(tp, value, tb=None, cause=None): + if cause is not None: + value.__cause__ = cause + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + + def raise_from_cause(exception, exc_info): + exc_type, exc_value, exc_tb = exc_info + reraise(type(exception), exception, tb=exc_tb, cause=exc_value) +else: + exec("def reraise(tp, value, tb=None, cause=None):\n" + " raise tp, value, tb\n") + + def raise_from_cause(exception, exc_info): + # not as nice as that of Py3K, but at least preserves + # the code line where the issue occurred + exc_type, exc_value, exc_tb = exc_info + reraise(type(exception), exception, tb=exc_tb) + + + + diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index e3aed24d8..f6d9164e6 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -20,6 +20,7 @@ from .compat import set_types, threading, \ from functools import update_wrapper from .. import exc import hashlib +from . import compat def md5_hex(x): # Py3K @@ -28,6 +29,37 @@ def md5_hex(x): m.update(x) return m.hexdigest() +class safe_reraise(object): + """Reraise an exception after invoking some + handler code. + + Stores the existing exception info before + invoking so that it is maintained across a potential + coroutine context switch. + + e.g.:: + + try: + sess.commit() + except: + with safe_reraise(): + sess.rollback() + + """ + + def __enter__(self): + self._exc_info = sys.exc_info() + + def __exit__(self, type_, value, traceback): + # see #2703 for notes + if type_ is None: + exc_type, exc_value, exc_tb = self._exc_info + self._exc_info = None # remove potential circular references + compat.reraise(exc_type, exc_value, exc_tb) + else: + self._exc_info = None # remove potential circular references + compat.reraise(type_, value, traceback) + def decode_slice(slc): """decode a slice object as sent to __getitem__. diff --git a/test/dialect/test_mssql.py b/test/dialect/test_mssql.py index 0dfda9015..f1cd3fe85 100644 --- a/test/dialect/test_mssql.py +++ b/test/dialect/test_mssql.py @@ -1949,7 +1949,7 @@ class TypeRoundTripTest(fixtures.TestBase, AssertsExecutionResults, ComparesTabl engine.execute(tbl.delete()) class MonkeyPatchedBinaryTest(fixtures.TestBase): - __only_on__ = 'mssql' + __only_on__ = 'mssql+pymssql' def test_unicode(self): module = __import__('pymssql') diff --git a/test/engine/test_reconnect.py b/test/engine/test_reconnect.py index 86f646f33..9aecb81a9 100644 --- a/test/engine/test_reconnect.py +++ b/test/engine/test_reconnect.py @@ -51,6 +51,7 @@ class MockCursor(object): def __init__(self, parent): self.explode = parent.explode self.description = () + self.closed = False def execute(self, *args, **kwargs): if self.explode == 'execute': raise MockDisconnect("Lost the DB connection on execute") @@ -60,10 +61,20 @@ class MockCursor(object): elif self.explode in ('rollback', 'rollback_no_disconnect'): raise MockError( "something broke on execute but we didn't lose the connection") + elif args and "select" in args[0]: + self.description = [('foo', None, None, None, None, None)] else: return + def fetchall(self): + if self.closed: + raise MockError("cursor closed") + return [] + def fetchone(self): + if self.closed: + raise MockError("cursor closed") + return None def close(self): - pass + self.closed = True db, dbapi = None, None class MockReconnectTest(fixtures.TestBase): @@ -294,6 +305,17 @@ class MockReconnectTest(fixtures.TestBase): conn.execute, select([1]) ) + def test_check_disconnect_no_cursor(self): + conn = db.connect() + result = conn.execute("select 1") + result.cursor.close() + conn.close() + assert_raises_message( + tsa.exc.DBAPIError, + "cursor closed", + list, result + ) + class CursorErrTest(fixtures.TestBase): def setup(self): diff --git a/test/orm/test_instrumentation.py b/test/orm/test_instrumentation.py index 3b548f0cd..3f8fc67b6 100644 --- a/test/orm/test_instrumentation.py +++ b/test/orm/test_instrumentation.py @@ -445,6 +445,20 @@ class MapperInitTest(fixtures.ORMTest): # C is not mapped in the current implementation assert_raises(sa.orm.exc.UnmappedClassError, class_mapper, C) + def test_del_warning(self): + class A(object): + def __del__(self): + pass + + assert_raises_message( + sa.exc.SAWarning, + r"__del__\(\) method on class " + "<class 'test.orm.test_instrumentation.A'> will cause " + "unreachable cycles and memory leaks, as SQLAlchemy " + "instrumentation often creates reference cycles. " + "Please remove this method.", + mapper, A, self.fixture() + ) class OnLoadTest(fixtures.ORMTest): """Check that Events.load is not hit in regular attributes operations.""" diff --git a/test/orm/test_session.py b/test/orm/test_session.py index 5c8968842..7c2e8a3b8 100644 --- a/test/orm/test_session.py +++ b/test/orm/test_session.py @@ -857,6 +857,150 @@ class SessionStateWFixtureTest(_fixtures.FixtureTest): assert sa.orm.attributes.instance_state(a).session_id is None +class NoCyclesOnTransientDetachedTest(_fixtures.FixtureTest): + """Test the instance_state._strong_obj link that it + is present only on persistent/pending objects and never + transient/detached. + + """ + run_inserts = None + + def setup(self): + mapper(self.classes.User, self.tables.users) + + def _assert_modified(self, u1): + assert sa.orm.attributes.instance_state(u1).modified + + def _assert_not_modified(self, u1): + assert not sa.orm.attributes.instance_state(u1).modified + + def _assert_cycle(self, u1): + assert sa.orm.attributes.instance_state(u1)._strong_obj is not None + + def _assert_no_cycle(self, u1): + assert sa.orm.attributes.instance_state(u1)._strong_obj is None + + def _persistent_fixture(self): + User = self.classes.User + u1 = User() + u1.name = "ed" + sess = Session() + sess.add(u1) + sess.flush() + return sess, u1 + + def test_transient(self): + User = self.classes.User + u1 = User() + u1.name = 'ed' + self._assert_no_cycle(u1) + self._assert_modified(u1) + + def test_transient_to_pending(self): + User = self.classes.User + u1 = User() + u1.name = 'ed' + self._assert_modified(u1) + self._assert_no_cycle(u1) + sess = Session() + sess.add(u1) + self._assert_cycle(u1) + sess.flush() + self._assert_no_cycle(u1) + self._assert_not_modified(u1) + + def test_dirty_persistent_to_detached_via_expunge(self): + sess, u1 = self._persistent_fixture() + u1.name = 'edchanged' + self._assert_cycle(u1) + sess.expunge(u1) + self._assert_no_cycle(u1) + + def test_dirty_persistent_to_detached_via_close(self): + sess, u1 = self._persistent_fixture() + u1.name = 'edchanged' + self._assert_cycle(u1) + sess.close() + self._assert_no_cycle(u1) + + def test_clean_persistent_to_detached_via_close(self): + sess, u1 = self._persistent_fixture() + self._assert_no_cycle(u1) + self._assert_not_modified(u1) + sess.close() + u1.name = 'edchanged' + self._assert_modified(u1) + self._assert_no_cycle(u1) + + def test_detached_to_dirty_deleted(self): + sess, u1 = self._persistent_fixture() + sess.expunge(u1) + u1.name = 'edchanged' + self._assert_no_cycle(u1) + sess.delete(u1) + self._assert_cycle(u1) + + def test_detached_to_dirty_persistent(self): + sess, u1 = self._persistent_fixture() + sess.expunge(u1) + u1.name = 'edchanged' + self._assert_modified(u1) + self._assert_no_cycle(u1) + sess.add(u1) + self._assert_cycle(u1) + self._assert_modified(u1) + + def test_detached_to_clean_persistent(self): + sess, u1 = self._persistent_fixture() + sess.expunge(u1) + self._assert_no_cycle(u1) + self._assert_not_modified(u1) + sess.add(u1) + self._assert_no_cycle(u1) + self._assert_not_modified(u1) + + def test_move_persistent_clean(self): + sess, u1 = self._persistent_fixture() + sess.close() + s2 = Session() + s2.add(u1) + self._assert_no_cycle(u1) + self._assert_not_modified(u1) + + def test_move_persistent_dirty(self): + sess, u1 = self._persistent_fixture() + u1.name = 'edchanged' + self._assert_cycle(u1) + self._assert_modified(u1) + sess.close() + self._assert_no_cycle(u1) + s2 = Session() + s2.add(u1) + self._assert_cycle(u1) + self._assert_modified(u1) + + @testing.requires.predictable_gc + def test_move_gc_session_persistent_dirty(self): + sess, u1 = self._persistent_fixture() + u1.name = 'edchanged' + self._assert_cycle(u1) + self._assert_modified(u1) + del sess + gc_collect() + self._assert_cycle(u1) + s2 = Session() + s2.add(u1) + self._assert_cycle(u1) + self._assert_modified(u1) + + def test_persistent_dirty_to_expired(self): + sess, u1 = self._persistent_fixture() + u1.name = 'edchanged' + self._assert_cycle(u1) + self._assert_modified(u1) + sess.expire(u1) + self._assert_no_cycle(u1) + self._assert_not_modified(u1) class WeakIdentityMapTest(_fixtures.FixtureTest): run_inserts = None diff --git a/test/orm/test_subquery_relations.py b/test/orm/test_subquery_relations.py index 80dd73e98..3ee94cae9 100644 --- a/test/orm/test_subquery_relations.py +++ b/test/orm/test_subquery_relations.py @@ -1036,7 +1036,7 @@ class BaseRelationFromJoinedSubclassTest(_Polymorphic): sess.add_all([e1, e2]) sess.flush() - def test_correct_subquery(self): + def test_correct_subquery_nofrom(self): sess = create_session() # use Person.paperwork here just to give the least # amount of context @@ -1083,6 +1083,57 @@ class BaseRelationFromJoinedSubclassTest(_Polymorphic): ) ) + def test_correct_subquery_existingfrom(self): + sess = create_session() + # use Person.paperwork here just to give the least + # amount of context + q = sess.query(Engineer).\ + filter(Engineer.primary_language == 'java').\ + join(Engineer.paperwork).\ + filter(Paperwork.description == "tps report #2").\ + options(subqueryload(Person.paperwork)) + def go(): + eq_(q.one().paperwork, + [Paperwork(description="tps report #1"), + Paperwork(description="tps report #2")], + + ) + self.assert_sql_execution( + testing.db, + go, + CompiledSQL( + "SELECT people.person_id AS people_person_id, " + "people.name AS people_name, people.type AS people_type, " + "engineers.engineer_id AS engineers_engineer_id, " + "engineers.primary_language AS engineers_primary_language " + "FROM people JOIN engineers " + "ON people.person_id = engineers.engineer_id " + "JOIN paperwork ON people.person_id = paperwork.person_id " + "WHERE engineers.primary_language = :primary_language_1 " + "AND paperwork.description = :description_1", + {"primary_language_1": "java", + "description_1": "tps report #2"} + ), + CompiledSQL( + "SELECT paperwork.paperwork_id AS paperwork_paperwork_id, " + "paperwork.description AS paperwork_description, " + "paperwork.person_id AS paperwork_person_id, " + "anon_1.people_person_id AS anon_1_people_person_id " + "FROM (SELECT people.person_id AS people_person_id " + "FROM people JOIN engineers ON people.person_id = " + "engineers.engineer_id JOIN paperwork " + "ON people.person_id = paperwork.person_id " + "WHERE engineers.primary_language = :primary_language_1 AND " + "paperwork.description = :description_1) AS anon_1 " + "JOIN paperwork ON anon_1.people_person_id = " + "paperwork.person_id " + "ORDER BY anon_1.people_person_id, paperwork.paperwork_id", + {"primary_language_1": "java", + "description_1": "tps report #2"} + ) + ) + + class SelfReferentialTest(fixtures.MappedTest): |