]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
revert MySQL to use DESCRIBE for has_table()
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 5 Jan 2023 04:32:23 +0000 (23:32 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 5 Jan 2023 14:38:04 +0000 (09:38 -0500)
Restored the behavior of :meth:`.Inspector.has_table` to report on
temporary tables for MySQL / MariaDB. This is currently the behavior for
all other included dialects, but was removed for MySQL in 1.4 due to no
longer using the DESCRIBE command; there was no documented support for temp
tables being reported by the :meth:`.Inspector.has_table` method in this
version or on any previous version, so the previous behavior was undefined.

As SQLAlchemy 2.0 has added formal support for temp table status via
:meth:`.Inspector.has_table`, the MySQL /MariaDB dialect has been reverted
to use the "DESCRIBE" statement as it did in the SQLAlchemy 1.3 series and
previously, and test support is added to include MySQL / MariaDB for
this behavior.   The previous issues with ROLLBACK being emitted which
1.4 sought to improve upon don't apply in SQLAlchemy 2.0 due to
simplifications in how :class:`.Connection` handles transactions.

DESCRIBE is necessary as MariaDB in particular has no consistently
available public information schema of any kind in order to report on temp
tables other than DESCRIBE/SHOW COLUMNS, which rely on throwing an error
in order to report no results.

Fixes: #9058
Change-Id: Ic511bd5989ec17beb37b7cddd913732b626af0e6

doc/build/changelog/unreleased_20/9058.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/mysql/base.py
lib/sqlalchemy/engine/interfaces.py
lib/sqlalchemy/engine/reflection.py
test/requirements.py

diff --git a/doc/build/changelog/unreleased_20/9058.rst b/doc/build/changelog/unreleased_20/9058.rst
new file mode 100644 (file)
index 0000000..7ea1a4d
--- /dev/null
@@ -0,0 +1,23 @@
+.. change::
+    :tags: bug, mysql
+    :tickets: 9058
+
+    Restored the behavior of :meth:`.Inspector.has_table` to report on
+    temporary tables for MySQL / MariaDB. This is currently the behavior for
+    all other included dialects, but was removed for MySQL in 1.4 due to no
+    longer using the DESCRIBE command; there was no documented support for temp
+    tables being reported by the :meth:`.Inspector.has_table` method in this
+    version or on any previous version, so the previous behavior was undefined.
+
+    As SQLAlchemy 2.0 has added formal support for temp table status via
+    :meth:`.Inspector.has_table`, the MySQL /MariaDB dialect has been reverted
+    to use the "DESCRIBE" statement as it did in the SQLAlchemy 1.3 series and
+    previously, and test support is added to include MySQL / MariaDB for
+    this behavior.   The previous issues with ROLLBACK being emitted which
+    1.4 sought to improve upon don't apply in SQLAlchemy 2.0 due to
+    simplifications in how :class:`.Connection` handles transactions.
+
+    DESCRIBE is necessary as MariaDB in particular has no consistently
+    available public information schema of any kind in order to report on temp
+    tables other than DESCRIBE/SHOW COLUMNS, which rely on throwing an error
+    in order to report no results.
index fbd20a0ba89ca57f962070f362f401838ec6e6a2..9de6169848bfb335502fdc7659c2e42441934af0 100644 (file)
@@ -1013,7 +1013,6 @@ from itertools import compress
 import re
 
 from sqlalchemy import literal_column
-from sqlalchemy import text
 from sqlalchemy.sql import visitors
 from . import reflection as _reflection
 from .enumerated import ENUM
@@ -1070,7 +1069,6 @@ from ...sql import operators
 from ...sql import roles
 from ...sql import sqltypes
 from ...sql import util as sql_util
-from ...sql.sqltypes import Unicode
 from ...types import BINARY
 from ...types import BLOB
 from ...types import BOOLEAN
@@ -2671,21 +2669,30 @@ class MySQLDialect(default.DefaultDialect):
         if schema is None:
             schema = self.default_schema_name
 
-        rs = connection.execute(
-            text(
-                "SELECT COUNT(*) FROM information_schema.tables WHERE "
-                "table_schema = :table_schema AND "
-                "table_name = :table_name"
-            ).bindparams(
-                sql.bindparam("table_schema", type_=Unicode),
-                sql.bindparam("table_name", type_=Unicode),
-            ),
-            {
-                "table_schema": str(schema),
-                "table_name": str(table_name),
-            },
+        assert schema is not None
+
+        full_name = ".".join(
+            self.identifier_preparer._quote_free_identifiers(
+                schema, table_name
+            )
         )
-        return bool(rs.scalar())
+
+        # DESCRIBE *must* be used because there is no information schema
+        # table that returns information on temp tables that is consistently
+        # available on MariaDB / MySQL / engine-agnostic etc.
+        # therefore we have no choice but to use DESCRIBE and an error catch
+        # to detect "False".  See issue #9058
+
+        try:
+            with connection.exec_driver_sql(
+                f"DESCRIBE {full_name}",
+                execution_options={"skip_user_error_events": True},
+            ) as rs:
+                return rs.fetchone() is not None
+        except exc.DBAPIError as e:
+            if self._extract_error_code(e.orig) == 1146:
+                return False
+            raise
 
     @reflection.cache
     def has_sequence(self, connection, sequence_name, schema=None, **kw):
index c32b1a1fbd0b52424ebda4253bdb5b6cd4169f27..c1de13221b08d49c41da7fef4559035679ed2af8 100644 (file)
@@ -1790,15 +1790,19 @@ class Dialect(EventTarget):
            published so that third-party dialects may provide an
            implementation. It is **not** the public API for checking for table
            presence. Please use the :meth:`.Inspector.has_table` method.
-           Alternatively, for legacy cross-compatibility, the
-           :meth:`_engine.Engine.has_table` method may be used.
 
-        .. versionchanged:: 2.0
+        .. versionchanged:: 2.0:: :meth:`_engine.Dialect.has_table` now
+           formally supports checking for additional table-like objects:
 
-            The :meth:`_engine.Dialect.has_table` method should also check
-            for the presence of views.  In previous versions this
-            behavior was dialect specific. New dialect suite tests were added
-            to ensure that dialects conform with this behavior consistently.
+           * any type of views (plain or materialized)
+           * temporary tables of any kind
+
+           Previously, these two checks were not formally specified and
+           different dialects would vary in their behavior.   The dialect
+           testing suite now includes tests for all of these object types,
+           and dialects to the degree that the backing database supports views
+           or temporary tables should seek to support locating these objects
+           for full compliance.
 
         """
 
index 7e1fca0e513d9d8e177e348313afed3c1dd3dfa0..050f18f2fcb6928fd00df16014c0c047809bfbb2 100644 (file)
@@ -401,7 +401,8 @@ class Inspector(inspection.Inspectable["Inspector"]):
     def has_table(
         self, table_name: str, schema: Optional[str] = None, **kw: Any
     ) -> bool:
-        r"""Return True if the backend has a table or view of the given name.
+        r"""Return True if the backend has a table, view, or temporary
+        table of the given name.
 
         :param table_name: name of the table to check
         :param schema: schema name to query, if not the default schema.
@@ -412,11 +413,17 @@ class Inspector(inspection.Inspectable["Inspector"]):
         .. versionadded:: 1.4 - the :meth:`.Inspector.has_table` method
            replaces the :meth:`_engine.Engine.has_table` method.
 
-        .. versionchanged:: 2.0:: The method checks also for any type of
-           views (plain or materialized).
-           In previous version this behaviour was dialect specific. New
-           dialect suite tests were added to ensure all dialect conform with
-           this behaviour.
+        .. versionchanged:: 2.0:: :meth:`.Inspector.has_table` now formally
+           supports checking for additional table-like objects:
+
+           * any type of views (plain or materialized)
+           * temporary tables of any kind
+
+           Previously, these two checks were not formally specified and
+           different dialects would vary in their behavior.   The dialect
+           testing suite now includes tests for all of these object types
+           and should be supported by all SQLAlchemy-included dialects.
+           Support among third party dialects may be lagging, however.
 
         """
         with self._operation_context() as conn:
index 2e0ad0afe763bb4775b9ee786888a6ab8845a109..d91f056732d160ff32944576c2b21423b86041a8 100644 (file)
@@ -625,9 +625,9 @@ class DefaultRequirements(SuiteRequirements):
 
         """
 
-        return only_on(["sqlite", "oracle", "postgresql", "mssql"]) + skip_if(
-            self._sqlite_file_db
-        )
+        # SQLite file db "works", but there's some kind of issue when
+        # run in the full test suite that causes it not to work
+        return skip_if(self._sqlite_file_db)
 
     @property
     def temporary_views(self):