Skip to content

Commit 5ef3cc3

Browse files
Merge pull request #7952 from jorisvandenbossche/sql-schema
ENH: add schema support to sql functions
2 parents f505097 + 1e90ba3 commit 5ef3cc3

File tree

6 files changed

+152
-55
lines changed

6 files changed

+152
-55
lines changed

ci/requirements-2.6.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ pytz==2013b
55
http://www.crummy.com/software/BeautifulSoup/bs4/download/4.2/beautifulsoup4-4.2.0.tar.gz
66
html5lib==1.0b2
77
numexpr==1.4.2
8-
sqlalchemy==0.7.1
8+
sqlalchemy==0.7.4
99
pymysql==0.6.0
1010
psycopg2==2.5
1111
scipy==0.11.0

doc/source/io.rst

+14
Original file line numberDiff line numberDiff line change
@@ -3320,6 +3320,20 @@ to pass to :func:`pandas.to_datetime`:
33203320
33213321
You can check if a table exists using :func:`~pandas.io.sql.has_table`
33223322

3323+
Schema support
3324+
~~~~~~~~~~~~~~
3325+
3326+
.. versionadded:: 0.15.0
3327+
3328+
Reading from and writing to different schema's is supported through the ``schema``
3329+
keyword in the :func:`~pandas.read_sql_table` and :func:`~pandas.DataFrame.to_sql`
3330+
functions. Note however that this depends on the database flavor (sqlite does not
3331+
have schema's). For example:
3332+
3333+
.. code-block:: python
3334+
3335+
df.to_sql('table', engine, schema='other_schema')
3336+
pd.read_sql_table('table', engine, schema='other_schema')
33233337
33243338
Querying
33253339
~~~~~~~~

doc/source/v0.15.0.txt

+7
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,13 @@ Enhancements
430430

431431
- 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`)
432432
- Added support for writing ``datetime.date`` and ``datetime.time`` object columns with ``to_sql`` (:issue:`6932`).
433+
- Added support for specifying a ``schema`` to read from/write to with ``read_sql_table`` and ``to_sql`` (:issue:`7441`, :issue:`7952`).
434+
For example:
435+
436+
.. code-block:: python
437+
438+
df.to_sql('table', engine, schema='other_schema')
439+
pd.read_sql_table('table', engine, schema='other_schema')
433440

434441
- Added support for bool, uint8, uint16 and uint32 datatypes in ``to_stata`` (:issue:`7097`, :issue:`7365`)
435442

pandas/core/generic.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -915,8 +915,8 @@ def to_msgpack(self, path_or_buf=None, **kwargs):
915915
from pandas.io import packers
916916
return packers.to_msgpack(path_or_buf, self, **kwargs)
917917

918-
def to_sql(self, name, con, flavor='sqlite', if_exists='fail', index=True,
919-
index_label=None, chunksize=None):
918+
def to_sql(self, name, con, flavor='sqlite', schema=None, if_exists='fail',
919+
index=True, index_label=None, chunksize=None):
920920
"""
921921
Write records stored in a DataFrame to a SQL database.
922922
@@ -932,6 +932,9 @@ def to_sql(self, name, con, flavor='sqlite', if_exists='fail', index=True,
932932
The flavor of SQL to use. Ignored when using SQLAlchemy engine.
933933
'mysql' is deprecated and will be removed in future versions, but it
934934
will be further supported through SQLAlchemy engines.
935+
schema : string, default None
936+
Specify the schema (if database flavor supports this). If None, use
937+
default schema.
935938
if_exists : {'fail', 'replace', 'append'}, default 'fail'
936939
- fail: If table exists, do nothing.
937940
- replace: If table exists, drop it, recreate it, and insert data.
@@ -949,8 +952,8 @@ def to_sql(self, name, con, flavor='sqlite', if_exists='fail', index=True,
949952
"""
950953
from pandas.io import sql
951954
sql.to_sql(
952-
self, name, con, flavor=flavor, if_exists=if_exists, index=index,
953-
index_label=index_label, chunksize=chunksize)
955+
self, name, con, flavor=flavor, schema=schema, if_exists=if_exists,
956+
index=index, index_label=index_label, chunksize=chunksize)
954957

955958
def to_pickle(self, path):
956959
"""

pandas/io/sql.py

+59-41
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def _is_sqlalchemy_engine(con):
3838
try:
3939
import sqlalchemy
4040
_SQLALCHEMY_INSTALLED = True
41-
41+
4242
from distutils.version import LooseVersion
4343
ver = LooseVersion(sqlalchemy.__version__)
4444
# For sqlalchemy versions < 0.8.2, the BIGINT type is recognized
@@ -47,7 +47,7 @@ def _is_sqlalchemy_engine(con):
4747
if ver < '0.8.2':
4848
from sqlalchemy import BigInteger
4949
from sqlalchemy.ext.compiler import compiles
50-
50+
5151
@compiles(BigInteger, 'sqlite')
5252
def compile_big_int_sqlite(type_, compiler, **kw):
5353
return 'INTEGER'
@@ -145,7 +145,7 @@ def _safe_fetch(cur):
145145
if not isinstance(result, list):
146146
result = list(result)
147147
return result
148-
except Exception as e: # pragma: no cover
148+
except Exception as e: # pragma: no cover
149149
excName = e.__class__.__name__
150150
if excName == 'OperationalError':
151151
return []
@@ -187,7 +187,7 @@ def tquery(sql, con=None, cur=None, retry=True):
187187
con.commit()
188188
except Exception as e:
189189
excName = e.__class__.__name__
190-
if excName == 'OperationalError': # pragma: no cover
190+
if excName == 'OperationalError': # pragma: no cover
191191
print('Failed to commit, may need to restart interpreter')
192192
else:
193193
raise
@@ -199,7 +199,7 @@ def tquery(sql, con=None, cur=None, retry=True):
199199
if result and len(result[0]) == 1:
200200
# python 3 compat
201201
result = list(lzip(*result)[0])
202-
elif result is None: # pragma: no cover
202+
elif result is None: # pragma: no cover
203203
result = []
204204

205205
return result
@@ -253,8 +253,8 @@ def uquery(sql, con=None, cur=None, retry=True, params=None):
253253
#------------------------------------------------------------------------------
254254
#--- Read and write to DataFrames
255255

256-
def read_sql_table(table_name, con, index_col=None, coerce_float=True,
257-
parse_dates=None, columns=None):
256+
def read_sql_table(table_name, con, schema=None, index_col=None,
257+
coerce_float=True, parse_dates=None, columns=None):
258258
"""Read SQL database table into a DataFrame.
259259
260260
Given a table name and an SQLAlchemy engine, returns a DataFrame.
@@ -266,6 +266,9 @@ def read_sql_table(table_name, con, index_col=None, coerce_float=True,
266266
Name of SQL table in database
267267
con : SQLAlchemy engine
268268
Sqlite DBAPI connection mode not supported
269+
schema : string, default None
270+
Name of SQL schema in database to query (if database flavor supports this).
271+
If None, use default schema (default).
269272
index_col : string, optional
270273
Column to set as index
271274
coerce_float : boolean, default True
@@ -298,7 +301,7 @@ def read_sql_table(table_name, con, index_col=None, coerce_float=True,
298301
"SQLAlchemy engines.")
299302
import sqlalchemy
300303
from sqlalchemy.schema import MetaData
301-
meta = MetaData(con)
304+
meta = MetaData(con, schema=schema)
302305
try:
303306
meta.reflect(only=[table_name])
304307
except sqlalchemy.exc.InvalidRequestError:
@@ -437,8 +440,8 @@ def read_sql(sql, con, index_col=None, coerce_float=True, params=None,
437440
coerce_float=coerce_float, parse_dates=parse_dates)
438441

439442

440-
def to_sql(frame, name, con, flavor='sqlite', if_exists='fail', index=True,
441-
index_label=None, chunksize=None):
443+
def to_sql(frame, name, con, flavor='sqlite', schema=None, if_exists='fail',
444+
index=True, index_label=None, chunksize=None):
442445
"""
443446
Write records stored in a DataFrame to a SQL database.
444447
@@ -455,6 +458,9 @@ def to_sql(frame, name, con, flavor='sqlite', if_exists='fail', index=True,
455458
The flavor of SQL to use. Ignored when using SQLAlchemy engine.
456459
'mysql' is deprecated and will be removed in future versions, but it
457460
will be further supported through SQLAlchemy engines.
461+
schema : string, default None
462+
Name of SQL schema in database to write to (if database flavor supports
463+
this). If None, use default schema (default).
458464
if_exists : {'fail', 'replace', 'append'}, default 'fail'
459465
- fail: If table exists, do nothing.
460466
- replace: If table exists, drop it, recreate it, and insert data.
@@ -473,18 +479,19 @@ def to_sql(frame, name, con, flavor='sqlite', if_exists='fail', index=True,
473479
if if_exists not in ('fail', 'replace', 'append'):
474480
raise ValueError("'{0}' is not valid for if_exists".format(if_exists))
475481

476-
pandas_sql = pandasSQL_builder(con, flavor=flavor)
482+
pandas_sql = pandasSQL_builder(con, schema=schema, flavor=flavor)
477483

478484
if isinstance(frame, Series):
479485
frame = frame.to_frame()
480486
elif not isinstance(frame, DataFrame):
481487
raise NotImplementedError
482488

483489
pandas_sql.to_sql(frame, name, if_exists=if_exists, index=index,
484-
index_label=index_label, chunksize=chunksize)
490+
index_label=index_label, schema=schema,
491+
chunksize=chunksize)
485492

486493

487-
def has_table(table_name, con, flavor='sqlite'):
494+
def has_table(table_name, con, flavor='sqlite', schema=None):
488495
"""
489496
Check if DataBase has named table.
490497
@@ -500,12 +507,15 @@ def has_table(table_name, con, flavor='sqlite'):
500507
The flavor of SQL to use. Ignored when using SQLAlchemy engine.
501508
'mysql' is deprecated and will be removed in future versions, but it
502509
will be further supported through SQLAlchemy engines.
510+
schema : string, default None
511+
Name of SQL schema in database to write to (if database flavor supports
512+
this). If None, use default schema (default).
503513
504514
Returns
505515
-------
506516
boolean
507517
"""
508-
pandas_sql = pandasSQL_builder(con, flavor=flavor)
518+
pandas_sql = pandasSQL_builder(con, flavor=flavor, schema=schema)
509519
return pandas_sql.has_table(table_name)
510520

511521
table_exists = has_table
@@ -515,15 +525,15 @@ def has_table(table_name, con, flavor='sqlite'):
515525
"and will be removed in future versions. "
516526
"MySQL will be further supported with SQLAlchemy engines.")
517527

518-
def pandasSQL_builder(con, flavor=None, meta=None, is_cursor=False):
528+
def pandasSQL_builder(con, flavor=None, schema=None, meta=None, is_cursor=False):
519529
"""
520530
Convenience function to return the correct PandasSQL subclass based on the
521531
provided parameters
522532
"""
523533
# When support for DBAPI connections is removed,
524534
# is_cursor should not be necessary.
525535
if _is_sqlalchemy_engine(con):
526-
return PandasSQLAlchemy(con, meta=meta)
536+
return PandasSQLAlchemy(con, schema=schema, meta=meta)
527537
else:
528538
if flavor == 'mysql':
529539
warnings.warn(_MYSQL_WARNING, FutureWarning)
@@ -540,24 +550,26 @@ class PandasSQLTable(PandasObject):
540550
"""
541551
# TODO: support for multiIndex
542552
def __init__(self, name, pandas_sql_engine, frame=None, index=True,
543-
if_exists='fail', prefix='pandas', index_label=None):
553+
if_exists='fail', prefix='pandas', index_label=None,
554+
schema=None):
544555
self.name = name
545556
self.pd_sql = pandas_sql_engine
546557
self.prefix = prefix
547558
self.frame = frame
548559
self.index = self._index_name(index, index_label)
560+
self.schema = schema
549561

550562
if frame is not None:
551563
# We want to write a frame
552-
if self.pd_sql.has_table(self.name):
564+
if self.pd_sql.has_table(self.name, self.schema):
553565
if if_exists == 'fail':
554566
raise ValueError("Table '%s' already exists." % name)
555567
elif if_exists == 'replace':
556-
self.pd_sql.drop_table(self.name)
568+
self.pd_sql.drop_table(self.name, self.schema)
557569
self.table = self._create_table_statement()
558570
self.create()
559571
elif if_exists == 'append':
560-
self.table = self.pd_sql.get_table(self.name)
572+
self.table = self.pd_sql.get_table(self.name, self.schema)
561573
if self.table is None:
562574
self.table = self._create_table_statement()
563575
else:
@@ -568,13 +580,13 @@ def __init__(self, name, pandas_sql_engine, frame=None, index=True,
568580
self.create()
569581
else:
570582
# no data provided, read-only mode
571-
self.table = self.pd_sql.get_table(self.name)
583+
self.table = self.pd_sql.get_table(self.name, self.schema)
572584

573585
if self.table is None:
574586
raise ValueError("Could not init table '%s'" % name)
575587

576588
def exists(self):
577-
return self.pd_sql.has_table(self.name)
589+
return self.pd_sql.has_table(self.name, self.schema)
578590

579591
def sql_schema(self):
580592
from sqlalchemy.schema import CreateTable
@@ -709,7 +721,7 @@ def _create_table_statement(self):
709721
columns = [Column(name, typ)
710722
for name, typ in column_names_and_types]
711723

712-
return Table(self.name, self.pd_sql.meta, *columns)
724+
return Table(self.name, self.pd_sql.meta, *columns, schema=self.schema)
713725

714726
def _harmonize_columns(self, parse_dates=None):
715727
""" Make a data_frame's column type align with an sql_table
@@ -830,11 +842,11 @@ class PandasSQLAlchemy(PandasSQL):
830842
using SQLAlchemy to handle DataBase abstraction
831843
"""
832844

833-
def __init__(self, engine, meta=None):
845+
def __init__(self, engine, schema=None, meta=None):
834846
self.engine = engine
835847
if not meta:
836848
from sqlalchemy.schema import MetaData
837-
meta = MetaData(self.engine)
849+
meta = MetaData(self.engine, schema=schema)
838850

839851
self.meta = meta
840852

@@ -843,9 +855,10 @@ def execute(self, *args, **kwargs):
843855
return self.engine.execute(*args, **kwargs)
844856

845857
def read_table(self, table_name, index_col=None, coerce_float=True,
846-
parse_dates=None, columns=None):
858+
parse_dates=None, columns=None, schema=None):
847859

848-
table = PandasSQLTable(table_name, self, index=index_col)
860+
table = PandasSQLTable(
861+
table_name, self, index=index_col, schema=schema)
849862
return table.read(coerce_float=coerce_float,
850863
parse_dates=parse_dates, columns=columns)
851864

@@ -868,26 +881,31 @@ def read_sql(self, sql, index_col=None, coerce_float=True,
868881
return data_frame
869882

870883
def to_sql(self, frame, name, if_exists='fail', index=True,
871-
index_label=None, chunksize=None):
884+
index_label=None, schema=None, chunksize=None):
872885
table = PandasSQLTable(
873886
name, self, frame=frame, index=index, if_exists=if_exists,
874-
index_label=index_label)
887+
index_label=index_label, schema=schema)
875888
table.insert(chunksize)
876889

877890
@property
878891
def tables(self):
879892
return self.meta.tables
880893

881-
def has_table(self, name):
882-
return self.engine.has_table(name)
894+
def has_table(self, name, schema=None):
895+
return self.engine.has_table(name, schema or self.meta.schema)
883896

884-
def get_table(self, table_name):
885-
return self.meta.tables.get(table_name)
897+
def get_table(self, table_name, schema=None):
898+
schema = schema or self.meta.schema
899+
if schema:
900+
return self.meta.tables.get('.'.join([schema, table_name]))
901+
else:
902+
return self.meta.tables.get(table_name)
886903

887-
def drop_table(self, table_name):
888-
if self.engine.has_table(table_name):
889-
self.meta.reflect(only=[table_name])
890-
self.get_table(table_name).drop()
904+
def drop_table(self, table_name, schema=None):
905+
schema = schema or self.meta.schema
906+
if self.engine.has_table(table_name, schema):
907+
self.meta.reflect(only=[table_name], schema=schema)
908+
self.get_table(table_name, schema).drop()
891909
self.meta.clear()
892910

893911
def _create_sql_schema(self, frame, table_name):
@@ -1113,7 +1131,7 @@ def _fetchall_as_list(self, cur):
11131131
return result
11141132

11151133
def to_sql(self, frame, name, if_exists='fail', index=True,
1116-
index_label=None, chunksize=None):
1134+
index_label=None, schema=None, chunksize=None):
11171135
"""
11181136
Write records stored in a DataFrame to a SQL database.
11191137
@@ -1133,7 +1151,7 @@ def to_sql(self, frame, name, if_exists='fail', index=True,
11331151
index_label=index_label)
11341152
table.insert(chunksize)
11351153

1136-
def has_table(self, name):
1154+
def has_table(self, name, schema=None):
11371155
flavor_map = {
11381156
'sqlite': ("SELECT name FROM sqlite_master "
11391157
"WHERE type='table' AND name='%s';") % name,
@@ -1142,10 +1160,10 @@ def has_table(self, name):
11421160

11431161
return len(self.execute(query).fetchall()) > 0
11441162

1145-
def get_table(self, table_name):
1163+
def get_table(self, table_name, schema=None):
11461164
return None # not supported in Legacy mode
11471165

1148-
def drop_table(self, name):
1166+
def drop_table(self, name, schema=None):
11491167
drop_sql = "DROP TABLE %s" % name
11501168
self.execute(drop_sql)
11511169

0 commit comments

Comments
 (0)