Mike Bayer avatar Mike Bayer committed 1129e49 Merge

merge default

Comments (0)

Files changed (8)

doc/build/changelog/changelog_08.rst

     :version: 0.8.0
 
     .. change::
+        :tags: bug, orm
+        :tickets: 2662
+
+      A clear error message is emitted if an event handler
+      attempts to emit SQL on a Session within the after_commit()
+      handler, where there is not a viable transaction in progress.
+
+    .. change::
+        :tags: bug, orm
+        :tickets: 2665
+
+      Detection of a primary key change within the process
+      of cascading a natural primary key update will succeed
+      even if the key is composite and only some of the
+      attributes have changed.
+
+    .. change::
         :tags: feature, orm
         :tickets: 2658
 

lib/sqlalchemy/ext/declarative/base.py

     mapper_args_fn = None
     table_args = inherited_table_args = None
     tablename = None
-    parent_columns = ()
 
     declarative_props = (declared_attr, util.classproperty)
 
                 return
 
         class_mapped = _declared_mapping_info(base) is not None
-        if class_mapped:
-            parent_columns = base.__table__.c.keys()
 
         for name, obj in vars(base).items():
             if name == '__mapper_args__':

lib/sqlalchemy/orm/events.py

     def after_transaction_create(self, session, transaction):
         """Execute when a new :class:`.SessionTransaction` is created.
 
+        This event differs from :meth:`~.SessionEvents.after_begin`
+        in that it occurs for each :class:`.SessionTransaction`
+        overall, as opposed to when transactions are begun
+        on individual database connections.  It is also invoked
+        for nested transactions and subtransactions, and is always
+        matched by a corresponding
+        :meth:`~.SessionEvents.after_transaction_end` event
+        (assuming normal operation of the :class:`.Session`).
+
         :param session: the target :class:`.Session`.
         :param transaction: the target :class:`.SessionTransaction`.
 
         .. versionadded:: 0.8
 
+        .. seealso::
+
+            :meth:`~.SessionEvents.after_transaction_end`
+
         """
 
     def after_transaction_end(self, session, transaction):
         """Execute when the span of a :class:`.SessionTransaction` ends.
 
+        This event differs from :meth:`~.SessionEvents.after_commit`
+        in that it corresponds to all :class:`.SessionTransaction`
+        objects in use, including those for nested transactions
+        and subtransactions, and is always matched by a corresponding
+        :meth:`~.SessionEvents.after_transaction_create` event.
+
         :param session: the target :class:`.Session`.
         :param transaction: the target :class:`.SessionTransaction`.
 
         .. versionadded:: 0.8
 
+        .. seealso::
+
+            :meth:`~.SessionEvents.after_transaction_create`
+
         """
 
     def before_commit(self, session):
         """Execute before commit is called.
 
-        Note that this may not be per-flush if a longer running
-        transaction is ongoing.
+        .. note::
+
+            The :meth:`.before_commit` hook is *not* per-flush,
+            that is, the :class:`.Session` can emit SQL to the database
+            many times within the scope of a transaction.
+            For interception of these events, use the :meth:`~.SessionEvents.before_flush`,
+            :meth:`~.SessionEvents.after_flush`, or :meth:`~.SessionEvents.after_flush_postexec`
+            events.
 
         :param session: The target :class:`.Session`.
 
+        .. seealso::
+
+            :meth:`~.SessionEvents.after_commit`
+
+            :meth:`~.SessionEvents.after_begin`
+
+            :meth:`~.SessionEvents.after_transaction_create`
+
+            :meth:`~.SessionEvents.after_transaction_end`
+
         """
 
     def after_commit(self, session):
         """Execute after a commit has occurred.
 
-        Note that this may not be per-flush if a longer running
-        transaction is ongoing.
+        .. note::
+
+            The :meth:`~.SessionEvents.after_commit` hook is *not* per-flush,
+            that is, the :class:`.Session` can emit SQL to the database
+            many times within the scope of a transaction.
+            For interception of these events, use the :meth:`~.SessionEvents.before_flush`,
+            :meth:`~.SessionEvents.after_flush`, or :meth:`~.SessionEvents.after_flush_postexec`
+            events.
+
+        .. note::
+
+            The :class:`.Session` is not in an active tranasction
+            when the :meth:`~.SessionEvents.after_commit` event is invoked, and therefore
+            can not emit SQL.  To emit SQL corresponding to every transaction,
+            use the :meth:`~.SessionEvents.before_commit` event.
 
         :param session: The target :class:`.Session`.
 
+        .. seealso::
+
+            :meth:`~.SessionEvents.before_commit`
+
+            :meth:`~.SessionEvents.after_begin`
+
+            :meth:`~.SessionEvents.after_transaction_create`
+
+            :meth:`~.SessionEvents.after_transaction_end`
+
         """
 
     def after_rollback(self, session):
          objects which can be passed to the :meth:`.Session.flush` method
          (note this usage is deprecated).
 
+        .. seealso::
+
+            :meth:`~.SessionEvents.after_flush`
+
+            :meth:`~.SessionEvents.after_flush_postexec`
+
         """
 
     def after_flush(self, session, flush_context):
         :param flush_context: Internal :class:`.UOWTransaction` object
          which handles the details of the flush.
 
+        .. seealso::
+
+            :meth:`~.SessionEvents.before_flush`
+
+            :meth:`~.SessionEvents.after_flush_postexec`
+
         """
 
     def after_flush_postexec(self, session, flush_context):
         :param session: The target :class:`.Session`.
         :param flush_context: Internal :class:`.UOWTransaction` object
          which handles the details of the flush.
+
+
+        .. seealso::
+
+            :meth:`~.SessionEvents.before_flush`
+
+            :meth:`~.SessionEvents.after_flush`
+
         """
 
     def after_begin(self, session, transaction, connection):
         :param connection: The :class:`~.engine.Connection` object
          which will be used for SQL statements.
 
+        .. seealso::
+
+            :meth:`~.SessionEvents.before_commit`
+
+            :meth:`~.SessionEvents.after_commit`
+
+            :meth:`~.SessionEvents.after_transaction_create`
+
+            :meth:`~.SessionEvents.after_transaction_end`
+
         """
 
     def before_attach(self, session, instance):
            :meth:`.before_attach` is provided for those cases where
            the item should not yet be part of the session state.
 
+        .. seealso::
+
+            :meth:`~.SessionEvents.after_attach`
+
         """
 
     def after_attach(self, session, instance):
            yet complete) consider the
            new :meth:`.before_attach` event.
 
+        .. seealso::
+
+            :meth:`~.SessionEvents.before_attach`
+
         """
 
     def after_bulk_update(self, session, query, query_context, result):

lib/sqlalchemy/orm/session.py

         return object_session(instance)
 
 
+ACTIVE = util.symbol('ACTIVE')
+PREPARED = util.symbol('PREPARED')
+DEACTIVE = util.symbol('DEACTIVE')
+
 class SessionTransaction(object):
     """A :class:`.Session`-level transaction.
 
         self._connections = {}
         self._parent = parent
         self.nested = nested
-        self._active = True
-        self._prepared = False
+        self._state = ACTIVE
         if not parent and nested:
             raise sa_exc.InvalidRequestError(
                 "Can't start a SAVEPOINT transaction when no existing "
 
     @property
     def is_active(self):
-        return self.session is not None and self._active
+        return self.session is not None and self._state is ACTIVE
 
     def _assert_is_active(self):
         self._assert_is_open()
-        if not self._active:
+        if self._state is PREPARED:
+            raise sa_exc.InvalidRequestError(
+                    "This session is in 'prepared' state, where no further "
+                    "SQL can be emitted until the transaction is fully "
+                    "committed."
+                )
+        elif self._state is DEACTIVE:
             if self._rollback_exception:
                 raise sa_exc.InvalidRequestError(
                     "This Session's transaction has been rolled back "
                 self.rollback()
                 raise
 
-        self._deactivate()
-        self._prepared = True
+        self._state = PREPARED
 
     def commit(self):
         self._assert_is_open()
-        if not self._prepared:
+        if self._state is not PREPARED:
             self._prepare_impl()
 
         if self._parent is None or self.nested:
             for subtransaction in stx._iterate_parents(upto=self):
                 subtransaction.close()
 
-        if self.is_active or self._prepared:
+        if self._state in (ACTIVE, PREPARED):
             for transaction in self._iterate_parents():
                 if transaction._parent is None or transaction.nested:
                     transaction._rollback_impl()
-                    transaction._deactivate()
+                    transaction._state = DEACTIVE
                     break
                 else:
-                    transaction._deactivate()
+                    transaction._state = DEACTIVE
 
         sess = self.session
 
 
         self.session.dispatch.after_rollback(self.session)
 
-    def _deactivate(self):
-        self._active = False
-
     def close(self):
         self.session.transaction = self._parent
         if self._parent is None:
                 else:
                     transaction.close()
 
-        self._deactivate()
+        self._state = DEACTIVE
         if self.session.dispatch.after_transaction_end:
             self.session.dispatch.after_transaction_end(self.session, self)
 

lib/sqlalchemy/orm/sync.py

             _raise_col_to_prop(False, source_mapper, l, None, r)
         history = uowcommit.get_attribute_history(source, prop.key,
                                         attributes.PASSIVE_NO_INITIALIZE)
-        return bool(history.deleted)
+        if bool(history.deleted):
+            return True
     else:
         return False
 

lib/sqlalchemy/sql/expression.py

                     **params)
 
     def select(self, whereclause=None, **params):
-        """return a SELECT of this :class:`.FromClause`."""
+        """return a SELECT of this :class:`.FromClause`.
+
+        .. seealso::
+
+            :func:`~.sql.expression.select` - general purpose
+            method which allows for arbitrary column lists.
+
+        """
 
         return select([self], whereclause, **params)
 

test/orm/test_sync.py

             True
         )
 
+    def test_source_modified_composite(self):
+        uowcommit, a1, b1, a_mapper, b_mapper = self._fixture()
+        a1.obj().foo = 10
+        a1._commit_all(a1.dict)
+        a1.obj().foo = 12
+        pairs = [(a_mapper.c.id, b_mapper.c.id,),
+                (a_mapper.c.foo, b_mapper.c.id)]
+        eq_(
+            sync.source_modified(uowcommit, a1, a_mapper, pairs),
+            True
+        )
+
+    def test_source_modified_composite_unmodified(self):
+        uowcommit, a1, b1, a_mapper, b_mapper = self._fixture()
+        a1.obj().foo = 10
+        a1._commit_all(a1.dict)
+        pairs = [(a_mapper.c.id, b_mapper.c.id,),
+                (a_mapper.c.foo, b_mapper.c.id)]
+        eq_(
+            sync.source_modified(uowcommit, a1, a_mapper, pairs),
+            False
+        )
+
     def test_source_modified_no_unmapped(self):
         uowcommit, a1, b1, a_mapper, b_mapper = self._fixture()
         pairs = [(b_mapper.c.id, b_mapper.c.id,)]

test/orm/test_transaction.py

                               sess.begin, subtransactions=True)
         sess.close()
 
+    def test_no_sql_during_prepare(self):
+        sess = create_session(bind=testing.db, autocommit=False)
+
+        @event.listens_for(sess, "after_commit")
+        def go(session):
+            session.execute("select 1")
+        assert_raises_message(sa_exc.InvalidRequestError,
+                    "This session is in 'prepared' state, where no "
+                    "further SQL can be emitted until the "
+                    "transaction is fully committed.",
+                    sess.commit)
+
     def _inactive_flushed_session_fixture(self):
         users, User = self.tables.users, self.classes.User
 
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.