Drop the eight legacy columns SP8 left behind

Migrations 66 to 73 drop `actions.index`, `variants`, `variant_index`,
`variant_count`, `state_before`, and `world_state_before`, plus
`adventures.memory_cursor` and `summary_cursor`. `index` is a keyword in
SQLite, so migration 71 quotes it.

Nothing outside the migrations read these. `models.py`, `schemas.py`, and
`ACTION_LIST_COLUMNS` lose the same eight fields, `Adventure.actions` orders
by `id`, and `attempts.renumber`, `context.history.max_action_index`, and
`nodes.next_index` are deleted.

Two changes keep the migration replayable on a `create_all` database:

- `_split_variants_into_siblings` wrote through the live ORM table, so it
  stopped compiling once migration 66 removed five of its columns. It now
  writes through `_ACTIONS_AT_60`, a frozen `Table` with its own `MetaData`.
- Five data passes read columns these migrations drop. Each now calls
  `_has_columns` and returns early when the columns are absent.

`bootstrap` takes a `through` version so a migration test can stop at the
schema it asserts on.

555 tests pass, up from 549. Eight of the new cases assert each column is
gone after a real schema-45 database migrates all the way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198qDK3gmgSo7EtQ4GTPqqK
This commit is contained in:
parththakkar106
2026-08-29 17:15:49 +05:30
committed by Parth
co-authored by Claude Opus 5
parent e0bf2b61d9
commit f1bebe18d0
41 changed files with 477 additions and 378 deletions
+6 -10
View File
@@ -23,7 +23,7 @@ from ...sse import SSE_HEADERS, sse, turn_error
from ..settings import get_settings
from .deps import CurrentUser, current_adventure, router
from .nodes import _move_to_after, next_depth, next_index
from .nodes import _move_to_after, next_depth
from .paging import annotate_takes
@@ -275,11 +275,6 @@ async def _generate_turn(
reasoning = "".join(reasoning_chunks).strip() or None
ai_action = models.Action(
adventure_id=adventure.id,
# A sibling shares the turn's legacy index for the same reason it
# shares its depth: it is the same turn. Two rows then hold one index,
# which is safe, because `max_action_index` takes a maximum rather than
# a count, and nothing else reads the column.
index=retry_of.index if retry_of is not None else next_index(adventure),
depth=ai_depth,
type="ai",
text=text,
@@ -298,8 +293,10 @@ async def _generate_turn(
# a turn landed on top of it. See `memorybank`.
memorybank.forget_node(db, adventure, retry_of)
cursors.rewind_all(adventure, retry_of.branch_id, ai_depth - 1)
# Flush so the new attempt has an id. The session does not autoflush,
# and attempts page in id order, so a read taken before this point puts
# the newest attempt nowhere.
db.flush()
attempts.renumber(attempts.group(db, ai_action))
else:
tree.place_action(db, adventure, ai_action)
db.add(ai_action)
@@ -369,7 +366,6 @@ async def run_player_turn(
return
player_action = models.Action(
adventure_id=adventure.id,
index=next_index(adventure),
depth=next_depth(adventure),
type=payload.type,
text=modified,
@@ -384,8 +380,8 @@ async def run_player_turn(
db.refresh(player_action)
# The new action was added through its foreign key, so the loaded
# `adventure.actions` collection is stale. Without this expire,
# `build_context` and `next_index` for the AI action do not see the
# player action that was just saved.
# `build_context` for the AI action does not see the player action
# that was just saved.
db.expire(adventure, ["actions"])
yield sse({"type": "player", "action": action_json(player_action, db)})
if stop: