diff --git a/doc/source/io.rst b/doc/source/io.rst index 1aa6dde2c08b4..60a1ab01882a8 100644 --- a/doc/source/io.rst +++ b/doc/source/io.rst @@ -3159,9 +3159,9 @@ your database. .. versionadded:: 0.14.0 - -If SQLAlchemy is not installed a legacy fallback is provided for sqlite and mysql. -These legacy modes require Python database adapters which respect the `Python +If SQLAlchemy is not installed, a fallback is only provided for sqlite (and +for mysql for backwards compatibility, but this is deprecated). +This mode requires a Python database adapter which respect the `Python DB-API `__. See also some :ref:`cookbook examples ` for some advanced strategies. @@ -3335,9 +3335,14 @@ Engine connection examples engine = create_engine('sqlite:////absolute/path/to/foo.db') -Legacy -~~~~~~ -To use the sqlite support without SQLAlchemy, you can create connections like so: +Sqlite fallback +~~~~~~~~~~~~~~~ + +The use of sqlite is supported without using SQLAlchemy. +This mode requires a Python database adapter which respect the `Python +DB-API `__. + +You can create connections like so: .. code-block:: python @@ -3345,14 +3350,13 @@ To use the sqlite support without SQLAlchemy, you can create connections like so from pandas.io import sql cnx = sqlite3.connect(':memory:') -And then issue the following queries, remembering to also specify the flavor of SQL -you are using. +And then issue the following queries: .. code-block:: python - data.to_sql('data', cnx, flavor='sqlite') + data.to_sql('data', cnx) - sql.read_sql("SELECT * FROM data", cnx, flavor='sqlite') + sql.read_sql("SELECT * FROM data", cnx) .. _io.bigquery: diff --git a/doc/source/release.rst b/doc/source/release.rst index 728dddbe8b979..245c7492bffb9 100644 --- a/doc/source/release.rst +++ b/doc/source/release.rst @@ -246,6 +246,9 @@ Deprecations positional argument ``frame`` instead of ``data``. A ``FutureWarning`` is raised if the old ``data`` argument is used by name. (:issue:`6956`) +- The support for the 'mysql' flavor when using DBAPI connection objects has been deprecated. + MySQL will be further supported with SQLAlchemy engines (:issue:`6900`). + Prior Version Deprecations/Changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/source/v0.14.0.txt b/doc/source/v0.14.0.txt index cde6bf3bfd670..18e84426c6005 100644 --- a/doc/source/v0.14.0.txt +++ b/doc/source/v0.14.0.txt @@ -475,6 +475,9 @@ Deprecations returned if possible, otherwise a copy will be made. Previously the user could think that ``copy=False`` would ALWAYS return a view. (:issue:`6894`) +- The support for the 'mysql' flavor when using DBAPI connection objects has been deprecated. + MySQL will be further supported with SQLAlchemy engines (:issue:`6900`). + .. _whatsnew_0140.enhancements: Enhancements diff --git a/pandas/io/sql.py b/pandas/io/sql.py index c18a4aef5355b..7a604dcdaba5f 100644 --- a/pandas/io/sql.py +++ b/pandas/io/sql.py @@ -76,7 +76,7 @@ def _parse_date_columns(data_frame, parse_dates): return data_frame -def execute(sql, con, cur=None, params=None, flavor='sqlite'): +def execute(sql, con, cur=None, params=None): """ Execute the given SQL query using the provided connection object. @@ -84,24 +84,22 @@ def execute(sql, con, cur=None, params=None, flavor='sqlite'): ---------- sql : string Query to be executed - con : SQLAlchemy engine or DBAPI2 connection (legacy mode) + con : SQLAlchemy engine or sqlite3 DBAPI2 connection Using SQLAlchemy makes it possible to use any DB supported by that library. - If a DBAPI2 object, a supported SQL flavor must also be provided + If a DBAPI2 object, only sqlite3 is supported. cur : depreciated, cursor is obtained from connection params : list or tuple, optional List of parameters to pass to execute method. - flavor : string "sqlite", "mysql" - Specifies the flavor of SQL to use. - Ignored when using SQLAlchemy engine. Required when using DBAPI2 connection. + Returns ------- Results Iterable """ if cur is None: - pandas_sql = pandasSQL_builder(con, flavor=flavor) + pandas_sql = pandasSQL_builder(con) else: - pandas_sql = pandasSQL_builder(cur, flavor=flavor, is_cursor=True) + pandas_sql = pandasSQL_builder(cur, is_cursor=True) args = _convert_params(sql, params) return pandas_sql.execute(*args) @@ -235,7 +233,7 @@ def read_sql_table(table_name, con, meta=None, index_col=None, table_name : string Name of SQL table in database con : SQLAlchemy engine - Legacy mode not supported + Sqlite DBAPI conncection mode not supported meta : SQLAlchemy meta, optional If omitted MetaData is reflected from engine index_col : string, optional @@ -277,8 +275,8 @@ def read_sql_table(table_name, con, meta=None, index_col=None, raise ValueError("Table %s not found" % table_name, con) -def read_sql_query(sql, con, index_col=None, flavor='sqlite', - coerce_float=True, params=None, parse_dates=None): +def read_sql_query(sql, con, index_col=None, coerce_float=True, params=None, + parse_dates=None): """Read SQL query into a DataFrame. Returns a DataFrame corresponding to the result set of the query @@ -289,15 +287,12 @@ def read_sql_query(sql, con, index_col=None, flavor='sqlite', ---------- sql : string SQL query to be executed - con : SQLAlchemy engine or DBAPI2 connection (legacy mode) + con : SQLAlchemy engine or sqlite3 DBAPI2 connection Using SQLAlchemy makes it possible to use any DB supported by that library. - If a DBAPI2 object is given, a supported SQL flavor must also be provided + If a DBAPI2 object, only sqlite3 is supported. index_col : string, optional column name to use for the returned DataFrame object. - flavor : string, {'sqlite', 'mysql'} - The flavor of SQL to use. Ignored when using - SQLAlchemy engine. Required when using DBAPI2 connection. coerce_float : boolean, default True Attempt to convert values to non-string, non-numeric objects (like decimal.Decimal) to floating point, useful for SQL result sets @@ -324,7 +319,7 @@ def read_sql_query(sql, con, index_col=None, flavor='sqlite', read_sql """ - pandas_sql = pandasSQL_builder(con, flavor=flavor) + pandas_sql = pandasSQL_builder(con) return pandas_sql.read_sql( sql, index_col=index_col, params=params, coerce_float=coerce_float, parse_dates=parse_dates) @@ -342,12 +337,13 @@ def read_sql(sql, con, index_col=None, flavor='sqlite', coerce_float=True, con : SQLAlchemy engine or DBAPI2 connection (legacy mode) Using SQLAlchemy makes it possible to use any DB supported by that library. - If a DBAPI2 object is given, a supported SQL flavor must also be provided + If a DBAPI2 object, only sqlite3 is supported. index_col : string, optional column name to use for the returned DataFrame object. flavor : string, {'sqlite', 'mysql'} The flavor of SQL to use. Ignored when using SQLAlchemy engine. Required when using DBAPI2 connection. + 'mysql' is still supported, but will be removed in future versions. coerce_float : boolean, default True Attempt to convert values to non-string, non-numeric objects (like decimal.Decimal) to floating point, useful for SQL result sets @@ -417,13 +413,14 @@ def to_sql(frame, name, con, flavor='sqlite', if_exists='fail', index=True, frame : DataFrame name : string Name of SQL table - con : SQLAlchemy engine or DBAPI2 connection (legacy mode) + con : SQLAlchemy engine or sqlite3 DBAPI2 connection Using SQLAlchemy makes it possible to use any DB supported by that library. - If a DBAPI2 object is given, a supported SQL flavor must also be provided + If a DBAPI2 object, only sqlite3 is supported. flavor : {'sqlite', 'mysql'}, default 'sqlite' The flavor of SQL to use. Ignored when using SQLAlchemy engine. Required when using DBAPI2 connection. + 'mysql' is still supported, but will be removed in future versions. if_exists : {'fail', 'replace', 'append'}, default 'fail' - fail: If table exists, do nothing. - replace: If table exists, drop it, recreate it, and insert data. @@ -458,13 +455,14 @@ def has_table(table_name, con, flavor='sqlite'): ---------- table_name: string Name of SQL table - con: SQLAlchemy engine or DBAPI2 connection (legacy mode) + con: SQLAlchemy engine or sqlite3 DBAPI2 connection Using SQLAlchemy makes it possible to use any DB supported by that library. - If a DBAPI2 object is given, a supported SQL flavor name must also be provided + If a DBAPI2 object, only sqlite3 is supported. flavor: {'sqlite', 'mysql'}, default 'sqlite' The flavor of SQL to use. Ignored when using SQLAlchemy engine. Required when using DBAPI2 connection. + 'mysql' is still supported, but will be removed in future versions. Returns ------- @@ -476,6 +474,10 @@ def has_table(table_name, con, flavor='sqlite'): table_exists = has_table +_MYSQL_WARNING = ("The 'mysql' flavor with DBAPI connection is deprecated " + "and will be removed in future versions. " + "MySQL will be further supported with SQLAlchemy engines.") + def pandasSQL_builder(con, flavor=None, meta=None, is_cursor=False): """ Convenience function to return the correct PandasSQL subclass based on the @@ -489,21 +491,14 @@ def pandasSQL_builder(con, flavor=None, meta=None, is_cursor=False): if isinstance(con, sqlalchemy.engine.Engine): return PandasSQLAlchemy(con, meta=meta) else: - warnings.warn("Not an SQLAlchemy engine, " - "attempting to use as legacy DBAPI connection") - if flavor is None: - raise ValueError( - "PandasSQL must be created with an SQLAlchemy engine " - "or a DBAPI2 connection and SQL flavor") - else: - return PandasSQLLegacy(con, flavor, is_cursor=is_cursor) + if flavor == 'mysql': + warnings.warn(_MYSQL_WARNING, FutureWarning) + return PandasSQLLegacy(con, flavor, is_cursor=is_cursor) except ImportError: - warnings.warn("SQLAlchemy not installed, using legacy mode") - if flavor is None: - raise SQLAlchemyRequired - else: - return PandasSQLLegacy(con, flavor, is_cursor=is_cursor) + if flavor == 'mysql': + warnings.warn(_MYSQL_WARNING, FutureWarning) + return PandasSQLLegacy(con, flavor, is_cursor=is_cursor) class PandasSQLTable(PandasObject): @@ -893,7 +888,7 @@ def _create_sql_schema(self, frame, table_name): } -_SAFE_NAMES_WARNING = ("The spaces in these column names will not be changed." +_SAFE_NAMES_WARNING = ("The spaces in these column names will not be changed. " "In pandas versions < 0.14, spaces were converted to " "underscores.") @@ -991,6 +986,8 @@ class PandasSQLLegacy(PandasSQL): def __init__(self, con, flavor, is_cursor=False): self.is_cursor = is_cursor self.con = con + if flavor is None: + flavor = 'sqlite' if flavor not in ['sqlite', 'mysql']: raise NotImplementedError else: @@ -1098,6 +1095,8 @@ def get_schema(frame, name, flavor='sqlite', keys=None, con=None): """ if con is None: + if flavor == 'mysql': + warnings.warn(_MYSQL_WARNING, FutureWarning) return _get_schema_legacy(frame, name, flavor, keys) pandas_sql = pandasSQL_builder(con=con, flavor=flavor) diff --git a/pandas/io/tests/test_sql.py b/pandas/io/tests/test_sql.py index 9a34e84c153a0..35acfc0ac8bf4 100644 --- a/pandas/io/tests/test_sql.py +++ b/pandas/io/tests/test_sql.py @@ -332,7 +332,7 @@ def setUp(self): def test_read_sql_iris(self): iris_frame = sql.read_sql_query( - "SELECT * FROM iris", self.conn, flavor='sqlite') + "SELECT * FROM iris", self.conn) self._check_iris_loaded_frame(iris_frame) def test_legacy_read_frame(self): @@ -391,8 +391,7 @@ def test_to_sql_append(self): def test_to_sql_series(self): s = Series(np.arange(5, dtype='int64'), name='series') sql.to_sql(s, "test_series", self.conn, flavor='sqlite', index=False) - s2 = sql.read_sql_query("SELECT * FROM test_series", self.conn, - flavor='sqlite') + s2 = sql.read_sql_query("SELECT * FROM test_series", self.conn) tm.assert_frame_equal(s.to_frame(), s2) def test_to_sql_panel(self): @@ -416,8 +415,7 @@ def test_roundtrip(self): con=self.conn, flavor='sqlite') result = sql.read_sql_query( 'SELECT * FROM test_frame_roundtrip', - con=self.conn, - flavor='sqlite') + con=self.conn) # HACK! result.index = self.test_frame1.index @@ -428,41 +426,38 @@ def test_roundtrip(self): def test_execute_sql(self): # drop_sql = "DROP TABLE IF EXISTS test" # should already be done - iris_results = sql.execute( - "SELECT * FROM iris", con=self.conn, flavor='sqlite') + iris_results = sql.execute("SELECT * FROM iris", con=self.conn) row = iris_results.fetchone() tm.equalContents(row, [5.1, 3.5, 1.4, 0.2, 'Iris-setosa']) def test_date_parsing(self): # Test date parsing in read_sq # No Parsing - df = sql.read_sql_query("SELECT * FROM types_test_data", self.conn, - flavor='sqlite') + df = sql.read_sql_query("SELECT * FROM types_test_data", self.conn) self.assertFalse( issubclass(df.DateCol.dtype.type, np.datetime64), "DateCol loaded with incorrect type") df = sql.read_sql_query("SELECT * FROM types_test_data", self.conn, - flavor='sqlite', parse_dates=['DateCol']) + parse_dates=['DateCol']) self.assertTrue( issubclass(df.DateCol.dtype.type, np.datetime64), "DateCol loaded with incorrect type") df = sql.read_sql_query("SELECT * FROM types_test_data", self.conn, - flavor='sqlite', parse_dates={'DateCol': '%Y-%m-%d %H:%M:%S'}) self.assertTrue( issubclass(df.DateCol.dtype.type, np.datetime64), "DateCol loaded with incorrect type") df = sql.read_sql_query("SELECT * FROM types_test_data", self.conn, - flavor='sqlite', parse_dates=['IntDateCol']) + parse_dates=['IntDateCol']) self.assertTrue(issubclass(df.IntDateCol.dtype.type, np.datetime64), "IntDateCol loaded with incorrect type") df = sql.read_sql_query("SELECT * FROM types_test_data", self.conn, - flavor='sqlite', parse_dates={'IntDateCol': 's'}) + parse_dates={'IntDateCol': 's'}) self.assertTrue(issubclass(df.IntDateCol.dtype.type, np.datetime64), "IntDateCol loaded with incorrect type") @@ -471,7 +466,7 @@ def test_date_and_index(self): # Test case where same column appears in parse_date and index_col df = sql.read_sql_query("SELECT * FROM types_test_data", self.conn, - flavor='sqlite', index_col='DateCol', + index_col='DateCol', parse_dates=['DateCol', 'IntDateCol']) self.assertTrue(issubclass(df.index.dtype.type, np.datetime64), @@ -651,22 +646,19 @@ def test_sql_open_close(self): conn = self.connect(name) result = sql.read_sql_query("SELECT * FROM test_frame2_legacy;", - conn, flavor="sqlite") + conn) conn.close() tm.assert_frame_equal(self.test_frame2, result) def test_read_sql_delegate(self): - iris_frame1 = sql.read_sql_query( - "SELECT * FROM iris", self.conn, flavor=self.flavor) - iris_frame2 = sql.read_sql( - "SELECT * FROM iris", self.conn, flavor=self.flavor) + iris_frame1 = sql.read_sql_query("SELECT * FROM iris", self.conn) + iris_frame2 = sql.read_sql("SELECT * FROM iris", self.conn) tm.assert_frame_equal(iris_frame1, iris_frame2, "read_sql and read_sql_query have not the same" " result with a query") - self.assertRaises(ValueError, sql.read_sql, 'iris', self.conn, - flavor=self.flavor) + self.assertRaises(ValueError, sql.read_sql, 'iris', self.conn) def test_safe_names_warning(self): # GH 6798 @@ -1109,6 +1101,14 @@ def tearDown(self): self.conn.commit() self.conn.close() + def test_a_deprecation(self): + with tm.assert_produces_warning(FutureWarning): + sql.to_sql(self.test_frame1, 'test_frame1', self.conn, + flavor='mysql') + self.assertTrue( + sql.has_table('test_frame1', self.conn, flavor='mysql'), + 'Table not written to DB') + #------------------------------------------------------------------------------ #--- Old tests from 0.13.1 (before refactor using sqlalchemy) @@ -1277,8 +1277,6 @@ def _check_roundtrip(self, frame): expected = frame.copy() expected.index = Index(lrange(len(frame2))) + 10 expected.index.name = 'Idx' - print(expected.index.names) - print(result.index.names) tm.assert_frame_equal(expected, result) def test_tquery(self):