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"],