]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Added support for parameter-ordered SET clauses in an UPDATE
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 28 Nov 2015 19:30:05 +0000 (14:30 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 28 Nov 2015 19:30:05 +0000 (14:30 -0500)
statement.  This feature is available by passing the
:paramref:`~.sqlalchemy.sql.expression.update.preserve_parameter_order`
flag either to the core :class:`.Update` construct or alternatively
adding it to the :paramref:`.Query.update.update_args` dictionary at
the ORM-level, also passing the parameters themselves as a list of 2-tuples.
Thanks to Gorka Eguileor for implementation and tests.
adapted from pullreq github:200

.gitignore
doc/build/changelog/changelog_10.rst
doc/build/core/tutorial.rst
lib/sqlalchemy/orm/persistence.py
lib/sqlalchemy/orm/query.py
lib/sqlalchemy/sql/crud.py
lib/sqlalchemy/sql/dml.py
lib/sqlalchemy/sql/util.py
test/orm/test_cycles.py
test/orm/test_update_delete.py
test/sql/test_update.py

index 55066f84370acb6e8122ab3a79a7dc1a6485a769..81fd2d9ed71083bb7b5c69909a8b90d96fbf3a60 100644 (file)
@@ -19,3 +19,4 @@ coverage.xml
 sqlnet.log
 /mapping_setup.py
 /test.py
+/.cache/
index b2cfacbfa331adb4e7d93e893618200f475ba96d..7e289a526be0a866a027803cb3bf7b7b08d8e8ac 100644 (file)
 .. changelog::
     :version: 1.0.10
 
+    .. change::
+        :tags: feature, sql
+        :versions: 1.1.0b1
+        :pullreq: github:200
+
+        Added support for parameter-ordered SET clauses in an UPDATE
+        statement.  This feature is available by passing the
+        :paramref:`~.sqlalchemy.sql.expression.update.preserve_parameter_order`
+        flag either to the core :class:`.Update` construct or alternatively
+        adding it to the :paramref:`.Query.update.update_args` dictionary at
+        the ORM-level, also passing the parameters themselves as a list of 2-tuples.
+        Thanks to Gorka Eguileor for implementation and tests.
+
+        .. seealso::
+
+            :ref:`updates_order_parameters`
+
     .. change::
         :tags: bug, orm
         :versions: 1.1.0b1
index c15279236a779076e3f9dde783530b0c908f42d6..71d0d4e39787f0e2c240c023b334aff4ae52029c 100644 (file)
@@ -1873,6 +1873,53 @@ a non-supporting database.  The ``UPDATE FROM`` syntax generates by default
 when multiple tables are present, and the statement will be rejected
 by the database if this syntax is not supported.
 
+.. _updates_order_parameters:
+
+Parameter-Ordered Updates
+--------------------------
+
+The default behavior of the :func:`.update` construct when rendering the SET
+clauses is to render them using the column ordering given in the
+originating :class:`.Table` object.
+This is an important behavior, since it means that the rendering of a
+particular UPDATE statement with particular columns
+will be rendered the same each time, which has an impact on query caching systems
+that rely on the form of the statement, either client side or server side.
+Since the parameters themselves are passed to the :meth:`.Update.values`
+method as Python dictionary keys, there is no other fixed ordering
+available.
+
+However in some cases, the order of parameters rendered in the SET clause of an
+UPDATE statement can be significant.  The main example of this is when using
+MySQL and providing updates to column values based on that of other
+column values.  The end result of the following statement::
+
+    UPDATE some_table SET x = y + 10, y = 20
+
+Will have a different result than::
+
+    UPDATE some_table SET y = 20, x = y + 10
+
+This because on MySQL, the individual SET clauses are fully evaluated on
+a per-value basis, as opposed to on a per-row basis, and as each SET clause
+is evaluated, the values embedded in the row are changing.
+
+To suit this specific use case, the
+:paramref:`~sqlalchemy.sql.expression.update.preserve_parameter_order`
+flag may be used.  When using this flag, we supply a **Python list of 2-tuples**
+as the argument to the :meth:`.Update.values` method::
+
+    stmt = some_table.update(preserve_parameter_order=True).\
+        values([(some_table.c.y, 20), (some_table.c.x, some_table.c.y + 10)])
+
+The list of 2-tuples is essentially the same structure as a Python dictionary
+except it is ordered.  Using the above form, we are assured that the
+"y" column's SET clause will render first, then the "x" column's SET clause.
+
+.. versionadded:: 1.0.10 Added support for explicit ordering of UPDATE
+   parameters using the :paramref:`~sqlalchemy.sql.expression.update.preserve_parameter_order` flag.
+
+
 .. _deletes:
 
 Deletes
index 1f9d0a2352600fb540093deae85493fb4dfeb9db..768c1146a3ec7070d67a42f7a0aad24bad8e687d 100644 (file)
@@ -1258,10 +1258,16 @@ class BulkUpdate(BulkUD):
                 "Invalid expression type: %r" % key)
 
     def _do_exec(self):
-        values = dict(
+
+        values = [
             (self._resolve_string_to_expr(k), v)
-            for k, v in self.values.items()
-        )
+            for k, v in (
+                self.values.items() if hasattr(self.values, 'items')
+                else self.values)
+        ]
+        if not self.update_kwargs.get('preserve_parameter_order', False):
+            values = dict(values)
+
         update_stmt = sql.update(self.primary_table,
                                  self.context.whereclause, values,
                                  **self.update_kwargs)
@@ -1311,7 +1317,9 @@ class BulkUpdateEvaluate(BulkEvaluate, BulkUpdate):
 
     def _additional_evaluators(self, evaluator_compiler):
         self.value_evaluators = {}
-        for key, value in self.values.items():
+        values = (self.values.items() if hasattr(self.values, 'items')
+                  else self.values)
+        for key, value in values:
             key = self._resolve_key_to_attrname(key)
             if key is not None:
                 self.value_evaluators[key] = evaluator_compiler.process(
index 84fb04d80789d74204c44eba1d5df03a5c26b812..4441975bf7c0c85aeb446458b173b8cefa1d164e 100644 (file)
@@ -2910,7 +2910,12 @@ class Query(object):
 
         :param values: a dictionary with attributes names, or alternatively
          mapped attributes or SQL expressions, as keys, and literal
-         values or sql expressions as values.
+         values or sql expressions as values.   If :ref:`parameter-ordered
+         mode <updates_order_parameters>` is desired, the values can be
+         passed as a list of 2-tuples;
+         this requires that the :paramref:`~sqlalchemy.sql.expression.update.preserve_parameter_order`
+         flag is passed to the :paramref:`.Query.update.update_args` dictionary
+         as well.
 
           .. versionchanged:: 1.0.0 - string names in the values dictionary
              are now resolved against the mapped entity; previously, these
@@ -2941,7 +2946,8 @@ class Query(object):
         :param update_args: Optional dictionary, if present will be passed
          to the underlying :func:`.update` construct as the ``**kw`` for
          the object.  May be used to pass dialect-specific arguments such
-         as ``mysql_limit``.
+         as ``mysql_limit``, as well as other special arguments such as
+         :paramref:`~sqlalchemy.sql.expression.update.preserve_parameter_order`.
 
          .. versionadded:: 1.0.0
 
index 72b66c036c06a4116e2d24dd4c8b5a248287e4dc..67a8f09de65f9600560ffa900b23b2821a29b708 100644 (file)
@@ -208,7 +208,18 @@ def _scan_cols(
         implicit_return_defaults, postfetch_lastrowid = \
         _get_returning_modifiers(compiler, stmt)
 
-    cols = stmt.table.columns
+    if stmt._parameter_ordering:
+        parameter_ordering = [
+            _column_as_key(key) for key in stmt._parameter_ordering
+        ]
+        ordered_keys = set(parameter_ordering)
+        cols = [
+            stmt.table.c[key] for key in parameter_ordering
+        ] + [
+            c for c in stmt.table.c if c.key not in ordered_keys
+        ]
+    else:
+        cols = stmt.table.columns
 
     for c in cols:
         col_key = _getattr_col_key(c)
index 6756f1554598fd721bdeb87793bd48f5d964f55d..22c534153c94f5fd91490f5090631473526a1797 100644 (file)
@@ -27,6 +27,7 @@ class UpdateBase(DialectKWArgs, HasPrefixes, Executable, ClauseElement):
     _execution_options = \
         Executable._execution_options.union({'autocommit': True})
     _hints = util.immutabledict()
+    _parameter_ordering = None
     _prefixes = ()
 
     def _process_colparams(self, parameters):
@@ -39,6 +40,16 @@ class UpdateBase(DialectKWArgs, HasPrefixes, Executable, ClauseElement):
             else:
                 return p
 
+        if self._preserve_parameter_order and parameters is not None:
+            if not isinstance(parameters, list) or \
+                    (parameters and not isinstance(parameters[0], tuple)):
+                raise ValueError(
+                    "When preserve_parameter_order is True, "
+                    "values() only accepts a list of 2-tuples")
+            self._parameter_ordering = [key for key, value in parameters]
+
+            return dict(parameters), False
+
         if (isinstance(parameters, (list, tuple)) and parameters and
                 isinstance(parameters[0], (list, tuple, dict))):
 
@@ -178,6 +189,7 @@ class ValuesBase(UpdateBase):
 
     _supports_multi_parameters = False
     _has_multi_parameters = False
+    _preserve_parameter_order = False
     select = None
 
     def __init__(self, table, values, prefixes):
@@ -214,23 +226,32 @@ class ValuesBase(UpdateBase):
 
                 users.update().where(users.c.id==5).values(name="some name")
 
-        :param \*args: Alternatively, a dictionary, tuple or list
-         of dictionaries or tuples can be passed as a single positional
-         argument in order to form the VALUES or
-         SET clause of the statement.  The single dictionary form
-         works the same as the kwargs form::
+        :param \*args: As an alternative to passing key/value parameters,
+         a dictionary, tuple, or list of dictionaries or tuples can be passed
+         as a single positional argument in order to form the VALUES or
+         SET clause of the statement.  The forms that are accepted vary
+         based on whether this is an :class:`.Insert` or an :class:`.Update`
+         construct.
+
+         For either an :class:`.Insert` or :class:`.Update` construct, a
+         single dictionary can be passed, which works the same as that of
+         the kwargs form::
 
             users.insert().values({"name": "some name"})
 
-         If a tuple is passed, the tuple should contain the same number
-         of columns as the target :class:`.Table`::
+            users.update().values({"name": "some new name"})
+
+         Also for either form but more typically for the :class:`.Insert`
+         construct, a tuple that contains an entry for every column in the
+         table is also accepted::
 
             users.insert().values((5, "some name"))
 
-         The :class:`.Insert` construct also supports multiply-rendered VALUES
-         construct, for those backends which support this SQL syntax
-         (SQLite, Postgresql, MySQL).  This mode is indicated by passing a
-         list of one or more dictionaries/tuples::
+         The :class:`.Insert` construct also supports being passed a list
+         of dictionaries or full-table-tuples, which on the server will
+         render the less common SQL syntax of "multiple values" - this
+         syntax is supported on backends such as SQLite, Postgresql, MySQL,
+         but not necessarily others::
 
             users.insert().values([
                                 {"name": "some name"},
@@ -238,55 +259,61 @@ class ValuesBase(UpdateBase):
                                 {"name": "yet another name"},
                             ])
 
-         In the case of an :class:`.Update`
-         construct, only the single dictionary/tuple form is accepted,
-         else an exception is raised.  It is also an exception case to
-         attempt to mix the single-/multiple- value styles together,
-         either through multiple :meth:`.ValuesBase.values` calls
-         or by sending a list + kwargs at the same time.
-
-         .. note::
-
-             Passing a multiple values list is *not* the same
-             as passing a multiple values list to the
-             :meth:`.Connection.execute` method.  Passing a list of parameter
-             sets to :meth:`.ValuesBase.values` produces a construct of this
-             form::
-
-                INSERT INTO table (col1, col2, col3) VALUES
-                                (col1_0, col2_0, col3_0),
-                                (col1_1, col2_1, col3_1),
-                                ...
-
-             whereas a multiple list passed to :meth:`.Connection.execute`
-             has the effect of using the DBAPI
-             `executemany() <http://www.python.org/dev/peps/pep-0249/#id18>`_
-             method, which provides a high-performance system of invoking
-             a single-row INSERT or single-criteria UPDATE or DELETE statement
-             many times against a series
-             of parameter sets.   The "executemany" style is supported by
-             all database backends, and works equally well for INSERT,
-             UPDATE, and DELETE, as it does not depend on a special SQL
-             syntax.  See :ref:`execute_multiple` for an introduction to
-             the traditional Core method of multiple parameter set invocation
-             using this system.
-
-         .. versionadded:: 0.8
-             Support for multiple-VALUES INSERT statements.
-
-        .. versionchanged:: 1.0.0 an INSERT that uses a multiple-VALUES
-           clause, even a list of length one,
-           implies that the :paramref:`.Insert.inline` flag is set to
-           True, indicating that the statement will not attempt to fetch
-           the "last inserted primary key" or other defaults.  The statement
-           deals with an arbitrary number of rows, so the
-           :attr:`.ResultProxy.inserted_primary_key` accessor does not apply.
-
-        .. versionchanged:: 1.0.0 A multiple-VALUES INSERT now supports
-           columns with Python side default values and callables in the
-           same way as that of an "executemany" style of invocation; the
-           callable is invoked for each row.   See :ref:`bug_3288`
-           for other details.
+         The above form would render a multiple VALUES statement similar to::
+
+                INSERT INTO users (name) VALUES
+                                (:name_1),
+                                (:name_2),
+                                (:name_3)
+
+         It is essential to note that **passing multiple values is
+         NOT the same as using traditional executemany() form**.  The above
+         syntax is a **special** syntax not typically used.  To emit an
+         INSERT statement against mutliple rows, the normal method is
+         to pass a mutiple values list to the :meth:`.Connection.execute`
+         method, which is supported by all database backends and is generally
+         more efficient for a very large number of parameters.
+
+           .. seealso::
+
+               :ref:`execute_multiple` - an introduction to
+               the traditional Core method of multiple parameter set
+               invocation for INSERTs and other statements.
+
+           .. versionchanged:: 1.0.0 an INSERT that uses a multiple-VALUES
+              clause, even a list of length one,
+              implies that the :paramref:`.Insert.inline` flag is set to
+              True, indicating that the statement will not attempt to fetch
+              the "last inserted primary key" or other defaults.  The
+              statement deals with an arbitrary number of rows, so the
+              :attr:`.ResultProxy.inserted_primary_key` accessor does not
+              apply.
+
+           .. versionchanged:: 1.0.0 A multiple-VALUES INSERT now supports
+              columns with Python side default values and callables in the
+              same way as that of an "executemany" style of invocation; the
+              callable is invoked for each row.   See :ref:`bug_3288`
+              for other details.
+
+         The :class:`.Update` construct supports a special form which is a
+         list of 2-tuples, which when provided must be passed in conjunction
+         with the
+         :paramref:`~sqlalchemy.sql.expression.update.preserve_parameter_order`
+         parameter.
+         This form causes the UPDATE statement to render the SET clauses
+         using the order of parameters given to :meth:`.Update.values`, rather
+         than the ordering of columns given in the :class:`.Table`.
+
+           .. versionadded:: 1.0.10 - added support for parameter-ordered
+              UPDATE statements via the
+              :paramref:`~sqlalchemy.sql.expression.update.preserve_parameter_order`
+              flag.
+
+           .. seealso::
+
+              :ref:`updates_order_parameters` - full example of the
+              :paramref:`~sqlalchemy.sql.expression.update.preserve_parameter_order`
+              flag
 
         .. seealso::
 
@@ -582,6 +609,7 @@ class Update(ValuesBase):
                  prefixes=None,
                  returning=None,
                  return_defaults=False,
+                 preserve_parameter_order=False,
                  **dialect_kw):
         """Construct an :class:`.Update` object.
 
@@ -644,6 +672,19 @@ class Update(ValuesBase):
           be available in the dictionary returned from
           :meth:`.ResultProxy.last_updated_params`.
 
+        :param preserve_parameter_order: if True, the update statement is
+          expected to receive parameters **only** via the :meth:`.Update.values`
+          method, and they must be passed as a Python ``list`` of 2-tuples.
+          The rendered UPDATE statement will emit the SET clause for each
+          referenced column maintaining this order.
+
+          .. versionadded:: 1.0.10
+
+          .. seealso::
+
+            :ref:`updates_order_parameters` - full example of the
+            :paramref:`~sqlalchemy.sql.expression.update.preserve_parameter_order` flag
+
         If both ``values`` and compile-time bind parameters are present, the
         compile-time bind parameters override the information specified
         within ``values`` on a per-key basis.
@@ -685,6 +726,7 @@ class Update(ValuesBase):
 
 
         """
+        self._preserve_parameter_order = preserve_parameter_order
         ValuesBase.__init__(self, table, values, prefixes)
         self._bind = bind
         self._returning = returning
index 676e45a24c74528937dc58461b0e3ae8762ebd5c..f5aa9f22898b4a807ebec1456b7dee8a954750d1 100644 (file)
@@ -451,7 +451,6 @@ def criterion_as_pairs(expression, consider_as_foreign_keys=None,
     return pairs
 
 
-
 class ClauseAdapter(visitors.ReplacingCloningVisitor):
     """Clones and modifies clauses based on column correspondence.
 
index c95b8d152a65e251e5ab949cfb576c8542fff7be..56386e8d2b676f9b34f19981d7c4ecdf521c0e1e 100644 (file)
@@ -1181,9 +1181,10 @@ class PostUpdateBatchingTest(fixtures.MappedTest):
             testing.db,
             sess.flush,
             CompiledSQL(
-                "UPDATE parent SET c1_id=:c1_id, c2_id=:c2_id, "
-                "c3_id=:c3_id WHERE parent.id = :parent_id",
-                lambda ctx: {'c2_id': c23.id, 'parent_id': p1.id, 'c1_id': c12.id, 'c3_id': c31.id}
+                "UPDATE parent SET c1_id=:c1_id, c2_id=:c2_id, c3_id=:c3_id "
+                "WHERE parent.id = :parent_id",
+                lambda ctx: {'c2_id': c23.id, 'parent_id': p1.id,
+                             'c1_id': c12.id, 'c3_id': c31.id}
             )
         )
 
@@ -1193,8 +1194,9 @@ class PostUpdateBatchingTest(fixtures.MappedTest):
             testing.db,
             sess.flush,
             CompiledSQL(
-                "UPDATE parent SET c1_id=:c1_id, c2_id=:c2_id, "
-                "c3_id=:c3_id WHERE parent.id = :parent_id",
-                lambda ctx: {'c2_id': None, 'parent_id': p1.id, 'c1_id': None, 'c3_id': None}
+                "UPDATE parent SET c1_id=:c1_id, c2_id=:c2_id, c3_id=:c3_id "
+                "WHERE parent.id = :parent_id",
+                lambda ctx: {'c2_id': None, 'parent_id': p1.id,
+                             'c1_id': None, 'c3_id': None}
             )
         )
index 973053947c118fc71991f512dc7f9441e51544fc..593714a06ecf62edf26bbb7c7924ced5ddf6a041 100644 (file)
@@ -1,10 +1,11 @@
-from sqlalchemy.testing import eq_, assert_raises, assert_raises_message
+from sqlalchemy.testing import eq_, assert_raises, assert_raises_message, is_
 from sqlalchemy.testing import fixtures
 from sqlalchemy import Integer, String, ForeignKey, or_, exc, \
     select, func, Boolean, case, text, column
 from sqlalchemy.orm import mapper, relationship, backref, Session, \
     joinedload, synonym, query
 from sqlalchemy import testing
+from sqlalchemy.testing import mock
 
 from sqlalchemy.testing.schema import Table, Column
 
@@ -609,6 +610,42 @@ class UpdateDeleteTest(fixtures.MappedTest):
                 synchronize_session='fetch')
         assert john not in sess
 
+    def test_update_unordered_dict(self):
+        User = self.classes.User
+        session = Session()
+
+        # Do an update using unordered dict and check that the parameters used
+        # are ordered in table order
+        with mock.patch.object(session, "execute") as exec_:
+            session.query(User).filter(User.id == 15).update(
+                {'name': 'foob', 'id': 123})
+            # Confirm that parameters are a dict instead of tuple or list
+            params_type = type(exec_.mock_calls[0][1][0].parameters)
+            is_(params_type, dict)
+
+    def test_update_preserve_parameter_order(self):
+        User = self.classes.User
+        session = Session()
+
+        # Do update using a tuple and check that order is preserved
+        with mock.patch.object(session, "execute") as exec_:
+            session.query(User).filter(User.id == 15).update(
+                (('id', 123), ('name', 'foob')),
+                update_args={"preserve_parameter_order": True})
+            cols = [c.key
+                    for c in exec_.mock_calls[0][1][0]._parameter_ordering]
+            eq_(['id', 'name'], cols)
+
+        # Now invert the order and use a list instead, and check that order is
+        # also preserved
+        with mock.patch.object(session, "execute") as exec_:
+            session.query(User).filter(User.id == 15).update(
+                [('name', 'foob'), ('id', 123)],
+                update_args={"preserve_parameter_order": True})
+            cols = [c.key
+                    for c in exec_.mock_calls[0][1][0]._parameter_ordering]
+            eq_(['name', 'id'], cols)
+
 
 class UpdateDeleteIgnoresLoadersTest(fixtures.MappedTest):
 
index 58c86613be21d69de1522c62f7ad6dfbe04d4cd1..3ab580b112059ba16b2117ccde630c39f64a39e4 100644 (file)
@@ -4,6 +4,7 @@ from sqlalchemy.dialects import mysql
 from sqlalchemy.engine import default
 from sqlalchemy.testing import AssertsCompiledSQL, eq_, fixtures
 from sqlalchemy.testing.schema import Table, Column
+from sqlalchemy import util
 
 
 class _UpdateFromTestBase(object):
@@ -32,6 +33,11 @@ class _UpdateFromTestBase(object):
                      test_needs_autoincrement=True),
               Column('address_id', None, ForeignKey('addresses.id')),
               Column('data', String(30)))
+        Table('update_w_default', metadata,
+              Column('id', Integer, primary_key=True),
+              Column('x', Integer),
+              Column('ycol', Integer, key='y'),
+              Column('data', String(30), onupdate=lambda: "hi"))
 
     @classmethod
     def fixtures(cls):
@@ -165,6 +171,124 @@ class UpdateTest(_UpdateFromTestBase, fixtures.TablesTest, AssertsCompiledSQL):
             table1.c.name: table1.c.name + 'lala',
             table1.c.myid: func.do_stuff(table1.c.myid, literal('hoho'))
         }
+
+        self.assert_compile(
+            update(
+                table1,
+                (table1.c.myid == func.hoho(4)) & (
+                    table1.c.name == literal('foo') +
+                    table1.c.name +
+                    literal('lala')),
+                values=values),
+            'UPDATE mytable '
+            'SET '
+            'myid=do_stuff(mytable.myid, :param_1), '
+            'name=(mytable.name || :name_1) '
+            'WHERE '
+            'mytable.myid = hoho(:hoho_1) AND '
+            'mytable.name = :param_2 || mytable.name || :param_3')
+
+    def test_update_ordered_parameters_1(self):
+        table1 = self.tables.mytable
+
+        # Confirm that we can pass values as list value pairs
+        # note these are ordered *differently* from table.c
+        values = [
+            (table1.c.name, table1.c.name + 'lala'),
+            (table1.c.myid, func.do_stuff(table1.c.myid, literal('hoho'))),
+        ]
+        self.assert_compile(
+            update(
+                table1,
+                (table1.c.myid == func.hoho(4)) & (
+                    table1.c.name == literal('foo') +
+                    table1.c.name +
+                    literal('lala')),
+                preserve_parameter_order=True,
+                values=values),
+            'UPDATE mytable '
+            'SET '
+            'name=(mytable.name || :name_1), '
+            'myid=do_stuff(mytable.myid, :param_1) '
+            'WHERE '
+            'mytable.myid = hoho(:hoho_1) AND '
+            'mytable.name = :param_2 || mytable.name || :param_3')
+
+    def test_update_ordered_parameters_2(self):
+        table1 = self.tables.mytable
+
+        # Confirm that we can pass values as list value pairs
+        # note these are ordered *differently* from table.c
+        values = [
+            (table1.c.name, table1.c.name + 'lala'),
+            ('description', 'some desc'),
+            (table1.c.myid, func.do_stuff(table1.c.myid, literal('hoho')))
+        ]
+        self.assert_compile(
+            update(
+                table1,
+                (table1.c.myid == func.hoho(4)) & (
+                    table1.c.name == literal('foo') +
+                    table1.c.name +
+                    literal('lala')),
+                preserve_parameter_order=True).values(values),
+            'UPDATE mytable '
+            'SET '
+            'name=(mytable.name || :name_1), '
+            'description=:description, '
+            'myid=do_stuff(mytable.myid, :param_1) '
+            'WHERE '
+            'mytable.myid = hoho(:hoho_1) AND '
+            'mytable.name = :param_2 || mytable.name || :param_3')
+
+    def test_update_ordered_parameters_fire_onupdate(self):
+        table = self.tables.update_w_default
+
+        values = [
+            (table.c.y, table.c.x + 5),
+            ('x', 10)
+        ]
+
+        self.assert_compile(
+            table.update(preserve_parameter_order=True).values(values),
+            "UPDATE update_w_default SET ycol=(update_w_default.x + :x_1), "
+            "x=:x, data=:data"
+        )
+
+    def test_update_ordered_parameters_override_onupdate(self):
+        table = self.tables.update_w_default
+
+        values = [
+            (table.c.y, table.c.x + 5),
+            (table.c.data, table.c.x + 10),
+            ('x', 10)
+        ]
+
+        self.assert_compile(
+            table.update(preserve_parameter_order=True).values(values),
+            "UPDATE update_w_default SET ycol=(update_w_default.x + :x_1), "
+            "data=(update_w_default.x + :x_2), x=:x"
+        )
+
+    def test_update_preserve_order_reqs_listtups(self):
+        table1 = self.tables.mytable
+        testing.assert_raises_message(
+            ValueError,
+            "When preserve_parameter_order is True, values\(\) "
+            "only accepts a list of 2-tuples",
+            table1.update(preserve_parameter_order=True).values,
+            {"description": "foo", "name": "bar"}
+        )
+
+    def test_update_ordereddict(self):
+        table1 = self.tables.mytable
+
+        # Confirm that ordered dicts are treated as normal dicts,
+        # columns sorted in table order
+        values = util.OrderedDict((
+            (table1.c.name, table1.c.name + 'lala'),
+            (table1.c.myid, func.do_stuff(table1.c.myid, literal('hoho')))))
+
         self.assert_compile(
             update(
                 table1,