Archive page: load history and tags eagerly (#809)

GET /api/archive/<id> loaded each history Archive's FileGroup and Tags with
their own queries while serializing: two queries per history Archive.  An
Archive with 51 history entries ran 107 statements per request.  Both the
Archive and its history now use Archive.json_options(), so the count no
longer grows with the history (5 statements for the same Archive).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
lrnselfreliance 2026-10-09 19:45:13 -06:00 • committed by GitHub
parent dab333124e
commit a8dde44d4f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 40 additions and 1 deletions

View file

@ -1243,7 +1243,8 @@ def get_archive(session: Session, archive_id: int, skip_viewed: bool = False) ->
def get_archive_by_file_group_id(session: Session, file_group_id: int, skip_viewed: bool = False) -> Archive:
"""Get an Archive by its FileGroup ID."""
archive = session.query(Archive).filter_by(file_group_id=file_group_id).one_or_none()
archive = session.query(Archive).options(*Archive.json_options()) \
.filter_by(file_group_id=file_group_id).one_or_none()
if not archive:
raise UnknownArchive(f'Unknown Archive with FileGroup ID: {file_group_id}')
if not skip_viewed:

View file

@ -172,6 +172,7 @@ class Archive(Base, ModelHelper):
FileGroup.url != None, # noqa
) \
.join(FileGroup, FileGroup.id == Archive.file_group_id) \
.options(*Archive.json_options()) \
.order_by(FileGroup.published_datetime))
return history

View file

@ -590,3 +590,40 @@ async def test_delete_archive_atomic_when_some_tagged(async_client, test_session
assert all(p.is_file() for p in untagged_paths)
assert all(p.is_file() for p in tagged_paths)
assert test_session.query(Archive).count() == 2
@pytest.mark.asyncio
async def test_get_archive_loads_history_eagerly(test_session, archive_factory, async_client, tag_factory):
"""An Archive's page returns its history, with each Archive's Tags, without a query per history Archive."""
from sqlalchemy import event
from sqlalchemy.engine import Engine
tag = await tag_factory()
short = [archive_factory('example.com', 'https://example.com/short', tag_names=[tag.name]) for _ in range(2)]
long = [archive_factory('example.com', 'https://example.com/long', tag_names=[tag.name]) for _ in range(6)]
test_session.commit()
async def get(archive):
statements = []
def count(conn, cursor, statement, *_):
statements.append(statement)
event.listen(Engine, 'before_cursor_execute', count)
try:
request, response = await async_client.get(f'/api/archive/{archive.file_group_id}?skip_viewed=true')
finally:
event.remove(Engine, 'before_cursor_execute', count)
assert response.status_code == HTTPStatus.OK, response.json
return response.json, len(statements)
await get(short[-1]) # The first request does one-time work; measure the next ones.
short_json, short_statements = await get(short[-1])
long_json, long_statements = await get(long[-1])
assert len(short_json['history']) == 1
assert len(long_json['history']) == 5
assert long_json['file_group']['tags'] == [tag.name]
assert all(i['tags'] == [tag.name] for i in long_json['history'])
# Five history Archives cost no more queries than one.
assert long_statements == short_statements, (short_statements, long_statements)