From 19b33c09cf1e9c0cdf1bfdb303a0495e18539b80 Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Sat, 20 May 2017 11:53:24 -0400
Subject: [PATCH 1/4] added dependency

---
 setup.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.py b/setup.py
index cee2266..28ed25a 100644
--- a/setup.py
+++ b/setup.py
@@ -10,7 +10,7 @@
         "Topic :: Software Development :: Libraries :: Python Modules"
     ],
     description="CS50 library for Python",
-    install_requires=["SQLAlchemy"],
+    install_requires=["SQLAlchemy", "sqlparse"],
     keywords="cs50",
     name="cs50",
     packages=["cs50"],

From 508937a54c09056b8f3d7c9c8306f213d2f56875 Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Sat, 20 May 2017 19:51:12 -0400
Subject: [PATCH 2/4] trying support for all paramstyles

---
 cs50/sql.py | 39 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 39 insertions(+)

diff --git a/cs50/sql.py b/cs50/sql.py
index 2064133..1ad0664 100644
--- a/cs50/sql.py
+++ b/cs50/sql.py
@@ -1,4 +1,6 @@
+import re
 import sqlalchemy
+import sqlparse
 
 class SQL(object):
     """Wrap SQLAlchemy to provide a simple SQL API."""
@@ -20,8 +22,45 @@ def execute(self, text, *multiparams, **params):
         """
         Execute a SQL statement.
         """
+
+        # parse text
+        parsed = sqlparse.parse(text)
+        if len(parsed) == 0:
+            raise RuntimeError("missing statement")
+        elif len(parsed) > 1:
+            raise RuntimeError("too many statements")
+        statement = parsed[0]
+        if statement.get_type() == "UNKNOWN":
+            raise RuntimeError("unknown type of statement")
+
+        # infer paramstyle
+	# https://www.python.org/dev/peps/pep-0249/#paramstyle
+	paramstyle = None
+	for token in statement.flatten():
+	    if sqlparse.utils.imt(token.ttype, t=sqlparse.tokens.Token.Name.Placeholder):
+		_paramstyle = None
+		if re.search(r"^\?$", token.value):
+		    _paramstyle = "qmark"
+		elif re.search(r"^:\d+$", token.value):
+		    _paramstyle = "numeric"
+		elif re.search(r"^:\w+$", token.value):
+		    _paramstyle = "named"
+		elif re.search(r"^%s$", token.value):
+		    _paramstyle = "format"
+		elif re.search(r"^%\(\w+\)s$", token.value):
+		    _paramstyle = "pyformat"
+		else:
+		    raise RuntimeError("unknown paramstyle")
+		if paramstyle and paramstyle != _paramstyle:
+		    raise RuntimeError("inconsistent paramstyle")
+		paramstyle = _paramstyle
+
         try:
 
+            parsed = sqlparse.split("SELECT * FROM cs50 WHERE id IN (SELECT id FROM cs50); SELECT 1; CREATE TABLE foo")
+            print(parsed)
+            return 0
+
             # bind parameters before statement reaches database, so that bound parameters appear in exceptions
             # http://docs.sqlalchemy.org/en/latest/core/sqlelement.html#sqlalchemy.sql.expression.text
             # https://groups.google.com/forum/#!topic/sqlalchemy/FfLwKT1yQlg

From ab5d8366c6af32eaf76b3e54b225b7f9f33f79e0 Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Sat, 20 May 2017 21:51:39 -0400
Subject: [PATCH 3/4] added support for expandable parameters

---
 cs50/sql.py | 120 +++++++++++++++++++++++++++++++++-------------------
 1 file changed, 77 insertions(+), 43 deletions(-)

diff --git a/cs50/sql.py b/cs50/sql.py
index 1ad0664..fa9a864 100644
--- a/cs50/sql.py
+++ b/cs50/sql.py
@@ -1,6 +1,6 @@
-import re
+import datetime
 import sqlalchemy
-import sqlparse
+import sys
 
 class SQL(object):
     """Wrap SQLAlchemy to provide a simple SQL API."""
@@ -16,58 +16,91 @@ def __init__(self, url):
         try:
             self.engine = sqlalchemy.create_engine(url)
         except Exception as e:
+            e.__context__ = None
             raise RuntimeError(e)
 
-    def execute(self, text, *multiparams, **params):
+    def execute(self, text, **params):
         """
         Execute a SQL statement.
         """
 
-        # parse text
-        parsed = sqlparse.parse(text)
-        if len(parsed) == 0:
-            raise RuntimeError("missing statement")
-        elif len(parsed) > 1:
-            raise RuntimeError("too many statements")
-        statement = parsed[0]
-        if statement.get_type() == "UNKNOWN":
-            raise RuntimeError("unknown type of statement")
-
-        # infer paramstyle
-	# https://www.python.org/dev/peps/pep-0249/#paramstyle
-	paramstyle = None
-	for token in statement.flatten():
-	    if sqlparse.utils.imt(token.ttype, t=sqlparse.tokens.Token.Name.Placeholder):
-		_paramstyle = None
-		if re.search(r"^\?$", token.value):
-		    _paramstyle = "qmark"
-		elif re.search(r"^:\d+$", token.value):
-		    _paramstyle = "numeric"
-		elif re.search(r"^:\w+$", token.value):
-		    _paramstyle = "named"
-		elif re.search(r"^%s$", token.value):
-		    _paramstyle = "format"
-		elif re.search(r"^%\(\w+\)s$", token.value):
-		    _paramstyle = "pyformat"
-		else:
-		    raise RuntimeError("unknown paramstyle")
-		if paramstyle and paramstyle != _paramstyle:
-		    raise RuntimeError("inconsistent paramstyle")
-		paramstyle = _paramstyle
+        class UserDefinedType(sqlalchemy.TypeDecorator):
+            """
+            Add support for expandable values, a la https://bitbucket.org/zzzeek/sqlalchemy/issues/3953/expanding-parameter.
+            """
+            impl = sqlalchemy.types.UserDefinedType
+            def process_literal_param(self, value, dialect):
+                """Receive a literal parameter value to be rendered inline within a statement."""
+                def process(value):
+                    """Render a literal value, escaping as needed."""
+
+                    # bool
+                    if isinstance(value, bool):
+                        return sqlalchemy.types.Boolean().literal_processor(dialect)(value)
+
+                    # datetime.date
+                    elif isinstance(value, datetime.date):
+                        return sqlalchemy.types.String().literal_processor(dialect)(value.strftime("%Y-%m-%d"))
+
+                    # datetime.datetime
+                    elif isinstance(value, datetime.datetime):
+                        return sqlalchemy.types.String().literal_processor(dialect)(value.strftime("%Y-%m-%d %H:%M:%S"))
+
+                    # datetime.time
+                    elif isinstance(value, datetime.time):
+                        return sqlalchemy.types.String().literal_processor(dialect)(value.strftime("%H:%M:%S"))
+
+                    # float
+                    elif isinstance(value, float):
+                        return sqlalchemy.types.Float().literal_processor(dialect)(value)
+
+                    # int
+                    elif isinstance(value, int):
+                        return sqlalchemy.types.Integer().literal_processor(dialect)(value)
+
+                    # long
+                    elif sys.version_info.major != 3 and isinstance(value, long):
+                        return sqlalchemy.types.Integer().literal_processor(dialect)(value)
+
+                    # str
+                    elif isinstance(value, str):
+                        return sqlalchemy.types.String().literal_processor(dialect)(value)
+
+                    # None
+                    elif isinstance(value, sqlalchemy.sql.elements.Null):
+                        return sqlalchemy.types.NullType().literal_processor(dialect)(value)
+
+                    # unsupported value
+                    raise RuntimeError("unsupported value")
+
+                # process value(s), separating with commas as needed
+                if type(value) is list:
+                    return ", ".join([process(v) for v in value])
+                else:
+                    return process(value)
 
         try:
 
-            parsed = sqlparse.split("SELECT * FROM cs50 WHERE id IN (SELECT id FROM cs50); SELECT 1; CREATE TABLE foo")
-            print(parsed)
-            return 0
+            # construct a new TextClause clause
+            statement = sqlalchemy.text(text)
+
+            # iterate over parameters
+            for key, value in params.items():
 
-            # bind parameters before statement reaches database, so that bound parameters appear in exceptions
-            # http://docs.sqlalchemy.org/en/latest/core/sqlelement.html#sqlalchemy.sql.expression.text
-            # https://groups.google.com/forum/#!topic/sqlalchemy/FfLwKT1yQlg
-            # http://docs.sqlalchemy.org/en/latest/core/connections.html#sqlalchemy.engine.Engine.execute
+                # translate None to NULL
+                if value is None:
+                    value = sqlalchemy.sql.null()
+
+                # bind parameters before statement reaches database, so that bound parameters appear in exceptions
+                # http://docs.sqlalchemy.org/en/latest/core/sqlelement.html#sqlalchemy.sql.expression.text
+                statement = statement.bindparams(sqlalchemy.bindparam(key, value=value, type_=UserDefinedType()))
+
+            # stringify bound parameters
             # http://docs.sqlalchemy.org/en/latest/faq/sqlexpressions.html#how-do-i-render-sql-expressions-as-strings-possibly-with-bound-parameters-inlined
-            statement = sqlalchemy.text(text).bindparams(*multiparams, **params)
-            result = self.engine.execute(str(statement.compile(compile_kwargs={"literal_binds": True})))
+            self.statement = str(statement.compile(compile_kwargs={"literal_binds": True}))
+
+            # execute statement
+            result = self.engine.execute(self.statement)
 
             # if SELECT (or INSERT with RETURNING), return result set as list of dict objects
             if result.returns_rows:
@@ -88,4 +121,5 @@ def execute(self, text, *multiparams, **params):
 
         # else raise error
         except Exception as e:
+            e.__context__ = None
             raise RuntimeError(e)

From 64c1a04e2700378e37fb7853dc1a8fe89a4934e6 Mon Sep 17 00:00:00 2001
From: "David J. Malan" <malan@harvard.edu>
Date: Sat, 20 May 2017 21:51:45 -0400
Subject: [PATCH 4/4] removed sqlparse

---
 setup.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.py b/setup.py
index 28ed25a..cee2266 100644
--- a/setup.py
+++ b/setup.py
@@ -10,7 +10,7 @@
         "Topic :: Software Development :: Libraries :: Python Modules"
     ],
     description="CS50 library for Python",
-    install_requires=["SQLAlchemy", "sqlparse"],
+    install_requires=["SQLAlchemy"],
     keywords="cs50",
     name="cs50",
     packages=["cs50"],