Skip to content

Commit 6d9bad7

Browse files
authored
Merge pull request #163 from cs50/postgresql
Addresses PostgreSQL rollbacks
2 parents 8c08044 + be80c55 commit 6d9bad7

File tree

7 files changed

+93
-35
lines changed

7 files changed

+93
-35
lines changed

.github/workflows/main.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ jobs:
3030
pip install mysqlclient psycopg2-binary
3131
- name: Run tests
3232
run: python tests/sql.py
33+
env:
34+
MYSQL_HOST: 127.0.0.1
35+
POSTGRESQL_HOST: 127.0.0.1
3336
- name: Install pypa/build
3437
run: python -m pip install build --user
3538
- name: Build a binary wheel and a source tarball

Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
FROM cs50/cli
2+
3+
RUN sudo apt update && sudo apt install --yes libmysqlclient-dev pgloader postgresql
4+
RUN sudo pip3 install mysqlclient psycopg2-binary
5+
6+
WORKDIR /mnt

README.md

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,25 @@ s = cs50.get_string();
2222

2323
## Testing
2424

25-
1. Run `cli50` in `python-cs50`.
26-
1. Run `sudo su -`.
27-
1. Run `apt update`.
28-
1. Run `apt install -y libmysqlclient-dev mysql-server postgresql`.
29-
1. Run `pip3 install mysqlclient psycopg2-binary`.
30-
1. In `/etc/mysql/mysql.conf.d/mysqld.cnf`, add `skip-grant-tables` under `[mysqld]`.
31-
1. In `/etc/profile.d/cli.sh`, remove `valgrind` function for now.
32-
1. Run `service mysql start`.
33-
1. Run `mysql -e 'CREATE DATABASE IF NOT EXISTS test;'`.
34-
1. In `/etc/postgresql/12/main/pg_hba.conf`, change:
35-
```
36-
local all postgres peer
37-
host all all 127.0.0.1/32 md5
38-
```
39-
to:
40-
```
41-
local all postgres trust
42-
host all all 127.0.0.1/32 trust
43-
```
44-
1. Run `service postgresql start`.
45-
1. Run `psql -c 'create database test;' -U postgres`.
25+
1. In one terminal, execute:
26+
27+
```
28+
cd python-cs50
29+
docker compose build
30+
docker compose up
31+
```
32+
33+
1. In another terminal, execute:
34+
35+
```
36+
docker exec -it python-cs50 bash -l
37+
```
38+
39+
And then execute, e.g.:
40+
41+
```
42+
python tests/sql.py
43+
```
4644
4745
### Sample Tests
4846

docker-compose.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
services:
2+
cli:
3+
build: .
4+
container_name: python-cs50
5+
depends_on:
6+
- mysql
7+
- postgres
8+
environment:
9+
MYSQL_HOST: mysql
10+
POSTGRESQL_HOST: postgresql
11+
links:
12+
- mysql
13+
- postgres
14+
tty: true
15+
volumes:
16+
- .:/mnt
17+
mysql:
18+
environment:
19+
MYSQL_DATABASE: test
20+
MYSQL_ALLOW_EMPTY_PASSWORD: yes
21+
healthcheck:
22+
test: ["CMD", "mysqladmin", "-uroot", "ping"]
23+
image: cs50/mysql:8
24+
ports:
25+
- 3306:3306
26+
postgres:
27+
image: postgres:12
28+
environment:
29+
POSTGRES_USER: postgres
30+
POSTGRES_PASSWORD: postgres
31+
POSTGRES_DB: test
32+
ports:
33+
- 5432:5432
34+
version: "3.6"

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@
1616
package_dir={"": "src"},
1717
packages=["cs50"],
1818
url="https://github.com/cs50/python-cs50",
19-
version="8.0.1"
19+
version="8.0.2"
2020
)

src/cs50/sql.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,19 +61,17 @@ def __init__(self, url, **kwargs):
6161
if not os.path.isfile(matches.group(1)):
6262
raise RuntimeError("not a file: {}".format(matches.group(1)))
6363

64-
# Create engine, disabling SQLAlchemy's own autocommit mode, raising exception if back end's module not installed
65-
self._engine = sqlalchemy.create_engine(url, **kwargs).execution_options(autocommit=False)
64+
# Create engine, disabling SQLAlchemy's own autocommit mode raising exception if back end's module not installed;
65+
# without isolation_level, PostgreSQL warns with "there is already a transaction in progress" for our own BEGIN and
66+
# "there is no transaction in progress" for our own COMMIT
67+
self._engine = sqlalchemy.create_engine(url, **kwargs).execution_options(autocommit=False, isolation_level="AUTOCOMMIT")
6668

6769
# Get logger
6870
self._logger = logging.getLogger("cs50")
6971

7072
# Listener for connections
7173
def connect(dbapi_connection, connection_record):
7274

73-
# Disable underlying API's own emitting of BEGIN and COMMIT so we can ourselves
74-
# https://docs.sqlalchemy.org/en/13/dialects/sqlite.html#serializable-isolation-savepoints-transactional-ddl
75-
dbapi_connection.isolation_level = None
76-
7775
# Enable foreign key constraints
7876
if type(dbapi_connection) is sqlite3.Connection: # If back end is sqlite
7977
cursor = dbapi_connection.cursor()
@@ -353,12 +351,30 @@ def teardown_appcontext(exception):
353351

354352
# If INSERT, return primary key value for a newly inserted row (or None if none)
355353
elif command == "INSERT":
354+
355+
# If PostgreSQL
356356
if self._engine.url.get_backend_name() == "postgresql":
357-
try:
358-
result = connection.execute("SELECT LASTVAL()")
359-
ret = result.first()[0]
360-
except sqlalchemy.exc.OperationalError: # If lastval is not yet defined for this connection
361-
ret = None
357+
358+
# Return LASTVAL() or NULL, avoiding
359+
# "(psycopg2.errors.ObjectNotInPrerequisiteState) lastval is not yet defined in this session",
360+
# a la https://stackoverflow.com/a/24186770/5156190;
361+
# cf. https://www.psycopg.org/docs/errors.html re 55000
362+
result = connection.execute("""
363+
CREATE OR REPLACE FUNCTION _LASTVAL()
364+
RETURNS integer LANGUAGE plpgsql
365+
AS $$
366+
BEGIN
367+
BEGIN
368+
RETURN (SELECT LASTVAL());
369+
EXCEPTION
370+
WHEN SQLSTATE '55000' THEN RETURN NULL;
371+
END;
372+
END $$;
373+
SELECT _LASTVAL();
374+
""")
375+
ret = result.first()[0]
376+
377+
# If not PostgreSQL
362378
else:
363379
ret = result.lastrowid if result.rowcount == 1 else None
364380

tests/sql.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import os
23
import sys
34
import unittest
45
import warnings
@@ -155,7 +156,7 @@ def tearDownClass(self):
155156
class MySQLTests(SQLTests):
156157
@classmethod
157158
def setUpClass(self):
158-
self.db = SQL("mysql://root@127.0.0.1/test")
159+
self.db = SQL(f"mysql://root@{os.getenv('MYSQL_HOST')}/test")
159160

160161
def setUp(self):
161162
self.db.execute("CREATE TABLE IF NOT EXISTS cs50 (id INTEGER NOT NULL AUTO_INCREMENT, val VARCHAR(16), bin BLOB, PRIMARY KEY (id))")
@@ -165,7 +166,7 @@ def setUp(self):
165166
class PostgresTests(SQLTests):
166167
@classmethod
167168
def setUpClass(self):
168-
self.db = SQL("postgresql://postgres:postgres@127.0.0.1/test")
169+
self.db = SQL(f"postgresql://postgres:postgres@{os.getenv('POSTGRESQL_HOST')}/test")
169170

170171
def setUp(self):
171172
self.db.execute("CREATE TABLE IF NOT EXISTS cs50 (id SERIAL PRIMARY KEY, val VARCHAR(16), bin BYTEA)")

0 commit comments

Comments
 (0)