Modified the mariadb-connector driver to pre-load the ``cursor.rowcount``
value for all queries, to suit tools such as Pandas that hardcode to
calling :attr:`.Result.rowcount` in this way. SQLAlchemy normally pre-loads
``cursor.rowcount`` only for UPDATE/DELETE statements and otherwise passes
through to the DBAPI where it can return -1 if no value is available.
However, mariadb-connector does not support invoking ``cursor.rowcount``
after the cursor itself is closed, raising an error instead. Generic test
support has been added to ensure all backends support the allowing
:attr:`.Result.rowcount` to succceed (that is, returning an integer value
with -1 for "not available") after the result is closed.
This change also restores mariadb-connector to CI including
as part of the "dbdriver" suite; in
366a5e3e2e503a20ef0334fbf9f we had
taken it out of the DBAPI main job.
Additional fixes for the mariadb-connector dialect to support UUID data
values in the result in INSERT..RETURNING statements.
Added rounding to one remaining INSERT..RETURNING with floats test
to allow mariadbconnector to pass (likely similar issue as the one with
UUID but not worth making a new handler)
Fixes: #10396
Change-Id: Ic11b1b5d0c41356863829d0eacbb812d401e8dd1
--- /dev/null
+.. change::
+ :tags: bug, mariadb
+ :tickets: 10396
+
+ Modified the mariadb-connector driver to pre-load the ``cursor.rowcount``
+ value for all queries, to suit tools such as Pandas that hardcode to
+ calling :attr:`.Result.rowcount` in this way. SQLAlchemy normally pre-loads
+ ``cursor.rowcount`` only for UPDATE/DELETE statements and otherwise passes
+ through to the DBAPI where it can return -1 if no value is available.
+ However, mariadb-connector does not support invoking ``cursor.rowcount``
+ after the cursor itself is closed, raising an error instead. Generic test
+ support has been added to ensure all backends support the allowing
+ :attr:`.Result.rowcount` to succceed (that is, returning an integer
+ value with -1 for "not available") after the result is closed.
+
+
+
+.. change::
+ :tags: bug, mariadb
+
+ Additional fixes for the mariadb-connector dialect to support UUID data
+ values in the result in INSERT..RETURNING statements.
""" # noqa
import re
+from uuid import UUID as _python_UUID
from .base import MySQLCompiler
from .base import MySQLDialect
from .base import MySQLExecutionContext
from ... import sql
from ... import util
+from ...sql import sqltypes
+
mariadb_cpy_minimum_version = (1, 0, 1)
+class _MariaDBUUID(sqltypes.UUID[sqltypes._UUID_RETURN]):
+ # work around JIRA issue
+ # https://jira.mariadb.org/browse/CONPY-270. When that issue is fixed,
+ # this type can be removed.
+ def result_processor(self, dialect, coltype):
+ if self.as_uuid:
+
+ def process(value):
+ if value is not None:
+ if hasattr(value, "decode"):
+ value = value.decode("ascii")
+ value = _python_UUID(value)
+ return value
+
+ return process
+ else:
+
+ def process(value):
+ if value is not None:
+ if hasattr(value, "decode"):
+ value = value.decode("ascii")
+ value = str(_python_UUID(value))
+ return value
+
+ return process
+
+
class MySQLExecutionContext_mariadbconnector(MySQLExecutionContext):
_lastrowid = None
return self._dbapi_connection.cursor(buffered=True)
def post_exec(self):
+ self._rowcount = self.cursor.rowcount
+
if self.isinsert and self.compiled.postfetch_lastrowid:
self._lastrowid = self.cursor.lastrowid
+ @property
+ def rowcount(self):
+ if self._rowcount is not None:
+ return self._rowcount
+ else:
+ return self.cursor.rowcount
+
def get_lastrowid(self):
return self._lastrowid
supports_server_side_cursors = True
+ colspecs = util.update_copy(
+ MySQLDialect.colspecs, {sqltypes.Uuid: _MariaDBUUID}
+ )
+
@util.memoized_property
def _dbapi_version(self):
if self.dbapi and hasattr(self.dbapi, "__version__"):
driver = "cx_oracle"
- colspecs = OracleDialect.colspecs
- colspecs.update(
+ colspecs = util.update_copy(
+ OracleDialect.colspecs,
{
sqltypes.TIMESTAMP: _CXOracleTIMESTAMP,
sqltypes.Numeric: _OracleNumeric,
sqltypes.Uuid: _OracleUUID,
oracle.NCLOB: _OracleUnicodeTextNCLOB,
oracle.ROWID: _OracleRowid,
- }
+ },
)
execute_sequence_format = list
* :attr:`_engine.CursorResult.rowcount`
is *only* useful in conjunction
with an UPDATE or DELETE statement. Contrary to what the Python
- DBAPI says, it does *not* return the
+ DBAPI says, it does *not* reliably return the
number of rows available from the results of a SELECT statement
as DBAPIs cannot support this functionality when rows are
unbuffered.
True,
testing.requires.float_or_double_precision_behaves_generically,
),
- (Float(), 8.5514, False),
+ (Float(), 8.5514, True),
(
Float(8),
8.5514,
eq_(rows, self.data)
+ @testing.variation("statement", ["update", "delete", "insert", "select"])
+ @testing.variation("close_first", [True, False])
+ def test_non_rowcount_scenarios_no_raise(
+ self, connection, statement, close_first
+ ):
+ employees_table = self.tables.employees
+
+ # WHERE matches 3, 3 rows changed
+ department = employees_table.c.department
+
+ if statement.update:
+ r = connection.execute(
+ employees_table.update().where(department == "C"),
+ {"department": "Z"},
+ )
+ elif statement.delete:
+ r = connection.execute(
+ employees_table.delete().where(department == "C"),
+ {"department": "Z"},
+ )
+ elif statement.insert:
+ r = connection.execute(
+ employees_table.insert(),
+ [
+ {"employee_id": 25, "name": "none 1", "department": "X"},
+ {"employee_id": 26, "name": "none 2", "department": "Z"},
+ {"employee_id": 27, "name": "none 3", "department": "Z"},
+ ],
+ )
+ elif statement.select:
+ s = select(
+ employees_table.c.name, employees_table.c.department
+ ).where(employees_table.c.department == "C")
+ r = connection.execute(s)
+ r.all()
+ else:
+ statement.fail()
+
+ if close_first:
+ r.close()
+
+ assert r.rowcount in (-1, 3)
+
def test_update_rowcount1(self, connection):
employees_table = self.tables.employees
"""
- # mariadbconnector works. pyodbc we dont know, not supported in
- # testing.
+ # this may have worked with mariadbconnector at some point, but
+ # this now seems to not be the case. Since no other mysql driver
+ # supports these tests, that's fine
return exclusions.fails_on(
[
"+mysqldb",
"+pymysql",
"+asyncmy",
"+mysqlconnector",
+ "+mariadbconnector",
"+cymysql",
"+aiomysql",
]
mysql: pymysql
mysql: asyncmy
mysql: aiomysql
+ mysql: mariadb_connector
oracle: oracle
oracle: oracle_oracledb
memusage: WORKERS={env:TOX_WORKERS:-n2}
mysql: MYSQL={env:TOX_MYSQL:--db mysql}
- mysql: EXTRA_MYSQL_DRIVERS={env:EXTRA_MYSQL_DRIVERS:--dbdriver mysqldb --dbdriver pymysql}
-
- mysql: EXTRA_MYSQL_DRIVERS={env:EXTRA_MYSQL_DRIVERS:--dbdriver mysqldb --dbdriver pymysql --dbdriver asyncmy --dbdriver aiomysql}
+ mysql: EXTRA_MYSQL_DRIVERS={env:EXTRA_MYSQL_DRIVERS:--dbdriver mysqldb --dbdriver pymysql --dbdriver asyncmy --dbdriver aiomysql --dbdriver mariadbconnector}
mssql: MSSQL={env:TOX_MSSQL:--db mssql}
py{3,37,38,39,310,311}-mssql: EXTRA_MSSQL_DRIVERS={env:EXTRA_MSSQL_DRIVERS:--dbdriver pyodbc --dbdriver pymssql}