Skip to content

Record when each rotation card last changed: rotation_cards.last_changed_at - #3048

Merged
jakebromberg merged 3 commits into
mainfrom
orchestrator/record-when-each-rotation-card-last-changed-rotati
Oct 7, 2026
Merged

jakebromberg merged 3 commits into
mainfrom
orchestrator/record-when-each-rotation-card-last-changed-rotati

Conversation

@jakebromberg

Copy link
Copy Markdown
Member

Closes #2734

Adds rotation_cards.last_changed_at timestamptz NULL, kept by database triggers on rotation, and serves it on GET /library/rotation/cards.

What changes

  • Column (generated migration 0188): nullable, no default, no backfill. A migration-time value would claim every card changed on deploy day, so a card reads null until a rotation write touches it.
  • Triggers (--custom migration 0189): one function, wxyc_schema.stamp_rotation_card_last_changed(), which stamps now() on OLD.card_id and NEW.card_id (nulls ignored), and two row triggers on wxyc_schema.rotation:
    • AFTER INSERT OR DELETE
    • AFTER UPDATE OF card_id, kill_date ... WHEN (OLD.card_id IS DISTINCT FROM NEW.card_id OR OLD.kill_date IS DISTINCT FROM NEW.kill_date). The guard keeps the rotation-etl upsert, which lists kill_date = excluded.kill_date on every row it updates, from re-stamping a card whose membership did not change.
    • The function body names wxyc_schema explicitly, and the migration carries the COMMENT ON COLUMN, the @no-analyze-needed / @no-precondition-needed suppressions, and a note on the opposite-direction move deadlock (A to B racing B to A can abort one with 40P01; accepted, not engineered around).
  • Reads and writes: listRotationCardsFromDB selects last_changed_at. addRotationCard and renameRotationCard now .returning({ id, bin, number, name }) typed as RotationCardWire, so the POST and PATCH card responses stay exactly { id, bin, number, name } as the contract declares (last_changed_at rides the list item only).
  • tests/mocks/database.mock.ts gains the column on the rotation_cards double.

Notes for the reviewer

  • This PR carries a migration that auto-deploys on merge. Do not enable auto-merge; confirm the deploy-auto.yml run succeeds after merging (or run Manual Build & Deploy within 24 hours).
  • The migration number is provisional: a sibling PR also adds one. Migration artifacts (sql, snapshots, journal, applied-hashes) are in their own commit so they can be regenerated on rebase.
  • Every other rotation writer (legacy-linkage-resolve, the release-id backfills, linkRotationToAlbum) touches neither card_id nor kill_date, so the UPDATE OF trigger never fires for them.

Tests

  • Unit: the list select includes last_changed_at; both card write paths pin their .returning(...) projection to the four wire columns.
  • Integration (rotation-card-last-changed.spec.js): insert with and without a card; A to B stamps both; kill; unkill; SET kill_date = kill_date stamps nothing; an unrelated-column UPDATE stamps nothing; row delete stamps the card; deleting a card that holds killed rows succeeds and uncards them; GET returns null then an ISO instant. rotation-cards.spec.js pins the POST and PATCH response keys. "Stamps nothing" tests pre-set a sentinel timestamp, since now() cannot be pinned from Jest.
  • Local: test:unit, typecheck, lint, format check, lint:migrations, check-bulk-update-analyze --strict, check:db-mock-sync, check:sql-claim-docs, check:auth-tables-doc, check-cross-cache-identity-flags, check-precondition-guards, check-legacy-entry-id-writes, check-lml-caller-classification all pass. The full integration tier ran against an isolated Compose project with a fresh DB (which also applies the migration chain from scratch); three suites needed feature flags set in the local .env and pass with them.

@jakebromberg jakebromberg added enhancement New feature or request concern:schema-migration PR ships a Drizzle / sqlx / alembic migration labels Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

Schema constraint shape report

no new constraints detected in this diff (uniqueIndex, .unique(), SET NOT NULL, CHECK, FK)

@jakebromberg
jakebromberg force-pushed the orchestrator/record-when-each-rotation-card-last-changed-rotati branch from b34d2bd to e5c1810 Compare October 7, 2026 22:25
@jakebromberg
jakebromberg merged commit 39b3e19 into main Oct 7, 2026
7 checks passed
@jakebromberg
jakebromberg deleted the orchestrator/record-when-each-rotation-card-last-changed-rotati branch October 8, 2026 02:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

concern:schema-migration PR ships a Drizzle / sqlx / alembic migration enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Record when each rotation card last changed: rotation_cards.last_changed_at, kept by a trigger

1 participant