:ticket:`10300`
+.. _change_8130:
+
+Explicit USING support for DELETE (MySQL, PostgreSQL)
+------------------------------------------------------
+
+The :meth:`_sql.Delete.using` method has been added, allowing explicit
+``USING`` expressions to be specified in DELETE statements. This is
+useful for backend-specific multiple-table DELETE forms where the secondary
+FROM clause needs to be stated explicitly, such as joined DELETE on
+MySQL/MariaDB and PostgreSQL.
+
+Previously, multi-table DELETE was supported by inferring extra FROM entries
+from the WHERE clause, which works for simple cases. The new
+:meth:`_sql.Delete.using` method allows more complex expressions such as
+explicit joins to be stated::
+
+ from sqlalchemy import delete, table, column
+
+ user_table = table("users", column("id"), column("name"))
+ address_table = table("addresses", column("id"), column("user_id"), column("email"))
+
+ stmt = (
+ delete(user_table)
+ .using(
+ user_table.outerjoin(
+ address_table,
+ user_table.c.id == address_table.c.user_id,
+ )
+ )
+ .where(address_table.c.email == "patrick@aol.com")
+ )
+
+On MySQL/MariaDB, the above renders as:
+
+.. sourcecode:: sql
+
+ DELETE FROM users USING users LEFT OUTER JOIN addresses
+ ON users.id = addresses.user_id
+ WHERE addresses.email = %s
+
+On PostgreSQL, a similar form is rendered using the PostgreSQL-specific
+``DELETE .. USING`` syntax.
+
+.. seealso::
+
+ :ref:`tutorial_multi_table_deletes` - updated tutorial section for
+ multi-table deletes
+
+:ticket:`8130`
+
+
PostgreSQL
==========
--- /dev/null
+.. change::
+ :tags: usecase, sql
+ :tickets: 8130
+
+ Added :meth:`_sql.Delete.using`, allowing explicit FROM expressions such as
+ joins to be rendered in backend-specific multiple-table DELETE forms
+ including MySQL/MariaDB ``DELETE .. USING``. Pull request courtesy
+ cjc0013.
+
+ .. seealso::
+
+ :ref:`change_8130`
{printsql}DELETE FROM user_account USING user_account, address
WHERE user_account.id = address.user_id AND address.email_address = %s
+For backends that support a full FROM expression in the ``USING`` clause,
+the :meth:`_sql.Delete.using` method may be used to state that expression
+explicitly, such as a MySQL joined DELETE::
+
+ >>> delete_stmt = (
+ ... delete(user_table)
+ ... .using(
+ ... user_table.outerjoin(
+ ... address_table,
+ ... user_table.c.id == address_table.c.user_id,
+ ... )
+ ... )
+ ... .where(address_table.c.email_address == "patrick@aol.com")
+ ... )
+ >>> print(delete_stmt.compile(dialect=mysql.dialect()))
+ {printsql}DELETE FROM user_account USING user_account LEFT OUTER JOIN address
+ ON user_account.id = address.user_id
+ WHERE address.email_address = %s
+
+.. versionadded:: 2.1 Added :meth:`_sql.Delete.using` to support explicit
+ USING for DELETE statements on supported backends
+
.. _tutorial_update_delete_rowcount:
Getting Affected Row Count from UPDATE, DELETE
.. versionchanged:: 2.1 the :func:`_mysql.limit()` extension supersedes the
previous use of ``mysql_limit``
+* DELETE with an explicit ``USING`` expression such as a JOIN, use
+ :meth:`_sql.Delete.using`. See :ref:`tutorial_multi_table_deletes`.
+
+ .. versionadded:: 2.1
+
* optimizer hints, use :meth:`_expression.Select.prefix_with` and
:meth:`_query.Query.prefix_with`::
) -> str:
"""Render the DELETE .. USING clause specific to MySQL."""
kw["asfrom"] = True
+ if any(
+ from_table in elem._cloned_set
+ for extra_from in extra_froms
+ for elem in sql_util.surface_selectables_only(extra_from)
+ ):
+ froms = extra_froms
+ else:
+ froms = [from_table] + extra_froms
+
return "USING " + ", ".join(
t._compiler_dispatch(self, fromhints=from_hints, **kw)
- for t in [from_table] + extra_froms
+ for t in froms
)
def visit_empty_set_expr(
from ._typing import _DMLColumnArgument
from ._typing import _DMLColumnKeyMapping
from ._typing import _DMLTableArgument
+ from ._typing import _FromClauseArgument
from ._typing import _T0 # noqa
from ._typing import _T1 # noqa
from ._typing import _T2 # noqa
]
def _make_extra_froms(
- self, statement: DMLWhereBase
+ self,
+ statement: DMLWhereBase,
+ explicit_froms: Sequence[FromClause] = (),
) -> Tuple[FromClause, List[FromClause]]:
froms: List[FromClause] = []
primary_table = all_tables[0]
seen = {primary_table}
+ def _consider_from(
+ from_: FromClause, include_surface_selectables: bool = False
+ ) -> None:
+ if not seen.intersection(from_._cloned_set):
+ froms.append(from_)
+ seen.update(from_._cloned_set)
+ if include_surface_selectables:
+ for elem in sql_util.surface_selectables_only(from_):
+ seen.update(elem._cloned_set)
+
+ for from_ in explicit_froms:
+ _consider_from(from_, include_surface_selectables=True)
+
consider = statement._where_criteria
if self._dict_parameters:
consider += tuple(self._dict_parameters.values())
for crit in consider:
for item in _from_objects(crit):
- if not seen.intersection(item._cloned_set):
- froms.append(item)
- seen.update(item._cloned_set)
+ _consider_from(item)
froms.extend(all_tables[1:])
return primary_table, froms
self.statement = statement
self.isdelete = True
- t, ef = self._make_extra_froms(statement)
+ t, ef = self._make_extra_froms(statement, statement._extra_froms)
self._primary_table = t
self._extra_froms = ef
self.is_multitable = ef
_traverse_internals = (
[
("table", InternalTraversal.dp_clauseelement),
+ ("_extra_froms", InternalTraversal.dp_clauseelement_tuple),
("_where_criteria", InternalTraversal.dp_clauseelement_tuple),
("_returning", InternalTraversal.dp_clauseelement_tuple),
("_hints", InternalTraversal.dp_table_hint_list),
{"post_criteria": "_post_criteria_clause"}
)
+ _extra_froms: Tuple[FromClause, ...] = ()
+
def __init__(self, table: _DMLTableArgument):
self.table = coercions.expect(
roles.DMLTableRole, table, apply_propagate_attrs=self
)
+ @_generative
+ def using(self, *froms: _FromClauseArgument) -> Self:
+ r"""Add one or more explicit ``USING`` expressions to this DELETE.
+
+ This method may be used for backend-specific multiple-table DELETE
+ forms where the secondary FROM expression needs to be stated
+ explicitly, such as MySQL's ``DELETE FROM table USING <join>`` form.
+
+ .. versionadded:: 2.1
+
+ .. seealso::
+
+ :ref:`tutorial_multi_table_deletes`
+
+ """
+
+ self._extra_froms += tuple(
+ coercions.expect(
+ roles.FromClauseRole, from_, apply_propagate_attrs=self
+ )
+ for from_ in froms
+ )
+ return self
+
def _apply_syntax_extension_to_self(
self, extension: SyntaxExtension
) -> None:
table_b.delete().with_dialect_options(sqlite_foo="some value"),
table_b.delete().where(table_b.c.a == 5),
table_b.delete().where(table_b.c.b == 5),
+ table_b.delete().using(table_a),
+ table_b.delete().using(
+ table_b.join(table_a, table_b.c.a == table_a.c.a)
+ ),
),
lambda: (
values(
from sqlalchemy import String
from sqlalchemy import testing
from sqlalchemy.dialects import mysql
+from sqlalchemy.dialects import postgresql
from sqlalchemy.engine import default
from sqlalchemy.testing import assert_raises_message
from sqlalchemy.testing import AssertsCompiledSQL
"WHERE mytable.myid = myothertable.otherid",
)
+ def test_delete_explicit_using(self):
+ table1, table2 = self.tables.mytable, self.tables.myothertable
+
+ stmt = table1.delete().using(table2).where(table1.c.myid == 7)
+ self.assert_compile(
+ stmt,
+ "DELETE FROM mytable , myothertable "
+ "WHERE mytable.myid = :myid_1",
+ checkparams={"myid_1": 7},
+ )
+
+ def test_delete_explicit_using_dedupe_implicit_from(self):
+ table1, table2 = self.tables.mytable, self.tables.myothertable
+
+ stmt = (
+ table1.delete()
+ .using(table2)
+ .where(table1.c.myid == table2.c.otherid)
+ )
+ self.assert_compile(
+ stmt,
+ "DELETE FROM mytable , myothertable "
+ "WHERE mytable.myid = myothertable.otherid",
+ )
+
+ def test_delete_explicit_using_join_mysql(self):
+ table1, table2 = self.tables.mytable, self.tables.myothertable
+
+ stmt = (
+ table1.delete()
+ .using(table1.outerjoin(table2, table1.c.myid == table2.c.otherid))
+ .where(table2.c.othername == None)
+ )
+ self.assert_compile(
+ stmt,
+ "DELETE FROM mytable USING mytable LEFT OUTER JOIN myothertable "
+ "ON mytable.myid = myothertable.otherid "
+ "WHERE myothertable.othername IS NULL",
+ dialect=mysql.dialect(),
+ )
+
+ def test_delete_explicit_using_join_postgresql(self):
+ table1, table2 = self.tables.mytable, self.tables.myothertable
+
+ stmt = (
+ table1.delete()
+ .using(table2.outerjoin(table1, table1.c.myid == table2.c.otherid))
+ .where(table2.c.othername == None)
+ )
+ self.assert_compile(
+ stmt,
+ "DELETE FROM mytable USING myothertable LEFT OUTER JOIN mytable "
+ "ON mytable.myid = myothertable.otherid "
+ "WHERE myothertable.othername IS NULL",
+ dialect=postgresql.dialect(),
+ )
+
def test_correlation_to_extra(self):
table1, table2 = self.tables.mytable, self.tables.myothertable