Skip to content

feat: version aware listing - #1346

Open
TylerHillery wants to merge 10 commits into
masterfrom
tyler/feat/object-versioning-wave-2
Open

feat: version aware listing#1346
TylerHillery wants to merge 10 commits into
masterfrom
tyler/feat/object-versioning-wave-2

Conversation

@TylerHillery

@TylerHillery TylerHillery commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

Feature

What is the current behavior?

Currently, listing (v1 and v2) doesn't allow including deleteMarkers or noncurrentVersions, or performing an exactMatch on the prefix.

What is the new behavior?

This change adds the above parameters to the list endpoints and wires them through all the necessary functions. deleteMarkers and noncurrentVersions only allow exclude, include, and only, with exclude being the default. exactMatch is a Boolean flag that changes the predicate on name from name LIKE '$1%' to name = $1.

exactMatch will be useful when you want to find all versions of a particular object. Without it, you could have an object like file.config and file.config.backup, where the backup would be included in the list results.

Additional context

It is important to note that this PR still does not change any of the write operations that would allow multiple versions per object, but lays the groundwork for #1347 when it lands. I removed the bucket_id, name uniqueness constraint so that we can fully test the new listing options under various conditions by seeding the database directly.

@TylerHillery
TylerHillery force-pushed the tyler/feat/object-versioning-wave-2 branch 2 times, most recently from 8480320 to 30fd898 Compare August 24, 2026 02:33
@coveralls

coveralls commented Aug 24, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 33525890038

Coverage decreased (-0.3%) to 81.063%

Details

  • Coverage decreased (-0.3%) from the base build.
  • Patch coverage: 40 uncovered changes across 4 files (64 of 104 lines covered, 61.54%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
src/storage/database/pg.ts 79 43 54.43%
src/storage/object.ts 18 16 88.89%
src/http/routes/object/listObjects.ts 3 2 66.67%
src/http/routes/object/listObjectsV2.ts 3 2 66.67%
Total (5 files) 104 64 61.54%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 13346
Covered Lines: 11269
Line Coverage: 84.44%
Relevant Branches: 7999
Covered Branches: 6034
Branch Coverage: 75.43%
Branches in Coverage %: Yes
Coverage Strength: 612.84 hits per line

💛 - Coveralls

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have parsed out all the functions from before this change and after this change for an easier git diff to review:

tyler/chore/function-diffs-before...tyler/chore/function-diffs-after

@TylerHillery TylerHillery Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

benchmarking code can be found here: tyler/feat/object-versioning-wave-2a...tyler/chore/object-versioning-benchmark

Note, this was primarily AI generated benchmark I still need to validate results. This benchmarking code will not be committed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for sharing!
From the look of it, it is missing an index:

| list_objects_with_delimiter: deleteMarkers=only | — | — | 3175.92ms | 4081.87ms | 2425.58ms |

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed: 17795e8

benchmark includes the new results: 3d54626

| list_objects_with_delimiter: deleteMarkers=only | — | — | 3175.92ms | 4081.87ms | 2425.58ms | 2.10ms |

@TylerHillery
TylerHillery force-pushed the tyler/feat/object-versioning-wave-2 branch 5 times, most recently from 8534d33 to c2009f2 Compare August 27, 2026 01:13
Base automatically changed from tyler/feat/object-versioning-wave-1 to master August 27, 2026 13:41
@TylerHillery
TylerHillery force-pushed the tyler/feat/object-versioning-wave-2 branch 4 times, most recently from 8104dfa to 1b461d6 Compare August 27, 2026 18:50
@TylerHillery
TylerHillery marked this pull request as ready for review August 27, 2026 18:55
@TylerHillery
TylerHillery requested a review from a team as a code owner August 27, 2026 18:55
Copilot AI lite review requested due to automatic review settings August 27, 2026 18:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds version-aware listing capabilities to the Storage object list endpoints (v1 and v2), enabling clients to include/exclude noncurrent versions and delete markers and to perform exact key matches (useful for retrieving all versions of a single object).

Changes:

  • Add noncurrentVersions, deleteMarkers (tri-state) and exactMatch to list endpoints and wire them through storage/database layers.
  • Extend V2 continuation token pagination to support multi-version key pagination using (archived_at, version) as additional cursor tiebreakers.
  • Introduce tenant migrations updating DB listing/search functions to return versioning metadata, and drop the legacy (bucket_id, name) uniqueness index to support multi-version testing/groundwork.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/test/object.test.ts Adds v1 route coverage for versioning filters and exact-match behavior.
src/test/object-list-v2.test.ts Adds extensive v2 versioning + pagination tests and DB seeding helpers.
src/storage/schemas/object.ts Introduces objectListEntrySchema/ObjectListEntry for list responses (nullable versioning fields).
src/storage/object.ts Threads new list options, adjusts delimiter collapsing for exactMatch, and extends continuation token fields.
src/storage/database/pg.ts Implements filtering/exactMatch and pagination tiebreaks in PG list/search queries and function calls.
src/storage/database/adapter.ts Updates DB interfaces to accept new list/search options and return ObjectListEntry[].
src/internal/database/migrations/types.ts Registers new migrations: version-aware listing and index drop.
src/http/routes/object/listObjectsV2.ts Adds request params + migration gating for versioning filters on v2 endpoint.
src/http/routes/object/listObjects.ts Adds request params + migration gating and updates response schema for v1 endpoint.
migrations/tenant/0068-list-objects-with-versions.sql Updates storage SQL functions to support version-aware listing outputs and filtering.
migrations/tenant/0069-drop-bucketid-objname-index.sql Drops the legacy unique index concurrently to allow multi-version rows per key.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/storage/object.ts

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline finding on the timestamp-cursor tiebreak collapsing to '' for all rows, I also checked the upsertObject conflict-target switch (pg.ts, keyed on the pre-existing objects-current-version-index migration) and the noncurrentVersions/deleteMarkers cursor-locking in object.ts — both correctly gate on migration state and aren't affected by the same class of bug.

Extended reasoning...

A confirmed bug was already reported inline (storage.search_by_timestamp's version tiebreak collapsing to '' for all rows, not just the boundary row, when the previous page's last row had a NULL/empty version — a realistic scenario since version is nullable and defaults to NULL for pre-existing objects). Given that finding is present, per policy I only add a short note about what else was checked and ruled out, rather than a full approve/defer writeup.

I spot-checked the three ruled-out candidates: the upsertObject ON CONFLICT target switch in pg.ts (ties migration 0069's dropped unique index to the pre-existing partial index from migration 0066, consistent with the PR's stated intent to remove the bucket_id/name uniqueness constraint), and the cursor-locking/enum-validation logic in object.ts for noncurrentVersions/deleteMarkers. Nothing beyond the already-reported finding stood out as a new, independently-worth-flagging issue.

Comment thread migrations/tenant/0068-list-objects-with-versions.sql
Comment thread src/storage/database/pg.ts Outdated

export type Obj = FromSchema<typeof objectSchema>

export const objectListEntrySchema = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason we don't want to add these properties directly to the Obj type instead of having a new one?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nullability only exists because list responses can return synthetic folder entries with no version/marker/versioned fields at all, so a separate schema keeps that "might be a folder" case scoped to listing instead of forcing null-checks onto call sites that can never hit it.

If you still prefer having on Obj let me know

Comment thread src/storage/database/pg.ts
Comment thread migrations/tenant/0068-list-objects-with-versions.sql Outdated
Comment thread src/test/object.test.ts Outdated
}
}, 5000)

test('v1 desc listing advances to the next key after exhausting a key at a batch boundary', async () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are skipping some keys wrongly. Can you add an ascending version of this test while adding a sibling object between deepKey and targetKey?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A test like this? (note I have moved the tests to new branch/PR as part of the 2 phase rollout): 8f764a9

@blacksmith-sh

This comment has been minimized.

Comment thread src/storage/database/pg.ts
@TylerHillery
TylerHillery force-pushed the tyler/feat/object-versioning-wave-2 branch from 1537989 to 0bea5a1 Compare August 31, 2026 18:59
@TylerHillery
TylerHillery changed the base branch from master to tyler/fix/index-collation August 31, 2026 19:05
@TylerHillery
TylerHillery force-pushed the tyler/feat/object-versioning-wave-2 branch from 55dfa10 to 6249965 Compare August 31, 2026 20:14
@TylerHillery
TylerHillery force-pushed the tyler/feat/object-versioning-wave-2 branch from 6249965 to 431f5c4 Compare August 31, 2026 20:54
@TylerHillery
TylerHillery force-pushed the tyler/feat/object-versioning-wave-2 branch from 431f5c4 to 14275c7 Compare August 31, 2026 22:32
Base automatically changed from tyler/fix/index-collation to master August 31, 2026 23:11
@TylerHillery
TylerHillery force-pushed the tyler/feat/object-versioning-wave-2 branch from 14275c7 to f869840 Compare August 31, 2026 23:11
Comment thread migrations/tenant/0068-list-objects-with-versions.sql
-- CREATE OR REPLACE FUNCTION doesn't work when parameters or return value changes
DROP FUNCTION IF EXISTS storage.list_objects_with_delimiter(text, text, text, integer, text, text, text);

CREATE OR REPLACE FUNCTION storage.list_objects_with_delimiter(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's time to refactor this big function into smaller more manageable one?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the function's size comes from planner-sensitive query variants. For example, we need explicit branches for sort direction, prefix bounds, multi-version pagination, and delete-marker filtering:

  IF delete_markers = 'only' THEN
    -- Literal is_delete_marker predicate so Postgres can use the partial index
  ELSIF v_multi_row THEN
    -- Version-aware keyset query
  ELSE
    -- Current-version query
  END IF;

It is difficult to collapse these into one parameterized static query because PostgreSQL's cached generic plan cannot reliably use the name bounds or partial index. Dynamic SQL would preserve those predicates, but it would be replanned on every peek.

I also investigated extracting helpers for tri-state normalization and prefix upper-bound calculation. Those blocks are small and used in only a few places, so I do not think it's worth additional schema-level functions.

Did you have some ideas around what the refactor would look like?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants