From b0089eb6fe0fff6327b39a78e758d027c59b15be Mon Sep 17 00:00:00 2001 From: Kareem Zidane Date: Thu, 26 Apr 2018 11:22:09 -0400 Subject: [PATCH 1/8] added sqlite foreign key support --- src/cs50/sql.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index eef4af9..11a1520 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -5,6 +5,7 @@ import os import re import sqlalchemy +import sqlite3 import sqlparse import sys import termcolor @@ -32,6 +33,22 @@ def __init__(self, url, **kwargs): if not os.path.isfile(matches.group(1)): raise RuntimeError("not a file: {}".format(matches.group(1))) + # Optionally enable foreign key constraints + # http://docs.sqlalchemy.org/en/latest/dialects/sqlite.html#foreign-key-support + if kwargs.pop("pragma_foreign_keys", False): + @sqlalchemy.event.listens_for(sqlalchemy.engine.Engine, "connect") + def _set_sqlite_pragma(dbapi_connection, connection_record): + """Enables foreign key support.""" + + # Ensure backend is sqlite + if type(dbapi_connection) is sqlite3.Connection: + cursor = dbapi_connection.cursor() + + # Respect foreign key constraints by default + cursor.execute("PRAGMA foreign_keys=ON") + cursor.close() + + # Create engine, raising exception if back end's module not installed self.engine = sqlalchemy.create_engine(url, **kwargs) From 89657ea6aa3d62425f55d0636d6a01ec81671805 Mon Sep 17 00:00:00 2001 From: Kareem Zidane Date: Thu, 26 Apr 2018 11:52:09 -0400 Subject: [PATCH 2/8] restoring logger's original value --- src/cs50/sql.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index 11a1520..50e3fdf 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -55,6 +55,7 @@ def _set_sqlite_pragma(dbapi_connection, connection_record): # Log statements to standard error logging.basicConfig(level=logging.DEBUG) self.logger = logging.getLogger("cs50") + disabled = self.logger.disabled # Test database try: @@ -65,7 +66,7 @@ def _set_sqlite_pragma(dbapi_connection, connection_record): e.__cause__ = None raise e else: - self.logger.disabled = False + self.logger.disabled = disabled def _parse(self, e): """Parses an exception, returns its message.""" From 87087747822ae9dec84df6d992175be6fb74fbbd Mon Sep 17 00:00:00 2001 From: Kareem Zidane Date: Thu, 26 Apr 2018 13:30:16 -0400 Subject: [PATCH 3/8] handling connect on engine level --- .travis.yml | 2 +- src/cs50/sql.py | 37 ++++++++++++++++++++++--------------- tests/sql.py | 14 +++++++++++--- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/.travis.yml b/.travis.yml index a3dfb1d..82338b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ install: before_script: - mysql -e 'CREATE DATABASE IF NOT EXISTS test;' - psql -c 'create database test;' -U postgres -- touch test.db +- touch test.db test1.db script: python tests/sql.py after_script: rm -f test.db jobs: diff --git a/src/cs50/sql.py b/src/cs50/sql.py index 50e3fdf..fcd5e1a 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -33,24 +33,18 @@ def __init__(self, url, **kwargs): if not os.path.isfile(matches.group(1)): raise RuntimeError("not a file: {}".format(matches.group(1))) - # Optionally enable foreign key constraints - # http://docs.sqlalchemy.org/en/latest/dialects/sqlite.html#foreign-key-support - if kwargs.pop("pragma_foreign_keys", False): - @sqlalchemy.event.listens_for(sqlalchemy.engine.Engine, "connect") - def _set_sqlite_pragma(dbapi_connection, connection_record): - """Enables foreign key support.""" + pragma_foreign_keys = kwargs.pop("pragma_foreign_keys", False) - # Ensure backend is sqlite - if type(dbapi_connection) is sqlite3.Connection: - cursor = dbapi_connection.cursor() - - # Respect foreign key constraints by default - cursor.execute("PRAGMA foreign_keys=ON") - cursor.close() + # Create engine, raising exception if back end's module not installed + self.engine = sqlalchemy.create_engine(url, **kwargs) + # Whether to enable foreign key constraints + if pragma_foreign_keys: + sqlalchemy.event.listen(self.engine, "connect", _on_connect) + else: + # Create engine, raising exception if back end's module not installed + self.engine = sqlalchemy.create_engine(url, **kwargs) - # Create engine, raising exception if back end's module not installed - self.engine = sqlalchemy.create_engine(url, **kwargs) # Log statements to standard error logging.basicConfig(level=logging.DEBUG) @@ -229,3 +223,16 @@ def process(value): else: self.logger.debug(termcolor.colored(log, "green")) return ret + + +# http://docs.sqlalchemy.org/en/latest/dialects/sqlite.html#foreign-key-support +def _on_connect(dbapi_connection, connection_record): + """Enables foreign key support.""" + + # Ensure backend is sqlite + if type(dbapi_connection) is sqlite3.Connection: + cursor = dbapi_connection.cursor() + + # Respect foreign key constraints by default + cursor.execute("PRAGMA foreign_keys=ON") + cursor.close() diff --git a/tests/sql.py b/tests/sql.py index c2af662..1fdee42 100644 --- a/tests/sql.py +++ b/tests/sql.py @@ -107,12 +107,19 @@ class SQLiteTests(SQLTests): @classmethod def setUpClass(self): self.db = SQL("sqlite:///test.db") + self.db1 = SQL("sqlite:///test1.db", pragma_foreign_keys=True) def setUp(self): self.db.execute("CREATE TABLE cs50(id INTEGER PRIMARY KEY, val TEXT)") - def multi_inserts_enabled(self): - return False + def test_foreign_key_support(self): + self.db.execute("CREATE TABLE foo(id INTEGER PRIMARY KEY)") + self.db.execute("CREATE TABLE bar(foo_id INTEGER, FOREIGN KEY (foo_id) REFERENCES foo(id))") + self.assertEqual(self.db.execute("INSERT INTO bar VALUES(50)"), 1) + + self.db1.execute("CREATE TABLE foo(id INTEGER PRIMARY KEY)") + self.db1.execute("CREATE TABLE bar(foo_id INTEGER, FOREIGN KEY (foo_id) REFERENCES foo(id))") + self.assertEqual(self.db1.execute("INSERT INTO bar VALUES(50)"), None) if __name__ == "__main__": suite = unittest.TestSuite([ @@ -120,5 +127,6 @@ def multi_inserts_enabled(self): unittest.TestLoader().loadTestsFromTestCase(MySQLTests), unittest.TestLoader().loadTestsFromTestCase(PostgresTests) ]) - logging.getLogger("cs50.sql").disabled = True + + logging.getLogger("cs50").disabled = True sys.exit(not unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful()) From a94c652cbb00587aeea97790bfcde7cb60401678 Mon Sep 17 00:00:00 2001 From: Kareem Zidane Date: Thu, 26 Apr 2018 13:30:33 -0400 Subject: [PATCH 4/8] upped version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9f11ae5..1a9c080 100644 --- a/setup.py +++ b/setup.py @@ -16,5 +16,5 @@ package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="2.4.0" + version="2.4.1" ) From 60cd1b071c4f4b857faf83d33ace0f81e1199015 Mon Sep 17 00:00:00 2001 From: Kareem Zidane Date: Thu, 26 Apr 2018 13:32:50 -0400 Subject: [PATCH 5/8] renamed argument --- src/cs50/sql.py | 4 ++-- tests/sql.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index fcd5e1a..ea1d793 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -33,13 +33,13 @@ def __init__(self, url, **kwargs): if not os.path.isfile(matches.group(1)): raise RuntimeError("not a file: {}".format(matches.group(1))) - pragma_foreign_keys = kwargs.pop("pragma_foreign_keys", False) + foreign_keys_enabled = kwargs.pop("foreign_keys_enabled", False) # Create engine, raising exception if back end's module not installed self.engine = sqlalchemy.create_engine(url, **kwargs) # Whether to enable foreign key constraints - if pragma_foreign_keys: + if foreign_keys_enabled: sqlalchemy.event.listen(self.engine, "connect", _on_connect) else: # Create engine, raising exception if back end's module not installed diff --git a/tests/sql.py b/tests/sql.py index 1fdee42..8e46777 100644 --- a/tests/sql.py +++ b/tests/sql.py @@ -107,7 +107,7 @@ class SQLiteTests(SQLTests): @classmethod def setUpClass(self): self.db = SQL("sqlite:///test.db") - self.db1 = SQL("sqlite:///test1.db", pragma_foreign_keys=True) + self.db1 = SQL("sqlite:///test1.db", foreign_keys_enabled=True) def setUp(self): self.db.execute("CREATE TABLE cs50(id INTEGER PRIMARY KEY, val TEXT)") From d5eda23c7e399531c79737ef57ccc0c0d0741239 Mon Sep 17 00:00:00 2001 From: Kareem Zidane Date: Thu, 26 Apr 2018 14:37:56 -0400 Subject: [PATCH 6/8] name tweaks --- src/cs50/sql.py | 11 ++++++----- tests/sql.py | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index ea1d793..f5e614a 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -33,14 +33,15 @@ def __init__(self, url, **kwargs): if not os.path.isfile(matches.group(1)): raise RuntimeError("not a file: {}".format(matches.group(1))) - foreign_keys_enabled = kwargs.pop("foreign_keys_enabled", False) + # Remember foreign_keys and remove it from kwargs + foreign_keys = kwargs.pop("foreign_keys", False) # Create engine, raising exception if back end's module not installed self.engine = sqlalchemy.create_engine(url, **kwargs) - # Whether to enable foreign key constraints - if foreign_keys_enabled: - sqlalchemy.event.listen(self.engine, "connect", _on_connect) + # Enable foreign key constraints + if foreign_keys: + sqlalchemy.event.listen(self.engine, "connect", _connect) else: # Create engine, raising exception if back end's module not installed self.engine = sqlalchemy.create_engine(url, **kwargs) @@ -226,7 +227,7 @@ def process(value): # http://docs.sqlalchemy.org/en/latest/dialects/sqlite.html#foreign-key-support -def _on_connect(dbapi_connection, connection_record): +def _connect(dbapi_connection, connection_record): """Enables foreign key support.""" # Ensure backend is sqlite diff --git a/tests/sql.py b/tests/sql.py index 8e46777..67b4e94 100644 --- a/tests/sql.py +++ b/tests/sql.py @@ -107,7 +107,7 @@ class SQLiteTests(SQLTests): @classmethod def setUpClass(self): self.db = SQL("sqlite:///test.db") - self.db1 = SQL("sqlite:///test1.db", foreign_keys_enabled=True) + self.db1 = SQL("sqlite:///test1.db", foreign_keys=True) def setUp(self): self.db.execute("CREATE TABLE cs50(id INTEGER PRIMARY KEY, val TEXT)") From 48ca2e0f13dc18cac7aa657e4b9e10d1f45d169c Mon Sep 17 00:00:00 2001 From: Kareem Zidane Date: Thu, 26 Apr 2018 14:56:30 -0400 Subject: [PATCH 7/8] added comment about multi statements --- src/cs50/sql.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index f5e614a..8a4d944 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -146,6 +146,8 @@ def process(value): return process(value) # Allow only one statement at a time + # SQLite does not support executing many statements + # https://docs.python.org/3/library/sqlite3.html#sqlite3.Cursor.execute if len(sqlparse.split(text)) > 1: raise RuntimeError("too many statements at once") From afb6cbaa9b473af0fb1c2ad8a2c2beef05b4f5b8 Mon Sep 17 00:00:00 2001 From: Kareem Zidane Date: Thu, 26 Apr 2018 15:01:53 -0400 Subject: [PATCH 8/8] added a blank line before comment --- src/cs50/sql.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index 8a4d944..2ede1ca 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -43,6 +43,7 @@ def __init__(self, url, **kwargs): if foreign_keys: sqlalchemy.event.listen(self.engine, "connect", _connect) else: + # Create engine, raising exception if back end's module not installed self.engine = sqlalchemy.create_engine(url, **kwargs)