Skip to content

Commit

Permalink
CLN: Removed the flavor='mysql' option and deprecate flavor in DataFr…
Browse files Browse the repository at this point in the history
…ame.to_sql (#13611)
  • Loading branch information
gfyoung authored and jorisvandenbossche committed Jul 19, 2016
1 parent 4c9ae94 commit 8acfad3
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 298 deletions.
2 changes: 2 additions & 0 deletions doc/source/whatsnew/v0.19.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ Deprecations
- ``Categorical.reshape`` has been deprecated and will be removed in a subsequent release (:issue:`12882`)
- ``Series.reshape`` has been deprecated and will be removed in a subsequent release (:issue:`12882`)

- ``DataFrame.to_sql()`` has deprecated the ``flavor`` parameter, as it is superfluous when SQLAlchemy is not installed (:issue:`13611`)
- ``compact_ints`` and ``use_unsigned`` have been deprecated in ``pd.read_csv()`` and will be removed in a future version (:issue:`13320`)
- ``buffer_lines`` has been deprecated in ``pd.read_csv()`` and will be removed in a future version (:issue:`13360`)
- ``as_recarray`` has been deprecated in ``pd.read_csv()`` and will be removed in a future version (:issue:`13373`)
Expand All @@ -541,6 +542,7 @@ Removal of prior version deprecations/changes
- ``DataFrame.to_dict()`` has dropped the ``outtype`` parameter in favor of ``orient`` (:issue:`13627`, :issue:`8486`)
- ``pd.Categorical`` has dropped setting of the ``ordered`` attribute directly in favor of the ``set_ordered`` method (:issue:`13671`)
- ``pd.Categorical`` has dropped the ``levels`` attribute in favour of ``categories`` (:issue:`8376`)
- ``DataFrame.to_sql()`` has dropped the ``mysql`` option for the ``flavor`` parameter (:issue:`13611`)

- Removal of the legacy time rules (offset aliases), deprecated since 0.17.0 (this has been alias since 0.8.0) (:issue:`13590`)

Expand Down
13 changes: 6 additions & 7 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1144,7 +1144,7 @@ def to_msgpack(self, path_or_buf=None, encoding='utf-8', **kwargs):
return packers.to_msgpack(path_or_buf, self, encoding=encoding,
**kwargs)

def to_sql(self, name, con, flavor='sqlite', schema=None, if_exists='fail',
def to_sql(self, name, con, flavor=None, schema=None, if_exists='fail',
index=True, index_label=None, chunksize=None, dtype=None):
"""
Write records stored in a DataFrame to a SQL database.
Expand All @@ -1155,12 +1155,11 @@ def to_sql(self, name, con, flavor='sqlite', schema=None, if_exists='fail',
Name of SQL table
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, only sqlite3 is supported.
flavor : {'sqlite', 'mysql'}, default 'sqlite'
The flavor of SQL to use. Ignored when using SQLAlchemy engine.
'mysql' is deprecated and will be removed in future versions, but
it will be further supported through SQLAlchemy engines.
library. If a DBAPI2 object, only sqlite3 is supported.
flavor : 'sqlite', default None
DEPRECATED: this parameter will be removed in a future version,
as 'sqlite' is the only supported option if SQLAlchemy is not
installed.
schema : string, default None
Specify the schema (if database flavor supports this). If None, use
default schema.
Expand Down
165 changes: 53 additions & 112 deletions pandas/io/sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ class DatabaseError(IOError):
_SQLALCHEMY_INSTALLED = None


def _validate_flavor_parameter(flavor):
"""
Checks whether a database 'flavor' was specified.
If not None, produces FutureWarning if 'sqlite' and
raises a ValueError if anything else.
"""
if flavor is not None:
if flavor == 'sqlite':
warnings.warn("the 'flavor' parameter is deprecated "
"and will be removed in a future version, "
"as 'sqlite' is the only supported option "
"when SQLAlchemy is not installed.",
FutureWarning, stacklevel=2)
else:
raise ValueError("database flavor {flavor} is not "
"supported".format(flavor=flavor))


def _is_sqlalchemy_connectable(con):
global _SQLALCHEMY_INSTALLED
if _SQLALCHEMY_INSTALLED is None:
Expand Down Expand Up @@ -517,7 +535,7 @@ def read_sql(sql, con, index_col=None, coerce_float=True, params=None,
chunksize=chunksize)


def to_sql(frame, name, con, flavor='sqlite', schema=None, if_exists='fail',
def to_sql(frame, name, con, flavor=None, schema=None, if_exists='fail',
index=True, index_label=None, chunksize=None, dtype=None):
"""
Write records stored in a DataFrame to a SQL database.
Expand All @@ -532,10 +550,8 @@ def to_sql(frame, name, con, flavor='sqlite', schema=None, if_exists='fail',
Using SQLAlchemy makes it possible to use any DB supported by that
library.
If a DBAPI2 object, only sqlite3 is supported.
flavor : {'sqlite', 'mysql'}, default 'sqlite'
The flavor of SQL to use. Ignored when using SQLAlchemy connectable.
'mysql' is deprecated and will be removed in future versions, but it
will be further supported through SQLAlchemy connectables.
flavor : 'sqlite', default None
DEPRECATED: this parameter will be removed in a future version
schema : string, default None
Name of SQL schema in database to write to (if database flavor
supports this). If None, use default schema (default).
Expand Down Expand Up @@ -573,7 +589,7 @@ def to_sql(frame, name, con, flavor='sqlite', schema=None, if_exists='fail',
chunksize=chunksize, dtype=dtype)


def has_table(table_name, con, flavor='sqlite', schema=None):
def has_table(table_name, con, flavor=None, schema=None):
"""
Check if DataBase has named table.
Expand All @@ -585,10 +601,8 @@ def has_table(table_name, con, flavor='sqlite', schema=None):
Using SQLAlchemy makes it possible to use any DB supported by that
library.
If a DBAPI2 object, only sqlite3 is supported.
flavor: {'sqlite', 'mysql'}, default 'sqlite'
The flavor of SQL to use. Ignored when using SQLAlchemy connectable.
'mysql' is deprecated and will be removed in future versions, but it
will be further supported through SQLAlchemy connectables.
flavor : 'sqlite', default None
DEPRECATED: this parameter will be removed in a future version
schema : string, default None
Name of SQL schema in database to write to (if database flavor supports
this). If None, use default schema (default).
Expand All @@ -603,12 +617,6 @@ def has_table(table_name, con, flavor='sqlite', schema=None):
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 "
"connectables.")


def _engine_builder(con):
"""
Returns a SQLAlchemy engine from a URI (if con is a string)
Expand All @@ -632,15 +640,15 @@ def pandasSQL_builder(con, flavor=None, schema=None, meta=None,
Convenience function to return the correct PandasSQL subclass based on the
provided parameters
"""
_validate_flavor_parameter(flavor)

# When support for DBAPI connections is removed,
# is_cursor should not be necessary.
con = _engine_builder(con)
if _is_sqlalchemy_connectable(con):
return SQLDatabase(con, schema=schema, meta=meta)
else:
if flavor == 'mysql':
warnings.warn(_MYSQL_WARNING, FutureWarning, stacklevel=3)
return SQLiteDatabase(con, flavor, is_cursor=is_cursor)
return SQLiteDatabase(con, is_cursor=is_cursor)


class SQLTable(PandasObject):
Expand Down Expand Up @@ -1035,11 +1043,11 @@ class PandasSQL(PandasObject):

def read_sql(self, *args, **kwargs):
raise ValueError("PandasSQL must be created with an SQLAlchemy "
"connectable or connection+sql flavor")
"connectable or sqlite connection")

def to_sql(self, *args, **kwargs):
raise ValueError("PandasSQL must be created with an SQLAlchemy "
"connectable or connection+sql flavor")
"connectable or sqlite connection")


class SQLDatabase(PandasSQL):
Expand Down Expand Up @@ -1308,38 +1316,16 @@ def _create_sql_schema(self, frame, table_name, keys=None, dtype=None):


# ---- SQL without SQLAlchemy ---
# Flavour specific sql strings and handler class for access to DBs without
# SQLAlchemy installed
# SQL type convertions for each DB
# sqlite-specific sql strings and handler class
# dictionary used for readability purposes
_SQL_TYPES = {
'string': {
'mysql': 'VARCHAR (63)',
'sqlite': 'TEXT',
},
'floating': {
'mysql': 'DOUBLE',
'sqlite': 'REAL',
},
'integer': {
'mysql': 'BIGINT',
'sqlite': 'INTEGER',
},
'datetime': {
'mysql': 'DATETIME',
'sqlite': 'TIMESTAMP',
},
'date': {
'mysql': 'DATE',
'sqlite': 'DATE',
},
'time': {
'mysql': 'TIME',
'sqlite': 'TIME',
},
'boolean': {
'mysql': 'BOOLEAN',
'sqlite': 'INTEGER',
}
'string': 'TEXT',
'floating': 'REAL',
'integer': 'INTEGER',
'datetime': 'TIMESTAMP',
'date': 'DATE',
'time': 'TIME',
'boolean': 'INTEGER',
}


Expand All @@ -1351,22 +1337,6 @@ def _get_unicode_name(name):
return uname


def _get_valid_mysql_name(name):
# Filter for unquoted identifiers
# See http://dev.mysql.com/doc/refman/5.0/en/identifiers.html
uname = _get_unicode_name(name)
if not len(uname):
raise ValueError("Empty table or column name specified")

basere = r'[0-9,a-z,A-Z$_]'
for c in uname:
if not re.match(basere, c):
if not (0x80 < ord(c) < 0xFFFF):
raise ValueError("Invalid MySQL identifier '%s'" % uname)

return '`' + uname + '`'


def _get_valid_sqlite_name(name):
# See http://stackoverflow.com/questions/6514274/how-do-you-escape-strings\
# -for-sqlite-table-column-names-in-python
Expand All @@ -1385,19 +1355,6 @@ def _get_valid_sqlite_name(name):
return '"' + uname.replace('"', '""') + '"'


# SQL enquote and wildcard symbols
_SQL_WILDCARD = {
'mysql': '%s',
'sqlite': '?'
}

# Validate and return escaped identifier
_SQL_GET_IDENTIFIER = {
'mysql': _get_valid_mysql_name,
'sqlite': _get_valid_sqlite_name,
}


_SAFE_NAMES_WARNING = ("The spaces in these column names will not be changed. "
"In pandas versions < 0.14, spaces were converted to "
"underscores.")
Expand Down Expand Up @@ -1428,9 +1385,8 @@ def _execute_create(self):

def insert_statement(self):
names = list(map(text_type, self.frame.columns))
flv = self.pd_sql.flavor
wld = _SQL_WILDCARD[flv] # wildcard char
escape = _SQL_GET_IDENTIFIER[flv]
wld = '?' # wildcard char
escape = _get_valid_sqlite_name

if self.index is not None:
[names.insert(0, idx) for idx in self.index[::-1]]
Expand Down Expand Up @@ -1460,8 +1416,7 @@ def _create_table_setup(self):
if any(map(pat.search, column_names)):
warnings.warn(_SAFE_NAMES_WARNING, stacklevel=6)

flv = self.pd_sql.flavor
escape = _SQL_GET_IDENTIFIER[flv]
escape = _get_valid_sqlite_name

create_tbl_stmts = [escape(cname) + ' ' + ctype
for cname, ctype, _ in column_names_and_types]
Expand Down Expand Up @@ -1514,33 +1469,25 @@ def _sql_type_name(self, col):
if col_type not in _SQL_TYPES:
col_type = "string"

return _SQL_TYPES[col_type][self.pd_sql.flavor]
return _SQL_TYPES[col_type]


class SQLiteDatabase(PandasSQL):
"""
Version of SQLDatabase to support sqlite connections (fallback without
sqlalchemy). This should only be used internally.
For now still supports `flavor` argument to deal with 'mysql' database
for backwards compatibility, but this will be removed in future versions.
Parameters
----------
con : sqlite connection object
"""

def __init__(self, con, flavor, is_cursor=False):
def __init__(self, con, flavor=None, is_cursor=False):
_validate_flavor_parameter(flavor)

self.is_cursor = is_cursor
self.con = con
if flavor is None:
flavor = 'sqlite'
if flavor not in ['sqlite', 'mysql']:
raise NotImplementedError("flavors other than SQLite and MySQL "
"are not supported")
else:
self.flavor = flavor

@contextmanager
def run_transaction(self):
Expand Down Expand Up @@ -1665,24 +1612,20 @@ def to_sql(self, frame, name, if_exists='fail', index=True,

def has_table(self, name, schema=None):
# TODO(wesm): unused?
# escape = _SQL_GET_IDENTIFIER[self.flavor]
# escape = _get_valid_sqlite_name
# esc_name = escape(name)

wld = _SQL_WILDCARD[self.flavor]
flavor_map = {
'sqlite': ("SELECT name FROM sqlite_master "
"WHERE type='table' AND name=%s;") % wld,
'mysql': "SHOW TABLES LIKE %s" % wld}
query = flavor_map.get(self.flavor)
wld = '?'
query = ("SELECT name FROM sqlite_master "
"WHERE type='table' AND name=%s;") % wld

return len(self.execute(query, [name, ]).fetchall()) > 0

def get_table(self, table_name, schema=None):
return None # not supported in fallback mode

def drop_table(self, name, schema=None):
escape = _SQL_GET_IDENTIFIER[self.flavor]
drop_sql = "DROP TABLE %s" % escape(name)
drop_sql = "DROP TABLE %s" % _get_valid_sqlite_name(name)
self.execute(drop_sql)

def _create_sql_schema(self, frame, table_name, keys=None, dtype=None):
Expand All @@ -1691,7 +1634,7 @@ def _create_sql_schema(self, frame, table_name, keys=None, dtype=None):
return str(table.sql_schema())


def get_schema(frame, name, flavor='sqlite', keys=None, con=None, dtype=None):
def get_schema(frame, name, flavor=None, keys=None, con=None, dtype=None):
"""
Get the SQL db table schema for the given frame.
Expand All @@ -1700,16 +1643,14 @@ def get_schema(frame, name, flavor='sqlite', keys=None, con=None, dtype=None):
frame : DataFrame
name : string
name of SQL table
flavor : {'sqlite', 'mysql'}, default 'sqlite'
The flavor of SQL to use. Ignored when using SQLAlchemy connectable.
'mysql' is deprecated and will be removed in future versions, but it
will be further supported through SQLAlchemy engines.
keys : string or sequence, default: None
columns to use a primary key
con: an open SQL database connection object or a SQLAlchemy connectable
Using SQLAlchemy makes it possible to use any DB supported by that
library, default: None
If a DBAPI2 object, only sqlite3 is supported.
flavor : 'sqlite', default None
DEPRECATED: this parameter will be removed in a future version
dtype : dict of column name to SQL type, default None
Optional specifying the datatype for columns. The SQL type should
be a SQLAlchemy type, or a string for sqlite3 fallback connection.
Expand Down
Loading

0 comments on commit 8acfad3

Please sign in to comment.