--- /dev/null
+.. change::
+ :tags: bug, mariadb, regression
+ :tickets: 10505
+
+ Established a workaround for what seems to be an intrinsic issue across
+ MySQL/MariaDB drivers where a RETURNING result for DELETE DML which returns
+ no rows using SQLAlchemy's "empty IN" criteria fails to provide a
+ cursor.description, which then yields result that returns no rows,
+ leading to regressions for the ORM that in the 2.0 series uses RETURNING
+ for bulk DELETE statements for the "synchronize session" feature. To
+ resolve, when the specific case of "no description when RETURNING was
+ given" is detected, an "empty result" with a correct cursor description is
+ generated and used in place of the non-working cursor.
)
""" # noqa
+from __future__ import annotations
from array import array as _array
from collections import defaultdict
from itertools import compress
import re
+from typing import cast
-from sqlalchemy import literal_column
-from sqlalchemy.sql import visitors
from . import reflection as _reflection
from .enumerated import ENUM
from .enumerated import SET
from .types import VARCHAR
from .types import YEAR
from ... import exc
+from ... import literal_column
from ... import log
from ... import schema as sa_schema
from ... import sql
from ... import util
+from ...engine import cursor as _cursor
from ...engine import default
from ...engine import reflection
from ...engine.reflection import ReflectionDefaults
from ...sql import roles
from ...sql import sqltypes
from ...sql import util as sql_util
+from ...sql import visitors
from ...sql.compiler import InsertmanyvaluesSentinelOpts
+from ...sql.compiler import SQLCompiler
from ...sql.schema import SchemaConst
from ...types import BINARY
from ...types import BLOB
class MySQLExecutionContext(default.DefaultExecutionContext):
+ def post_exec(self):
+ if (
+ self.isdelete
+ and cast(SQLCompiler, self.compiled).effective_returning
+ and not self.cursor.description
+ ):
+ # All MySQL/mariadb drivers appear to not include
+ # cursor.description for DELETE..RETURNING with no rows if the
+ # WHERE criteria is a straight "false" condition such as our EMPTY
+ # IN condition. manufacture an empty result in this case (issue
+ # #10505)
+ #
+ # taken from cx_Oracle implementation
+ self.cursor_fetch_strategy = (
+ _cursor.FullyBufferedCursorFetchStrategy(
+ self.cursor,
+ [
+ (entry.keyname, None)
+ for entry in cast(
+ SQLCompiler, self.compiled
+ )._result_columns
+ ],
+ [],
+ )
+ )
+
def create_server_side_cursor(self):
if self.dialect.supports_server_side_cursors:
return self._dbapi_connection.cursor(self.dialect._sscursor)
return self._dbapi_connection.cursor(buffered=True)
def post_exec(self):
+ super().post_exec()
+
self._rowcount = self.cursor.rowcount
if self.isinsert and self.compiled.postfetch_lastrowid:
from ..schema import Table
from ... import Integer
from ... import String
+from ... import testing
class SimpleUpdateDeleteTest(fixtures.TablesTest):
[(1, "d1"), (3, "d3")],
)
+ @testing.variation("criteria", ["rows", "norows", "emptyin"])
+ @testing.requires.update_returning
+ def test_update_returning(self, connection, criteria):
+ t = self.tables.plain_pk
+
+ stmt = t.update().returning(t.c.id, t.c.data)
+
+ if criteria.norows:
+ stmt = stmt.where(t.c.id == 10)
+ elif criteria.rows:
+ stmt = stmt.where(t.c.id == 2)
+ elif criteria.emptyin:
+ stmt = stmt.where(t.c.id.in_([]))
+ else:
+ criteria.fail()
+
+ r = connection.execute(stmt, dict(data="d2_new"))
+ assert not r.is_insert
+ assert r.returns_rows
+ eq_(r.keys(), ["id", "data"])
+
+ if criteria.rows:
+ eq_(r.all(), [(2, "d2_new")])
+ else:
+ eq_(r.all(), [])
+
+ eq_(
+ connection.execute(t.select().order_by(t.c.id)).fetchall(),
+ [(1, "d1"), (2, "d2_new"), (3, "d3")]
+ if criteria.rows
+ else [(1, "d1"), (2, "d2"), (3, "d3")],
+ )
+
+ @testing.variation("criteria", ["rows", "norows", "emptyin"])
+ @testing.requires.delete_returning
+ def test_delete_returning(self, connection, criteria):
+ t = self.tables.plain_pk
+
+ stmt = t.delete().returning(t.c.id, t.c.data)
+
+ if criteria.norows:
+ stmt = stmt.where(t.c.id == 10)
+ elif criteria.rows:
+ stmt = stmt.where(t.c.id == 2)
+ elif criteria.emptyin:
+ stmt = stmt.where(t.c.id.in_([]))
+ else:
+ criteria.fail()
+
+ r = connection.execute(stmt)
+ assert not r.is_insert
+ assert r.returns_rows
+ eq_(r.keys(), ["id", "data"])
+
+ if criteria.rows:
+ eq_(r.all(), [(2, "d2")])
+ else:
+ eq_(r.all(), [])
+
+ eq_(
+ connection.execute(t.select().order_by(t.c.id)).fetchall(),
+ [(1, "d1"), (3, "d3")]
+ if criteria.rows
+ else [(1, "d1"), (2, "d2"), (3, "d3")],
+ )
+
__all__ = ("SimpleUpdateDeleteTest",)