perf(artifacts): serve the library listing from UserArtifactsIndex - #989
Merged
Merged
Conversation
⚠️ MUST NOT MERGE until `UserArtifactsIndex` reports ACTIVE in the target environment. `DescribeTable`, not CloudFormation — CFN reports UPDATE_COMPLETE while DynamoDB is still backfilling the index. `list_for_user` now queries the index (GSI2PK=USER#{uid}, GSI2SK descending) instead of the base table. HEAD and version rows share the base partition, so the old Query spanned roughly 3x the rows it returned and then date-sorted them in memory; only HEAD rows carry the GSI2 keys, so the index holds one row per artifact already newest-first. The amplification and the sort both go away, and the ordering now comes from the store rather than being recomputed per request. Still returns the whole library in one response, paging the index internally. Exposing pagination is a bigger change than it looks — search and the type filter live in the SPA, and a filter that sees only the loaded page is worse than no filter because it looks authoritative, so both would have to move server-side in the same change. The index makes that possible whenever it is wanted. ## Two things this turned up **The library tests were passing against the old code path.** My first edit spliced the new method in with inverted slice boundaries, leaving DUPLICATE `list_for_user` and `heads_for_session` definitions — Python took the last, which was the original base-table version. The suite went green while testing nothing new. Caught by asking why tests that should have needed a GSI passed without one; the fixture had no GlobalSecondaryIndexes at all. The fixture now declares the index, so moto raises ResourceNotFoundException if the query ever stops using it — which is what makes these tests exercise the index rather than silently falling back. **Undated rows would have vanished.** A sparse index omits any HEAD row without GSI2PK, permanently and silently. Rows predating `updated_at` cannot carry a real timestamp, so under the first version of the backfill they were skipped — and would have dropped out of their owner's library, which `test_undated_legacy_rows_are_returned_and_sort_last` exists to forbid. They are now stamped with an EMPTY timestamp segment (`ARTIFACT##{aid}`). That is not a fabricated time: "#" sorts below every digit, so read descending the row lands last — exactly where the old in-memory sort put it. Neither dev nor prod holds such a row today; this is the defensive branch, and it preserves a contract the tests already assert. The pagination stub now yields the NEWER row on page 1, matching what a descending index does. With the old stub a client-side re-sort would have passed either way and hidden a broken sort key. Backend: 942 passed across app_api, architecture and the artifact writer suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Completes the index work.
list_for_usernow queriesUserArtifactsIndex(GSI2PK=USER#{uid}, GSI2SK descending) instead of the base table.Safe to merge: the index is
ACTIVEin dev and verified populated — see below. Do not merge tomainuntil the same is confirmed there.What it changes
HEAD and version rows share the base partition, so the old Query spanned ~3× the rows it returned and then date-sorted them in memory. Only HEAD rows carry the GSI2 keys, so the index holds one row per artifact already newest-first. The amplification and the sort both go away, and ordering comes from the store rather than being recomputed per request.
Still returns the whole library in one response, paging the index internally. Exposing pagination is a bigger change than it looks — search and the type filter live in the SPA, and a filter that sees only the loaded page is worse than no filter because it looks authoritative, so both would move server-side in the same change. The index makes that possible whenever it's wanted.
Verified against the live dev index
ItemCountstill reads 0 inDescribeTable— that figure refreshes about every 6 hours, so it's stale metadata, not truth. The scan and query above are the real check.Two bugs this turned up
The library tests were passing against the old code path. My first splice used inverted slice boundaries and left duplicate
list_for_userandheads_for_sessiondefinitions — Python takes the last, which was the original base-table version. The suite went green while testing nothing new. I caught it by asking why tests that should need a GSI passed against a fixture that had none.The fixture now declares the index, so moto raises
ResourceNotFoundExceptionif the query ever stops using it. That's what makes these tests exercise the index instead of silently falling back.Undated rows would have vanished. A sparse index omits any HEAD row without
GSI2PK, permanently and silently. Rows predatingupdated_atcan't carry a real timestamp, so the first version of the backfill skipped them — and they'd have dropped out of their owner's library, whichtest_undated_legacy_rows_are_returned_and_sort_lastexists to forbid.They're now stamped with an empty timestamp segment (
ARTIFACT##{aid}). That isn't a fabricated time:#sorts below every digit, so read descending the row lands last — exactly where the old in-memory sort put it. Neither dev nor prod holds such a row today; this is the defensive branch preserving a contract the tests already assert.The pagination stub now yields the newer row on page 1, matching a descending index. With the old stub a client-side re-sort would have passed either way and hidden a broken sort key.
Tests
942 passed across
app_api,architectureand the artifact writer suites.🤖 Generated with Claude Code