priv/repo/migrations/20260826103000_add_thread_terminal_outcome_check.exs

main at 58e6347eeb72 · 1 KB

defmodule OpenAgents.Repo.Migrations.AddThreadTerminalOutcomeCheck do
  use Ecto.Migration

  @moduledoc """
  A thread's terminal status and its error code have to agree.

  `succeeded` means no error code; every other terminal status names one. The
  rule exists because the terminal row is the durable record of what a session
  did, and the two ways it can lie are symmetric: a session that answered and
  exited 0 recorded as `cancelled` (issue #106), and a session that failed, was
  interrupted, or ran out of steps recorded as `succeeded`. Giving
  `OpenAgents.Threads.finish/2` an HTTP route makes the second one reachable by
  any client, so the pair is refused here as well as in the changeset.

  The constraint is created `NOT VALID`: it is enforced on every insert and
  update from this point on, and the historical scan is skipped so the
  migration does not hold a lock over the whole table while the previous
  release is still serving. Every writer that has ever ended a thread —
  `cancel/2`, the authority reaper, and `finish/2`'s default — already writes a
  pair this admits, so there is nothing for the scan to find; skipping it is a
  deployment courtesy rather than a concession.
  """

  def up do
    execute("""
    ALTER TABLE threads
    ADD CONSTRAINT threads_terminal_outcome_check
    CHECK (
      status = 'open'
      OR (status = 'succeeded' AND (error_code IS NULL OR btrim(error_code) = ''))
      OR (status <> 'succeeded' AND error_code IS NOT NULL AND btrim(error_code) <> '')
    )
    NOT VALID
    """)
  end

  def down do
    execute("ALTER TABLE threads DROP CONSTRAINT threads_terminal_outcome_check")
  end
end