Repository: pandas-dev/pandas

Issue:
DBAPI 2.0 broken when using dictionary as parameters
The function [pandas.io.sql._convert_params](https://github.com/pydata/pandas/blob/b77eb1f11bcd65515ac33e1510fe03fd365bc433/pandas/io/sql.py#L30-L35) assume that the parameters are passed as a list. However the [DBAPI2.0](http://legacy.python.org/dev/peps/pep-0249/#id14) specify that the parameters can be a list or a mapping. This is a regression compared to the current stable version 0.13.1.



Discussion:

Resolving patch:
diff --git a/pandas/io/sql.py b/pandas/io/sql.py
--- a/pandas/io/sql.py
+++ b/pandas/io/sql.py
@@ -31,7 +31,10 @@ def _convert_params(sql, params):
     """convert sql and params args to DBAPI2.0 compliant format"""
     args = [sql]
     if params is not None:
-        args += list(params)
+        if hasattr(params, 'keys'):  # test if params is a mapping
+            args += [params]
+        else:
+            args += [list(params)]
     return args
 
 
@@ -200,7 +203,7 @@ def read_sql(sql, con, index_col=None, flavor='sqlite', coerce_float=True,
         Attempt to convert values to non-string, non-numeric objects (like
         decimal.Decimal) to floating point, useful for SQL result sets
     cur : depreciated, cursor is obtained from connection
-    params : list or tuple, optional
+    params : list, tuple or dict, optional
         List of parameters to pass to execute method.
     parse_dates : list or dict
         - List of column names to parse as dates


Test patch:
diff --git a/pandas/io/tests/test_sql.py b/pandas/io/tests/test_sql.py
--- a/pandas/io/tests/test_sql.py
+++ b/pandas/io/tests/test_sql.py
@@ -95,6 +95,24 @@
                 INSERT INTO types_test_data
                 VALUES(%s, %s, %s, %s, %s, %s, %s, %s)
                 """
+    },
+    'read_parameters': {
+        'sqlite': "SELECT * FROM iris WHERE Name=? AND SepalLength=?",
+        'mysql': 'SELECT * FROM iris WHERE `Name`="%s" AND `SepalLength`=%s',
+        'postgresql': 'SELECT * FROM iris WHERE "Name"=%s AND "SepalLength"=%s'
+    },
+    'read_named_parameters': {
+        'sqlite': """
+                SELECT * FROM iris WHERE Name=:name AND SepalLength=:length
+                """,
+        'mysql': """
+                SELECT * FROM iris WHERE
+                `Name`="%(name)s" AND `SepalLength`=%(length)s
+                """,
+        'postgresql': """
+                SELECT * FROM iris WHERE
+                "Name"=%(name)s AND "SepalLength"=%(length)s
+                """
     }
 }
 
@@ -168,6 +186,18 @@ def _read_sql_iris(self):
         iris_frame = self.pandasSQL.read_sql("SELECT * FROM iris")
         self._check_iris_loaded_frame(iris_frame)
 
+    def _read_sql_iris_parameter(self):
+        query = SQL_STRINGS['read_parameters'][self.flavor]
+        params = ['Iris-setosa', 5.1]
+        iris_frame = self.pandasSQL.read_sql(query, params=params)
+        self._check_iris_loaded_frame(iris_frame)
+
+    def _read_sql_iris_named_parameter(self):
+        query = SQL_STRINGS['read_named_parameters'][self.flavor]
+        params = {'name': 'Iris-setosa', 'length': 5.1}
+        iris_frame = self.pandasSQL.read_sql(query, params=params)
+        self._check_iris_loaded_frame(iris_frame)
+
     def _to_sql(self):
         self.drop_table('test_frame1')
 
@@ -491,6 +521,12 @@ class _TestSQLAlchemy(PandasSQLTest):
     def test_read_sql(self):
         self._read_sql_iris()
 
+    def test_read_sql_parameter(self):
+        self._read_sql_iris_parameter()
+
+    def test_read_sql_named_parameter(self):
+        self._read_sql_iris_named_parameter()
+
     def test_to_sql(self):
         self._to_sql()
 
@@ -703,6 +739,12 @@ def test_invalid_flavor(self):
     def test_read_sql(self):
         self._read_sql_iris()
 
+    def test_read_sql_parameter(self):
+        self._read_sql_iris_parameter()
+
+    def test_read_sql_named_parameter(self):
+        self._read_sql_iris_named_parameter()
+
     def test_to_sql(self):
         self._to_sql()