diff --git a/doc/source/v0.15.0.txt b/doc/source/v0.15.0.txt index ecfd7b5ada055..13931c3c104be 100644 --- a/doc/source/v0.15.0.txt +++ b/doc/source/v0.15.0.txt @@ -427,6 +427,7 @@ Enhancements ~~~~~~~~~~~~ - Added support for a ``chunksize`` parameter to ``to_sql`` function. This allows DataFrame to be written in chunks and avoid packet-size overflow errors (:issue:`8062`) +- Added support for writing ``datetime.date`` and ``datetime.time`` object columns with ``to_sql`` (:issue:`6932`). - Added support for bool, uint8, uint16 and uint32 datatypes in ``to_stata`` (:issue:`7097`, :issue:`7365`) diff --git a/pandas/io/sql.py b/pandas/io/sql.py index 914ade45adaa1..40e103a8604a4 100644 --- a/pandas/io/sql.py +++ b/pandas/io/sql.py @@ -11,6 +11,7 @@ import re import numpy as np +import pandas.lib as lib import pandas.core.common as com from pandas.compat import lzip, map, zip, raise_with_traceback, string_types from pandas.core.api import DataFrame, Series @@ -684,13 +685,14 @@ def _get_column_names_and_types(self, dtype_mapper): if self.index is not None: for i, idx_label in enumerate(self.index): idx_type = dtype_mapper( - self.frame.index.get_level_values(i).dtype) + self.frame.index.get_level_values(i)) column_names_and_types.append((idx_label, idx_type)) - column_names_and_types += zip( - list(map(str, self.frame.columns)), - map(dtype_mapper, self.frame.dtypes) - ) + column_names_and_types += [ + (str(self.frame.columns[i]), + dtype_mapper(self.frame.iloc[:,i])) + for i in range(len(self.frame.columns)) + ] return column_names_and_types def _create_table_statement(self): @@ -756,30 +758,33 @@ def _harmonize_columns(self, parse_dates=None): except KeyError: pass # this column not in results - def _sqlalchemy_type(self, arr_or_dtype): + def _sqlalchemy_type(self, col): from sqlalchemy.types import (BigInteger, Float, Text, Boolean, - DateTime, Date, Interval) + DateTime, Date, Time, Interval) - if arr_or_dtype is date: - return Date - if com.is_datetime64_dtype(arr_or_dtype): + if com.is_datetime64_dtype(col): try: - tz = arr_or_dtype.tzinfo + tz = col.tzinfo return DateTime(timezone=True) except: return DateTime - if com.is_timedelta64_dtype(arr_or_dtype): + if com.is_timedelta64_dtype(col): warnings.warn("the 'timedelta' type is not supported, and will be " "written as integer values (ns frequency) to the " "database.", UserWarning) return BigInteger - elif com.is_float_dtype(arr_or_dtype): + elif com.is_float_dtype(col): return Float - elif com.is_integer_dtype(arr_or_dtype): + elif com.is_integer_dtype(col): # TODO: Refine integer size. return BigInteger - elif com.is_bool_dtype(arr_or_dtype): + elif com.is_bool_dtype(col): return Boolean + inferred = lib.infer_dtype(com._ensure_object(col)) + if inferred == 'date': + return Date + if inferred == 'time': + return Time return Text def _numpy_type(self, sqltype): @@ -908,7 +913,11 @@ def _create_sql_schema(self, frame, table_name): }, 'date': { 'mysql': 'DATE', - 'sqlite': 'TIMESTAMP', + 'sqlite': 'DATE', + }, + 'time': { + 'mysql': 'TIME', + 'sqlite': 'TIME', }, 'bool': { 'mysql': 'BOOLEAN', @@ -1014,8 +1023,8 @@ def _create_table_statement(self): create_statement = template % {'name': self.name, 'columns': columns} return create_statement - def _sql_type_name(self, dtype): - pytype = dtype.type + def _sql_type_name(self, col): + pytype = col.dtype.type pytype_name = "text" if issubclass(pytype, np.floating): pytype_name = "float" @@ -1029,10 +1038,14 @@ def _sql_type_name(self, dtype): elif issubclass(pytype, np.datetime64) or pytype is datetime: # Caution: np.datetime64 is also a subclass of np.number. pytype_name = "datetime" - elif pytype is datetime.date: - pytype_name = "date" elif issubclass(pytype, np.bool_): pytype_name = "bool" + elif issubclass(pytype, np.object): + pytype = lib.infer_dtype(com._ensure_object(col)) + if pytype == "date": + pytype_name = "date" + elif pytype == "time": + pytype_name = "time" return _SQL_TYPES[pytype_name][self.pd_sql.flavor] diff --git a/pandas/io/tests/test_sql.py b/pandas/io/tests/test_sql.py index 68f170759b666..0d55f4c1dbcd8 100644 --- a/pandas/io/tests/test_sql.py +++ b/pandas/io/tests/test_sql.py @@ -26,7 +26,7 @@ import warnings import numpy as np -from datetime import datetime +from datetime import datetime, date, time from pandas import DataFrame, Series, Index, MultiIndex, isnull from pandas import date_range, to_datetime, to_timedelta @@ -35,6 +35,7 @@ from pandas.core.datetools import format as date_format import pandas.io.sql as sql +from pandas.io.sql import read_sql_table, read_sql_query import pandas.util.testing as tm @@ -976,6 +977,21 @@ def test_datetime_NaT(self): else: tm.assert_frame_equal(result, df) + def test_datetime_date(self): + # test support for datetime.date + df = DataFrame([date(2014, 1, 1), date(2014, 1, 2)], columns=["a"]) + df.to_sql('test_date', self.conn, index=False) + res = read_sql_table('test_date', self.conn) + # comes back as datetime64 + tm.assert_series_equal(res['a'], to_datetime(df['a'])) + + def test_datetime_time(self): + # test support for datetime.time + df = DataFrame([time(9, 0, 0), time(9, 1, 30)], columns=["a"]) + df.to_sql('test_time', self.conn, index=False) + res = read_sql_table('test_time', self.conn) + tm.assert_frame_equal(res, df) + def test_mixed_dtype_insert(self): # see GH6509 s1 = Series(2**25 + 1,dtype=np.int32) @@ -1269,6 +1285,21 @@ def test_roundtrip(self): def test_execute_sql(self): self._execute_sql() + def test_datetime_date(self): + # test support for datetime.date + df = DataFrame([date(2014, 1, 1), date(2014, 1, 2)], columns=["a"]) + df.to_sql('test_date', self.conn, index=False, flavor=self.flavor) + res = read_sql_query('SELECT * FROM test_date', self.conn) + if self.flavor == 'sqlite': + # comes back as strings + tm.assert_frame_equal(res, df.astype(str)) + elif self.flavor == 'mysql': + tm.assert_frame_equal(res, df) + + def test_datetime_time(self): + # test support for datetime.time + raise nose.SkipTest("datetime.time not supported for sqlite fallback") + class TestMySQLLegacy(TestSQLiteLegacy): """