From: Mike Bayer Date: Wed, 11 Nov 2020 16:41:25 +0000 (-0500) Subject: Allow multiple returning() calls X-Git-Tag: rel_1_4_0b2~158 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=5b674ac6319a373e21dac1e4faf37c7354e61429;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Allow multiple returning() calls Multiple calls to "returning", e.g. :meth:`_sql.Insert.returning`, may now be chained to add new columns to the RETURNING clause. Fixes: #5695 Change-Id: Ie2dac4162f686c730e000e31dccfb38f9ce9c96e --- diff --git a/doc/build/changelog/unreleased_14/5695.rst b/doc/build/changelog/unreleased_14/5695.rst new file mode 100644 index 0000000000..e76c4d46fc --- /dev/null +++ b/doc/build/changelog/unreleased_14/5695.rst @@ -0,0 +1,7 @@ +.. change:: + :tags: usecase, sql + :tickets: 5695 + + Multiple calls to "returning", e.g. :meth:`_sql.Insert.returning`, + may now be chained to add new columns to the RETURNING clause. + diff --git a/doc/build/tutorial/data.rst b/doc/build/tutorial/data.rst index 849b706cc1..55d65c4f47 100644 --- a/doc/build/tutorial/data.rst +++ b/doc/build/tutorial/data.rst @@ -219,14 +219,14 @@ construct automatically. ('sandy', 'sandy@squirrelpower.org')) COMMIT{stop} -Other INSERT Options -^^^^^^^^^^^^^^^^^^^^^ +.. _tutorial_insert_from_select: -A quick overview of some other patterns that are available with :func:`_sql.insert`: +INSERT...FROM SELECT +^^^^^^^^^^^^^^^^^^^^^ -* **INSERT..FROM SELECT** - the :class:`_sql.Insert` construct can compose - an INSERT that gets rows directly from a SELECT using the :meth:`_sql.Insert.from_select` - method:: +The :class:`_sql.Insert` construct can compose +an INSERT that gets rows directly from a SELECT using the :meth:`_sql.Insert.from_select` +method:: >>> select_stmt = select(user_table.c.id, user_table.c.name + "@aol.com") >>> insert_stmt = insert(address_table).from_select( @@ -237,26 +237,41 @@ A quick overview of some other patterns that are available with :func:`_sql.inse SELECT user_account.id, user_account.name || :name_1 AS anon_1 FROM user_account - .. +.. _tutorial_insert_returning: + +INSERT...RETURNING +^^^^^^^^^^^^^^^^^^^^^ + +The RETURNING clause for supported backends is used +automatically in order to retrieve the last inserted primary key value +as well as the values for server defaults. However the RETURNING clause +may also be specified explicitly using the :meth:`_sql.Insert.returning` +method; in this case, the :class:`_engine.Result` +object that's returned when the statement is executed has rows which +can be fetched. It is only supported for single-statement +forms, and for some backends may only support single-row INSERT statements +overall:: + + >>> insert_stmt = insert(address_table).returning(address_table.c.id, address_table.c.email_address) + >>> print(insert_stmt) + {opensql}INSERT INTO address (id, user_id, email_address) + VALUES (:id, :user_id, :email_address) + RETURNING address.id, address.email_address + -* **RETURNING clause** - the RETURNING clause for supported backends is used - automatically in order to retrieve the last inserted primary key value - as well as the values for server defaults. However the RETURNING clause - may also be specified explicitly using the :meth:`_sql.Insert.returning` - method; in this case, the :class:`_engine.Result` - object that's returned when the statement is executed has rows which - can be fetched. It is only supported for single-statement - forms, and for some backends may only support single-row INSERT statements - overall. It can also be combined with :meth:`_sql.Insert.from_select`, - as in the example below that builds upon the previous example:: +It can also be combined with :meth:`_sql.Insert.from_select`, +as in the example below that builds upon the example stated in +:ref:`tutorial_insert_from_select`:: + >>> select_stmt = select(user_table.c.id, user_table.c.name + "@aol.com") + >>> insert_stmt = insert(address_table).from_select( + ... ["user_id", "email_address"], select_stmt + ... ) >>> print(insert_stmt.returning(address_table.c.id, address_table.c.email_address)) {opensql}INSERT INTO address (user_id, email_address) SELECT user_account.id, user_account.name || :name_1 AS anon_1 FROM user_account RETURNING address.id, address.email_address - .. - .. seealso:: :class:`_sql.Insert` - in the SQL Expression API documentation diff --git a/lib/sqlalchemy/sql/dml.py b/lib/sqlalchemy/sql/dml.py index ddb85224aa..51a81fe021 100644 --- a/lib/sqlalchemy/sql/dml.py +++ b/lib/sqlalchemy/sql/dml.py @@ -208,6 +208,7 @@ class UpdateBase( named_with_column = False _return_defaults = None + _returning = () is_dml = True @@ -334,25 +335,41 @@ class UpdateBase( def returning(self, *cols): r"""Add a :term:`RETURNING` or equivalent clause to this statement. - e.g.:: + e.g.: + + .. sourcecode:: pycon+sql - stmt = table.update().\ - where(table.c.data == 'value').\ - values(status='X').\ - returning(table.c.server_flag, - table.c.updated_timestamp) + >>> stmt = ( + ... table.update() + ... .where(table.c.data == "value") + ... .values(status="X") + ... .returning(table.c.server_flag, table.c.updated_timestamp) + ... ) + >>> print(stmt) + UPDATE some_table SET status=:status + WHERE some_table.data = :data_1 + RETURNING some_table.server_flag, some_table.updated_timestamp - for server_flag, updated_timestamp in connection.execute(stmt): - print(server_flag, updated_timestamp) + The method may be invoked multiple times to add new entries to the + list of expressions to be returned. + + .. versionadded:: 1.4.0b2 The method may be invoked multiple times to + add new entries to the list of expressions to be returned. The given collection of column expressions should be derived from the table that is the target of the INSERT, UPDATE, or DELETE. While :class:`_schema.Column` objects are typical, the elements can also be - expressions:: + expressions: + + .. sourcecode:: pycon+sql - stmt = table.insert().returning( - (table.c.first_name + " " + table.c.last_name). - label('fullname')) + >>> stmt = table.insert().returning( + ... (table.c.first_name + " " + table.c.last_name).label("fullname") + ... ) + >>> print(stmt) + INSERT INTO some_table (first_name, last_name) + VALUES (:first_name, :last_name) + RETURNING some_table.first_name || :first_name_1 || some_table.last_name AS fullname Upon compilation, a RETURNING clause, or database equivalent, will be rendered within the statement. For INSERT and UPDATE, @@ -382,19 +399,14 @@ class UpdateBase( towards efficient fetching of server-side defaults and triggers for single-row INSERTs or UPDATEs. + :ref:`tutorial_insert_returning` - in the :ref:`unified_tutorial` - """ + """ # noqa E501 if self._return_defaults: raise exc.InvalidRequestError( "return_defaults() is already configured on this statement" ) - if self._returning: - util.warn( - "The returning() method does not currently support multiple " - "additive calls. The existing RETURNING clause being " - "replaced by new columns." - ) - self._returning = cols + self._returning += cols def _exported_columns_iterator(self): """Return the RETURNING columns as a sequence for this statement. @@ -403,7 +415,7 @@ class UpdateBase( """ - return self._returning or () + return self._returning @property def exported_columns(self): @@ -852,9 +864,10 @@ class Insert(ValuesBase): .. seealso:: - :ref:`coretutorial_insert_expressions` - in the 1.x tutorial + :ref:`coretutorial_insert_expressions` - in the + :ref:`1.x tutorial ` - :ref:`tutorial_core_insert` - in the 2.0 tutorial + :ref:`tutorial_core_insert` - in the :ref:`unified_tutorial` :param table: :class:`_expression.TableClause` @@ -1146,9 +1159,10 @@ class Update(DMLWhereBase, ValuesBase): .. seealso:: - :ref:`inserts_and_updates` - in the 1.x tutorial + :ref:`inserts_and_updates` - in the + :ref:`1.x tutorial ` - :ref:`tutorial_core_update_delete` - in the 2.0 tutorial + :ref:`tutorial_core_update_delete` - in the :ref:`unified_tutorial` @@ -1238,7 +1252,8 @@ class Update(DMLWhereBase, ValuesBase): self._preserve_parameter_order = preserve_parameter_order super(Update, self).__init__(table, values, prefixes) self._bind = bind - self._returning = returning + if returning: + self._returning = returning if whereclause is not None: self._where_criteria += ( coercions.expect(roles.WhereHavingRole, whereclause), @@ -1368,9 +1383,10 @@ class Delete(DMLWhereBase, UpdateBase): .. seealso:: - :ref:`inserts_and_updates` - in the 1.x tutorial + :ref:`inserts_and_updates` - in the + :ref:`1.x tutorial ` - :ref:`tutorial_core_update_delete` - in the 2.0 tutorial + :ref:`tutorial_core_update_delete` - in the :ref:`unified_tutorial` :param table: The table to delete rows from. @@ -1389,7 +1405,8 @@ class Delete(DMLWhereBase, UpdateBase): self.table = coercions.expect( roles.DMLTableRole, table, apply_propagate_attrs=self ) - self._returning = returning + if returning: + self._returning = returning if prefixes: self._setup_prefixes(prefixes) diff --git a/test/sql/test_returning.py b/test/sql/test_returning.py index 13a1b025d4..065205c45a 100644 --- a/test/sql/test_returning.py +++ b/test/sql/test_returning.py @@ -57,15 +57,11 @@ class ReturnCombinationTests(fixtures.TestBase, AssertsCompiledSQL): stmt = stmt.returning(t.c.x) - with testing.expect_warnings( - r"The returning\(\) method does not currently " - "support multiple additive calls." - ): - stmt = stmt.returning(t.c.y) + stmt = stmt.returning(t.c.y) self.assert_compile( stmt, - "%s RETURNING foo.y" % (sql_frag), + "%s RETURNING foo.x, foo.y" % (sql_frag), ) def test_return_no_return_defaults(self, table_fixture):