[Python-modules-commits] [sqlalchemy] 02/06: Import sqlalchemy_1.1.3+ds1.orig.tar.gz

Piotr Ożarowski piotr at moszumanska.debian.org
Sat Nov 5 22:31:43 UTC 2016


This is an automated email from the git hooks/post-receive script.

piotr pushed a commit to branch master
in repository sqlalchemy.

commit ea616a8a0fe33469561d26ce6adb5086ca68ccb9
Author: Piotr Ożarowski <piotr at debian.org>
Date:   Sat Nov 5 22:07:16 2016 +0100

    Import sqlalchemy_1.1.3+ds1.orig.tar.gz
---
 PKG-INFO                                   |   2 +-
 doc/build/changelog/changelog_11.rst       |  63 +++++++++++++++
 doc/build/changelog/migration_11.rst       | 122 +++++++++++++++++++++++------
 doc/build/conf.py                          |   4 +-
 lib/sqlalchemy/__init__.py                 |   2 +-
 lib/sqlalchemy/dialects/postgresql/base.py |   3 +-
 lib/sqlalchemy/ext/hybrid.py               |   4 +-
 lib/sqlalchemy/orm/persistence.py          |   6 ++
 lib/sqlalchemy/orm/query.py                |   7 +-
 lib/sqlalchemy/orm/session.py              |   9 ++-
 lib/sqlalchemy/orm/unitofwork.py           |   3 +
 lib/sqlalchemy/sql/base.py                 |   2 -
 lib/sqlalchemy/sql/sqltypes.py             |  20 +++++
 lib/sqlalchemy/sql/type_api.py             |  11 +++
 test/dialect/postgresql/test_reflection.py |  16 ++++
 test/orm/test_query.py                     |  30 +++++++
 test/orm/test_session.py                   |  33 +++++++-
 test/orm/test_unitofworkv2.py              |  20 +++++
 test/sql/test_metadata.py                  |  67 ++++++++++++++++
 test/sql/test_types.py                     |  19 +++++
 20 files changed, 406 insertions(+), 37 deletions(-)

diff --git a/PKG-INFO b/PKG-INFO
index c5de66b..612c0e7 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: SQLAlchemy
-Version: 1.1.2
+Version: 1.1.3
 Summary: Database Abstraction Library
 Home-page: http://www.sqlalchemy.org
 Author: Mike Bayer
diff --git a/doc/build/changelog/changelog_11.rst b/doc/build/changelog/changelog_11.rst
index ea14432..9c43374 100644
--- a/doc/build/changelog/changelog_11.rst
+++ b/doc/build/changelog/changelog_11.rst
@@ -19,6 +19,69 @@
         :start-line: 5
 
 .. changelog::
+    :version: 1.1.3
+    :released: October 27, 2016
+
+    .. change::
+        :tags: bug, orm
+        :tickets: 3839
+
+        Fixed regression caused by :ticket:`2677` whereby calling
+        :meth:`.Session.delete` on an object that was already flushed as
+        deleted in that session would fail to set up the object in the
+        identity map (or reject the object), causing flush errors as the
+        object were in a state not accommodated by the unit of work.
+        The pre-1.1 behavior in this case has been restored, which is that
+        the object is put back into the identity map so that the DELETE
+        statement will be attempted again, which emits a warning that the number
+        of expected rows was not matched (unless the row were restored outside
+        of the session).
+
+    .. change::
+        :tags: bug, postgresql
+        :tickets: 3835
+
+        Postgresql table reflection will ensure that the
+        :paramref:`.Column.autoincrement` flag is set to False when reflecting
+        a primary key column that is not of an :class:`.Integer` datatype,
+        even if the default is related to an integer-generating sequence.
+        This can happen if a column is created as SERIAL and the datatype
+        is changed.  The autoincrement flag can only be True if the datatype
+        is of integer affinity in the 1.1 series.
+
+    .. change::
+        :tags: bug, orm
+        :tickets: 3836
+
+        Fixed regression where some :class:`.Query` methods like
+        :meth:`.Query.update` and others would fail if the :class:`.Query`
+        were against a series of mapped columns, rather than the mapped
+        entity as a whole.
+
+    .. change::
+        :tags: bug, sql
+        :tickets: 3833
+
+        Fixed bug involving new value translation and validation feature
+        in :class:`.Enum` whereby using the enum object in a string
+        concatenation would maintain the :class:`.Enum` type as the type
+        of the expression overall, producing missing lookups.  A string
+        concatenation against an :class:`.Enum`-typed column now uses
+        :class:`.String` as the datatype of the expression itself.
+
+    .. change::
+        :tags: bug, sql
+        :tickets: 3832
+
+        Fixed regression which occurred as a side effect of :ticket:`2919`,
+        which in the less typical case of a user-defined
+        :class:`.TypeDecorator` that was also itself an instance of
+        :class:`.SchemaType` (rather than the implementation being such)
+        would cause the column attachment events to be skipped for the
+        type itself.
+
+
+.. changelog::
     :version: 1.1.2
     :released: October 17, 2016
 
diff --git a/doc/build/changelog/migration_11.rst b/doc/build/changelog/migration_11.rst
index eddb203..04bdd72 100644
--- a/doc/build/changelog/migration_11.rst
+++ b/doc/build/changelog/migration_11.rst
@@ -1528,6 +1528,13 @@ string values::
     e.execute(t.insert(), {"value": MyEnum.two})
     assert e.scalar(t.select()) is MyEnum.two
 
+The ``Enum.enums`` collection is now a list instead of a tuple
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+As part of the changes to :class:`.Enum`, the :attr:`.Enum.enums` collection
+of elements is now a list instead of a tuple.  This because lists
+are appropriate for variable length sequences of homogeneous items where
+the position of the element is not semantically significant.
 
 :ticket:`3292`
 
@@ -1676,30 +1683,64 @@ NULL values as well as expression handling.
 
 .. _change_3514:
 
-JSON "null" is inserted as expected with ORM operations, regardless of column default present
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+JSON "null" is inserted as expected with ORM operations, omitted when not present
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 The :class:`.types.JSON` type and its descendant types :class:`.postgresql.JSON`
 and :class:`.mysql.JSON` have a flag :paramref:`.types.JSON.none_as_null` which
 when set to True indicates that the Python value ``None`` should translate
 into a SQL NULL rather than a JSON NULL value.  This flag defaults to False,
-which means that the column should *never* insert SQL NULL or fall back
-to a default unless the :func:`.null` constant were used.  However, this would
-fail in the ORM under two circumstances; one is when the column also contained
-a default or server_default value, a positive value of ``None`` on the mapped
-attribute would still result in the column-level default being triggered,
+which means that the Python value ``None`` should result in a JSON NULL value.
+
+This logic would fail, and is now corrected, in the following circumstances:
+
+1. When the column also contained a default or server_default value,
+a positive value of ``None`` on the mapped attribute that expects to persist
+JSON "null" would still result in the column-level default being triggered,
 replacing the ``None`` value::
 
+    class MyObject(Base):
+        # ...
+
+        json_value = Column(JSON(none_as_null=False), default="some default")
+
+    # would insert "some default" instead of "'null'",
+    # now will insert "'null'"
     obj = MyObject(json_value=None)
     session.add(obj)
-    session.commit()   # would fire off default / server_default, not encode "'none'"
+    session.commit()
 
-The other is when the :meth:`.Session.bulk_insert_mappings`
-method were used, ``None`` would be ignored in all cases::
+2. When the column *did not* contain a default or server_default value, a missing
+value on a JSON column configured with none_as_null=False would still render
+JSON NULL rather than falling back to not inserting any value, behaving
+inconsistently vs. all other datatypes::
 
+    class MyObject(Base):
+        # ...
+
+        some_other_value = Column(String(50))
+        json_value = Column(JSON(none_as_null=False))
+
+    # would result in NULL for some_other_value,
+    # but json "'null'" for json_value.  Now results in NULL for both
+    # (the json_value is omitted from the INSERT)
+    obj = MyObject()
+    session.add(obj)
+    session.commit()
+
+This is a behavioral change that is backwards incompatible for an application
+that was relying upon this to default a missing value as JSON null.  This
+essentially establishes that a **missing value is distinguished from a present
+value of None**.  See :ref:`behavior_change_3514` for further detail.
+
+3. When the :meth:`.Session.bulk_insert_mappings` method were used, ``None``
+would be ignored in all cases::
+
+    # would insert SQL NULL and/or trigger defaults,
+    # now inserts "'null'"
     session.bulk_insert_mappings(
         MyObject,
-        [{"json_value": None}])  # would insert SQL NULL and/or trigger defaults
+        [{"json_value": None}])
 
 The :class:`.types.JSON` type now implements the
 :attr:`.TypeEngine.should_evaluate_none` flag,
@@ -1708,18 +1749,6 @@ automatically based on the value of :paramref:`.types.JSON.none_as_null`.
 Thanks to :ticket:`3061`, we can differentiate when the value ``None`` is actively
 set by the user versus when it was never set at all.
 
-If the attribute is not set at all, then column level defaults *will*
-fire off and/or SQL NULL will be inserted as expected, as was the behavior
-previously.  Below, the two variants are illustrated::
-
-    obj = MyObject(json_value=None)
-    session.add(obj)
-    session.commit()   # *will not* fire off column defaults, will insert JSON 'null'
-
-    obj = MyObject()
-    session.add(obj)
-    session.commit()   # *will* fire off column defaults, and/or insert SQL NULL
-
 The feature applies as well to the new base :class:`.types.JSON` type
 and its descendant types.
 
@@ -2063,6 +2092,53 @@ as intended by the :func:`.type_coerce` function.
 Key Behavioral Changes - ORM
 ============================
 
+.. _behavior_change_3514:
+
+JSON Columns will not insert JSON NULL if no value is supplied and no default is established
+--------------------------------------------------------------------------------------------
+
+As detailed in :ref:`change_3514`, :class:`.types.JSON` will not render
+a JSON "null" value if the value is missing entirely.  To prevent SQL NULL,
+a default should be set up.  Given the following mapping::
+
+    class MyObject(Base):
+        # ...
+
+        json_value = Column(JSON(none_as_null=False), nullable=False)
+
+The following flush operation will fail with an integrity error::
+
+    obj = MyObject()  # note no json_value
+    session.add(obj)
+    session.commit()  # will fail with integrity error
+
+If the default for the column should be JSON NULL, set this on the
+Column::
+
+    class MyObject(Base):
+        # ...
+
+        json_value = Column(
+            JSON(none_as_null=False), nullable=False, default=JSON.NULL)
+
+Or, ensure the value is present on the object::
+
+    obj = MyObject(json_value=None)
+    session.add(obj)
+    session.commit()  # will insert JSON NULL
+
+Note that setting ``None`` for the default is the same as omitting it entirely;
+the :paramref:`.types.JSON.none_as_null` flag does not impact the value of ``None``
+passed to :paramref:`.Column.default` or :paramref:`.Column.server_default`::
+
+    # default=None is the same as omitting it entirely, does not apply JSON NULL
+    json_value = Column(JSON(none_as_null=False), nullable=False, default=None)
+
+
+.. seealso::
+
+    :ref:`change_3514`
+
 .. _change_3641:
 
 Columns no longer added redundantly with DISTINCT + ORDER BY
diff --git a/doc/build/conf.py b/doc/build/conf.py
index 9050b53..f5785a7 100644
--- a/doc/build/conf.py
+++ b/doc/build/conf.py
@@ -107,9 +107,9 @@ copyright = u'2007-2016, the SQLAlchemy authors and contributors'
 # The short X.Y version.
 version = "1.1"
 # The full version, including alpha/beta/rc tags.
-release = "1.1.2"
+release = "1.1.3"
 
-release_date = "October 17, 2016"
+release_date = "October 27, 2016"
 
 site_base = os.environ.get("RTD_SITE_BASE", "http://www.sqlalchemy.org")
 site_adapter_template = "docs_adapter.mako"
diff --git a/lib/sqlalchemy/__init__.py b/lib/sqlalchemy/__init__.py
index 74e7685..ca681a0 100644
--- a/lib/sqlalchemy/__init__.py
+++ b/lib/sqlalchemy/__init__.py
@@ -128,7 +128,7 @@ from .schema import (
 from .inspection import inspect
 from .engine import create_engine, engine_from_config
 
-__version__ = '1.1.2'
+__version__ = '1.1.3'
 
 
 def __go(lcls):
diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py
index 85f82ec..9898e4b 100644
--- a/lib/sqlalchemy/dialects/postgresql/base.py
+++ b/lib/sqlalchemy/dialects/postgresql/base.py
@@ -2442,7 +2442,8 @@ class PGDialect(default.DefaultDialect):
         if default is not None:
             match = re.search(r"""(nextval\(')([^']+)('.*$)""", default)
             if match is not None:
-                autoincrement = True
+                if issubclass(coltype._type_affinity, sqltypes.Integer):
+                    autoincrement = True
                 # the default is related to a Sequence
                 sch = schema
                 if '.' not in match.group(2) and sch is not None:
diff --git a/lib/sqlalchemy/ext/hybrid.py b/lib/sqlalchemy/ext/hybrid.py
index 99f938e..90e4818 100644
--- a/lib/sqlalchemy/ext/hybrid.py
+++ b/lib/sqlalchemy/ext/hybrid.py
@@ -188,7 +188,7 @@ Working with Relationships
 
 There's no essential difference when creating hybrids that work with
 related objects as opposed to column-based data. The need for distinct
-expressions tends to be greater.  Two variants of we'll illustrate
+expressions tends to be greater.  The two variants we'll illustrate
 are the "join-dependent" hybrid, and the "correlated subquery" hybrid.
 
 Join-Dependent Relationship Hybrid
@@ -505,7 +505,7 @@ into a hierarchical tree pattern::
 
     class Node(Base):
         __tablename__ = 'node'
-        id =Column(Integer, primary_key=True)
+        id = Column(Integer, primary_key=True)
         parent_id = Column(Integer, ForeignKey('node.id'))
         parent = relationship("Node", remote_side=id)
 
diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py
index 24a33ee..2bc189c 100644
--- a/lib/sqlalchemy/orm/persistence.py
+++ b/lib/sqlalchemy/orm/persistence.py
@@ -396,6 +396,12 @@ def _collect_insert_commands(
                 params[col.key] = value
 
         if not bulk:
+            # for all the columns that have no default and we don't have
+            # a value and where "None" is not a special value, add
+            # explicit None to the INSERT.   This is a legacy behavior
+            # which might be worth removing, as it should not be necessary
+            # and also produces confusion, given that "missing" and None
+            # now have distinct meanings
             for colkey in mapper._insert_cols_as_none[table].\
                     difference(params).difference(value_params):
                 params[colkey] = None
diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py
index d6a81ff..23d33b0 100644
--- a/lib/sqlalchemy/orm/query.py
+++ b/lib/sqlalchemy/orm/query.py
@@ -165,7 +165,7 @@ class Query(object):
             info = inspect(from_obj)
             if hasattr(info, 'mapper') and \
                     (info.is_mapper or info.is_aliased_class):
-                self._select_from_entity = from_obj
+                self._select_from_entity = info
                 if set_base_alias:
                     raise sa_exc.ArgumentError(
                         "A selectable (FromClause) instance is "
@@ -3940,8 +3940,10 @@ class _ColumnEntity(_QueryEntity):
             self.entity_zero = _entity
             if _entity:
                 self.entities = [_entity]
+                self.mapper = _entity.mapper
             else:
                 self.entities = []
+                self.mapper = None
             self._from_entities = set(self.entities)
         else:
             all_elements = [
@@ -3963,10 +3965,13 @@ class _ColumnEntity(_QueryEntity):
             ])
             if self.entities:
                 self.entity_zero = self.entities[0]
+                self.mapper = self.entity_zero.mapper
             elif self.namespace is not None:
                 self.entity_zero = self.namespace
+                self.mapper = None
             else:
                 self.entity_zero = None
+                self.mapper = None
 
     supports_single_entity = False
 
diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py
index b492cbb..b39ba14 100644
--- a/lib/sqlalchemy/orm/session.py
+++ b/lib/sqlalchemy/orm/session.py
@@ -1726,8 +1726,9 @@ class Session(_SessionClassMethods):
         if state in self._deleted:
             return
 
+        self.identity_map.add(state)
+
         if to_attach:
-            self.identity_map.add(state)
             self._after_attach(state, obj)
 
         if head:
@@ -2196,7 +2197,8 @@ class Session(_SessionClassMethods):
         for state in proc:
             is_orphan = (
                 _state_mapper(state)._is_orphan(state) and state.has_identity)
-            flush_context.register_object(state, isdelete=is_orphan)
+            _reg = flush_context.register_object(state, isdelete=is_orphan)
+            assert _reg, "Failed to add object to the flush context!"
             processed.add(state)
 
         # put all remaining deletes into the flush context.
@@ -2205,7 +2207,8 @@ class Session(_SessionClassMethods):
         else:
             proc = deleted.difference(processed)
         for state in proc:
-            flush_context.register_object(state, isdelete=True)
+            _reg = flush_context.register_object(state, isdelete=True)
+            assert _reg, "Failed to add object to the flush context!"
 
         if not flush_context.has_work:
             return
diff --git a/lib/sqlalchemy/orm/unitofwork.py b/lib/sqlalchemy/orm/unitofwork.py
index f3e39d9..de4842d 100644
--- a/lib/sqlalchemy/orm/unitofwork.py
+++ b/lib/sqlalchemy/orm/unitofwork.py
@@ -242,6 +242,9 @@ class UOWTransaction(object):
                         listonly=False, cancel_delete=False,
                         operation=None, prop=None):
         if not self.session._contains_state(state):
+            # this condition is normal when objects are registered
+            # as part of a relationship cascade operation.  it should
+            # not occur for the top-level register from Session.flush().
             if not state.deleted and operation is not None:
                 util.warn("Object of type %s not in session, %s operation "
                           "along '%s' will not proceed" %
diff --git a/lib/sqlalchemy/sql/base.py b/lib/sqlalchemy/sql/base.py
index cf7dcfd..0b03684 100644
--- a/lib/sqlalchemy/sql/base.py
+++ b/lib/sqlalchemy/sql/base.py
@@ -426,8 +426,6 @@ class SchemaEventTarget(object):
     def _set_parent(self, parent):
         """Associate with this SchemaEvent's parent object."""
 
-        raise NotImplementedError()
-
     def _set_parent_with_dispatch(self, parent):
         self.dispatch.before_parent_attach(self, parent)
         self._set_parent(parent)
diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py
index 118c260..ef1624f 100644
--- a/lib/sqlalchemy/sql/sqltypes.py
+++ b/lib/sqlalchemy/sql/sqltypes.py
@@ -1296,6 +1296,19 @@ class Enum(String, SchemaType):
                 raise LookupError(
                     '"%s" is not among the defined enum values' % elem)
 
+    class Comparator(String.Comparator):
+
+        def _adapt_expression(self, op, other_comparator):
+            op, typ = super(Enum.Comparator, self)._adapt_expression(
+                op, other_comparator)
+            if op is operators.concat_op:
+                typ = String(
+                    self.type.length,
+                    convert_unicode=self.type.convert_unicode)
+            return op, typ
+
+    comparator_factory = Comparator
+
     def _object_value_for_elem(self, elem):
         try:
             return self._object_lookup[elem]
@@ -1796,6 +1809,13 @@ class JSON(Indexable, TypeEngine):
              from sqlalchemy import null
              conn.execute(table.insert(), data=null())
 
+         .. note::
+
+              :paramref:`.JSON.none_as_null` does **not** apply to the
+              values passed to :paramref:`.Column.default` and
+              :paramref:`.Column.server_default`; a value of ``None`` passed for
+              these parameters means "no default present".
+
          .. seealso::
 
               :attr:`.types.JSON.NULL`
diff --git a/lib/sqlalchemy/sql/type_api.py b/lib/sqlalchemy/sql/type_api.py
index 217f701..98ede4e 100644
--- a/lib/sqlalchemy/sql/type_api.py
+++ b/lib/sqlalchemy/sql/type_api.py
@@ -182,6 +182,13 @@ class TypeEngine(Visitable):
         the :obj:`~.expression.null` SQL construct in an INSERT statement
         or associated with an ORM-mapped attribute.
 
+        .. note::
+
+            The "evaulates none" flag does **not** apply to a value
+            of ``None`` passed to :paramref:`.Column.default` or
+            :paramref:`.Column.server_default`; in these cases, ``None``
+            still means "no default".
+
         .. versionadded:: 1.1
 
         .. seealso::
@@ -853,12 +860,16 @@ class TypeDecorator(SchemaEventTarget, TypeEngine):
     def _set_parent(self, column):
         """Support SchemaEentTarget"""
 
+        super(TypeDecorator, self)._set_parent(column)
+
         if isinstance(self.impl, SchemaEventTarget):
             self.impl._set_parent(column)
 
     def _set_parent_with_dispatch(self, parent):
         """Support SchemaEentTarget"""
 
+        super(TypeDecorator, self)._set_parent_with_dispatch(parent)
+
         if isinstance(self.impl, SchemaEventTarget):
             self.impl._set_parent_with_dispatch(parent)
 
diff --git a/test/dialect/postgresql/test_reflection.py b/test/dialect/postgresql/test_reflection.py
index 84aeef1..5f9e6df 100644
--- a/test/dialect/postgresql/test_reflection.py
+++ b/test/dialect/postgresql/test_reflection.py
@@ -344,6 +344,22 @@ class ReflectionTest(fixtures.TestBase):
         eq_(r.inserted_primary_key, [2])
 
     @testing.provide_metadata
+    def test_altered_type_autoincrement_pk_reflection(self):
+        metadata = self.metadata
+        t = Table(
+            't', metadata,
+            Column('id', Integer, primary_key=True),
+            Column('x', Integer)
+        )
+        metadata.create_all()
+        testing.db.connect().execution_options(autocommit=True).\
+            execute('alter table t alter column id type varchar(50)')
+        m2 = MetaData(testing.db)
+        t2 = Table('t', m2, autoload=True)
+        eq_(t2.c.id.autoincrement, False)
+        eq_(t2.c.x.autoincrement, False)
+
+    @testing.provide_metadata
     def test_renamed_pk_reflection(self):
         metadata = self.metadata
         t = Table('t', metadata, Column('id', Integer, primary_key=True))
diff --git a/test/orm/test_query.py b/test/orm/test_query.py
index 493f6a7..57408e1 100644
--- a/test/orm/test_query.py
+++ b/test/orm/test_query.py
@@ -813,6 +813,36 @@ class InvalidGenerationsTest(QueryTest, AssertsCompiledSQL):
         q = s.query(User, Address)
         assert_raises(sa_exc.InvalidRequestError, q.get, 5)
 
+    def test_entity_or_mapper_zero(self):
+        User, Address = self.classes.User, self.classes.Address
+        s = create_session()
+
+        q = s.query(User, Address)
+        is_(q._mapper_zero(), inspect(User))
+        is_(q._entity_zero(), inspect(User))
+
+        u1 = aliased(User)
+        q = s.query(u1, Address)
+        is_(q._mapper_zero(), inspect(User))
+        is_(q._entity_zero(), inspect(u1))
+
+        q = s.query(User).select_from(Address)
+        is_(q._mapper_zero(), inspect(User))
+        is_(q._entity_zero(), inspect(Address))
+
+        q = s.query(User.name, Address)
+        is_(q._mapper_zero(), inspect(User))
+        is_(q._entity_zero(), inspect(User))
+
+        q = s.query(u1.name, Address)
+        is_(q._mapper_zero(), inspect(User))
+        is_(q._entity_zero(), inspect(u1))
+
+        q1 = s.query(User).exists()
+        q = s.query(q1)
+        is_(q._mapper_zero(), None)
+        is_(q._entity_zero(), None)
+
     def test_from_statement(self):
         User = self.classes.User
 
diff --git a/test/orm/test_session.py b/test/orm/test_session.py
index caeb085..24666d0 100644
--- a/test/orm/test_session.py
+++ b/test/orm/test_session.py
@@ -1,5 +1,5 @@
 from sqlalchemy.testing import eq_, assert_raises, \
-    assert_raises_message
+    assert_raises_message, assertions
 from sqlalchemy.testing.util import gc_collect
 from sqlalchemy.testing import pickleable
 from sqlalchemy.util import pickle
@@ -347,6 +347,37 @@ class SessionStateTest(_fixtures.FixtureTest):
 
         eq_(sess.query(User).count(), 1)
 
+    def test_deleted_adds_to_imap_unconditionally(self):
+        users, User = self.tables.users, self.classes.User
+
+        mapper(User, users)
+
+        sess = Session()
+        u1 = User(name='u1')
+        sess.add(u1)
+        sess.commit()
+
+        sess.delete(u1)
+        sess.flush()
+
+        # object is not in session
+        assert u1 not in sess
+
+        # but it *is* attached
+        assert u1._sa_instance_state.session_id == sess.hash_key
+
+        # mark as deleted again
+        sess.delete(u1)
+
+        # in the session again
+        assert u1 in sess
+
+        # commit proceeds w/ warning
+        with assertions.expect_warnings(
+                "DELETE statement on table 'users' "
+                r"expected to delete 1 row\(s\); 0 were matched."):
+            sess.commit()
+
     def test_autoflush_expressions(self):
         """test that an expression which is dependent on object state is
         evaluated after the session autoflushes.   This is the lambda
diff --git a/test/orm/test_unitofworkv2.py b/test/orm/test_unitofworkv2.py
index ae8454f..b2fefe6 100644
--- a/test/orm/test_unitofworkv2.py
+++ b/test/orm/test_unitofworkv2.py
@@ -1567,6 +1567,26 @@ class BasicStaleChecksTest(fixtures.MappedTest):
                 )
 
     @testing.requires.sane_multi_rowcount
+    def test_delete_twice(self):
+        Parent, Child = self._fixture()
+        sess = Session()
+        p1 = Parent(id=1, data=2, child=None)
+        sess.add(p1)
+        sess.commit()
+
+        sess.delete(p1)
+        sess.flush()
+
+        sess.delete(p1)
+
+        assert_raises_message(
+            exc.SAWarning,
+            "DELETE statement on table 'parent' expected to "
+            "delete 1 row\(s\); 0 were matched.",
+            sess.commit
+        )
+
+    @testing.requires.sane_multi_rowcount
     def test_delete_multi_missing_warning(self):
         Parent, Child = self._fixture()
         sess = Session()
diff --git a/test/sql/test_metadata.py b/test/sql/test_metadata.py
index f2df4da..f790c2a 100644
--- a/test/sql/test_metadata.py
+++ b/test/sql/test_metadata.py
@@ -1574,6 +1574,66 @@ class SchemaTypeTest(fixtures.TestBase):
     class MyTypeImpl(MyTypeWImpl):
         pass
 
+    class MyTypeDecAndSchema(TypeDecorator, sqltypes.SchemaType):
+        impl = String()
+
+        evt_targets = ()
+
+        def __init__(self):
+            TypeDecorator.__init__(self)
+            sqltypes.SchemaType.__init__(self)
+
+        def _on_table_create(self, target, bind, **kw):
+            self.evt_targets += (target,)
+
+        def _on_metadata_create(self, target, bind, **kw):
+            self.evt_targets += (target,)
+
+    def test_before_parent_attach_plain(self):
+        typ = self.MyType()
+        self._test_before_parent_attach(typ)
+
+    def test_before_parent_attach_typedec_enclosing_schematype(self):
+        # additional test for [ticket:2919] as part of test for
+        # [ticket:3832]
+
+        class MySchemaType(sqltypes.TypeEngine, sqltypes.SchemaType):
+            pass
+
+        target_typ = MySchemaType()
+
+        class MyType(TypeDecorator):
+            impl = target_typ
+
+        typ = MyType()
+        self._test_before_parent_attach(typ, target_typ)
+
+    def test_before_parent_attach_typedec_of_schematype(self):
+        class MyType(TypeDecorator, sqltypes.SchemaType):
+            impl = String
+
+        typ = MyType()
+        self._test_before_parent_attach(typ)
+
+    def test_before_parent_attach_schematype_of_typedec(self):
+        class MyType(sqltypes.SchemaType, TypeDecorator):
+            impl = String
+
+        typ = MyType()
+        self._test_before_parent_attach(typ)
+
+    def _test_before_parent_attach(self, typ, evt_target=None):
+        canary = mock.Mock()
+
+        if evt_target is None:
+            evt_target = typ
+
+        event.listen(evt_target, "before_parent_attach", canary.go)
+
+        c = Column('q', typ)
+
+        eq_(canary.mock_calls, [mock.call.go(evt_target, c)])
+
     def test_independent_schema(self):
         m = MetaData()
         type_ = self.MyType(schema="q")
@@ -1709,6 +1769,13 @@ class SchemaTypeTest(fixtures.TestBase):
         dialect_impl = typ.dialect_impl(testing.db.dialect)
         eq_(dialect_impl.evt_targets, (m1, ))
 
+    def test_table_dispatch_decorator_schematype(self):
+        m1 = MetaData()
+        typ = self.MyTypeDecAndSchema()
+        t1 = Table('t1', m1, Column('x', typ))
+        m1.dispatch.before_create(t1, testing.db)
+        eq_(typ.evt_targets, (t1, ))
+
     def test_table_dispatch_no_new_impl(self):
         m1 = MetaData()
         typ = self.MyType()
diff --git a/test/sql/test_types.py b/test/sql/test_types.py
index 3374a67..7f49991 100644
--- a/test/sql/test_types.py
+++ b/test/sql/test_types.py
@@ -1264,6 +1264,25 @@ class EnumTest(AssertsCompiledSQL, fixtures.TablesTest):
             ]
         )
 
+    def test_validators_not_in_concatenate_roundtrip(self):
+        enum_table = self.tables['non_native_enum_table']
+
+        enum_table.insert().execute([
+            {'id': 1, 'someenum': 'two'},
+            {'id': 2, 'someenum': 'two'},
+            {'id': 3, 'someenum': 'one'},
+        ])
+
+        eq_(
+            select(['foo' + enum_table.c.someenum]).
+            order_by(enum_table.c.id).execute().fetchall(),
+            [
+                ('footwo', ),
+                ('footwo', ),
+                ('fooone', )
+            ]
+        )
+
     @testing.fails_on(
         'postgresql+zxjdbc',
         'zxjdbc fails on ENUM: column "XXX" is of type XXX '

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/sqlalchemy.git



More information about the Python-modules-commits mailing list