diff --git a/pandas/io/sql.py b/pandas/io/sql.py index df9c7e28bff69..afd045bd8bb2b 100644 --- a/pandas/io/sql.py +++ b/pandas/io/sql.py @@ -47,10 +47,6 @@ from pandas.util.version import Version -class SQLAlchemyRequired(ImportError): - pass - - class DatabaseError(IOError): pass @@ -58,26 +54,6 @@ class DatabaseError(IOError): # ----------------------------------------------------------------------------- # -- Helper functions -_SQLALCHEMY_INSTALLED: bool | None = None - - -def _is_sqlalchemy_connectable(con): - global _SQLALCHEMY_INSTALLED - if _SQLALCHEMY_INSTALLED is None: - try: - import sqlalchemy - - _SQLALCHEMY_INSTALLED = True - except ImportError: - _SQLALCHEMY_INSTALLED = False - - if _SQLALCHEMY_INSTALLED: - import sqlalchemy # noqa: F811 - - return isinstance(con, sqlalchemy.engine.Connectable) - else: - return False - def _gt14() -> bool: """ @@ -303,21 +279,14 @@ def read_sql_table( -------- >>> pd.read_sql_table('table_name', 'postgres:///db_name') # doctest:+SKIP """ - con = _engine_builder(con) - if not _is_sqlalchemy_connectable(con): - raise NotImplementedError( - "read_sql_table only supported for SQLAlchemy connectable." - ) - import sqlalchemy - from sqlalchemy.schema import MetaData + from sqlalchemy.exc import InvalidRequestError - meta = MetaData(con, schema=schema) + pandas_sql = pandasSQL_builder(con, schema=schema) try: - meta.reflect(only=[table_name], views=True) - except sqlalchemy.exc.InvalidRequestError as err: + pandas_sql.meta.reflect(only=[table_name], views=True) + except InvalidRequestError as err: raise ValueError(f"Table {table_name} not found") from err - pandas_sql = SQLDatabase(con, meta=meta) table = pandas_sql.read_table( table_name, index_col=index_col, @@ -752,37 +721,29 @@ def has_table(table_name: str, con, schema: str | None = None): table_exists = has_table -def _engine_builder(con): - """ - Returns a SQLAlchemy engine from a URI (if con is a string) - else it just return con without modifying it. - """ - global _SQLALCHEMY_INSTALLED - if isinstance(con, str): - try: - import sqlalchemy - except ImportError: - _SQLALCHEMY_INSTALLED = False - else: - con = sqlalchemy.create_engine(con) - return con - - return con - - -def pandasSQL_builder(con, schema: str | None = None, meta=None): +def pandasSQL_builder(con, schema: str | None = None): """ Convenience function to return the correct PandasSQL subclass based on the provided parameters. """ - con = _engine_builder(con) - if _is_sqlalchemy_connectable(con): - return SQLDatabase(con, schema=schema, meta=meta) - elif isinstance(con, str): - raise ImportError("Using URI string without sqlalchemy installed.") - else: + import sqlite3 + + if isinstance(con, sqlite3.Connection) or con is None: return SQLiteDatabase(con) + sqlalchemy = import_optional_dependency("sqlalchemy") + + if isinstance(con, str): + con = sqlalchemy.create_engine(con) + + if isinstance(con, sqlalchemy.engine.Connectable): + return SQLDatabase(con, schema=schema) + + raise ValueError( + "pandas only support SQLAlchemy connectable(engine/connection) or" + "database string URI or sqlite3 DBAPI2 connection" + ) + class SQLTable(PandasObject): """ @@ -1387,21 +1348,14 @@ class SQLDatabase(PandasSQL): schema : string, default None Name of SQL schema in database to write to (if database flavor supports this). If None, use default schema (default). - meta : SQLAlchemy MetaData object, default None - If provided, this MetaData object is used instead of a newly - created. This allows to specify database flavor specific - arguments in the MetaData object. """ - def __init__(self, engine, schema: str | None = None, meta=None): - self.connectable = engine - if not meta: - from sqlalchemy.schema import MetaData - - meta = MetaData(self.connectable, schema=schema) + def __init__(self, engine, schema: str | None = None): + from sqlalchemy.schema import MetaData - self.meta = meta + self.connectable = engine + self.meta = MetaData(self.connectable, schema=schema) @contextmanager def run_transaction(self): diff --git a/pandas/tests/io/test_sql.py b/pandas/tests/io/test_sql.py index 9320bf385ce0a..df9ba5f206146 100644 --- a/pandas/tests/io/test_sql.py +++ b/pandas/tests/io/test_sql.py @@ -1372,8 +1372,7 @@ def test_sql_open_close(self): @pytest.mark.skipif(SQLALCHEMY_INSTALLED, reason="SQLAlchemy is installed") def test_con_string_import_error(self): conn = "mysql://root@localhost/pandas" - msg = "Using URI string without sqlalchemy installed" - with pytest.raises(ImportError, match=msg): + with pytest.raises(ImportError, match="SQLAlchemy"): sql.read_sql("SELECT * FROM iris", conn) def test_read_sql_delegate(self): @@ -2314,8 +2313,7 @@ def test_schema_support(self): # because of transactional schemas if isinstance(self.conn, sqlalchemy.engine.Engine): engine2 = self.connect() - meta = sqlalchemy.MetaData(engine2, schema="other") - pdsql = sql.SQLDatabase(engine2, meta=meta) + pdsql = sql.SQLDatabase(engine2, schema="other") pdsql.to_sql(df, "test_schema_other2", index=False) pdsql.to_sql(df, "test_schema_other2", index=False, if_exists="replace") pdsql.to_sql(df, "test_schema_other2", index=False, if_exists="append")