feat(maven): enforce maven_files version↔️package consistency via composite FK

What this MR does

Closes #188 (closed).

maven_files carries two independent ancestor FKs:

  • maven_package_id (NOT NULL) → maven_packages(id, namespace_id)
  • maven_version_id (nullable) → maven_versions(id, namespace_id)

Neither referenced the other, so a version-level file's maven_package_id could disagree with the package its maven_version actually belongs to — both single-column FKs pass independently and the inconsistent row lands. Not a live bug today (the intended FindOrCreatePackage → FindOrCreateVersion(pkg.ID) → Upsert*File(pkg.ID, ver.ID) flow builds the ids consistently), but unguarded for a future Steps 6–9 handler.

The fix

A new migration 20260618150000_add_maven_files_version_package_composite_fk.sql:

  1. Adds a unique index on maven_versions (id, maven_package_id, namespace_id) to serve as the composite FK's referenced key ((id, namespace_id) is already the PK, so adding maven_package_id stays unique).
  2. Drops the standalone fk_maven_files_maven_version_id_maven_versions.
  3. Adds fk_maven_files_version_belongs_to_package on (maven_version_id, maven_package_id, namespace_id) → maven_versions (id, maven_package_id, namespace_id).

Under Postgres MATCH SIMPLE (the default), the composite FK is skipped whenever any referenced column is NULL:

  • Package-level files (maven_version_id IS NULL): composite FK not enforced; the kept standalone maven_package_id FK still guarantees the package exists.
  • Version-level files (both NOT NULL): composite FK enforced → forces the file's maven_package_id to equal the version's package.

DB-enforced, zero application cost, and keeps ADR-007's denormalized package→files scan path intact.

Migration mechanics

Follows the 20260612130200_add_blob_storage_attachments_blob_fk.sql precedent: maven_files is HASH-partitioned, so the FK is added as a single validating ADD CONSTRAINT (Postgres forbids NOT VALID FKs on partitioned tables, SQLSTATE 42809). Safe and trivially fast here because the table is empty in the greenfield window. The squawk-ignore-file header silences mirror that precedent.

Tests

Adds four cases to internal/datastore/migrations/maven_schema_integration_test.go:

  • TestMavenConstraints_FilesCompositeVersionPackageFKShape — the 3-column composite FK exists and the old 2-column version-only FK is gone.
  • TestMavenConstraints_FilesRejectVersionUnderDifferentPackage — a file claiming package A while pointing at a version under package B is rejected with SQLSTATE 23503.
  • TestMavenConstraints_FilesAcceptConsistentVersionAndPackage — the consistent version-level flow still inserts.
  • TestMavenConstraints_FilesPackageLevelMetadataUnaffectedByCompositeFK — maven_version_id IS NULL metadata files still insert (MATCH SIMPLE skips the composite FK).

The migration token list is extended so the existing NO TRANSACTION / one-statement-per-drop mechanics checks cover the new file.

Out of scope / follow-ups

  • structure.sql regeneration is left to CI's db:structure-check (mise run db:dump-structure); per project convention the dump is a generated artifact and is not hand-edited.
  • jet regen: index/constraint only, no model-column change — a no-op for the generated types, so no internal/datastore/jet/** changes.
  • ADR-007 update documenting the composite FK is synced via the handbook repo, not edited in-tree.

Related: !564 (merged), S10 Maven hosted #21 (closed).

Database Review Evidence

Migrations

Note

Timings are from CI (db:migrate matrix, goose verbose) against an empty database, in apply / rollback order per PG version. Production-scale validation via Database Lab is not yet available. See Database review evidence for the matrix rationale and how to read the numbers.

Migration PG 16 PG 17 PG 18
20260618150000_add_maven_files_version_package_composite_fk.sql OK (336.02ms / 200.36ms) OK (404.37ms / 361.23ms) OK (414.43ms / 386.19ms)
Edited by Moaz Khalifa

Merge request reports

Loading
Loading