Compare commits

...

6 Commits

Author SHA1 Message Date
bvandeusen 52fff00353 Merge pull request 'v26.05.26.4 — hotfix: migration 0022 pre-DELETE colliding ImageProvenance before UPDATE' (#26) from dev into main 2026-05-26 18:06:20 -04:00
bvandeusen f3e8f30a8f fix(migration-0022): pre-DELETE colliding image_provenance rows before the UPDATE post_id — same row-by-row UNIQUE pattern as the post-collision case, just one level deeper. When image X has provenance under both keep and drop, UPDATE drop→keep would fire uq_image_provenance_image_post on the row that'd collide with the existing (X, keep). Pre-delete those rows (their info is already represented by the keep-side provenance) before the UPDATE moves the rest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 18:05:49 -04:00
bvandeusen eee107766e fix(migration-0022): rename unused _epid loop var (ruff B007)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 18:03:11 -04:00
bvandeusen c14338cbce Merge pull request 'v26.05.26.3 — hotfix: migration 0022 pre-merge across ENTIRE (canonical+others) group' (#25) from dev into main 2026-05-26 17:52:59 -04:00
bvandeusen 7a64730bd2 fix(migration-0022): pre-merge ALL duplicate-external_post_id Posts across the (canonical+others) group, not just canonical-vs-others — operator's v26.05.26.2 deploy still tripped uq_post_source_external_id because two non-canonical Sources both had Posts with epid=6166997. Bulk UPDATE moved the first cleanly then collided on the second. New pre-merge groups all Posts in the (artist, platform) by external_post_id; for any group with count>1, picks the keep (prefer one under canonical; else lowest id) and merges the rest before the bulk reparent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:52:29 -04:00
bvandeusen 1803a09306 ci(workflow): remove the 4 Cache pip wheels steps entirely — act_runner's cache backend has been broken for 11+ days and the cached path (~/.cache/pip) wasn't even the primary install tool's cache anyway (uv uses ~/.cache/uv). Net cost ~30s/job of wheel downloads. Long-term: mount ~/.cache/uv as a docker volume at the runner level (skips actions/cache entirely) or fix the runner-side cache backend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:54:05 -04:00
2 changed files with 81 additions and 109 deletions
+11 -64
View File
@@ -24,22 +24,17 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Cache pip wheels # Cache step removed 2026-05-26: act_runner's cache backend has been
# continue-on-error: act_runner's cache backend has been broken on # broken on this homelab runner since 2026-05-15 (first as request-
# this homelab setup since 2026-05-15 — bolt.db + cache/ dir exist # timeout warnings, then as hard "Cannot find module .../dist/restore/
# but the action's JS bundle now fails to load at all # index.js" failures that tank the whole job). The cache step targeted
# ("Cannot find module .../dist/restore/index.js"), hard-failing # ~/.cache/pip but the install below uses `uv pip install` primarily,
# the whole job. The install step that follows handles cold caches # whose own cache lives at ~/.cache/uv — so the cache step's real
# natively (uv pip / pip install both work without it), so net # benefit was marginal even when working. Cost of removal: ~30s of
# cost of disabling the cache here is ~30s of wheel downloads per # wheel downloads per job. Future re-enable: mount ~/.cache/uv as a
# job. Re-enable once the runner-side cache backend is fixed. # docker volume at the runner level (skips actions/cache entirely),
continue-on-error: true # or fix the runner-side cache backend (clear /var/run/act/actions/*,
uses: actions/cache@v4 # pin act_runner version, etc.).
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-py314-${{ hashFiles('requirements.txt') }}
restore-keys: |
pip-${{ runner.os }}-py314-
- name: Install Python deps - name: Install Python deps
# ruff is pre-installed in the ci-python image (see CI-Runner/CI-python/ # ruff is pre-installed in the ci-python image (see CI-Runner/CI-python/
@@ -133,22 +128,6 @@ jobs:
--health-retries 10 --health-retries 10
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Cache pip wheels
# continue-on-error: act_runner's cache backend has been broken on
# this homelab setup since 2026-05-15 — bolt.db + cache/ dir exist
# but the action's JS bundle now fails to load at all
# ("Cannot find module .../dist/restore/index.js"), hard-failing
# the whole job. The install step that follows handles cold caches
# natively (uv pip / pip install both work without it), so net
# cost of disabling the cache here is ~30s of wheel downloads per
# job. Re-enable once the runner-side cache backend is fixed.
continue-on-error: true
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-py314-${{ hashFiles('requirements.txt') }}
restore-keys: |
pip-${{ runner.os }}-py314-
- name: API integration shard (resolve service IPs, migrate, test) - name: API integration shard (resolve service IPs, migrate, test)
run: | run: |
set -eux set -eux
@@ -207,22 +186,6 @@ jobs:
--health-retries 10 --health-retries 10
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Cache pip wheels
# continue-on-error: act_runner's cache backend has been broken on
# this homelab setup since 2026-05-15 — bolt.db + cache/ dir exist
# but the action's JS bundle now fails to load at all
# ("Cannot find module .../dist/restore/index.js"), hard-failing
# the whole job. The install step that follows handles cold caches
# natively (uv pip / pip install both work without it), so net
# cost of disabling the cache here is ~30s of wheel downloads per
# job. Re-enable once the runner-side cache backend is fixed.
continue-on-error: true
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-py314-${{ hashFiles('requirements.txt') }}
restore-keys: |
pip-${{ runner.os }}-py314-
- name: Importer integration shard (resolve service IPs, migrate, test) - name: Importer integration shard (resolve service IPs, migrate, test)
run: | run: |
set -eux set -eux
@@ -281,22 +244,6 @@ jobs:
--health-retries 10 --health-retries 10
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Cache pip wheels
# continue-on-error: act_runner's cache backend has been broken on
# this homelab setup since 2026-05-15 — bolt.db + cache/ dir exist
# but the action's JS bundle now fails to load at all
# ("Cannot find module .../dist/restore/index.js"), hard-failing
# the whole job. The install step that follows handles cold caches
# natively (uv pip / pip install both work without it), so net
# cost of disabling the cache here is ~30s of wheel downloads per
# job. Re-enable once the runner-side cache backend is fixed.
continue-on-error: true
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-py314-${{ hashFiles('requirements.txt') }}
restore-keys: |
pip-${{ runner.os }}-py314-
- name: Core integration shard (everything not api / importer / migration / phash / sidecar / scan / archive / backfill) - name: Core integration shard (everything not api / importer / migration / phash / sidecar / scan / archive / backfill)
run: | run: |
set -eux set -eux
@@ -83,55 +83,80 @@ def upgrade() -> None:
if not other_ids: if not other_ids:
continue continue
# STEP 2: PRE-merge colliding Posts BEFORE the bulk reparent. # STEP 2: PRE-merge ALL Posts with duplicate external_post_id
# Find pairs (keep, drop) where: # across the entire (canonical + others) group, BEFORE the bulk
# keep = a Post under the canonical source # reparent. Two cases must both be handled:
# drop = a Post under one of the non-canonical sources whose # (A) canonical has Post X with epid=N; an "other" source has
# external_post_id equals keep.external_post_id # Post Y with epid=N → after bulk UPDATE, (canonical, N)
# If we let the bulk UPDATE try to repoint drop onto canonical, # collides with itself.
# uq_post_source_external_id fires row-by-row and aborts the # (B) two different "other" sources each have a Post with
# whole migration. # epid=N; canonical has none → after bulk UPDATE, both
colliding = conn.execute( # are repointed to (canonical, N) and the second collides.
# The earlier version of this migration only handled (A); the
# operator's deploy 2026-05-26 tripped (B) at line 139.
# Fix: group ALL Posts in the (artist, platform) by epid; for
# any group with count>1, pick the keep (prefer one already
# under canonical; else lowest id) and merge the rest into it.
all_posts = conn.execute(
text(""" text("""
SELECT keep.id AS keep_id, drop_.id AS drop_id SELECT external_post_id, id, source_id
FROM post AS keep FROM post
JOIN post AS drop_ WHERE source_id = :canonical OR source_id = ANY(:others)
ON drop_.external_post_id = keep.external_post_id ORDER BY external_post_id, id
AND drop_.id != keep.id
WHERE keep.source_id = :canonical
AND drop_.source_id = ANY(:others)
"""), """),
{"canonical": canonical_id, "others": other_ids}, {"canonical": canonical_id, "others": other_ids},
).fetchall() ).fetchall()
for keep_id, drop_id in colliding: by_epid: dict = {}
conn.execute( for epid, post_id, src_id in all_posts:
text(""" by_epid.setdefault(epid, []).append((post_id, src_id))
UPDATE image_provenance SET post_id = :keep for _epid, posts in by_epid.items():
WHERE post_id = :drop_ if len(posts) <= 1:
"""), continue
{"keep": keep_id, "drop_": drop_id}, # Prefer a Post already under canonical as the keep.
) canonical_posts = [p for p in posts if p[1] == canonical_id]
conn.execute( if canonical_posts:
text(""" keep_id = canonical_posts[0][0]
UPDATE image_record SET primary_post_id = :keep else:
WHERE primary_post_id = :drop_ keep_id = posts[0][0] # already sorted by id ASC
"""), drop_ids = [p[0] for p in posts if p[0] != keep_id]
{"keep": keep_id, "drop_": drop_id}, for drop_id in drop_ids:
) # Pre-delete image_provenance rows under drop_ whose
# Repointed provenance may now collide on # image_record_id ALREADY has a provenance under keep —
# uq_image_provenance_image_post (alembic 0021). Dedupe: # the UPDATE below would otherwise repoint them and
# keep min(id) per (image_record_id, post_id). # trip uq_image_provenance_image_post (alembic 0021)
conn.execute(text(""" # row-by-row before any after-the-fact dedupe could
DELETE FROM image_provenance ip1 # run. Operator's v26.05.26.3 deploy 2026-05-26 tripped
USING image_provenance ip2 # this at line 123.
WHERE ip1.image_record_id = ip2.image_record_id conn.execute(
AND ip1.post_id = ip2.post_id text("""
AND ip1.id > ip2.id DELETE FROM image_provenance
""")) WHERE post_id = :drop_
conn.execute( AND image_record_id IN (
text("DELETE FROM post WHERE id = :drop_"), SELECT image_record_id FROM image_provenance
{"drop_": drop_id}, WHERE post_id = :keep
) )
"""),
{"keep": keep_id, "drop_": drop_id},
)
# Now safe to repoint the survivors.
conn.execute(
text("""
UPDATE image_provenance SET post_id = :keep
WHERE post_id = :drop_
"""),
{"keep": keep_id, "drop_": drop_id},
)
conn.execute(
text("""
UPDATE image_record SET primary_post_id = :keep
WHERE primary_post_id = :drop_
"""),
{"keep": keep_id, "drop_": drop_id},
)
conn.execute(
text("DELETE FROM post WHERE id = :drop_"),
{"drop_": drop_id},
)
# STEP 3: Bulk reparent the remaining Posts off the other # STEP 3: Bulk reparent the remaining Posts off the other
# Sources. After step 2, no collisions on # Sources. After step 2, no collisions on