Skip to content

Commit 4bbab1a

Browse files
committed
Merge pull request pandas-dev#4333 from danielballan/sql
SQL file structure for legacy / sql alchemy
2 parents 8c0a34f + 4d37747 commit 4bbab1a

9 files changed

+939
-485
lines changed

ci/requirements-2.6.txt

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ python-dateutil==2.1
44
pytz==2013b
55
http://www.crummy.com/software/BeautifulSoup/bs4/download/4.2/beautifulsoup4-4.2.0.tar.gz
66
html5lib==1.0b2
7+
sqlalchemy==0.8

ci/requirements-2.7.txt

+1
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ scikits.timeseries==0.91.3
1616
MySQL-python==1.2.4
1717
scipy==0.10.0
1818
beautifulsoup4==4.2.1
19+
sqlalchemy==0.8

ci/requirements-2.7_LOCALE.txt

+1
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ html5lib==1.0b2
1414
lxml==3.2.1
1515
scipy==0.10.0
1616
beautifulsoup4==4.2.1
17+
sqlalchemy==0.8

ci/requirements-3.2.txt

+1
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ patsy==0.1.0
1111
lxml==3.2.1
1212
scipy==0.12.0
1313
beautifulsoup4==4.2.1
14+
sqlalchemy==0.8

ci/requirements-3.3.txt

+1
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ patsy==0.1.0
1212
lxml==3.2.1
1313
scipy==0.12.0
1414
beautifulsoup4==4.2.1
15+
sqlalchemy==0.8

pandas/io/sql.py

+114-13
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
import numpy as np
88
import traceback
99

10+
import sqlite3
11+
import warnings
12+
1013
from pandas.core.datetools import format as date_format
1114
from pandas.core.api import DataFrame, isnull
15+
from pandas.io import sql_legacy
1216

1317
#------------------------------------------------------------------------------
1418
# Helper execution function
@@ -132,8 +136,85 @@ def uquery(sql, con=None, cur=None, retry=True, params=None):
132136
return uquery(sql, con, retry=False)
133137
return result
134138

139+
class SQLAlchemyRequired(Exception):
140+
pass
141+
142+
class LegacyMySQLConnection(Exception):
143+
pass
135144

136-
def read_frame(sql, con, index_col=None, coerce_float=True, params=None):
145+
def get_connection(con, dialect, driver, username, password,
146+
host, port, database):
147+
if isinstance(con, basestring):
148+
try:
149+
import sqlalchemy
150+
return _alchemy_connect_sqlite(con)
151+
except:
152+
return sqlite3.connect(con)
153+
if isinstance(con, sqlite3.Connection):
154+
return con
155+
try:
156+
import MySQLdb
157+
except ImportError:
158+
# If we don't have MySQLdb, this can't be a MySQLdb connection.
159+
pass
160+
else:
161+
if isinstance(con, MySQLdb.connection):
162+
raise LegacyMySQLConnection
163+
# If we reach here, SQLAlchemy will be needed.
164+
try:
165+
import sqlalchemy
166+
except ImportError:
167+
raise SQLAlchemyRequired
168+
if isinstance(con, sqlalchemy.engine.Engine):
169+
return con.connect()
170+
if isinstance(con, sqlalchemy.engine.Connection):
171+
return con
172+
if con is None:
173+
url_params = (dialect, driver, username, \
174+
password, host, port, database)
175+
url = _build_url(*url_params)
176+
engine = sqlalchemy.create_engine(url)
177+
return engine.connect()
178+
if hasattr(con, 'cursor') and callable(con.cursor):
179+
# This looks like some Connection object from a driver module.
180+
raise NotImplementedError, \
181+
"""To ensure robust support of varied SQL dialects, pandas
182+
only supports database connections from SQLAlchemy. (Legacy
183+
support for MySQLdb connections are available but buggy.)"""
184+
else:
185+
raise ValueError, \
186+
"""con must be a string, a Connection to a sqlite Database,
187+
or a SQLAlchemy Connection or Engine object."""
188+
189+
190+
def _alchemy_connect_sqlite(path):
191+
if path == ':memory:':
192+
return create_engine('sqlite://').connect()
193+
else:
194+
return create_engine('sqlite:///%s' % path).connect()
195+
196+
def _build_url(dialect, driver, username, password, host, port, database):
197+
# Create an Engine and from that a Connection.
198+
# We use a string instead of sqlalchemy.engine.url.URL because
199+
# we do not necessarily know the driver; we know the dialect.
200+
required_params = [dialect, username, password, host, database]
201+
for p in required_params:
202+
if not isinstance(p, basestring):
203+
raise ValueError, \
204+
"Insufficient information to connect to a database;" \
205+
"see docstring."
206+
url = dialect
207+
if driver is not None:
208+
url += "+%s" % driver
209+
url += "://%s:%s@%s" % (username, password, host)
210+
if port is not None:
211+
url += ":%d" % port
212+
url += "/%s" % database
213+
return url
214+
215+
def read_sql(sql, con=None, index_col=None, flavor=None, driver=None,
216+
username=None, password=None, host=None, port=None,
217+
database=None, coerce_float=True, params=None):
137218
"""
138219
Returns a DataFrame corresponding to the result set of the query
139220
string.
@@ -145,32 +226,52 @@ def read_frame(sql, con, index_col=None, coerce_float=True, params=None):
145226
----------
146227
sql: string
147228
SQL query to be executed
148-
con: DB connection object, optional
229+
con : Connection object, SQLAlchemy Engine object, a filepath string
230+
(sqlite only) or the string ':memory:' (sqlite only). Alternatively,
231+
specify a user, passwd, host, and db below.
149232
index_col: string, optional
150233
column name to use for the returned DataFrame object.
234+
flavor : string specifying the flavor of SQL to use
235+
driver : string specifying SQL driver (e.g., MySQLdb), optional
236+
username: username for database authentication
237+
only needed if a Connection, Engine, or filepath are not given
238+
password: password for database authentication
239+
only needed if a Connection, Engine, or filepath are not given
240+
host: host for database connection
241+
only needed if a Connection, Engine, or filepath are not given
242+
database: database name
243+
only needed if a Connection, Engine, or filepath are not given
151244
coerce_float : boolean, default True
152245
Attempt to convert values to non-string, non-numeric objects (like
153246
decimal.Decimal) to floating point, useful for SQL result sets
154247
params: list or tuple, optional
155248
List of parameters to pass to execute method.
156249
"""
157-
cur = execute(sql, con, params=params)
158-
rows = _safe_fetch(cur)
159-
columns = [col_desc[0] for col_desc in cur.description]
160-
161-
cur.close()
162-
con.commit()
163-
164-
result = DataFrame.from_records(rows, columns=columns,
165-
coerce_float=coerce_float)
250+
dialect = flavor
251+
try:
252+
connection = get_connection(con, dialect, driver, username, password,
253+
host, port, database)
254+
except LegacyMySQLConnection:
255+
warnings.warn("For more robust support, connect using " \
256+
"SQLAlchemy. See documentation.")
257+
return sql_legacy.read_frame(sql, con, index_col, coerce_float, params)
258+
259+
if params is None:
260+
params = []
261+
cursor = connection.execute(sql, *params)
262+
result = _safe_fetch(cursor)
263+
columns = [col_desc[0] for col_desc in cursor.description]
264+
cursor.close()
265+
266+
result = DataFrame.from_records(result, columns=columns)
166267

167268
if index_col is not None:
168269
result = result.set_index(index_col)
169270

170271
return result
171272

172-
frame_query = read_frame
173-
read_sql = read_frame
273+
frame_query = read_sql
274+
read_frame = read_sql
174275

175276
def write_frame(frame, name, con, flavor='sqlite', if_exists='fail', **kwargs):
176277
"""

0 commit comments

Comments
 (0)