Name the columns a list response carries
deferred=True keeps the four heavy Action columns out of a bulk read, but it makes narrowness the thing a future column has to remember to ask for -- and both egress blowouts this project has had were a column nobody remembered. Listing what each list response renders inverts the default: a new column costs nothing on these paths until someone adds it to the tuple. The adventures index was not merely a future risk. It loaded whole Adventure entities to render a title, a stamp and a snippet, and an Adventure carries script_state, world_state, placeholders, story_summary, memory, authors_note and ai_instructions -- ~15 kB a row in production, none of it on that screen, all of it fetched once per adventure on every index load. Measured on six adventures with 78 kB of body each: 469.7 kB entity-loaded against 318 B projected. The memories drawer stops walking adventure.memories. The walk is what retrieval used to do and the reason a turn cost megabytes; a relationship load takes whole entities, so it picks up whatever the model happens to grow. Nothing changes today -- embedding_blob is already deferred -- which is the point. world_delta stays on the action list because ActionOut.world_changes is computed from it. Leaving it off would not save the bytes, it would spend them one row at a time as a lazy load. Two tests cover the index: one asserts the listing query names none of the body columns, one puts a byte ceiling on six adventures carrying 80 kB apiece. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Dvvqn9ZDR4ixeFPHNbww7
This commit is contained in:
co-authored by
Claude Opus 5
parent
12d57afdac
commit
a6cb49293c
@@ -347,6 +347,74 @@ def test_reading_one_action_does_not_cost_the_whole_story(client, meter):
|
||||
)
|
||||
|
||||
|
||||
def _fat_adventures(user_id: int, count: int = 5, body: int = 20_000) -> None:
|
||||
"""Adventures whose bodies are heavy and whose index cards are not.
|
||||
|
||||
script_state, world_state and story_summary belong to the play screen. The
|
||||
index shows a title, a stamp and a snippet, and used to load all of it.
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
for i in range(count):
|
||||
db.add(models.Adventure(
|
||||
user_id=user_id,
|
||||
title=f"Adventure {i}",
|
||||
script_state={"log": "s" * body},
|
||||
world_state={"player": {"notes": "w" * body}},
|
||||
story_summary="y" * body,
|
||||
memory="m" * body,
|
||||
))
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def test_the_index_does_not_read_the_adventure_body(client, sql_log):
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user_id = db.query(models.User.id).first()[0]
|
||||
finally:
|
||||
db.close()
|
||||
_fat_adventures(user_id)
|
||||
|
||||
r = client.get("/api/adventures")
|
||||
assert r.status_code == 200
|
||||
assert len(r.json()) == 6 # the fixture's one, plus five
|
||||
|
||||
listing = [
|
||||
s for s in sql_log
|
||||
if "FROM adventures" in s and s.lstrip().upper().startswith("SELECT")
|
||||
]
|
||||
assert listing, "expected a listing query"
|
||||
for column in ("script_state", "world_state", "story_summary", "memory",
|
||||
"authors_note", "ai_instructions", "placeholders"):
|
||||
assert not any(column in s for s in listing), (
|
||||
f"the index read adventures.{column}, which nothing on that "
|
||||
f"screen displays"
|
||||
)
|
||||
|
||||
|
||||
def test_the_index_stays_under_its_byte_ceiling(client, meter):
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user_id = db.query(models.User.id).first()[0]
|
||||
finally:
|
||||
db.close()
|
||||
_fat_adventures(user_id)
|
||||
|
||||
with meter.scope("index"):
|
||||
r = client.get("/api/adventures")
|
||||
assert r.status_code == 200
|
||||
|
||||
# Six adventures carrying 80 kB of body each. A card is a title, a stamp
|
||||
# and a 220-character snippet; 4 kB apiece is already generous.
|
||||
budget = 6 * 4_000
|
||||
assert fetched(meter) < budget, (
|
||||
f"the index fetched {fetched(meter):,} B for six adventures, over "
|
||||
f"{budget:,} B — it is reading the bodies again"
|
||||
)
|
||||
|
||||
|
||||
def test_the_ceiling_discriminates(client, meter):
|
||||
"""A ceiling is only worth having if the thing it excludes would breach it.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user