Skip to content

Commit de167f7

Browse files
Merge pull request #6642 from jorisvandenbossche/sql-multiindex
SQL: add index_label keyword to to_sql
2 parents b67848e + 578ae49 commit de167f7

File tree

3 files changed

+82
-19
lines changed

3 files changed

+82
-19
lines changed

pandas/core/generic.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -908,7 +908,8 @@ def to_msgpack(self, path_or_buf=None, **kwargs):
908908
from pandas.io import packers
909909
return packers.to_msgpack(path_or_buf, self, **kwargs)
910910

911-
def to_sql(self, name, con, flavor='sqlite', if_exists='fail', index=True):
911+
def to_sql(self, name, con, flavor='sqlite', if_exists='fail', index=True,
912+
index_label=None):
912913
"""
913914
Write records stored in a DataFrame to a SQL database.
914915
@@ -928,12 +929,17 @@ def to_sql(self, name, con, flavor='sqlite', if_exists='fail', index=True):
928929
- replace: If table exists, drop it, recreate it, and insert data.
929930
- append: If table exists, insert data. Create if does not exist.
930931
index : boolean, default True
931-
Write DataFrame index as a column
932+
Write DataFrame index as a column.
933+
index_label : string or sequence, default None
934+
Column label for index column(s). If None is given (default) and
935+
`index` is True, then the index names are used.
936+
A sequence should be given if the DataFrame uses MultiIndex.
932937
933938
"""
934939
from pandas.io import sql
935940
sql.to_sql(
936-
self, name, con, flavor=flavor, if_exists=if_exists, index=index)
941+
self, name, con, flavor=flavor, if_exists=if_exists, index=index,
942+
index_label=index_label)
937943

938944
def to_pickle(self, path):
939945
"""

pandas/io/sql.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,8 @@ def read_sql(sql, con, index_col=None, flavor='sqlite', coerce_float=True,
229229
parse_dates=parse_dates)
230230

231231

232-
def to_sql(frame, name, con, flavor='sqlite', if_exists='fail', index=True):
232+
def to_sql(frame, name, con, flavor='sqlite', if_exists='fail', index=True,
233+
index_label=None):
233234
"""
234235
Write records stored in a DataFrame to a SQL database.
235236
@@ -251,6 +252,11 @@ def to_sql(frame, name, con, flavor='sqlite', if_exists='fail', index=True):
251252
- append: If table exists, insert data. Create if does not exist.
252253
index : boolean, default True
253254
Write DataFrame index as a column
255+
index_label : string or sequence, default None
256+
Column label for index column(s). If None is given (default) and
257+
`index` is True, then the index names are used.
258+
A sequence should be given if the DataFrame uses MultiIndex.
259+
254260
"""
255261
pandas_sql = pandasSQL_builder(con, flavor=flavor)
256262

@@ -259,7 +265,8 @@ def to_sql(frame, name, con, flavor='sqlite', if_exists='fail', index=True):
259265
elif not isinstance(frame, DataFrame):
260266
raise NotImplementedError
261267

262-
pandas_sql.to_sql(frame, name, if_exists=if_exists, index=index)
268+
pandas_sql.to_sql(frame, name, if_exists=if_exists, index=index,
269+
index_label=index_label)
263270

264271

265272
def has_table(table_name, con, meta=None, flavor='sqlite'):
@@ -377,12 +384,12 @@ class PandasSQLTable(PandasObject):
377384
"""
378385
# TODO: support for multiIndex
379386
def __init__(self, name, pandas_sql_engine, frame=None, index=True,
380-
if_exists='fail', prefix='pandas'):
387+
if_exists='fail', prefix='pandas', index_label=None):
381388
self.name = name
382389
self.pd_sql = pandas_sql_engine
383390
self.prefix = prefix
384391
self.frame = frame
385-
self.index = self._index_name(index)
392+
self.index = self._index_name(index, index_label)
386393

387394
if frame is not None:
388395
# We want to write a frame
@@ -473,9 +480,11 @@ def read(self, coerce_float=True, parse_dates=None, columns=None):
473480

474481
return self.frame
475482

476-
def _index_name(self, index):
483+
def _index_name(self, index, index_label):
477484
if index is True:
478-
if self.frame.index.name is not None:
485+
if index_label is not None:
486+
return _safe_col_name(index_label)
487+
elif self.frame.index.name is not None:
479488
return _safe_col_name(self.frame.index.name)
480489
else:
481490
return self.prefix + '_index'
@@ -652,9 +661,11 @@ def read_sql(self, sql, index_col=None, coerce_float=True,
652661

653662
return data_frame
654663

655-
def to_sql(self, frame, name, if_exists='fail', index=True):
664+
def to_sql(self, frame, name, if_exists='fail', index=True,
665+
index_label=None):
656666
table = PandasSQLTable(
657-
name, self, frame=frame, index=index, if_exists=if_exists)
667+
name, self, frame=frame, index=index, if_exists=if_exists,
668+
index_label=index_label)
658669
table.insert()
659670

660671
@property
@@ -882,7 +893,8 @@ def _fetchall_as_list(self, cur):
882893
result = list(result)
883894
return result
884895

885-
def to_sql(self, frame, name, if_exists='fail', index=True):
896+
def to_sql(self, frame, name, if_exists='fail', index=True,
897+
index_label=None):
886898
"""
887899
Write records stored in a DataFrame to a SQL database.
888900
@@ -895,6 +907,7 @@ def to_sql(self, frame, name, if_exists='fail', index=True):
895907
fail: If table exists, do nothing.
896908
replace: If table exists, drop it, recreate it, and insert data.
897909
append: If table exists, insert data. Create if does not exist.
910+
index_label : ignored (only used in sqlalchemy mode)
898911
"""
899912
table = PandasSQLTableLegacy(
900913
name, self, frame=frame, index=index, if_exists=if_exists)

pandas/io/tests/test_sql.py

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ def _tquery(self):
255255
tm.equalContents(row, [5.1, 3.5, 1.4, 0.2, 'Iris-setosa'])
256256

257257

258-
class TestSQLApi(PandasSQLTest):
258+
class _TestSQLApi(PandasSQLTest):
259259

260260
"""Test the public API as it would be used
261261
directly, including legacy names
@@ -269,12 +269,6 @@ class TestSQLApi(PandasSQLTest):
269269
"""
270270
flavor = 'sqlite'
271271

272-
def connect(self):
273-
if SQLALCHEMY_INSTALLED:
274-
return sqlalchemy.create_engine('sqlite:///:memory:')
275-
else:
276-
return sqlite3.connect(':memory:')
277-
278272
def setUp(self):
279273
self.conn = self.connect()
280274
self._load_iris_data()
@@ -436,6 +430,56 @@ def test_date_and_index(self):
436430
issubclass(df.IntDateCol.dtype.type, np.datetime64),
437431
"IntDateCol loaded with incorrect type")
438432

433+
class TestSQLApi(_TestSQLApi):
434+
"""Test the public API as it would be used directly
435+
"""
436+
flavor = 'sqlite'
437+
438+
def connect(self):
439+
if SQLALCHEMY_INSTALLED:
440+
return sqlalchemy.create_engine('sqlite:///:memory:')
441+
else:
442+
raise nose.SkipTest('SQLAlchemy not installed')
443+
444+
def test_to_sql_index_label(self):
445+
temp_frame = DataFrame({'col1': range(4)})
446+
447+
# no index name, defaults to 'pandas_index'
448+
sql.to_sql(temp_frame, 'test_index_label', self.conn)
449+
frame = sql.read_table('test_index_label', self.conn)
450+
self.assertEqual(frame.columns[0], 'pandas_index')
451+
452+
# specifying index_label
453+
sql.to_sql(temp_frame, 'test_index_label', self.conn,
454+
if_exists='replace', index_label='other_label')
455+
frame = sql.read_table('test_index_label', self.conn)
456+
self.assertEqual(frame.columns[0], 'other_label',
457+
"Specified index_label not written to database")
458+
459+
# using the index name
460+
temp_frame.index.name = 'index'
461+
sql.to_sql(temp_frame, 'test_index_label', self.conn,
462+
if_exists='replace')
463+
frame = sql.read_table('test_index_label', self.conn)
464+
self.assertEqual(frame.columns[0], 'index',
465+
"Index name not written to database")
466+
467+
# has index name, but specifying index_label
468+
sql.to_sql(temp_frame, 'test_index_label', self.conn,
469+
if_exists='replace', index_label='other_label')
470+
frame = sql.read_table('test_index_label', self.conn)
471+
self.assertEqual(frame.columns[0], 'other_label',
472+
"Specified index_label not written to database")
473+
474+
475+
class TestSQLLegacyApi(_TestSQLApi):
476+
"""Test the public legacy API
477+
"""
478+
flavor = 'sqlite'
479+
480+
def connect(self):
481+
return sqlite3.connect(':memory:')
482+
439483

440484
class _TestSQLAlchemy(PandasSQLTest):
441485
"""

0 commit comments

Comments
 (0)