chore(datastore): maven metadata discovery queries (S20-A Maven metadata purge plan: 3/5)

What

Adds an unregistered discovery query for tombstoned hosted Maven package-level maven-metadata.xml files:

ScanTombstonedMavenMetadataFiles returns one bounded, namespace-scoped page of eligible tombstones, oldest-first, built on the metadata-only partial index Step 1 of this plan already landed on main (index_maven_files_on_ns_id_soft_deleted_at_metadata). It follows the same shape as the existing ScanTombstonedRepositories/*Packages/ *Versions scans: shared argument guards (validateLifecycleScanArgs), an identifier-free error wrap, and a MaxLifecycleScanPageSize-bounded LIMIT.

This is discovery only. Nothing calls it yet — internal/lifecycle/purge.go's DefaultPurgeLevels and the CountTombstonesByLevel backlog gauge are unchanged. Wiring it into the purge sweep is Step 5 of this plan; Step 2's single-row reaper (!2731, merged) is what it will eventually feed.

Testing

  • Unit (lifecycle_scan_maven_metadata_test.go): pins the statement's sort key, its three bound arguments, and its two index-matching conjuncts; covers every guard-rejection path (nil context, zero namespace, zero/ negative/oversized limit, negative window) on a client-less store.
  • Integration: TestLifecycleScanStore_ScanTombstonedMavenMetadataFiles (lifecycle_scan_maven_metadata_integration_test.go) covers the four cases specific to the metadata level — discovery while the package and repository stand, version-level exclusion, other-file-name exclusion, and two tombstones at one coordinate. The cases this scan shares with the other package-level scans (window handling, live-row exclusion, ordering, paging, namespace isolation, and the query-failure wrap) run once, table-driven, via the maven metadata files row added to packageLevelScans / packageLevelScanTargets (lifecycle_scan_packages_integration_test.go, lifecycle_scan_packages_test.go). The EXPLAIN-backed proof that the query is index-backed, oldest-first, and one-partition runs the same way, as a row in TestLifecycleScanStore_PackageLevelScans_AreIndexBackedOldestFirst (lifecycle_explain_integration_test.go).

Database Review Evidence

Note

Plans are from EXPLAIN (ANALYZE, BUFFERS) against an ephemeral PostgreSQL 17.11 container (matching GL_PG_CURR_VERSION from .gitlab-ci-other-versions.yml), with synthesized seed data rolled back per query and the container torn down at the end of the run. Numbers reflect moderate cardinality and do not capture production-scale effects. See Database review evidence for seed sizing, methodology, and the anomalies the skill flags. Expand the row's details for the seed shape, rendered SQL, bound args, and raw plan.

Queries

Method Plan node Index Rows (plan / actual) Cost Time Buffers (hit / read) Partitions
datastore.ScanTombstonedMavenMetadataFiles Limit maven_files_p14_namespace_id_soft_deleted_at_metadata_idx 25 / 25 3.15 0.015ms 4 / 0 1/64
datastore.ScanTombstonedMavenMetadataFiles

Summary: Plan matches the method's intent: a backward Index Scan over the metadata-only partial index, with the namespace_id equality pruning to 1 of 64 partitions and no separate sort node. Actual rows match the LIMIT estimate (25 / 25) and execution stays under 0.03ms against 5000 seeded tombstones. No anomalies.

Seed shape: namespaces=1, repositories=1, maven_repositories=1, maven_packages=1, blob_storage_blobs=1, blob_storage_attachments=1, maven_files=5000 (one package coordinate, all tombstoned — legal because the live-row unique index is partial on soft_deleted_at IS NULL)

Rendered SQL:

SELECT maven_files.namespace_id AS "tombstone_row.namespace_id",
     maven_files.id AS "tombstone_row.id",
     maven_files.soft_deleted_at AS "tombstone_row.soft_deleted_at"
FROM public.maven_files
WHERE (((maven_files.namespace_id = $1::uuid) AND (maven_files.maven_version_id IS NULL)) AND (maven_files.file_name = $2::text)) AND (maven_files.soft_deleted_at < (NOW() - INTERVAL '1 HOUR'))
ORDER BY maven_files.soft_deleted_at ASC
LIMIT $3;

Bound args: [<seeded namespace_id>, "maven-metadata.xml", 25]

Plan (EXPLAIN (ANALYZE, BUFFERS) output):

Limit  (cost=0.29..3.15 rows=25 width=40) (actual time=0.011..0.015 rows=25 loops=1)
  Buffers: shared hit=4
  ->  Index Scan Backward using maven_files_p14_namespace_id_soft_deleted_at_metadata_idx on maven_files_p14 maven_files  (cost=0.29..573.04 rows=5000 width=40) (actual time=0.011..0.013 rows=25 loops=1)
        Index Cond: ((namespace_id = 'eacca14d-e1e6-7d9c-8c09-cd29243a07c6'::uuid) AND (soft_deleted_at < (now() - '01:00:00'::interval)))
        Buffers: shared hit=4
Planning:
  Buffers: shared hit=452 read=2
Planning Time: 2.043 ms
Execution Time: 0.025 ms

Timings: planning 2.043ms, execution 0.025ms, total 2.068ms.

Related to #1167 (closed)

Edited by Sylvia Shen

Merge request reports

Loading
Loading