From: Mike Bayer Date: Wed, 14 Apr 2021 16:12:08 +0000 (-0400) Subject: Explicitly test for Connection in dialect.has_table() X-Git-Tag: rel_1_4_8~7 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=b26cf96462b195a4c12ccdf8283ef028f91eb872;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Explicitly test for Connection in dialect.has_table() The :meth:`_engine.Dialect.has_table` method now raises an informative exception if a non-Connection is passed to it, as this incorrect behavior seems to be common. This method is not intended for external use outside of a dialect. Please use the :meth:`.Inspector.has_table` method or for cross-compatibility with older SQLAlchemy versions, the :meth:`_engine.Engine.has_table` method. Fixes: #5780 Fixes: #6062 Fixes: #6260 Change-Id: I9b2439675167019b68d682edee3dcdcfce836987 --- diff --git a/doc/build/changelog/unreleased_14/assert_ht_connection.rst b/doc/build/changelog/unreleased_14/assert_ht_connection.rst new file mode 100644 index 0000000000..8e79c6f868 --- /dev/null +++ b/doc/build/changelog/unreleased_14/assert_ht_connection.rst @@ -0,0 +1,10 @@ +.. change:: + :tags: bug, engine + + The :meth:`_engine.Dialect.has_table` method now raises an informative + exception if a non-Connection is passed to it, as this incorrect behavior + seems to be common. This method is not intended for external use outside + of a dialect. Please use the :meth:`.Inspector.has_table` method + or for cross-compatibility with older SQLAlchemy versions, the + :meth:`_engine.Engine.has_table` method. + diff --git a/lib/sqlalchemy/dialects/firebird/base.py b/lib/sqlalchemy/dialects/firebird/base.py index fcf0c31d39..1f0c66ffb9 100644 --- a/lib/sqlalchemy/dialects/firebird/base.py +++ b/lib/sqlalchemy/dialects/firebird/base.py @@ -685,6 +685,7 @@ class FBDialect(default.DefaultDialect): def has_table(self, connection, table_name, schema=None): """Return ``True`` if the given table exists, ignoring the `schema`.""" + self._ensure_has_table_connection(connection) tblqry = """ SELECT 1 AS has_table FROM rdb$database diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index 1fef42c53e..e093224870 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -2799,6 +2799,7 @@ class MSDialect(default.DefaultDialect): @_db_plus_owner def has_table(self, connection, tablename, dbname, owner, schema): + self._ensure_has_table_connection(connection) if tablename.startswith("#"): # temporary table tables = ischema.mssql_temp_table_columns diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index 3966126e2d..f45d5ec919 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -2809,6 +2809,8 @@ class MySQLDialect(default.DefaultDialect): return connection.exec_driver_sql("SELECT DATABASE()").scalar() def has_table(self, connection, table_name, schema=None): + self._ensure_has_table_connection(connection) + if schema is None: schema = self.default_schema_name diff --git a/lib/sqlalchemy/dialects/oracle/base.py b/lib/sqlalchemy/dialects/oracle/base.py index 11ad61675e..6496c4f71f 100644 --- a/lib/sqlalchemy/dialects/oracle/base.py +++ b/lib/sqlalchemy/dialects/oracle/base.py @@ -1596,6 +1596,8 @@ class OracleDialect(default.DefaultDialect): raise NotImplementedError("implemented by cx_Oracle dialect") def has_table(self, connection, table_name, schema=None): + self._ensure_has_table_connection(connection) + if not schema: schema = self.default_schema_name cursor = connection.execute( diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index d0915a0c97..3b419ed169 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -3303,6 +3303,7 @@ class PGDialect(default.DefaultDialect): return bool(cursor.first()) def has_table(self, connection, table_name, schema=None): + self._ensure_has_table_connection(connection) # seems like case gets folded in pg_class... if schema is None: cursor = connection.execute( diff --git a/lib/sqlalchemy/dialects/sqlite/base.py b/lib/sqlalchemy/dialects/sqlite/base.py index 83c2a8ea75..28901d0282 100644 --- a/lib/sqlalchemy/dialects/sqlite/base.py +++ b/lib/sqlalchemy/dialects/sqlite/base.py @@ -1996,6 +1996,8 @@ class SQLiteDialect(default.DefaultDialect): return [row[0] for row in rs] def has_table(self, connection, table_name, schema=None): + self._ensure_has_table_connection(connection) + info = self._get_table_pragma( connection, "table_info", table_name, schema=schema ) diff --git a/lib/sqlalchemy/dialects/sybase/base.py b/lib/sqlalchemy/dialects/sybase/base.py index fd5e5b3b63..7c10973e62 100644 --- a/lib/sqlalchemy/dialects/sybase/base.py +++ b/lib/sqlalchemy/dialects/sybase/base.py @@ -1089,6 +1089,8 @@ class SybaseDialect(default.DefaultDialect): return [v["name"] for v in views] def has_table(self, connection, table_name, schema=None): + self._ensure_has_table_connection(connection) + try: self.get_table_id(connection, table_name, schema) except exc.NoSuchTableError: diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index d45b6d7a7e..375a93a484 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -22,6 +22,7 @@ import weakref from . import characteristics from . import cursor as _cursor from . import interfaces +from .base import Connection from .. import event from .. import exc from .. import pool @@ -323,6 +324,19 @@ class DefaultDialect(interfaces.Dialect): self._encoder = codecs.getencoder(self.encoding) self._decoder = processors.to_unicode_processor_factory(self.encoding) + def _ensure_has_table_connection(self, arg): + + if not isinstance(arg, Connection): + raise exc.ArgumentError( + "The argument passed to Dialect.has_table() should be a " + "%s, got %s. " + "Additionally, the Dialect.has_table() method is for " + "internal dialect " + "use only; please use " + "``inspect(some_engine).has_table(>)`` " + "for public API use." % (Connection, type(arg)) + ) + @util.memoized_property def _supports_statement_cache(self): return ( diff --git a/lib/sqlalchemy/engine/interfaces.py b/lib/sqlalchemy/engine/interfaces.py index 5e6cc524e9..47348ba767 100644 --- a/lib/sqlalchemy/engine/interfaces.py +++ b/lib/sqlalchemy/engine/interfaces.py @@ -467,12 +467,25 @@ class Dialect(object): raise NotImplementedError() def has_table(self, connection, table_name, schema=None, **kw): - """Check the existence of a particular table in the database. + """For internal dialect use, check the existence of a particular table + in the database. + + Given a :class:`_engine.Connection` object, a string table_name and + optional schema name, return True if the given table exists in the + database, False otherwise. + + This method serves as the underlying implementation of the + public facing :meth:`.Inspector.has_table` method, and is also used + internally to implement the "checkfirst" behavior for methods like + :meth:`_schema.Table.create` and :meth:`_schema.MetaData.create_all`. + + .. note:: This method is used internally by SQLAlchemy, and is + 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. - Given a :class:`_engine.Connection` object and a string - `table_name`, return True if the given table (possibly within - the specified `schema`) exists in the database, False - otherwise. """ raise NotImplementedError() diff --git a/lib/sqlalchemy/engine/reflection.py b/lib/sqlalchemy/engine/reflection.py index cff2095752..2cdd9ac3be 100644 --- a/lib/sqlalchemy/engine/reflection.py +++ b/lib/sqlalchemy/engine/reflection.py @@ -274,7 +274,8 @@ class Inspector(object): :param table_name: name of the table to check :param schema: schema name to query, if not the default schema. - .. versionadded:: 1.4 + .. versionadded:: 1.4 - the :meth:`.Inspector.has_table` method + replaces the :meth:`_engine.Engine.has_table` method. """ # TODO: info_cache? diff --git a/test/engine/test_execute.py b/test/engine/test_execute.py index 0de5ed1243..0b65b3055c 100644 --- a/test/engine/test_execute.py +++ b/test/engine/test_execute.py @@ -261,6 +261,13 @@ class ExecuteTest(fixtures.TablesTest): (4, "sally"), ] + def test_dialect_has_table_assertion(self): + with expect_raises_message( + tsa.exc.ArgumentError, + r"The argument passed to Dialect.has_table\(\) should be a", + ): + testing.db.dialect.has_table(testing.db, "some_table") + def test_exception_wrapping_dbapi(self): with testing.db.connect() as conn: # engine does not have exec_driver_sql