mirror of
https://github.com/sqlalchemy/sqlalchemy.git
synced 2026-06-01 05:18:44 -04:00
merge tip
This commit is contained in:
@@ -15,6 +15,17 @@ CHANGES
|
||||
ConcurrentModificationError in an "except:"
|
||||
clause.
|
||||
|
||||
- An object that's been deleted now gets a flag
|
||||
'deleted', which prohibits the object from
|
||||
being re-add()ed to the session, as previously
|
||||
the object would live in the identity map
|
||||
silently until its attributes were accessed.
|
||||
The make_transient() function now resets this
|
||||
flag along with the "key" flag.
|
||||
|
||||
- make_transient() can be safely called on an
|
||||
already transient instance.
|
||||
|
||||
- a warning is emitted in mapper() if the polymorphic_on
|
||||
column is not present either in direct or derived
|
||||
form in the mapped selectable or in the
|
||||
@@ -135,21 +146,6 @@ CHANGES
|
||||
of 64 for index names, separate from their
|
||||
overall max length of 255. [ticket:1412]
|
||||
|
||||
- Calling fetchone() or similar on a result that
|
||||
has already been exhausted, has been closed,
|
||||
or is not a result-returning result now
|
||||
raises ResourceClosedError, a subclass of
|
||||
InvalidRequestError, in all cases, regardless
|
||||
of backend. Previously, some DBAPIs would
|
||||
raise ProgrammingError (i.e. pysqlite), others
|
||||
would return None leading to downstream breakages
|
||||
(i.e. MySQL-python).
|
||||
|
||||
- Connection, ResultProxy, as well as Session use
|
||||
ResourceClosedError for all "this
|
||||
connection/transaction/result is closed" types of
|
||||
errors.
|
||||
|
||||
- the text() construct, if placed in a column
|
||||
oriented situation, will at least return NULLTYPE
|
||||
for its type instead of None, allowing it to
|
||||
@@ -175,7 +171,33 @@ CHANGES
|
||||
operations which depend on the identity of the
|
||||
_Labels themselves to return the correct result
|
||||
- fixes ORM bug [ticket:1852].
|
||||
|
||||
|
||||
- engine
|
||||
|
||||
- Calling fetchone() or similar on a result that
|
||||
has already been exhausted, has been closed,
|
||||
or is not a result-returning result now
|
||||
raises ResourceClosedError, a subclass of
|
||||
InvalidRequestError, in all cases, regardless
|
||||
of backend. Previously, some DBAPIs would
|
||||
raise ProgrammingError (i.e. pysqlite), others
|
||||
would return None leading to downstream breakages
|
||||
(i.e. MySQL-python).
|
||||
|
||||
- Fixed bug in Connection whereby if a "disconnect"
|
||||
event occurred in the "initialize" phase of the
|
||||
first connection pool connect, an AttributeError
|
||||
would be raised when the Connection would attempt
|
||||
to invalidate the DBAPI connection. [ticket:1894]
|
||||
|
||||
- Connection, ResultProxy, as well as Session use
|
||||
ResourceClosedError for all "this
|
||||
connection/transaction/result is closed" types of
|
||||
errors.
|
||||
|
||||
- Connection.invalidate() can be called more than
|
||||
once and subsequent calls do nothing.
|
||||
|
||||
- declarative
|
||||
- if @classproperty is used with a regular class-bound
|
||||
mapper property attribute, it will be called to get the
|
||||
|
||||
@@ -62,6 +62,14 @@ class CachingQuery(Query):
|
||||
"""override __iter__ to pull results from Beaker
|
||||
if particular attributes have been configured.
|
||||
|
||||
Note that this approach does *not* detach the loaded objects from
|
||||
the current session. If the cache backend is an in-process cache
|
||||
(like "memory") and lives beyond the scope of the current session's
|
||||
transaction, those objects may be expired. The method here can be
|
||||
modified to first expunge() each loaded item from the current
|
||||
session before returning the list of items, so that the items
|
||||
in the cache are not the same ones in the current Session.
|
||||
|
||||
"""
|
||||
if hasattr(self, '_cache_parameters'):
|
||||
return self.get_value(createfunc=lambda: list(Query.__iter__(self)))
|
||||
|
||||
@@ -910,6 +910,14 @@ class Connection(Connectable):
|
||||
return self.__connection
|
||||
raise exc.ResourceClosedError("This Connection is closed")
|
||||
|
||||
@property
|
||||
def _connection_is_valid(self):
|
||||
# use getattr() for is_valid to support exceptions raised in
|
||||
# dialect initializer, where the connection is not wrapped in
|
||||
# _ConnectionFairy
|
||||
|
||||
return getattr(self.__connection, 'is_valid', False)
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
"""A collection of per-DB-API connection instance properties."""
|
||||
@@ -954,15 +962,18 @@ class Connection(Connectable):
|
||||
operations in a non-transactional state.
|
||||
|
||||
"""
|
||||
|
||||
if self.invalidated:
|
||||
return
|
||||
|
||||
if self.closed:
|
||||
raise exc.ResourceClosedError("This Connection is closed")
|
||||
|
||||
if self.__connection.is_valid:
|
||||
if self._connection_is_valid:
|
||||
self.__connection.invalidate(exception)
|
||||
del self.__connection
|
||||
self.__invalid = True
|
||||
|
||||
|
||||
|
||||
def detach(self):
|
||||
"""Detach the underlying DB-API connection from its connection pool.
|
||||
|
||||
@@ -1055,11 +1066,8 @@ class Connection(Connectable):
|
||||
raise
|
||||
|
||||
def _rollback_impl(self):
|
||||
# use getattr() for is_valid to support exceptions raised in
|
||||
# dialect initializer,
|
||||
# where we do not yet have the pool wrappers plugged in
|
||||
if not self.closed and not self.invalidated and \
|
||||
getattr(self.__connection, 'is_valid', False):
|
||||
self._connection_is_valid:
|
||||
if self._echo:
|
||||
self.engine.logger.info("ROLLBACK")
|
||||
try:
|
||||
@@ -1085,37 +1093,37 @@ class Connection(Connectable):
|
||||
if name is None:
|
||||
self.__savepoint_seq += 1
|
||||
name = 'sa_savepoint_%s' % self.__savepoint_seq
|
||||
if self.__connection.is_valid:
|
||||
if self._connection_is_valid:
|
||||
self.engine.dialect.do_savepoint(self, name)
|
||||
return name
|
||||
|
||||
def _rollback_to_savepoint_impl(self, name, context):
|
||||
if self.__connection.is_valid:
|
||||
if self._connection_is_valid:
|
||||
self.engine.dialect.do_rollback_to_savepoint(self, name)
|
||||
self.__transaction = context
|
||||
|
||||
def _release_savepoint_impl(self, name, context):
|
||||
if self.__connection.is_valid:
|
||||
if self._connection_is_valid:
|
||||
self.engine.dialect.do_release_savepoint(self, name)
|
||||
self.__transaction = context
|
||||
|
||||
def _begin_twophase_impl(self, xid):
|
||||
if self.__connection.is_valid:
|
||||
if self._connection_is_valid:
|
||||
self.engine.dialect.do_begin_twophase(self, xid)
|
||||
|
||||
def _prepare_twophase_impl(self, xid):
|
||||
if self.__connection.is_valid:
|
||||
if self._connection_is_valid:
|
||||
assert isinstance(self.__transaction, TwoPhaseTransaction)
|
||||
self.engine.dialect.do_prepare_twophase(self, xid)
|
||||
|
||||
def _rollback_twophase_impl(self, xid, is_prepared):
|
||||
if self.__connection.is_valid:
|
||||
if self._connection_is_valid:
|
||||
assert isinstance(self.__transaction, TwoPhaseTransaction)
|
||||
self.engine.dialect.do_rollback_twophase(self, xid, is_prepared)
|
||||
self.__transaction = None
|
||||
|
||||
def _commit_twophase_impl(self, xid, is_prepared):
|
||||
if self.__connection.is_valid:
|
||||
if self._connection_is_valid:
|
||||
assert isinstance(self.__transaction, TwoPhaseTransaction)
|
||||
self.engine.dialect.do_commit_twophase(self, xid, is_prepared)
|
||||
self.__transaction = None
|
||||
|
||||
@@ -278,8 +278,11 @@ class SessionTransaction(object):
|
||||
|
||||
for s in set(self._new).union(self.session._new):
|
||||
self.session._expunge_state(s)
|
||||
|
||||
|
||||
for s in set(self._deleted).union(self.session._deleted):
|
||||
if s.deleted:
|
||||
# assert s in self._deleted
|
||||
del s.deleted
|
||||
self.session._update_impl(s)
|
||||
|
||||
assert not self.session._deleted
|
||||
@@ -1102,6 +1105,7 @@ class Session(object):
|
||||
|
||||
self.identity_map.discard(state)
|
||||
self._deleted.pop(state, None)
|
||||
state.deleted = True
|
||||
|
||||
def _save_without_cascade(self, instance):
|
||||
"""Used by scoping.py to save on init without cascade."""
|
||||
@@ -1309,7 +1313,13 @@ class Session(object):
|
||||
raise sa_exc.InvalidRequestError(
|
||||
"Instance '%s' is not persisted" %
|
||||
mapperutil.state_str(state))
|
||||
|
||||
|
||||
if state.deleted:
|
||||
raise sa_exc.InvalidRequestError(
|
||||
"Instance '%s' has been deleted. Use the make_transient() "
|
||||
"function to send this object back to the transient state." %
|
||||
mapperutil.state_str(state)
|
||||
)
|
||||
self._attach(state)
|
||||
self._deleted.pop(state, None)
|
||||
self.identity_map.add(state)
|
||||
@@ -1655,7 +1665,9 @@ def make_transient(instance):
|
||||
This will remove its association with any
|
||||
session and additionally will remove its "identity key",
|
||||
such that it's as though the object were newly constructed,
|
||||
except retaining its values.
|
||||
except retaining its values. It also resets the
|
||||
"deleted" flag on the state if this object
|
||||
had been explicitly deleted by its session.
|
||||
|
||||
Attributes which were "expired" or deferred at the
|
||||
instance level are reverted to undefined, and
|
||||
@@ -1670,8 +1682,10 @@ def make_transient(instance):
|
||||
# remove expired state and
|
||||
# deferred callables
|
||||
state.callables.clear()
|
||||
del state.key
|
||||
|
||||
if state.key:
|
||||
del state.key
|
||||
if state.deleted:
|
||||
del state.deleted
|
||||
|
||||
def object_session(instance):
|
||||
"""Return the ``Session`` to which instance belongs.
|
||||
|
||||
@@ -22,6 +22,7 @@ class InstanceState(object):
|
||||
_strong_obj = None
|
||||
modified = False
|
||||
expired = False
|
||||
deleted = False
|
||||
|
||||
def __init__(self, obj, manager):
|
||||
self.class_ = obj.__class__
|
||||
|
||||
@@ -668,7 +668,7 @@ class QueuePoolTest(PoolTestBase):
|
||||
c1 = None
|
||||
c1 = p.connect()
|
||||
assert c1.connection.id != c_id
|
||||
|
||||
|
||||
def test_recreate(self):
|
||||
dbapi = MockDBAPI()
|
||||
p = pool.QueuePool(creator=lambda : dbapi.connect('foo.db'),
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
from sqlalchemy.test.testing import eq_
|
||||
from sqlalchemy.test.testing import eq_, assert_raises
|
||||
import time
|
||||
import weakref
|
||||
from sqlalchemy import select, MetaData, Integer, String, pool
|
||||
from sqlalchemy.test.schema import Table
|
||||
from sqlalchemy.test.schema import Column
|
||||
from sqlalchemy.test.schema import Table, Column
|
||||
import sqlalchemy as tsa
|
||||
from sqlalchemy.test import TestBase, testing, engines
|
||||
from sqlalchemy.test.util import gc_collect
|
||||
|
||||
from sqlalchemy import exc
|
||||
|
||||
class MockDisconnect(Exception):
|
||||
pass
|
||||
@@ -257,7 +256,39 @@ class RealReconnectTest(TestBase):
|
||||
assert not conn.invalidated
|
||||
|
||||
conn.close()
|
||||
|
||||
def test_invalidate_twice(self):
|
||||
conn = engine.connect()
|
||||
conn.invalidate()
|
||||
conn.invalidate()
|
||||
|
||||
def test_explode_in_initializer(self):
|
||||
engine = engines.testing_engine()
|
||||
def broken_initialize(connection):
|
||||
connection.execute("select fake_stuff from _fake_table")
|
||||
|
||||
engine.dialect.initialize = broken_initialize
|
||||
|
||||
# raises a DBAPIError, not an AttributeError
|
||||
assert_raises(exc.DBAPIError, engine.connect)
|
||||
|
||||
# dispose connections so we get a new one on
|
||||
# next go
|
||||
engine.dispose()
|
||||
|
||||
p1 = engine.pool
|
||||
|
||||
def is_disconnect(e):
|
||||
return True
|
||||
|
||||
engine.dialect.is_disconnect = is_disconnect
|
||||
|
||||
# invalidate() also doesn't screw up
|
||||
assert_raises(exc.DBAPIError, engine.connect)
|
||||
|
||||
# pool was recreated
|
||||
assert engine.pool is not p1
|
||||
|
||||
def test_null_pool(self):
|
||||
engine = \
|
||||
engines.reconnecting_engine(options=dict(poolclass=pool.NullPool))
|
||||
|
||||
@@ -1566,14 +1566,11 @@ class DictHelpersTest(_base.MappedTest):
|
||||
def test_column_mapped_assertions(self):
|
||||
assert_raises_message(sa_exc.ArgumentError,
|
||||
"Column-based expression object expected "
|
||||
"for argument 'mapping_spec'; got: 'a', "
|
||||
"type <type 'str'>",
|
||||
"for argument 'mapping_spec'; got: 'a'",
|
||||
collections.column_mapped_collection, 'a')
|
||||
assert_raises_message(sa_exc.ArgumentError,
|
||||
"Column-based expression object expected "
|
||||
"for argument 'mapping_spec'; got: 'a', "
|
||||
"type <class 'sqlalchemy.sql.expression._"
|
||||
"TextClause'>",
|
||||
"for argument 'mapping_spec'; got: 'a'",
|
||||
collections.column_mapped_collection,
|
||||
text('a'))
|
||||
|
||||
|
||||
@@ -89,6 +89,10 @@ class ExpireTest(_fixtures.FixtureTest):
|
||||
assert s.query(User).get(10) is None
|
||||
assert u not in s # and expunges
|
||||
|
||||
# trick the "deleted" flag so we can re-add for the sake
|
||||
# of this test
|
||||
del attributes.instance_state(u).deleted
|
||||
|
||||
# add it back
|
||||
s.add(u)
|
||||
# nope, raises ObjectDeletedError
|
||||
|
||||
@@ -280,6 +280,52 @@ class SessionTest(_fixtures.FixtureTest):
|
||||
assert u1.id is None
|
||||
assert u1.name is None
|
||||
|
||||
# works twice
|
||||
make_transient(u1)
|
||||
|
||||
sess.close()
|
||||
|
||||
u1.name = 'test2'
|
||||
sess.add(u1)
|
||||
sess.flush()
|
||||
assert u1 in sess
|
||||
sess.delete(u1)
|
||||
sess.flush()
|
||||
assert u1 not in sess
|
||||
|
||||
assert_raises(sa.exc.InvalidRequestError, sess.add, u1)
|
||||
make_transient(u1)
|
||||
sess.add(u1)
|
||||
sess.flush()
|
||||
assert u1 in sess
|
||||
|
||||
@testing.resolve_artifact_names
|
||||
def test_deleted_flag(self):
|
||||
mapper(User, users)
|
||||
|
||||
sess = sessionmaker()()
|
||||
|
||||
u1 = User(name='u1')
|
||||
sess.add(u1)
|
||||
sess.commit()
|
||||
|
||||
sess.delete(u1)
|
||||
sess.flush()
|
||||
assert u1 not in sess
|
||||
assert_raises(sa.exc.InvalidRequestError, sess.add, u1)
|
||||
sess.rollback()
|
||||
assert u1 in sess
|
||||
|
||||
sess.delete(u1)
|
||||
sess.commit()
|
||||
assert u1 not in sess
|
||||
assert_raises(sa.exc.InvalidRequestError, sess.add, u1)
|
||||
|
||||
make_transient(u1)
|
||||
sess.add(u1)
|
||||
sess.commit()
|
||||
|
||||
eq_(sess.query(User).count(), 1)
|
||||
|
||||
@testing.resolve_artifact_names
|
||||
def test_autoflush_expressions(self):
|
||||
|
||||
Reference in New Issue
Block a user