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):