fix(ingest): retry on bibkey UNIQUE collision (audit C-15)
The id-keyed upsert (ON CONFLICT (id)) does not catch the papers.bibkey UNIQUE constraint, so a second paper whose auto-generated key matches an existing one (two same-author/year works → e.g. 'BobenkoLutz2023') failed the whole ingest with a UniqueViolation. Surfaced by the canonical-id re-ingest: 2305.10988 collided with 2310.17529. ingest now catches a bibkey UniqueViolation, rolls back, and retries the upsert with an a/b/c… suffix. Non-bibkey violations re-raise unchanged. Mocks don't raise, so the existing happy-path tests are untouched; a new test drives the collision-and-retry path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import numpy as np
|
||||
import psycopg
|
||||
import pytest
|
||||
|
||||
from codex.ingest import IngestResult, _make_bibkey, ingest_paper
|
||||
@@ -525,6 +526,40 @@ def test_ingest_sets_bibkey_when_none(tmp_path: Any) -> None:
|
||||
assert upsert_params["bibkey"] == "AliceBob2023"
|
||||
|
||||
|
||||
def test_ingest_disambiguates_bibkey_on_unique_violation() -> None:
|
||||
"""A papers.bibkey UNIQUE collision is retried with an a/b/c suffix (audit C-15)."""
|
||||
paper = _make_paper() # bibkey auto-generates to "AliceBob2023"
|
||||
mock_conn = MagicMock()
|
||||
state = {"first": True}
|
||||
|
||||
def execute_side_effect(sql: str, params: Any = None) -> MagicMock:
|
||||
# The first papers INSERT collides on the bibkey; the retry must succeed.
|
||||
if "INSERT INTO papers" in sql and state["first"]:
|
||||
state["first"] = False
|
||||
raise psycopg.errors.UniqueViolation(
|
||||
'duplicate key value violates unique constraint "papers_bibkey_key"'
|
||||
)
|
||||
return MagicMock()
|
||||
|
||||
mock_conn.execute.side_effect = execute_side_effect
|
||||
|
||||
with (
|
||||
patch("codex.ingest.openalex.fetch_paper", return_value=paper),
|
||||
patch("codex.ingest.openalex.fetch_citations", return_value=[]),
|
||||
patch("codex.ingest.get_embedder", return_value=_fake_embedder()),
|
||||
patch("codex.ingest.get_conn", side_effect=_make_conn_cm(mock_conn)),
|
||||
):
|
||||
ingest_paper(paper.id)
|
||||
|
||||
papers_inserts = [
|
||||
c for c in mock_conn.execute.call_args_list if "INSERT INTO papers" in c[0][0]
|
||||
]
|
||||
assert len(papers_inserts) == 2, "expected one retry after the bibkey collision"
|
||||
assert papers_inserts[0][0][1]["bibkey"] == "AliceBob2023"
|
||||
assert papers_inserts[1][0][1]["bibkey"] == "AliceBob2023a"
|
||||
mock_conn.rollback.assert_called_once()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user