79089b50b0
Normalize tagger predictions out of the image_record.tagger_predictions JSON blob into a queryable per-prediction table. Step 1 of the cutover (expand): additive + low-risk — reads still use the JSON, this just adds the table and keeps it populated. - ImagePrediction(image_record_id, raw_name, category, score) — stores the RAW tagger vocab name (not tag_id) so read-time alias→canonical resolution is unchanged. Indexed for per-image reads + by (raw_name, score). - Migration 0045: create table + set-based backfill from the JSON via json_each (fast post-#764-prune). The old column stays (vestigial) and is dropped in a later follow-up — DROP needs an ACCESS EXCLUSIVE lock on the hot image_record table, so it waits for a quiesced-worker window. - tag_and_embed dual-writes the rows (delete-then-insert, idempotent); tagger_store_floor already applied in infer(). Next: switch suggestion + allowlist reads to the table, then drop the JSON write. Plan-task #768. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
58 lines
1.7 KiB
Python
58 lines
1.7 KiB
Python
"""#768: image_prediction table — model + constraints round-trip."""
|
|
import pytest
|
|
from sqlalchemy import select
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
from backend.app.models import ImagePrediction, ImageRecord
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
async def _make_image(db, path="/img/p0.jpg", sha="0"):
|
|
rec = ImageRecord(
|
|
path=path, sha256=sha.ljust(64, "0")[:64], size_bytes=10,
|
|
mime="image/jpeg", origin="imported_filesystem",
|
|
)
|
|
db.add(rec)
|
|
await db.flush()
|
|
return rec
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_image_prediction_round_trip(db):
|
|
rec = await _make_image(db)
|
|
db.add_all([
|
|
ImagePrediction(
|
|
image_record_id=rec.id, raw_name="blue_eyes",
|
|
category="general", score=0.92,
|
|
),
|
|
ImagePrediction(
|
|
image_record_id=rec.id, raw_name="hatsune_miku",
|
|
category="character", score=0.88,
|
|
),
|
|
])
|
|
await db.commit()
|
|
|
|
rows = (await db.execute(
|
|
select(ImagePrediction.raw_name, ImagePrediction.score)
|
|
.where(ImagePrediction.image_record_id == rec.id)
|
|
.order_by(ImagePrediction.score.desc())
|
|
)).all()
|
|
assert [r.raw_name for r in rows] == ["blue_eyes", "hatsune_miku"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_image_prediction_unique_per_image_name(db):
|
|
rec = await _make_image(db, path="/img/p1.jpg", sha="1")
|
|
db.add(ImagePrediction(
|
|
image_record_id=rec.id, raw_name="dup",
|
|
category="general", score=0.9,
|
|
))
|
|
await db.commit()
|
|
db.add(ImagePrediction(
|
|
image_record_id=rec.id, raw_name="dup",
|
|
category="general", score=0.7,
|
|
))
|
|
with pytest.raises(IntegrityError):
|
|
await db.commit()
|