summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/build/changelog/changelog_08.rst110
-rw-r--r--doc/build/dialects/mysql.rst5
-rw-r--r--examples/dogpile_caching/caching_query.py37
-rw-r--r--lib/sqlalchemy/dialects/mssql/information_schema.py4
-rw-r--r--lib/sqlalchemy/dialects/mysql/gaerdbms.py4
-rw-r--r--lib/sqlalchemy/engine/base.py66
-rw-r--r--lib/sqlalchemy/engine/default.py2
-rw-r--r--lib/sqlalchemy/engine/result.py6
-rw-r--r--lib/sqlalchemy/orm/instrumentation.py7
-rw-r--r--lib/sqlalchemy/orm/mapper.py3
-rw-r--r--lib/sqlalchemy/orm/session.py23
-rw-r--r--lib/sqlalchemy/orm/state.py27
-rw-r--r--lib/sqlalchemy/orm/strategies.py9
-rw-r--r--lib/sqlalchemy/util/__init__.py5
-rw-r--r--lib/sqlalchemy/util/compat.py25
-rw-r--r--lib/sqlalchemy/util/langhelpers.py32
-rw-r--r--test/dialect/test_mssql.py2
-rw-r--r--test/engine/test_reconnect.py24
-rw-r--r--test/orm/test_instrumentation.py14
-rw-r--r--test/orm/test_session.py144
-rw-r--r--test/orm/test_subquery_relations.py53
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):