Skip to content

Commit 9a9cb10

Browse files
committed
fix: Improve registration of the event listener that enables FKs in sqlite
1 parent 01eec0e commit 9a9cb10

File tree

4 files changed

+63
-57
lines changed

4 files changed

+63
-57
lines changed

grader_service/main.py

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@
1111
import secrets
1212
import shutil
1313
import signal
14+
import sqlite3
1415
import subprocess
1516
import sys
17+
from typing import Any, Callable
1618

1719
import tornado
18-
import uvloop as uvloop
1920
from jupyterhub.log import log_request
20-
from sqlalchemy import create_engine
21+
from sqlalchemy import Engine, create_engine, event
2122
from sqlalchemy.orm import scoped_session, sessionmaker
2223
from tornado.httpserver import HTTPServer
2324
from traitlets import (
@@ -63,6 +64,47 @@ def get_session_maker(url) -> scoped_session:
6364
return scoped_session(sessionmaker(bind=engine))
6465

6566

67+
def enable_foreign_keys_for_sqlite() -> Callable[[sqlite3.Connection, Any], None] | None:
68+
"""
69+
Register a listener that enables foreign key constraints for SQLite databases.
70+
71+
It returns the function that sets the foreign keys pragma, and which is
72+
registered with the event listener on Engine.connect. The function can be
73+
used to later remove the listener:
74+
75+
e.g.::
76+
77+
from sqlalchemy import Engine, event
78+
79+
event.remove(Engine, "connect", set_sqlite_pragma)
80+
81+
This function is a no-op if the DATABASE_TYPE environment variable is not set to "sqlite".
82+
In this case it returns `None`.
83+
"""
84+
database_type = os.getenv("DATABASE_TYPE")
85+
if database_type == "sqlite":
86+
# The following function is needed to enable foreign key constraints in SQLite.
87+
# The code was copied from the SQLAlchemy documentation:
88+
# https://docs.sqlalchemy.org/en/20/dialects/sqlite.html#foreign-key-support
89+
@event.listens_for(Engine, "connect")
90+
def set_sqlite_pragma(dbapi_connection, connection_record):
91+
# the sqlite3 driver will not set PRAGMA foreign_keys
92+
# if autocommit=False; set to True temporarily
93+
ac = dbapi_connection.autocommit
94+
dbapi_connection.autocommit = True
95+
96+
cursor = dbapi_connection.cursor()
97+
# Note: this is a SQLite-specific pragma
98+
cursor.execute("PRAGMA foreign_keys=ON")
99+
cursor.close()
100+
101+
# restore previous autocommit setting
102+
dbapi_connection.autocommit = ac
103+
104+
return set_sqlite_pragma
105+
return None
106+
107+
66108
class GraderService(config.Application):
67109
name = "grader-service"
68110
version = __version__
@@ -111,13 +153,13 @@ def _default_db_url(self):
111153
allow_none=False,
112154
).tag(config=True)
113155

114-
max_body_size = Int(104857600, help="Sets the max buffer size in bytes, default to 100mb").tag(
156+
max_body_size = Int(104857600, help="Sets the max body size in bytes, default to 100mb").tag(
115157
config=True
116158
)
117159

118-
max_buffer_size = Int(104857600, help="Sets the max body size in bytes, default to 100mb").tag(
119-
config=True
120-
)
160+
max_buffer_size = Int(
161+
104857600, help="Sets the max buffer size in bytes, default to 100mb"
162+
).tag(config=True)
121163

122164
service_git_username = Unicode("grader-service", allow_none=False).tag(config=True)
123165

@@ -329,6 +371,8 @@ def initialize(self, argv, *args, **kwargs):
329371
self.load_config_file(self.config_file)
330372
self.setup_loggers(self.log_level)
331373

374+
enable_foreign_keys_for_sqlite()
375+
332376
self.session_maker = get_session_maker(self.db_url)
333377
self.init_roles()
334378
# use uvloop instead of default asyncio loop
@@ -398,7 +442,10 @@ def init_roles(self):
398442

399443
user = db.query(User).filter(User.name == username).one_or_none()
400444
if user is None:
401-
self.log.info(f"Adding new user with username {username} and display name {display_name}")
445+
self.log.info(
446+
f"Adding new user with username {username} "
447+
f"and display name {display_name}"
448+
)
402449
user = User()
403450
user.name = username
404451
user.display_name = display_name

grader_service/orm/base.py

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,13 @@
55
# LICENSE file in the root directory of this source tree.
66

77
import enum
8-
import os
98

10-
from sqlalchemy import event
11-
from sqlalchemy.engine import Engine
129
from sqlalchemy.orm import declarative_base
1310

1411
from grader_service.api.models.base_model import Model
1512

1613
Base = declarative_base()
1714

18-
database_type = os.getenv("DATABASE_TYPE")
19-
if database_type == "sqlite":
20-
# The following function is needed to enable foreign key constraints in SQLite;
21-
# The code was copied from the SQLAlchemy documentation:
22-
# https://docs.sqlalchemy.org/en/20/dialects/sqlite.html#foreign-key-support
23-
@event.listens_for(Engine, "connect")
24-
def set_sqlite_pragma(dbapi_connection, connection_record):
25-
# the sqlite3 driver will not set PRAGMA foreign_keys
26-
# if autocommit=False; set to True temporarily
27-
ac = dbapi_connection.autocommit
28-
dbapi_connection.autocommit = True
29-
30-
cursor = dbapi_connection.cursor()
31-
# Note: this is a SQLite-specific pragma
32-
cursor.execute("PRAGMA foreign_keys=ON")
33-
cursor.close()
34-
35-
# restore previous autocommit setting
36-
dbapi_connection.autocommit = ac
37-
3815

3916
class Serializable(object):
4017
@property

grader_service/tests/conftest.py

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@
1010
import pytest
1111
from alembic import config
1212
from alembic.command import upgrade
13-
from sqlalchemy import event
14-
from sqlalchemy.engine import Engine
13+
from sqlalchemy import Engine, event
1514
from sqlalchemy.orm import Session, scoped_session, sessionmaker
1615

1716
from grader_service import GraderService, handlers
1817
from grader_service.auth.dummy import DummyAuthenticator
19-
from grader_service.main import get_session_maker
18+
from grader_service.main import enable_foreign_keys_for_sqlite, get_session_maker
2019
from grader_service.orm import User
2120
from grader_service.registry import HandlerPathRegistry
2221
from grader_service.server import GraderServer
@@ -34,37 +33,22 @@ def set_database_type_to_sqlite():
3433
which enables foreign keys support for SQLite database.
3534
Unset the var after the test runs.
3635
37-
This is a hack, because normally `DATABASE_TYPE` is not set when tests run.
36+
This is a necessary hack, because normally `DATABASE_TYPE` is not set when tests run.
3837
It also cannot be set to "sqlite" by default, because we have some tests
3938
running on PostgreSQL, where executing the sqlite pragma would cause an error.
4039
4140
Outside the test setting, the DATABASE_TYPE environment variable is set, and
42-
the event listener is registered in grader_service/orm/base.py.
41+
the event listener is registered in `grader_service/orm/base.py`.
4342
"""
4443
os.environ["DATABASE_TYPE"] = "sqlite"
45-
46-
# Original code taken from the SQLAlchemy documentation, here slightly adjusted:
47-
# https://docs.sqlalchemy.org/en/20/dialects/sqlite.html#foreign-key-support
48-
@event.listens_for(Engine, "connect")
49-
def set_sqlite_pragma(dbapi_connection, connection_record):
50-
database_type = os.getenv("DATABASE_TYPE")
51-
if database_type == "sqlite":
52-
# the sqlite3 driver will not set PRAGMA foreign_keys
53-
# if autocommit=False; set to True temporarily
54-
ac = dbapi_connection.autocommit
55-
dbapi_connection.autocommit = True
56-
57-
cursor = dbapi_connection.cursor()
58-
# Note: this is a SQLite-specific pragma
59-
cursor.execute("PRAGMA foreign_keys=ON")
60-
cursor.close()
61-
62-
# restore previous autocommit setting
63-
dbapi_connection.autocommit = ac
64-
44+
set_sqlite_pragma = enable_foreign_keys_for_sqlite()
45+
assert event.contains(Engine, "connect", set_sqlite_pragma)
6546
yield
47+
6648
# Unset the variable for other tests, which may use a different database type
6749
os.environ.pop("DATABASE_TYPE")
50+
# Remove the event listener to avoid interference with other tests
51+
event.remove(Engine, "connect", set_sqlite_pragma)
6852

6953

7054
@pytest.fixture(scope="function")

grader_service/tests/handlers/test_assignment_handler.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ def test_foreign_key_constraints_in_sqlite(
3333
engine = session.get_bind()
3434
sub = insert_submission(engine, a_id, "ubuntu", 1)
3535

36-
session = sql_alchemy_sessionmaker()
37-
3836
with pytest.raises(IntegrityError, match="FOREIGN KEY constraint failed"):
3937
# Try to delete an existing assignment with a submission
4038
session.query(AssignmentORM).filter(AssignmentORM.id == a_id).delete()

0 commit comments

Comments
 (0)