fix: atomic upsert for import runs (Issue #12)

Replace check-then-insert race condition with INSERT ON CONFLICT.
- upsert_pending uses RETURNING id for atomic upsert
- UNIQUE(source_id, content_hash) constraint from migration 6
- blocklist_import_workflow updated to use upsert_pending
- test_import_source_success fixed for async mock patterns

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-02 23:39:43 +02:00
parent 1285bc8571
commit e436727942
11 changed files with 144 additions and 164 deletions

View File

@@ -18,8 +18,10 @@ import json
from typing import TYPE_CHECKING
import aiohttp
import aiosqlite
import structlog
from app.exceptions import BlocklistSourceHasLogsError
from app.models.blocklist import (
BlocklistSource,
ImportLogEntry,
@@ -40,7 +42,6 @@ from app.utils.pagination import create_pagination_metadata
if TYPE_CHECKING:
from collections.abc import Awaitable, Callable
import aiosqlite
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from app.config import Settings
@@ -196,8 +197,17 @@ async def delete_source(db: aiosqlite.Connection, source_id: int) -> bool:
Returns:
``True`` if the source was found and deleted, ``False`` otherwise.
Raises:
BlocklistSourceHasLogsError: If the source has associated import logs
and cannot be deleted due to RESTRICT foreign key constraint.
"""
deleted = await blocklist_repo.delete_source(db, source_id)
try:
deleted = await blocklist_repo.delete_source(db, source_id)
except aiosqlite.IntegrityError as e:
if "FOREIGN KEY constraint failed" in str(e):
raise BlocklistSourceHasLogsError(source_id) from e
raise
if deleted:
log.info("blocklist_source_deleted", id=source_id)
return deleted