Skip to content

Commit 9342365

Browse files
committed
Fixes #128 by catch PostgreSQL exceptions that otherwise roll back transactions
1 parent 8c08044 commit 9342365

File tree

4 files changed

+70
-12
lines changed

4 files changed

+70
-12
lines changed

Dockerfile

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

docker-compose.yml

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

0 commit comments

Comments
 (0)