diff --git a/backend/app/services/wip_title.py b/backend/app/services/wip_title.py index 4197486..7ad13d4 100644 --- a/backend/app/services/wip_title.py +++ b/backend/app/services/wip_title.py @@ -65,22 +65,37 @@ def resolve_wip_tag_id(session: Session) -> int | None: def apply_wip_image_tags(session: Session, image_ids, tag_id: int) -> int: - """Attach ``tag_id`` (source='wip_title') to each image id, idempotently - (ON CONFLICT DO NOTHING — never disturbs an existing tag or its source). - Returns the number of image_tag rows newly inserted. Does NOT commit.""" + """Attach ``tag_id`` (source='wip_title') to each image id, idempotently — + never disturbs an existing tag or its source. Returns the number of image_tag + rows newly inserted. Does NOT commit. + + The insert count is computed from a pre-SELECT of already-tagged ids rather + than the statement's ``rowcount``: psycopg reports -1 for a multi-row + ON CONFLICT DO NOTHING insert (it runs via an executemany path), so rowcount + is unusable here. The SELECT is accurate within this single transaction (no + concurrent writer touches these (image, wip) rows); ON CONFLICT DO NOTHING + stays as a race-safety belt so a rare concurrent insert can't error.""" ids = list({int(i) for i in image_ids}) if not ids: return 0 inserted = 0 for start in range(0, len(ids), _INSERT_CHUNK): chunk = ids[start:start + _INSERT_CHUNK] - result = session.execute( + already = set(session.execute( + select(image_tag.c.image_record_id) + .where(image_tag.c.tag_id == tag_id) + .where(image_tag.c.image_record_id.in_(chunk)) + ).scalars()) + to_insert = [iid for iid in chunk if iid not in already] + if not to_insert: + continue + session.execute( pg_insert(image_tag) .values([ {"image_record_id": iid, "tag_id": tag_id, "source": WIP_TITLE_SOURCE} - for iid in chunk + for iid in to_insert ]) .on_conflict_do_nothing(index_elements=["image_record_id", "tag_id"]) ) - inserted += result.rowcount or 0 + inserted += len(to_insert) return inserted