M5: enable SQLite foreign_keys pragma for DB-level FK enforcement
This commit is contained in:
@@ -28,6 +28,7 @@ def _get_engine(database_url: str) -> Engine:
|
|||||||
def _enable_sqlite_wal(dbapi_connection, _connection_record):
|
def _enable_sqlite_wal(dbapi_connection, _connection_record):
|
||||||
cursor = dbapi_connection.cursor()
|
cursor = dbapi_connection.cursor()
|
||||||
cursor.execute("PRAGMA journal_mode=WAL")
|
cursor.execute("PRAGMA journal_mode=WAL")
|
||||||
|
cursor.execute("PRAGMA foreign_keys=ON")
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|
||||||
return engine
|
return engine
|
||||||
|
|||||||
@@ -403,3 +403,62 @@ def test_modbus_reading_restrict_prevents_device_deletion(tmp_path: Path):
|
|||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
engine.dispose()
|
engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
def test_modbus_reading_restrict_enforced_via_app_engine(tmp_path: Path):
|
||||||
|
"""FK RESTRICT is enforced by the *default* app engine (db.py) without any manual pragma.
|
||||||
|
|
||||||
|
After enabling ``PRAGMA foreign_keys=ON`` in the db.py connect event, the app
|
||||||
|
engine must enforce ON DELETE RESTRICT on modbus_reading.device_id without
|
||||||
|
callers having to set the pragma themselves. This test uses the real app engine
|
||||||
|
obtained via ``app.db.get_engine`` (routed through the lru_cache, just like the
|
||||||
|
running app) so it exercises the exact production code path.
|
||||||
|
"""
|
||||||
|
import sqlalchemy.exc
|
||||||
|
|
||||||
|
from app.db import _get_engine, reset_db_caches
|
||||||
|
|
||||||
|
db_path = tmp_path / "app_engine_restrict_test.db"
|
||||||
|
db_url = f"sqlite:///{db_path}"
|
||||||
|
|
||||||
|
alembic_cfg = _make_app_alembic_config(db_url)
|
||||||
|
command.upgrade(alembic_cfg, "head")
|
||||||
|
|
||||||
|
# Obtain engine through the same lru_cache path that production uses.
|
||||||
|
# Clear caches first so we get a fresh engine pointing at our tmp DB.
|
||||||
|
reset_db_caches()
|
||||||
|
engine = _get_engine(db_url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
now = datetime.now(tz=timezone.utc)
|
||||||
|
with Session(engine) as session:
|
||||||
|
device = ModbusDevice(
|
||||||
|
friendly_name="App-Engine RESTRICT Test",
|
||||||
|
host="10.0.0.2",
|
||||||
|
profile="sdm120",
|
||||||
|
created_at=now,
|
||||||
|
updated_at=now,
|
||||||
|
)
|
||||||
|
session.add(device)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
reading = ModbusReading(
|
||||||
|
device_id=device.id,
|
||||||
|
recorded_at=now,
|
||||||
|
payload={"voltage": 230.0},
|
||||||
|
)
|
||||||
|
session.add(reading)
|
||||||
|
session.commit()
|
||||||
|
device_id = device.id
|
||||||
|
|
||||||
|
# Without manual pragma, the app engine (db.py) must still enforce RESTRICT.
|
||||||
|
with pytest.raises(sqlalchemy.exc.IntegrityError):
|
||||||
|
with Session(engine) as session:
|
||||||
|
session.execute(
|
||||||
|
text("DELETE FROM modbus_device WHERE id = :did"),
|
||||||
|
{"did": device_id},
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
finally:
|
||||||
|
reset_db_caches()
|
||||||
|
engine.dispose()
|
||||||
|
|||||||
Reference in New Issue
Block a user