Skip to content

fix(credits): add attempt_index before indexing it on postgres boot - #433

Merged
FlyM1ss merged 5 commits into
Open-Finance-Lab:mainfrom
Allan-Feng:fix/credits-attempt-index-boot
Sep 4, 2026
Merged

fix(credits): add attempt_index before indexing it on postgres boot#433
FlyM1ss merged 5 commits into
Open-Finance-Lab:mainfrom
Allan-Feng:fix/credits-attempt-index-boot

Conversation

@Allan-Feng

Copy link
Copy Markdown
Collaborator

Summary

Test plan

  • pytest dashboard/backend/tests/domain/credits/test_repository_postgres.py::test_postgres_boot_ddl_indexes_attempt_index_only_after_add_column
  • pytest dashboard/backend/tests/test_store_twin_parity.py::test_postgres_twin_schema_columns_match_sqlite
  • With TEST_POSTGRES_URL set: pytest dashboard/backend/tests/domain/credits/test_repository_postgres.py::test_postgres_boot_migrates_pre_failover_reservation_table -v
  • Merge and confirm Render gets past credits_store backend: postgres (...) without UndefinedColumn: attempt_index

Made with Cursor

CREATE TABLE IF NOT EXISTS no-ops on the deployed reservation table, so
the Open-Finance-Lab#432 run-status index raised UndefinedColumn and killed Render import.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

@Allan-Feng is attempting to deploy a commit to the allan-feng's projects Team on Vercel.

A member of the Team first needs to authorize it.

FlyM1ss and others added 2 commits September 2, 2026 18:28
…ndex-boot

# Conflicts:
#	dashboard/backend/domain/credits/repository_postgres.py
#	dashboard/backend/tests/domain/credits/test_repository_postgres.py
Review follow-ups for Open-Finance-Lab#433 after Open-Finance-Lab#434 landed the same ordering fix on main.

- Merge main and keep this PR's DROP of prod's pre-Open-Finance-Lab#432 three-column
  idx_credit_llm_reservations_run_status. Open-Finance-Lab#434's bare CREATE INDEX IF NOT
  EXISTS matches by name alone, so it no-ops against that index and the
  four-column definition never lands; this PR is the only repair.
- Make the repair converge: drop only when pg_get_indexdef reports a column
  list other than (run_id, status, call_index, attempt_index). credits_store
  is built at import, so an unconditional DROP+CREATE rebuilt the index under
  ACCESS EXCLUSIVE on every deploy. The index now sits below the last ALTER
  on the table, and the base DDL carries no comment naming it (main's guard
  asserts the name is absent there).
- Generalise the guard: test_store_twin_parity now fails any Postgres twin
  that creates an index above the ADD COLUMN of a column it names, and
  _string_literals returns literals in source order so position reads as
  execution order. database_postgres created two agent_runs indexes on
  session_id above its ADD COLUMN; moved below the migrations.
- Tests: the live tier reads pg_get_indexdef / pg_get_constraintdef on the
  search path instead of an unscoped relname join, a second boot must keep
  the same index OID, and the three credits fixtures share one
  _isolated_schema / _create_users helper.
- Add the ADDING A COLUMN LATER? note the sibling twins carry, and amend the
  Open-Finance-Lab#434 design spec for the stale-index case.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VkzNY81VSr23dtSH5k8hHj
@FlyM1ss

FlyM1ss commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Pushed review follow-ups onto this branch as a merge from main plus c4f294ee (fast-forward, no force-push).

  • Kept the DROP INDEX. fix(credits): order postgres attempt index migration #434 merged the same ordering fix without it, but prod has carried idx_credit_llm_reservations_run_status over (run_id, status, call_index) since c0bcd86, and CREATE INDEX IF NOT EXISTS matches by name alone, so main's statement no-ops there. This PR is the only repair of that index.
  • Made the repair converge. The drop now fires only when pg_get_indexdef shows a column list other than (run_id, status, call_index, attempt_index). credits_store is built at import, so the unconditional DROP+CREATE rebuilt the index under ACCESS EXCLUSIVE on every deploy. The block sits below the last ALTER on the table; the SQL comment in the base DDL is gone because main's guard asserts the index name is absent there.
  • Generalised the guard. test_store_twin_parity now fails any Postgres twin that creates an index above the ADD COLUMN of a column it names. It immediately caught database_postgres.py (two agent_runs indexes on session_id), which is reordered in the same commit.
  • Tests. Catalog reads go through pg_get_indexdef / pg_get_constraintdef on the search path (the bare relname filter was unscoped), a second boot must keep the same index OID, and the three credits fixtures share one schema helper. The source guard from the test plan is now test_postgres_boot_ddl_repairs_the_stale_run_status_index_conditionally.

Full backend suite green locally with TEST_POSTGRES_URL on postgres:18; the pre-failover fixture (stale three-column index included) is what reproduces the Render crash.

FlyM1ss and others added 2 commits September 4, 2026 23:17
Review follow-ups on this branch.

- The staleness DO block's DROP INDEX gains IF EXISTS. Its predicate is
  evaluated before the lock is taken, so two concurrent boots could both
  reach it and the loser raised "index does not exist" -- aborting
  _init_schema, which kills the app, since credits_store is built at import.

- ADD CONSTRAINT ... logical_attempt_key now sits behind a conkey guard, so
  it stops rebuilding a full unique index under ACCESS EXCLUSIVE on every
  boot. Column identity comes from pg_constraint.conkey rather than a
  pg_get_constraintdef text match (the idiom already used by the legacy
  sweep above it); a mismatch falls back to the previous drop+add.
  The 12 CHECK/FK rebuilds nearby are deliberately left unconditional --
  recognising a CHECK means comparing deparsed text, which drifts -- and
  the DO block's comment now says so instead of implying the whole
  migration converges.

- The source guard no longer bans "DROP INDEX IF EXISTS": conditionality is
  the drop's position inside the guard, not its spelling, and the old
  assertion made the concurrency fix un-landable. Boot-idempotence is now
  pinned live by OID (con.oid + con.conindid), which is the only witness
  that survives a same-named drop+re-add.

- test_store_twin_parity: identifier patterns accept schema-qualified and
  quoted names, SQL `--` comments are blanked before parsing, and a new
  per-twin coverage test asserts every CREATE INDEX keyword actually parses
  -- the guard could previously read zero indexes and pass. Docstrings that
  claimed source position *is* execution order now name users_postgres.py,
  where a hoisted DDL constant already executes after later inline ALTERs.

All four new guards mutation-tested. Full backend suite: 4111 passed,
46 skipped, with the @pg_only tier live against postgres:18.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVXYsLrNzE1biGsEfj94yL
The 2026-09-02 amendment claimed convergence as the migration's property;
it is the property of the index repair and now of the logical-attempt
UNIQUE. Records why the other twelve constraint rebuilds are left
unconditional, and why the IF EXISTS on the drop is required despite the
surrounding guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVXYsLrNzE1biGsEfj94yL
@FlyM1ss

FlyM1ss commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Second review pass on c4f294ee; 5 findings, all fixed in b4930aad + 40e6d825 (fast-forward, no force-push).

DDL

  • DROP INDEX gains IF EXISTS. The guard's predicate is evaluated before the lock is taken, so two concurrent boots can both reach it; the loser raised index "..." does not exist, aborting _init_schema — fatal, since credits_store is built at import.
  • ADD CONSTRAINT ... logical_attempt_key now sits behind a conkey guard. It is a UNIQUE, i.e. exactly the full index build under ACCESS EXCLUSIVE the index repair was written to avoid, on the same table, running unconditionally three lines above it. Column identity comes from pg_constraint.conkey, not a pg_get_constraintdef text match; a mismatch falls back to the previous drop+add.
  • The other 12 CHECK/FK rebuilds are left unconditional on purpose — recognising a CHECK means comparing deparsed text, which drifts between versions, and a predicate that silently stops matching converges to nothing while still looking correct. The DO block's comment now says this instead of implying the whole migration converges. Recorded in the spec amendment.

Tests

  • The source guard no longer bans the DROP INDEX IF EXISTS spelling — conditionality is the drop's position inside the guard, and the old assertion made the concurrency fix un-landable. Convergence is now pinned live by con.oid + con.conindid; conindid is load-bearing, since a drop and re-add keeps the constraint's name.
  • test_store_twin_parity: identifier patterns accept schema-qualified and quoted names (ON public.t(a) and ON "t"(a) previously matched nothing, so such a twin would pass with zero indexes read), SQL -- comments are blanked before parsing, and a new per-twin test asserts every CREATE INDEX keyword actually parses.
  • Docstrings claiming source position is execution order now name the counterexample: users_postgres.py executes AUTH_SESSIONS_DDL after later inline ALTERs.

All four new guards mutation-tested. Full backend suite 4111 passed / 46 skipped with the @pg_only tier live against a local postgres:18. CI green; CodeQL python + javascript both results=0 on merge ref 7930b04a.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AVXYsLrNzE1biGsEfj94yL

@FlyM1ss
FlyM1ss merged commit fa6996c into Open-Finance-Lab:main Sep 4, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants