fix(dump): sortAt must never trust the Image.sortAt column (no-backfill decision) - #328
Merged
Merged
Conversation
No-backfill decision: 92M historical Image rows stay un-backfilled, and their Image.sortAt column is NOT NULL but stale `now()`-at-migration garbage (measured on prod: 88.1% mismatch vs. the correct GREATEST value, clustering approx createdAt). The merged W2-1 dump copy_query used COALESCE(i."sortAt", GREATEST(...)) — COALESCE prefers the non-null column, so it would have ingested that garbage for ~92M rows. Fix: the dump computes sortAt as a PURE GREATEST(post.publishedAt, scannedAt, createdAt) and never reads i."sortAt" at all. Picked over a dump computed_field(max(...)) alternative because it's mechanically simpler and avoids a real limitation: dump top-level computed_fields evaluate via eval_indexed(indexed_fields_buf, ...) where indexed_fields_buf is filled from the raw CSV row (dump_processor.rs:1970) BEFORE enrichment runs (dump_processor.rs:1984) — so a computed_field cannot see the posts enrichment's publishedAtSecs without extending that code path. Pure SQL GREATEST needs no code changes. The steady-state ops emission expression (shared_ops_fields: [sortAtUnix], COALESCE(NEW."sortAt", GREATEST(...))) is UNCHANGED — it stays COALESCE-shaped because it's safe for a DIFFERENT reason: the model-share `image_sort_at_before` BEFORE INSERT OR UPDATE trigger recomputes NEW."sortAt" on every write that reaches PG, so any row this trigger fires on is correct-on-write by construction. The dump has no such guarantee (it reads all rows regardless of post-cutover write history). Pinned this pairing in comments in both trigger blocks: "ops emission may trust the column ONLY because image_sort_at_before recomputes it on every write; the dump must NEVER trust it (92M pre-trigger rows are stale)." Test (src/pg_sync/sync_config.rs, on the REAL deploy config): test_dump_sortat_never_trusts_the_column — dump copy_query has no COALESCE and no i."sortAt" read reference, computes sortAt via a type-correct extract(epoch from GREATEST(...))::bigint; ops trigger's sortAtUnix track field remains COALESCE-shaped (asserts the pairing didn't get flattened). cargo test --lib --features pg-sync: 1250 passed, 8 ignored (test_min_tracked_value_after_expansion skipped — known deadlock). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JustMaier
added a commit
that referenced
this pull request
Jul 17, 2026
…column self-heal, widened [PR-M2] - README: gate 1 remains valid without re-dump (prep computes pure GREATEST, never reads Image.sortAt — identical to #328's amended copy_query). Emptied-originals + stage_dir isolation warnings. - w3-stale-column.sql: proves the widened BEFORE trigger heals a stale sortAt column — an nsfwLevel-only update emits the CORRECT recomputed sortAtUnix (verified: stale 2020 -> correct). - w3-m2-latency.sql: widened-trigger per-update overhead +47us P50 / +390us P99 (sub-ms). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JustMaier
added a commit
that referenced
this pull request
Jul 17, 2026
…ates PASS) (#329) * test(w3): scheduled-publish + ingested-sortAt E2E + full-scale proof harness Pins the W3 critical cases (scheduled-publish-design §2.D, #325+#326): - tests/e2e/e2e-scheduled-publish-sortat.mjs — self-contained (no PG) Node E2E driving /ops with the exact #326 trigger payloads: ingested sortAtUnix(ms)->sortAt(s), scheduled->deferred, the CRITICAL no-op sweep activation at Tf (ingested future sortAt preserved through activation), unpublish/republish. 14/14 PASS. - tests/w3/ — full-scale reproducible harness (prep/dedup/dump-driver/assert, local-PG schema + model-share sortAt triggers, minimal poller, lifecycle, latency, re-emitter) + README with the gate evidence (all gates PASS: 114.8M dump 23m13s no-regression, sortAt invariant 600/600, model3dId 108,567, lifecycle incl. no-op sweep, heal-path, latency, PR-B3). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(w3): post-#328 amendments — gate1 valid vs pure-GREATEST, stale-column self-heal, widened [PR-M2] - README: gate 1 remains valid without re-dump (prep computes pure GREATEST, never reads Image.sortAt — identical to #328's amended copy_query). Emptied-originals + stage_dir isolation warnings. - w3-stale-column.sql: proves the widened BEFORE trigger heals a stale sortAt column — an nsfwLevel-only update emits the CORRECT recomputed sortAtUnix (verified: stale 2020 -> correct). - w3-m2-latency.sql: widened-trigger per-update overhead +47us P50 / +390us P99 (sub-ms). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
JustMaier
added a commit
that referenced
this pull request
Jul 17, 2026
…ortAt, model3dId (#332) * docs: scheduled-publish wrap-up — per-image fan-out, ingested sortAt, model3dId (#13) Documents the shipped scheduled-publish effort (PRs #325/#326/#328/#329/#330) across the doc set: - pg-sync-v2-final.md: Post fan-out is now per-image materialized (type: fan_out_per_row) not queryOpSet; shared PG functions bitdex_post_fanout_ops / bitdex_image_sortat_ops; sortAt ingested (computed: dropped), dump never trusts the column; ModelVersion/Model stay queryOpSet. - CLAUDE.md: Sync V2 section reflects new fan-out shape + ingested sortAt + model3dId. - scheduled-publish-design.md: §1 history corrected (2024 migration triggers, Post-side neutered by name-collision, new_image_sort_at absent from prod pg_trigger per zuri's live verification); §2.D marked EXECUTED with the no-backfill decision and W1-4 dominant-class (visible-but-misordered) finding. - execution-plan: W4 step 2 no-backfill correction note. - FOLLOWUP.md: 7 residuals (over-cap fan-out skip, flaky flush tests, retired combinedNsfwLevel, frankenstein docs, MV cascade cost, stage_dir isolation, E2E population gap). - HANDOFF.md: scheduled-publish design chain pointer + W4 rollout state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(reemitter): correct Tf-activation model to sweep-driven shadow flip Prod evidence (sortat-divergence-specimens-2026-07-16.md PR-m5 + execution plan corrected [PR-B2]) established that scheduled slots are ALIVE with isPublished=false (quarantine branch), NOT deferred-map-resident, so Tf activation is an overdue-sweep shadow flip, not activate_due. The re-emitter is therefore a co-driver of activation, not a pure safety net. W3 confirmed the no-op case (sweep alone flipped isPublished + preserved ingested sortAt, 14/14 E2E). Fixes fact #1 + the §7 'does not drive activation' bullet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Follow-up to #326 (merged W2-1). With the no-backfill decision (92M historical Image rows stay un-backfilled), the merged dump
copy_query'sCOALESCE(i."sortAt", GREATEST(...))shape is wrong:COALESCEprefers the column when non-null, and the historical column is NOT NULL but stale —now()-at-migration garbage (measured on prod: 88.1% mismatch vs. the correctGREATESTvalue, clustering ≈createdAt). The dump would have ingested that garbage for ~92M rows.Fix
The dump now computes
sortAtas a pureGREATEST(post.publishedAt, scannedAt, createdAt)and never readsi."sortAt"at all.Why SQL-only, not a dump
computed_fieldTwo options were on the table:
sortAtfromcopy_query, compute it as a dumpcomputed_fieldmax(publishedAtSecs, scannedAtSecs, createdAtSecs)(using the posts-enrichmentpublishedAtSecs).GREATEST, noCOALESCE.Picked (2) — mechanically simpler, and (1) has a real blocker: dump top-level
computed_fieldsevaluate viadef.eval_indexed(&indexed_fields_buf, col_idx, None)whereindexed_fields_bufis filled from the raw CSV row (dump_processor.rs:1970,row.fill_indexed_fields(...)) before enrichment runs (dump_processor.rs:1984,enrichment_mgr_ref.enrich_row_indexed_into(...)). A dump-level computed field therefore cannot see the posts-enrichment'spublishedAtSecswithout extending that evaluation-order code — not something to do inside a config-only fix. Pure SQL needs no code changes and is provably correct by inspection.The ops-emission pairing (unchanged, and why)
The steady-state ops trigger's
sortAtUnixexpression (shared_ops_fields: [sortAtUnix],COALESCE(NEW."sortAt", GREATEST(...))) is unchanged and staysCOALESCE-shaped — trusting the column there is safe for a different reason than the dump: model-share'simage_sort_at_before(BEFORE INSERT OR UPDATE) trigger recomputesNEW."sortAt"on every write that reaches PG, so any row this ops trigger fires on is correct-on-write by construction. The dump has no such guarantee — it reads all 92M+ rows regardless of whether they were ever touched post-cutover.Pinned this pairing in comments on both trigger blocks in
deploy/configs/sync-config-civitai.yaml:Test
src/pg_sync/sync_config.rs::test_dump_sortat_never_trusts_the_column(loads the real deploy config):copy_queryhas noCOALESCEand noi."sortAt"read reference;sortAtvia a type-correctextract(epoch from GREATEST(...))::bigint(single timestamp expression, never a bigint/timestamp mix — [PR-B3]);sortAtUnixtrack field remainsCOALESCE-shaped (guards against the pairing getting flattened by a future edit).cargo test --lib --features pg-sync(skiptest_min_tracked_value_after_expansion, known deadlock): 1250 passed, 8 ignored, 0 failed.🤖 Generated with Claude Code