fix(gallery+tests): alias Post inside artist EXISTS; tests stop asserting synthetic Source
gallery_service._provenance_clause artist branch was correlating its bare Post reference to the outer query's primary_post_id outer-join, so the artist filter silently matched zero rows for images with no primary post. Alias Post inside the EXISTS subquery so SQLAlchemy adds it to the inner FROM rather than treating it as a correlated outer table. Five sidecar/import tests still asserted that a synthetic Source row appears after a filesystem import. Alembic 0030 retired that behavior; the Post sits null-source and the artist linkage lives on Post.artist_id. Updated test_sidecar_creates_provenance, test_reimport_same_post_idempotent, test_sidecar_artist_used_when_no_folder_artist, test_supersede_applies_new_file_sidecar, and test_apply_sidecar_recovers_from_integrity_error to assert post.source_id IS NULL + post.artist_id linkage instead.
This commit is contained in:
@@ -20,6 +20,7 @@ from datetime import datetime
|
||||
|
||||
from sqlalchemy import Select, and_, exists, func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import aliased
|
||||
|
||||
from ..models import Artist, ImageProvenance, ImageRecord, Post, Tag
|
||||
from ..models.tag import image_tag
|
||||
@@ -139,10 +140,17 @@ def _provenance_clause(post_id, artist_id):
|
||||
# path now drops NULL-source provenance rows (filesystem-imported
|
||||
# content) which would otherwise vanish from artist-filtered
|
||||
# gallery views.
|
||||
# ALIAS Post: the gallery query outer-joins Post on
|
||||
# ImageRecord.primary_post_id (`_outer_join_primary_post`).
|
||||
# SQLAlchemy would otherwise correlate a bare `Post` reference
|
||||
# in this EXISTS subquery to that outer Post (which is NULL for
|
||||
# images with no primary post), and the filter would silently
|
||||
# match nothing.
|
||||
post_inner = aliased(Post)
|
||||
return exists().where(
|
||||
ImageProvenance.image_record_id == ImageRecord.id,
|
||||
ImageProvenance.post_id == Post.id,
|
||||
Post.artist_id == artist_id,
|
||||
ImageProvenance.post_id == post_inner.id,
|
||||
post_inner.artist_id == artist_id,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ from backend.app.models import (
|
||||
ImageProvenance,
|
||||
ImageRecord,
|
||||
ImportSettings,
|
||||
Source,
|
||||
)
|
||||
from backend.app.services.importer import Importer
|
||||
from backend.app.services.thumbnailer import Thumbnailer
|
||||
@@ -139,9 +138,11 @@ def test_apply_sidecar_recovers_from_integrity_error(
|
||||
r = importer.import_one(m)
|
||||
assert r.status == "imported"
|
||||
rec = importer.session.get(ImageRecord, r.image_id)
|
||||
src = importer.session.execute(select(Source)).scalar_one()
|
||||
# alembic 0030 stopped creating synthetic Source rows for filesystem
|
||||
# sidecars; the Post sits null-source and the provenance row points at
|
||||
# it directly. The race-recovery path tested below operates on
|
||||
# ImageProvenance regardless of Source presence.
|
||||
assert rec is not None
|
||||
assert src is not None
|
||||
|
||||
# Monkeypatch session.execute so the FIRST select inside _apply_sidecar's
|
||||
# existence-check returns a "no row" wrapper. Subsequent selects (e.g.
|
||||
|
||||
@@ -15,7 +15,6 @@ from backend.app.models import (
|
||||
ImageRecord,
|
||||
ImportSettings,
|
||||
Post,
|
||||
Source,
|
||||
Tag,
|
||||
TagKind,
|
||||
)
|
||||
@@ -197,10 +196,11 @@ def test_supersede_applies_new_file_sidecar(importer, import_layout):
|
||||
assert post.post_title == "Set 1"
|
||||
assert "big version" in (post.description or "")
|
||||
|
||||
source = importer.session.execute(
|
||||
select(Source).where(Source.id == post.source_id)
|
||||
).scalar_one()
|
||||
assert source.platform == "patreon"
|
||||
# Filesystem sidecars no longer create a synthetic Source (alembic 0030).
|
||||
# The Post sits null-source; the platform context is captured in the
|
||||
# sidecar JSON / raw_metadata, not in a phantom Source row.
|
||||
assert post.source_id is None
|
||||
assert post.artist_id is not None
|
||||
|
||||
prov_count = importer.session.execute(
|
||||
select(func.count(ImageProvenance.id))
|
||||
|
||||
@@ -75,9 +75,15 @@ def test_sidecar_creates_provenance(importer, import_layout):
|
||||
r = importer.import_one(m)
|
||||
assert r.status == "imported"
|
||||
rec = importer.session.get(ImageRecord, r.image_id)
|
||||
src = importer.session.execute(select(Source)).scalar_one()
|
||||
post = importer.session.execute(select(Post)).scalar_one()
|
||||
assert src.platform == "patreon"
|
||||
# Filesystem-imported sidecar posts no longer create a synthetic Source
|
||||
# (alembic 0030 / nullable post.source_id refactor). The Post is linked
|
||||
# to the artist via Post.artist_id; Post.source_id stays NULL until a
|
||||
# real subscription for the (artist, platform) gets added.
|
||||
assert post.source_id is None
|
||||
assert importer.session.execute(
|
||||
select(func.count()).select_from(Source)
|
||||
).scalar_one() == 0
|
||||
assert post.external_post_id == "555"
|
||||
assert post.post_url == "https://patreon.com/posts/555"
|
||||
assert post.post_title == "Set 1"
|
||||
@@ -106,9 +112,11 @@ def test_reimport_same_post_idempotent(importer, import_layout):
|
||||
_sidecar(m2, payload)
|
||||
r2 = importer.import_one(m2)
|
||||
assert r2.status == "imported"
|
||||
# No synthetic Source after alembic 0030; both imports still resolve to
|
||||
# a single null-source Post (deduped by uq_post_artist_external_id_null_source).
|
||||
assert importer.session.execute(
|
||||
select(func.count()).select_from(Source)
|
||||
).scalar_one() == 1
|
||||
).scalar_one() == 0
|
||||
assert importer.session.execute(
|
||||
select(func.count()).select_from(Post)
|
||||
).scalar_one() == 1
|
||||
@@ -164,5 +172,11 @@ def test_sidecar_artist_used_when_no_folder_artist(importer, import_layout):
|
||||
a = importer.session.execute(
|
||||
select(Artist).where(Artist.slug == "yuki")
|
||||
).scalar_one()
|
||||
src = importer.session.execute(select(Source)).scalar_one()
|
||||
assert src.artist_id == a.id
|
||||
# No synthetic Source after alembic 0030; the artist linkage lives on
|
||||
# Post.artist_id (NOT NULL FK).
|
||||
post = importer.session.execute(select(Post)).scalar_one()
|
||||
assert post.artist_id == a.id
|
||||
assert post.source_id is None
|
||||
assert importer.session.execute(
|
||||
select(func.count()).select_from(Source)
|
||||
).scalar_one() == 0
|
||||
|
||||
Reference in New Issue
Block a user