feat(datastore): drop Maven's interim plaintext credential columns part 2/2 (S04-A per-format columns plan: 5/11)
The stack has landed; this MR targets main
| Part | MR | Scope | Reviewable LOC | State |
|---|---|---|---|---|
| 1/2 | !2629 (merged) | stop binding the interim pair | 296 | merged 2026-09-15, squashed to 05f079626 |
| 2/2 | this MR | drop the columns | 2,543 | open, targets main |
The spec amendment the plan's Dependencies declares as a prerequisite, !2542 (merged), has merged too. So has npm's step 10 stack (!2625 (merged), !2626 (merged), !2627 (merged)), which the previous revision named as needing an agreed merge order with this branch.
This MR's LOC row is re-derived at its own head f9061bf10 against its merge
base with origin/main, ba67132b4: added plus removed, go-jet output excluded
for its Code generated ... DO NOT EDIT header, structure.sql counted for
having none. It reads 2,543, against 2,524 at 920d28f1f, 2,426 at
b0f28ca0f and 2,841 before the first rebase. The 19 lines between the first
two are the gate removal (12), the DO-block clarification (3) and the
record-count fix (4); the Step 5 plan amendment that briefly took this figure to
2,814 has moved to !2687 (merged) and is no longer in this diff. The 98 between the next two are review round 3's prose fixes, 63 of
them two revisions ago and 35 in the one after; the step from 2,841 to 2,426 was
the first rebase, not a change of method. The per-group split
under Size is re-derived at the same head. !2629 (merged)'s 296 is its last pre-merge figure and is
kept only so the split's two halves stay comparable; it is not re-derivable now
that its branch is gone.
What the rebase onto main had to resolve
!2629 (merged) merged squashed, so its seven commits arrived on main as
05f079626 and this branch carried seven duplicates of them. The rebase is
therefore --onto origin/main 3cac52f7b, replaying this MR's one commit alone;
a plain git rebase origin/main would have re-applied the merged half.
That replay conflicted in 18 files, and almost none of it was mechanical.
The reason is that steps 7, 8 and 10 landed on main while this branch sat on
!2629 (merged): the container credential store moved behind the row encryptor and
stopped writing its interim pair, and npm's moved too. The leftover-plaintext
boot report that this MR was written to retire was generalised over all
three formats in the meantime, and it now has per-format slots, a shared read
budget divided per slot, a write_path_moved field, an app.Component per
format and a suite of its own.
So the resolution is not the change this MR was cut as. It keeps that machinery and removes Maven's arm of it:
leftoverPlaintextReadersloses itsmavenfield andleftoverPlaintextReportsits Maven slot, so the shared window divides in two rather than three.wire.godropsmaven: mavenCredentialsfrom the registration and says why in the comment above it.RemoteCredentialStore.LeftoverPlaintextCredentials,InterimPlaintextWritePathMoved,mavenRemoteLeftoverPlaintextPredicate,countMavenRemoteLeftoverPlaintextStmtandselectMavenRemoteLeftoverPlaintextPageStmtgo, as planned.LeftoverPlaintextSampleLimit,LeftoverPlaintextRowandLeftoverPlaintextReportmove out ofmaven_remote_credentials.gointocredential_leftover_plaintext.goinstead of going with it. Container and npm both depend on them, so deleting them with the rest of Maven's half would not compile.- Three things the original commit added are dropped, because
mainnow supplies them or has made them meaningless: the exportedLeftoverPlaintext{Warn,Info,Error}MsgandLeftoverPlaintext{Format,Count,IDs}Keyconstants and theirTestLeftoverPlaintextLogContract(the rebuilt emitter they were exported for exists onmain, with its own unexported keys and its own tests), and the move ofcredentialColumnExprintocontainer_remote_credentials.go(that helper is gone frommain: the container update statement no longer names an interim column). - Two suites had to change arm rather than lose one. The DB-free statement
suite drove its first arm off Maven's retired builders, so it now drives
container's columns spelled out inline, which is how the npm arm is already
written. The container encryption suite's "the count is scoped to this table"
subtest planted a foreign row in
maven_remote_repositories; it plants it innpm_remote_repositoriesnow, because Maven no longer has a column to plant in. credential_table_integration_test.gowas restructured onmaininto fixture-driven shared helpers, so Maven's de-plaintexting had to be redone against the new shape: the fixture'sleftoverCount,leftoverPageandleftoverFilterColsare dropped, the seed and read-back lose the pair, andTestMavenRemoteCredentialTable_ExplainLeftoverPlaintextgoes with the statements it planned.
Three prose claims the rebase falsified are corrected in place rather than left
to a reviewer: the migration's ban-drop-column directive said
leftoverPlaintextReport was "gone from the tree as of this migration" (the
component stays; Maven's arm goes) and that container's and npm's interim
columns "are read and written today" (neither is the live store any more), and
both it and maven_remote_encrypted_credentials.md said "with the report gone".
The rebase also corrected three sentences that were already stale on main
before this branch touched them, in S13 and docs/dev/column-encryption.md:
they still describe Container's and npm's credential reads and writes as not
being behind the row encryptor, which steps 7, 8 and 10 changed. Those
sentences are ones this MR has to rewrite anyway, and shipping a knowingly
false half was the alternative. They are called out here because they widen the
diff past Maven.
git merge-tree --write-tree origin/main HEAD exits 0, and GitLab reports
has_conflicts: false at b0f28ca0f.
The second rebase, onto ba67132b4
Everything above describes the first rebase. A second one followed on
2026-09-15, after !2677 (merged) merged as squash 675d0e8cb: the replay onto
ba67132b4 conflicted in four files, cmd/artifact-registry/wire_remote.go,
docs/dev/column-encryption.md, docs/runbooks/key-rotation.md and
internal/datastore/credential_table.md. main's text was taken as the base in
the three documents and this branch's own delta re-applied on top;
wire_remote.go was resolved against the compiler. This branch's merge base is
now ba67132b4 itself.
What this changes
Drops tmp_plaintext_username and tmp_plaintext_password from
maven_remote_repositories, with the pair's own all-or-none CHECK, and removes
Maven's arm of the leftover-plaintext boot report that measured them. The
report itself stays, for the container and npm tables that still carry interim
columns. The encrypted unit that
sql/*_add_maven_remote_encrypted_credentials.sql added is the store of record
from this migration on.
The Up is one ALTER TABLE on the partitioned parent, which recurses into all
64 partitions. One statement rather than two buys one catalog pass and one
relcache invalidation, not one lock walk instead of two: goose wraps this Up in
a single transaction, so a second ALTER TABLE on the same parent would
re-request locks the transaction already holds, and PostgreSQL grants those from
its local lock table without queueing. DROP COLUMN takes the dependent CHECK
with it, so no DROP CONSTRAINT and no CASCADE appear. Two new assertions,
both inside the existing the_up_leaves_the_encrypted_unit_alone subtest, pin
those two claims, because the previous revision argued them in prose and
asserted neither. The subtest count stays seven.
The Up ends SQL access to the pair rather than erasing its bytes: it rewrites no
heap, so an environment that needs erasure needs a rewrite or a backup expiry as
well, and the migration's ban-drop-column rationale says so rather than reading
as erasure.
Step 4 of the root-key-compromise runbook loses
its Maven arm in the same change. That arm, added on !2629 (merged)'s branch one commit
after this one was cut, hands a responder an UPDATE over the two columns, cites
the pair's CHECK by name, and sources row ids from the boot line's
leftover_plaintext_count and leftover_plaintext_ids. This migration removes
all four, and the arm's own closing sentence delegated the retirement here. What
replaces it says the API clear now reaches all three formats, and that the arm
returns for one release per format when Container and npm move.
Locking, on both paths
The Down carries both bounds, SET lock_timeout = '5s' and
SET statement_timeout = '30s', because they bound different things.
docs/dev/database-migrations.md states the
mechanism: lock_timeout caps each lock acquisition attempt rather than the
statement, and the statement holds the locks it already took while it queues for
the next. The restoring ALTER TABLE acquires ACCESS EXCLUSIVE on the parent
and then on each of the 64 partitions in turn, so lock_timeout alone left the
accumulated worst case at 65 times 5s, a little over five minutes.
statement_timeout is what bounds that, and
20260911092233_create_container_virtual_repository_upstreams.sql pairs the two
the same way. The 30s is derived from the accumulation case alone, at six
near-maximal waits that each stay under 5s; failing fast on a genuinely
contended relation is lock_timeout's job, since such a relation aborts the
statement at 5s with 55P03 and lock_timeout does not retry. The 30s is also
sized on the row count below being zero, because the statement it bounds
includes a validating CHECK that scans all 65 relations.
The Up is the path with no bound at all, and that asymmetry is worth stating
plainly because it is the path that runs unattended. It takes ACCESS EXCLUSIVE
on the same 65 relations with neither lock_timeout nor statement_timeout;
its only cap is migrations.upTimeout at five minutes, shared across the whole
pending set and with goose's advisory-lock retry.
The exposure is the whole booting ReplicaSet, not one pod, and the previous
revision understated it by that factor. upTimeout is five minutes
(internal/datastore/migrations/runner.go:28) and goose's advisory-lock retry is
300 attempts at one second (migrations.go:62), so the two are equal.
migrations.go:59 asks for "a matching context timeout so waiting replicas don't
exit before the lock holder finishes", and matching exactly is what leaves the
replica no slack: a holder that spends its budget queued makes every other pod
booting in that rollout exhaust its own and exit 1. Meanwhile the queued
ACCESS EXCLUSIVE puts new AccessShare requests behind it, so pods on the
previous image stall their Maven remote reads for the whole wait. On failure the
async runner calls its fatal hook, the pod restarts, and the lock is re-queued,
so the stall repeats rather than happening once.
At the row count stated below, which is zero, the hold itself is free. The
operational precondition is that no long-running transaction is open on
maven_remote_repositories when the Up runs, and the engineer merging this MR
owns confirming it. Whether an Up of this shape
sets lock_timeout is a schema-wide question this file does not re-open:
#548 has the
decision and rules out a per-file SET, but its implementation is open and
unassigned, so the exposure is scheduled rather than owned. #548 covers only the
lock_timeout half; the accumulated-wait half is tracked by no issue, and the
directive now says so rather than implying #548 covers both.
20260902145931_add_ns_enc_keys_tombstone_state_checks.sql and
20260813150000_add_maven_versions_size_bytes.sql take that position, each
citing #548 for it. 20260818103702_drop_namespaces_organization_id.sql reaches
the same outcome for a different reason and is cited in the migration only for
the metadata-only lock scope: its own directive sets no bound because
"no statement holds a blocking lock long enough to need a bounded timeout",
which is an answer rather than a deferral.
Why the drop needs no per-environment reading
An earlier revision made a per-environment reading of the leftover-plaintext
boot line a precondition on merging, and carried an empty gstg/gprd table
for it. The operator ruled on 2026-09-15 that staging and production hold test
data only, so the interim pair holds no upstream credential a re-entry cannot
replace, and waived that gate for Maven's columns. Commit 3dad4ec69 removes
the gate prose here and at the three sites that asserted it: the migration's
ban-drop-column directive, maven_remote_encrypted_credentials.md, and the
per-slice rule in docs/dev/column-encryption.md, which now asks a slice to
record the basis for its premise rather than a boot-line reading specifically.
The waiver is Maven's alone. It rests on a claim about one table's data, so it is not a precedent for Container's columns (!2662 (merged)) or npm's (!2681 (merged)), and neither of those is a precedent for this one.
What still holds, and is not part of the waiver
The release ordering stands. The un-bind half has to reach each environment
before this drop merges, because it protects an in-flight Maven credential set,
replace or clear from 42703 on a pod still running the previous image. That is
a write-path failure rather than a data question, so no ruling about data waives
it.
The row count is still load-bearing, as a measurement rather than a control.
Measured 2026-09-03: .com carries no traffic and staging carries test traffic
only, so maven_remote_repositories holds no production row. That is why the
Up's ACCESS EXCLUSIVE hold is free, why the Down's validating scan is free,
and why the 30s bound is enough.
The two-release rule's reading half falls to this MR rather than to !2629 (merged): the statements that read the pair are the boot report's, and they retire here. So a pod still on the previous image runs that read after the Up applies and logs the report's error line once per boot for as long as such a pod runs, which an image rollback extends past the deploy: the binary exposes no rollback entrypoint, so rolling back restores a pod that still carries the report while the columns stay dropped. Boot proceeds, because the report's start hook returns nil whatever the read does, so the cost is one misleading could-not-measure line rather than an outage.
That boot books a metric as well as a line. instrumentQuery books
gitlab_artifact_registry_database_query_errors_total from a defer
(internal/datastore/queries.go:36-40), so the failed count increments that
family under the retiring query name once per boot.
docs/dev/observability.md reads it as the numerator
of a failure rate over database_queries_total{name}, and the new image
publishes neither series for that name, so for that name alone the ratio reads 1
until the last previous-image pod stops booting. Nothing selects either signal,
and not because this line is special: docs/dev/alerting.md records that no
alert rule lives in this repository at all.
What the waiver does not reach
An environment that migrates later than the 2026-09-03 measurement takes the Up on a premise nobody measured for it: a restored database, a new environment, a long-lived review app. With Maven's arm of the report gone nothing afterwards can name what it discarded, and the Down restores the columns without their values. The operator owns that residual.
No DO $$ guard was added in its place, and this MR's own tests are why: the
stays_inside_gooses_transaction subtest in
maven_remote_repositories_schema_integration_test.go hands the whole drop
migration to assertNoBatchingDoBlock, so a DO block in the Up reddens.
!2662 (merged) can carry one because container_remote_schema_integration_test.go passes
only its Down section to that assertion. The plain-SQL alternative, a
validating CHECK added and dropped inside the Up, trades a silent discard for
a restart-loop behind a readiness gate in exactly the environment nobody is
watching; maven_remote_encrypted_credentials.md records that cost in full.
The plan amendment is not in this MR, and where it went
The plan asserts the same reading this MR no longer takes, and correcting it is
a plan-file edit. That correction briefly rode this branch and has been moved
off it: !2687 (merged) (docs(plans): waive the leftover-plaintext reading for Maven and npm) carries Step 5a's Depends on, Step 5b's Acceptance and Step
11's Acceptance in one change, built from this branch's 3b4df7db2 text, so
nothing was re-authored. Commit f9061bf10 takes the plan file back out of this
diff, restored from this branch's merge base ba67132b4 rather than from
origin/main, whose tip has moved to f2cb550f9; restoring from main's tip
would have imported main's later edits to that file into this MR. git diff --name-only ba67132b4..HEAD -- docs/plans/ prints nothing.
So AGENTS.md guardrail 4 is satisfied here: no step MR edits the plan file, and
the plan file has one writer. !2633 (closed), an earlier docs(plans) amendment that
would have moved the reading to the merging engineer, was closed rather than
merged, so nothing waits on it either.
Nothing gates on !2687 (merged) and it gates nothing, and that is worth stating
rather than leaving a reader to find it. Guardrail 3's gate is the initial plan
MR, and docs/dev/agentic-development.md :235
treats a mid-flight amendment as a follow-up. So while !2687 (merged) is open, main can
carry this drop while the plan entry still states the reading this MR does not
take. That is a documentation disagreement rather than a merge block.
Open merge requests this branch conflicts with
Re-measured at this MR's own head f9061bf10 against origin/main
f2cb550f9, over the other 66 of the 67 merge requests open in the
project, every one of which publishes a refs/merge-requests/<iid>/head ref,
so nothing here is unchecked. Eleven share at least one path with this
branch's 39 changed files; that figure counts each MR's own diff, from its
merge base with its target to its head, rather than its distance from main's
tip. The 39 is this branch's own diff at f9061bf10 against its merge base
with origin/main, ba67132b4, and it is the same 39 ## Size counts; both
were re-derived at this head and checked against each other rather than
carried forward, and both fell by one file when the plan amendment moved to
!2687 (merged). 31 conflict against this head, and 25 of those
conflict with origin/main f2cb550f9 on their own as well, so they are not
this branch's to absorb. Of the six left, one does not target main: !2661 (merged)
targets 1150/step-15-parent-digests-by-child-ids and merges into its own
parent first. The method is the one the previous sweep used:
git merge-tree --write-tree --name-only --no-messages per MR head, run
twice, once against this head and once against origin/main, with no file
class excluded on either side.
Five are this branch's to absorb, and four of the five are the other open migrations, so their order is the asymmetric one the prefix section below states rather than "whichever merges second rebases".
| MR | Conflicting paths at f9061bf10 |
Shape | Order |
|---|---|---|---|
| !2681 (merged) | 10 of the 17 it shares: wire.go, wire_remote.go, wire_remote_test.go, column-encryption.md, logging.md, key-rotation.md, S13, credential_leftover_plaintext_test.go, credential_table.md, migrations_checksum_test.go |
npm's own drop, this MR's exact sibling one format over, no longer Draft | asymmetric, on 20260915085341; see the prefix section |
| !2579 (merged) | 1: migrations_checksum_test.go |
two migrations | asymmetric, on 20260914151144 |
| !2659 (merged) | 1: migrations_checksum_test.go |
two migrations | asymmetric, on 20260914171249 |
| !2682 (merged) | 1: migrations_checksum_test.go |
two migrations | asymmetric, on 20260915094500 |
| !2667 (merged) | 1: docs/roadmap/closed-beta.md |
both rewrite the S04 encryption row | no required order; whichever merges second rebases |
The two largest pairs, at these heads. This MR and !2681 (merged) (6f13a3321) meet at
merge base ba67132b4, share 17 paths and conflict on 10; the pair lost a
shared path when both branches moved their plan amendments off. This MR and
!2662 (merged) (1b92fcb96) meet at f9cd4b12f, share 18 paths and conflict on 15. !2662 (merged) is
the bigger conflict and it is still not in the table, because it conflicts
with origin/main f2cb550f9 on its own, on three paths:
docs/dev/column-encryption.md,
docs/runbooks/interim-plaintext-credential-drain.md and
docs/runbooks/key-rotation.md. The API agrees, has_conflicts: true and
merge_status: cannot_be_merged. Its merge base f9cd4b12f is 34 commits
behind this branch's ba67132b4, so part of its 15 is that staleness rather
than a real overlap with this MR, and the two cannot be told apart until
!2662 (merged) rebases. This branch and !2681 (merged) both merge cleanly into origin/main
f2cb550f9, and the API reports has_conflicts: false for both.
docs/dev/logging.md is new to the !2681 (merged) pair at this head, and it is a path
where neither branch's own text is right once the other merges. Both rewrite
the same line, the format row of the leftover-plaintext boot-report key
table: this branch sets it to container or npm, because it retires
Maven's arm, and !2681 (merged) sets it to container and maven, because it retires
npm's. leftoverPlaintextReaders at
cmd/artifact-registry/wire_remote.go:623 carries container, maven and npm at
the pair's merge base ba67132b4, this branch leaves container and npm, and
!2681 (merged) leaves container and maven, so container alone is what remains once
both have merged. Whichever merges second sets that cell to the arms that
actually remain after both, not to either branch's wording.
The subsection below records what the second rebase changed, so its per-MR
figures are the 2bbbfef9b ones it explains rather than this head's: it reads
nine conflicting paths, 16 shared and Draft for !2681 (merged), against 10, 17 and no
longer Draft here.
What the rebase changed in this section, and why
Three rows moved and the diff did not. git merge-tree merges from
merge-base(mine, theirs), and the rebase moved that base from f9cd4b12f to
ba67132b4, so a sibling's own conflict with main can surface in the pair
where it did not before, and a sibling can leave the pair once its own rebase
comes due. The headline counts moved for the same reason: this
description read 27 and 20 at b0f28ca0f, and 30 and 24 is what the same two
merge-tree runs give here.
- !2677 (merged) has merged, at 2026-09-15 11:51, and it is why this rebase was
owed. It is
675d0e8cbonmain, npm's un-bind, and it edited all four of the files that conflicted in the replay. The previous revision listed it as this branch's to absorb. - !2662 (merged) has left the set, and that supersedes a row in
## Corrections this revision makes. That row records the previous revision putting !2662 (merged) into the table after a review note showed it owedmainno rebase at9d7520783. Atba67132b4it does owe one: the API reportshas_conflicts: true, andmerge-tree --write-tree origin/main refs/merge-requests/2662/headexits non-zero. Its rebase is owed whatever this branch does, so it is not this branch's to absorb. It is still the largest overlap of any open MR, 18 shared paths and 15 conflicting against this head, and the two migrations' order is in the prefix section below. - !2681 (merged) has entered it, which supersedes the other half of the !2661 (merged)
correction row. That row says !2681 (merged)'s Draft status is what defers it. Draft
is why the previous table left it out; it entered this table because it
conflicted on nine paths at that basis and targets
main, and a then-Draft sibling sharing 16 paths was a merge order to state rather than one to defer. It is no longer Draft, and at this head the pair reads 10 conflicting and 17 shared. - !2682 (merged) has entered it. The previous revision named it as a fifth open
migration its sweep's prose did not cover. It now collides on
migrations_checksum_test.golike !2579 (merged) and !2659 (merged).
Overlapping and clean
Three more open MRs touch a file this branch touches and merge cleanly against
3b4df7db2, so none needs a merge order: !2622 (merged) on
docs/specs/S13-virtual-remote-foundation.md, !2665 (merged) on
internal/datastore/query_names.go, and !2666 (merged) on query_names.go plus
container_virtual_repositories_schema_integration_test.go.
The migration prefix, re-checked at this head
This branch's migration was renumbered in 6e0e27e07, replayed by the
rebase as 4124dd1d9, and that renumber is what the four migration orders
above turn on. It was 20260912104512, below five open prefixes.
internal/datastore/migrations/migrations.go:76 sets
goose.WithAllowOutofOrder(false), documented at :50, so goose rejects a
pending lower version even once a higher one has been applied: a release
shipping any of those five first would leave this branch's next boot failing
its migration pass, and cmd/artifact-registry/migration_runner.go:70 logs
"startup migrations failed; aborting boot" before :72 calls onFatal, which
:26 records as aborting the process for Kubernetes to restart and retry. It
is now 20260915130000, and knownHeadVersion at
internal/datastore/migrations/migrations_checksum_test.go:105 carries the
same value. The file body is byte-identical across the rename, the
squawk-ignore-file ban-drop-column directive on its first line included; git
records the rename at 100% similarity.
Re-derived at 3b4df7db2 by reading each open MR's own diff, across the other
63 of the 64 open merge requests, the other five open migrations are
!2579 (merged) 20260914151144, !2659 (merged) 20260914171249, !2681 (merged) 20260915085341, !2682 (merged)
20260915094500 and !2662 (merged) 20260915120000. origin/main's head is
20260911092233, the same at f2cb550f9 as at this branch's merge base
ba67132b4, because the four commits main gained in between added no
migration. 20260915130000 is above every one of them, so the renumber still
holds at this head.
The order is asymmetric, and it is the same shape against all five.
lint:migration-ordering (.gitlab-ci.yml:508) reads the target branch's
latest migration prefix and fails on any migration an MR adds whose prefix
is not strictly above it. Any of the five merging first leaves this branch's
prefix the higher one and owes it nothing. This MR merging first puts main's
head at 20260915130000, above all five of those prefixes, so each of them
goes red on that rule rather than merging and each owes its own renumber. That
arm is caught in CI rather than at boot. Nothing is asked of those authors
before this MR merges; what the asymmetry says is who pays after it does.
knownHeadVersion settles on this branch's value in either direction, because
20260915130000 is above all five; before the renumber it settled on theirs.
structure.sql is shared with the four migration MRs but merges cleanly, so
whichever merges second still re-runs mise run db:dump-structure to make the
dump match the migrations rather than the text merge.
Why the spec edits ride with this MR
docs/dev/conventions.md pairs docs/specs/ with a
spec MR and docs/dev/ with the implementation MR, and this feat MR carries
spec hunks in S04-a, S04-c, S13, S14 and S16. That is a deliberate
deviation: those sentences describe columns this migration removes, so splitting
them out lands main with specs that describe a schema that no longer exists.
The alternative considered and rejected was folding them into !2542 (merged), the cross-format spec amendment, which conflicted in all of the same files. It was rejected because !2542 (merged)'s scope is a ruling (the slices discard rather than backfill) that applies to all three formats, while these hunks record that Maven has now done it; a per-format completion note would have held !2542 (merged) un-mergeable until Maven's drop landed, inverting the dependency the plan declares. !2542 (merged) has since merged and this branch is rebased past it.
Size
2,543 reviewable LOC across 39 files, over the 500 ceiling
docs/dev/development-model.md sets, so here is
the split by file group. Re-derived at f9061bf10 against ba67132b4, which is
this branch's merge base after the second rebase and not origin/main's
tip, which has moved to f2cb550f9; every row below and the total were
recomputed rather than carried forward from 920d28f1f's 2,524, which is no
longer an ancestor of this branch, or from 2701782f7's 2,814, which included
the plan amendment now in !2687 (merged).
| Group | Added | Removed | Total |
|---|---|---|---|
| tests | 663 | 791 | 1454 |
| docs and sidecars | 430 | 231 | 661 |
| production Go | 31 | 99 | 130 |
structure.sql, regenerated |
0 | 195 | 195 |
| the migration | 103 | 0 | 103 |
| jet output, regenerated (excluded) | 2 | 10 | 12 |
The production-Go row fell from 289 to 130 and the tests row from 1,653 to 1,454, both for the same reason: the leftover-plaintext report and its suite are kept and narrowed rather than deleted, where the pre-rebase revision deleted them outright.
1,316 of the 2,543 lines are deletions. structure.sql and the jet files are
regenerated by mise run db:dump-structure and mise run db:jet-gen, so they
cannot be separated from the migration that causes them without leaving the tree
inconsistent between two MRs.
The rest of the hand-written work, named in full rather than gestured at, since the previous revision's inventory sentence omitted two separable pieces:
- the migration, 101 lines, and its schema assertions;
- the prose that stops describing columns this change removes, including the runbook arm;
- the bracket-helper extraction, 152 lines across
migrations/schema_helpers_test.go(48),npm_remote_schema_integration_test.go(51) andcontainer_virtual_repositories_schema_integration_test.go(53), two of which are suites for migrations this MR does not otherwise touch.
That extraction, 152 lines, is genuinely separable and could ship as a
refactor MR ahead of this one. It is kept here deliberately: it exists because
this MR's own Down-bounds subtest needed a second timeout bound, which is what
made the third copy of the walk worth removing, and the stack it would be added
to has no other member left to merge behind.
The previous revision listed a second separable piece here, a 55-line
credentialColumnExpr move into container_remote_credentials.go. The rebase
dropped it: that helper no longer exists on main, because step 8's first half
stopped the container update statement naming an interim column at all.
Tests
TestMavenRemoteRepositoriesSchema_InterimPlaintextCredentialsAreDropped counts
that neither column survives on the parent or any of the 64 partitions, that no
constraint answers to the pair's CHECK name, and, as the positive control, that
the encrypted unit's CHECK still answers on all 65 relations.
TestMavenRemoteRepositoriesSchema_PlaintextDropMigrationShape reads the file as
text, because the catalog after the whole chain cannot say what the Down
restores. Its seven subtests pin, between them: that the Up drops both columns
and names nothing of the encrypted unit; the columns the Down restores, their
text type and the guard's NULL-based predicate; both timeout bounds and the
RESET order; the goose transaction; the version ordering against the validate
migration; and the absence of the greenfield rationale.
TestMigrations_UpDownUp executes the Down green.
Two assertions are new this revision, both closing gaps where the description argued a property and nothing pinned it. Measured, with the migration broken in place one mutation at a time and reverted byte-identical afterwards:
| Mutation | Result |
|---|---|
split the Up into two ALTER TABLEs |
reddens: "Up must carry exactly one ALTER TABLE" |
add CASCADE to a DROP COLUMN |
reddens: "Up must not name CASCADE" |
CASCADE is the one worth having: DROP COLUMN already takes the pair's CHECK,
so a CASCADE there could only remove some other dependent object, silently.
The bracket subtest matches both adding forms rather than ADD COLUMN alone,
and asserts each bound separately. Also measured by mutation:
| Mutation | Result |
|---|---|
move the ADD CONSTRAINT into its own ALTER TABLE after both RESETs |
reddens: "must carry 1 statements for this assertion to bracket; got 2" |
remove the statement_timeout bracket |
reddens: "must carry exactly one SET statement_timeout = '30s'" |
swap the two RESETs |
reddens: "must reset statement_timeout before lock_timeout" |
Matching ADD COLUMN alone passed the first of those green, with the
constraint's validating scan then unbounded, which is why the predicate was
widened. Its lock acquisitions were never the exposure: inside goose's one
transaction the ALTER above already holds them.
The RESET-order assertion is honest about what it is worth, which the previous
revision was not. Two independent session GUCs reset in any order, and an
aborted transaction discards both SETs regardless, so no ordering of the two
RESETs changes behaviour: the assertion pins the order the file states, not a
property of the database. Both the file's comment and the failure message also
said "mirroring the order they were set", which is the inverse of what happens,
since the resets run in the reverse of the set order. Both now say so.
TestLeftoverPlaintextBuilders_RunOverContainerRows runs both surviving builders
over container_remote_repositories, which still carries the interim pair. What
it uniquely proves is real-database execution and the count diverging from the
capped page at 13 planted rows, a figure derived from
LeftoverPlaintextSampleLimit + 3 rather than hardcoded; dropping the LIMIT
returns all 13 and dropping idCol collapses the page to a single row there.
Neither mutation is exclusive to it: both also redden
TestSelectLeftoverPlaintextPageStmt in the DB-free suite, which asserts LIMIT
and the bound 10 and projects container_remote_repositories.id. Removing the
builder's AS("count") reddens nothing, because PostgreSQL already names a bare
COUNT column count, so no assertion claims to pin the alias.
The previous revision described a TestLeftoverPlaintextLogContract here,
pinning three exported message constants and three field keys against a rebuilt
emitter that did not exist yet. The rebase deletes all of it. That emitter
exists on main now, rebuilt over all three formats with its own unexported
logKeyLeftoverPlaintext* keys and its own suite in
cmd/artifact-registry/wire_remote_test.go, so the exported constants would be
dead duplicates and a two-site value pin of them would be worth strictly less
than the tests already on main.
What this MR does to that suite instead is narrow it from three formats to two:
the nil-reader table drops its Maven row, ShareOneReadBudget asserts a half
share over two slots rather than a third over three, CarriesEveryFormat and
HandsOverEveryReport expect two component names, StarvedSlotNamesTheWindow
starves npm behind container, and the arms that drove the free function with the
literal "maven" drive it with "container". The seam anchor drops
*datastore.RemoteCredentialStore, which no longer carries the method.
TestMavenRemoteRepositoryWritesCarryNoPlaintextCredential's anonymous-create
control gained a real assertion. It had been reduced to
require.NotEmpty(tt, query), which any non-empty SQL satisfies, so it no longer
distinguished an anonymous create from anything else.
The obvious strengthening was wrong and is worth recording, because it reads
right: asserting the rendered INSERT names none of the five unit columns
fails on a correct statement. Measured, not assumed. The builder renders one
fixed column list whatever the input, so an anonymous create names all five and
binds them NULL::bytea, NULL::uuid, NULL::integer, NULL::bytea, NULL::bytea.
The property this arm actually owes is a uniformly NULL unit, and that is what is
now asserted. Falsifiable: half-setting the nil arm of
mavenCredentialUnitBindsFor in a scratch copy reddens it with
"must bind the whole unit NULL, so no arm leaves it half-set".
golangci-lint 2.13.2 run --build-tags=integration --max-same-issues=0 --max-issues-per-linter=0 --uniq-by-line=false over ./internal/datastore/...
and ./cmd/artifact-registry/..., with a per-run GOLANGCI_LINT_CACHE, reports
no finding on any of the 689 Go lines this branch adds or edits, across the 23 .go files it touches, measured by
intersecting each finding's line number with the diff hunks. The tagged baseline
in those trees is 5,464 pre-existing findings, re-derived at b0f28ca0f;
the previous revision's 5,579 was measured at d6955e3db, before the rebase.
The intersection rather than the count is what was checked. Because that
intersection is line-local it cannot see dupl or maintidx, so those two were
checked separately: neither reports on any file this MR touches.
No e2e scenario is added or affected, and the reason the previous revision gave
was not the operative one. docs/testing/ does carry upstream-credential
scenarios: e2e.oci.remote.credential-never-served
(oci.md:151) and
e2e.npm.remote.credential-never-served (npm.md:207).
The reasons that hold are that
maven.md:32 puts Maven remote outside its scope
entirely ("Virtual and remote (proxy/cache) Maven repositories — owned by S30 and
S14"), and that those two surviving credential scenarios are npm's and OCI's,
whose columns this MR does not touch.
ADR conformance
Checked against the local mirror, whose freshness script reports one unsynced
upstream commit touching 009_api_design.md only. This MR makes no API change,
so that drift does not bear on it. Dropping the pair moves
maven_remote_repositories onto exactly
ADR-007's published column list: the 13
columns it publishes are all present and none is missing, leaving
snapshot_metadata_always_revalidate as the single divergence, which S14's data
model declares and owns. One internal ADR never mirrors locally and is
unverified from here.
Corrections this revision makes
Rationales that read as verified but did not operate, fixed at every site each one lived at rather than only where it was found.
| Claim | Why it did not operate | Sites fixed |
|---|---|---|
merge-tree against !2629 (merged) is clean, on its tip 1ca569ea9 |
the tip moved three commits the same day; GitLab reported has_conflicts: true, and the compile break was in an auto-merged hunk outside the markers |
this description, and the rebase itself |
the count "cannot fall on its own", the UPDATE being "the only thing that moves it" |
the parent repository's reap removes the whole row through ON DELETE CASCADE, which the same section says two paragraphs down |
column-encryption.md, porting !2629 (merged)'s own correction |
| the rollback window runs to "this MR merging" | the Up applies at pod boot, so a rollback between merge and rollout is inside the window, not after it | the .sql, the sidecar, this description; column-encryption.md was already right and is the reference wording |
| the Up's five-minute exposure is one pod's | upTimeout equals goose's 300-attempt advisory-lock budget exactly, so every pod booting in that rollout exhausts its own and exits 1 |
the .sql, this description |
| the previous image's read costs one log line | instrumentQuery books ..._database_query_errors_total from a defer too, and the new image publishes neither series for that name |
the .sql, the sidecar, this description |
| "No alert selects that line" | true, but because alerting.md records that no alert rule lives in this repository at all, which is also what makes the human reading the only control |
the sidecar, this description |
| a panicking read "unwinds into the critical-component path" | LabKit's app/app.go carries no recover() anywhere, and assembleApp's covers composition rather than Start, so the process dies without the reverse-order shutdown |
column-encryption.md |
| the message contract is "held by constants in the tree with a test" | with no emitter, the test is a two-site value pin and cannot bind a rebuild to the constants | column-encryption.md, this description |
RESET order "mirroring the order they were set" |
the resets run in the reverse of the set order, and resetting two independent GUCs pins no behaviour at all | the .sql, a test failure message |
leftoverPlaintextReport.Start named as current code |
the symbol is deleted by this commit and exists nowhere else; its three sibling statements carry a "gone from the tree" note and it did not | the .sql |
| "two of those conflicts are new since the previous revision" | both files were already in the changed set at 91289787b, and the S04-c blob was byte-identical between the two revisions |
this description |
| !2629 (merged)'s table "reads 2,443 for this MR" | it reads 2,597, measured at the basis it states, and 296 for itself; the declined finding rested on a premise !2629 (merged) had already fixed | this description |
the e2e exemption rests on docs/testing/ covering only GitLab-authenticated flows |
it carries two upstream-credential scenarios; the operative reasons are Maven remote being out of maven.md's scope and those two being npm's and OCI's |
this description |
| the anonymous create "writes no plaintext column", strengthened to "names no unit column" | the builder renders one fixed column list, so a correct anonymous create names all five and binds them NULL; the absence assertion fails on correct code | maven_remote_repositories_writes_test.go |
| 2,747 LOC, 5,595 lint baseline, 259 for !2629 (merged) | all three were measured before the base moved; re-derived at d6955e3db as 2,841, 5,579 and 296 |
this description |
!2662 (merged) excluded from the conflict table because it "conflicts with origin/main too" |
merge-tree --write-tree origin/main refs/merge-requests/2662/head exits 0 at 9d7520783, has_conflicts is false, and its merge base is f9cd4b12f, the same one this branch has; the conflict is this branch's alone, and it falls on 13 paths rather than 17 |
this description |
| !2661 (merged) listed among the conflicts this branch absorbs | it targets 1150/step-15-parent-digests-by-child-ids, not main, so it merges into its own parent first and never meets this branch there. The row's closing clause put !2681 (merged) out of the table for that same reason and is itself wrong: !2681 (merged) targets main, and its Draft status is what defers it. Corrected under the renumber above |
this description |
nil leftoverCount and leftoverPage "leaves it out of assertCredentialTableExplainLeftover" |
the helper calls both unconditionally at credential_table_integration_test.go:817-818, so nil fields panic rather than opt a fixture out; what leaves Maven out is its deleted test |
the fixture comment, and credential_table.md at both its sites |
20260912104512 "stays lexically last only until one of them merges with a later prefix" |
!2579 (merged) and !2659 (merged) already carried later prefixes, 20260914151144 and 20260914171249, so it was not lexically last and the knownHeadVersion resolution ran the other way whenever either of them merged first. What that left unfixed was the boot hazard rather than the staleness, and 6e0e27e07 renumbers the file to 20260915130000 to close it |
this description, the .sql file name and knownHeadVersion |
Two merged S04-C sentences that this drop falsifies are corrected here rather
than left to no owner: :31's status cell, and :73, which said the
tmp_plaintext_* columns are still where Container's and npm's credentials are
read from. Review round 3 corrected :73 again: no format is in that posture,
all three read the encrypted unit, and what is left is each table's own column
drop.
Decisions recorded
- The runbook arm is retired here, not in !2629 (merged). !2629 (merged) introduced the arm and
merges first, so it is the cheaper place to edit. It is done here because this
MR is what falsifies the arm: !2629 (merged) deliberately wrote it as live for one
release, and deleting it there would make that MR describe a state it does not
create. The cost of this choice is that
maincarries the arm for exactly the one release between the two merges, which is the release in which it is true. - The 207 lines of separable refactor stay in this MR. Reasoning in
## Size. - The drop gate stays a log line, not a gauge. A gauge would be durable, queryable and alertable, which a line read once by a human is not. It is still a line because the reading is a one-time precondition per environment on an irreversible migration, so a gauge that must read zero before each of two remaining drops and then be deleted adds a collector and an alert surface for a signal with three total reads. Written into docs/dev/column-encryption.md so steps 7 and 10 inherit the choice instead of re-deciding it.
- The six log-contract constants are exported rather than moved with their
emitter. Both rebuilds register their emitter from package
main, which the plan fixes atcmd/artifact-registry/wire_remote.gofor steps 7 and 10, so unexported constants ininternal/datastorecould only be retyped there, not bound: the recorded mechanism would not have operated. Exporting them is the smaller of the two fixes and restores what the sample cap already had. The alternative, moving the emitting function intointernal/datastoresocmdonly registers it, would put aslogcall in the datastore package and split the component from its registration; it stays open for step 7, and it is also the cheapest thing that would turn the two-site value pin into a real contract. - The report component is rebuilt per format rather than kept and registered
for container now. Keeping it would arm container's gate earlier and keep the
message contract compile-enforced, but registering it is scope this step did
not plan. What holds the contract instead is weaker, and
## Testssays how much weaker.
Review round 2, 2026-09-13
A second branch review raised 2 blocking-tier, 8 warning-tier and 13 lower-tier findings. All are addressed; one was rejected on verification. What changed in the tree:
| Fix | Where |
|---|---|
Rebased onto !2629 (merged)'s live tip 3cac52f7b, resolving a compile break that sat in an auto-merged hunk rather than in the conflict markers |
maven_remote_repositories_update_integration_test.go |
| Ported !2629 (merged)'s reap correction, which this MR's own rewrite of the paragraph had reintroduced | docs/dev/column-encryption.md |
| Retired step 4's Maven arm, which pointed a responder at dropped columns, a dropped CHECK and an unemitted log line | docs/runbooks/key-rotation.md |
| The gate's rollback window now ends when the Up applies in that environment, not at merge | the .sql, the sidecar, this description |
The Up's lock exposure is stated as the whole booting ReplicaSet, with the upTimeout-equals-retry-budget mechanism and a named precondition owner |
the .sql, this description |
| The previous image's per-boot cost names the query-error metric as well as the line, and says why nothing selects either | the .sql, the sidecar, this description |
| The panic path is LabKit's uncaught one, not the critical-component path | docs/dev/column-encryption.md |
| The log-contract claim is stated as a two-site value pin | docs/dev/column-encryption.md, this description |
Two new Up-shape subtests, one ALTER TABLE and no CASCADE, both mutation-checked |
maven_remote_repositories_schema_integration_test.go |
The RESET-order pin says it pins the file's order and not a behaviour, and both sites drop the inverted "mirroring" clause |
the .sql, the same test |
| The anonymous-create control asserts a uniformly NULL unit, after the obvious absence assertion was measured to fail on correct code | maven_remote_repositories_writes_test.go |
leftoverPlaintextReport.Start carries the same "gone from the tree" note its three sibling statements already had |
the .sql |
Full path to docs/dev/column-encryption.md, in a package holding 21 sidecar .md files and no such file |
credential_leftover_plaintext.go |
| Both roadmap figures that moved to a 2026-09-13 basis are dated inline, under a header that pins every figure to 2026-09-06 | docs/roadmap/closed-beta.md:453, :537 |
The plan's Step 5 Acceptance reversal is declared; the amendment is on main, and !2633 (closed), the MR named for it in earlier revisions, was closed rather than merged |
this description |
| The withdrawn "two new conflicts" paragraph, the stale !2629 (merged) figures, the eight unnamed clean overlaps, the short LOC inventory and the non-operative e2e reason | this description |
One finding was rejected, and the reason matters because it reads
convincing. The review held that column-encryption.md's remedy for a non-zero
gate reading is destructive: nulling the pair erases the only copy of a
credential and leaves those repositories serving anonymous upstream requests.
The premise does not hold. column-encryption.md states that a row holding only
the interim pair "reads as the anonymous pair, with no error, because those
values are discarded rather than backfilled, so the read cannot use them and
has_credentials reads false". The row is already anonymous before any
nulling, so the UPDATE erases a value the service cannot use and changes no
behaviour. The discard is the ratified posture, which is !2542 (merged)'s whole ruling,
and the section already tells the operator to re-enter the credential. What did
survive from that finding is the narrower point about the log contract, above.
Measured after the rebase at b0f28ca0f, not carried forward: the full
pre-commit chain is green over origin/main...HEAD (go test,
golangci-lint, comment caps, squawk, pgFormatter, markdownlint,
lychee, vale, gitleaks, gitlint); go vet -tags=integration ./... is
clean; scripts/ci/check-comment-caps.sh --base origin/main passes; and the
whole ./internal/datastore/migrations/... package passes with
-tags=integration on PostgreSQL 16, unfiltered, which covers
TestMigrations_UpDownUp, TestMigrations_Checksum,
TestMavenRemoteRepositoriesSchema_PlaintextDropMigrationShape (all seven
subtests), TestMavenRemoteRepositoriesSchema_InterimPlaintextCredentialsAreDropped,
TestMavenRemoteRepositoriesSchema_Columns and
TestContainerVirtualRepositoriesSchema_DownBoundsLockWaits. On the
./internal/datastore side, TestLeftoverPlaintextBuilders_RunOverContainerRows,
TestMavenRemoteCredentialTable_Statements_CarryNoCredentialCiphertext,
TestMavenRemoteCredentialTable_UpdateRowKey,
TestMavenRemoteCredentialTable_UpdateRowKey_GuardAndWriteAreOneStatement,
TestContainerRemoteCredentialStore_LeftoverPlaintextCredentials and
TestContainerRemoteCredentialStore_LeftoverPlaintextCredentialsReport all pass.
cmd/artifact-registry's unit suite passes apart from the three
TestWireStorage_CloudCDN* tests, which need Application Default Credentials
and fail identically on origin/main on this machine.
Local PostgreSQL is 16, so that clears one of CI's three legs (16, 17, 18).
The falsification pass on the four new or changed assertions was done before the
rebase and not repeated, on a checked rather than assumed basis:
maven_remote_repositories_schema_integration_test.go is byte-identical between
d6955e3db and b0f28ca0f, and the migration's only change across the rebase is
one line of squawk-ignore-file prose. Its Up, Down and every SET,
RESET and ALTER TABLE in them are byte-identical, so the assertions read the
same statements they were falsified against.
Review round 3, 2026-09-15
A third branch review raised 7 blocking-tier findings, all of one class: prose sitting within a few lines of a sentence this branch rewrote correctly, still describing the pre-drop arrangement. All seven are fixed below. The seventh went to the operator first, because it decides a contract rather than correcting a statement; the ruling was to correct the prose and add no nil guard.
| Fix | Where |
|---|---|
npm has taken neither the code-only release nor the drop. Four statements said otherwise, two erring in each direction, while S13 :88-90, which this branch adds, already carried the correct three-way split |
credential_table.md, S04-a, closed-beta.md :76 and :454, credential_leftover_plaintext.go; plus S04-c:73 and S13's per-format credentials bullet, from the wider sweep |
| Three lines beside a rewritten one still described a live Maven leftover set, Maven columns awaiting a drop, and a container credential store shared with another consumer | docs/dev/column-encryption.md:140, S13:1243, cmd/artifact-registry/wire.go:499 |
The leftover-plaintext boot report books four query names, not six, and format=maven is no longer a value any boot can emit. Both files sat outside the diff |
docs/dev/observability.md, docs/dev/logging.md |
The runbook's collapsed Maven arm and its RETURNING reading both read as erasure. Worded together: no SQL reaches those values any more, and unreachability is not erasure |
docs/runbooks/key-rotation.md :360 and :393 |
The two-release image rollback, which re-enables both Maven write binds against dropped columns, is named with its cost (42703 on credential writes and credential-carrying URL changes) and its recovery. The upTimeout cap now carries what reaching it costs |
the .sql directives and its Down comment, maven_remote_encrypted_credentials.md |
| The table-scoping subtest read the container baseline after planting the npm row, so the assertion held whatever the container predicate did | container_remote_credentials_encryption_integration_test.go |
assertCredentialTableExplainLeftover has one caller, not two, and it calls fx.leftoverCount() and fx.leftoverPage() unconditionally, so nil fields panic instead of opting a fixture out. Maven is out of the helper because its test is deleted. Operator ruling: correct the prose, add no guard |
credential_table_integration_test.go :330-331, and credential_table.md at both its sites, the fixture seam's "called exactly twice" and the interim-plaintext section |
The two docs/dev/ files are what take the changed-file count from 37 to 39.
Between them they are 17 lines of the docs row, not the whole move from the
previous revision's 554: round 3's other prose is most of the rest, and 35
further lines arrived with the seventh finding's fix.
Residual work this MR does not close
Recorded here because a squash keeps no commit body.
| Residual | Owner |
|---|---|
Container's and npm's interim columns, still declared. Neither is the live credential store any more: both formats read and write the encrypted unit, no container statement names its pair, and npm's credential write binds its column NULL. What still reads them is each format's own leftover-plaintext predicate, feeding the boot-report arms this MR leaves standing |
Steps 8 and 11 of the plan, under #417 (closed); !2662 (merged) is step 8's drop |
Nothing. The row that stood here said the boot report's component, seam and read bound were deleted and would be rebuilt at steps 7 and 10. Steps 7, 8 and 10 have merged, so the report exists and this MR removes only Maven's arm of it. LeftoverPlaintextReport and LeftoverPlaintextRow are not deleted either; they move into credential_leftover_plaintext.go because container and npm depend on them. The two query_names.go label values are Maven's own and do go |
— |
Confirming no long-running transaction is open on maven_remote_repositories when the Up runs |
The engineer merging this MR. No test can stand in for it |
| The Up's accumulated-wait exposure, which #548 does not cover | Unowned and tracked by no issue. Named in the .sql directive, now with what reaching upTimeout costs; re-evaluate before the container and npm drops, which run the same shape on populated tables |
| An environment that migrates later than the 2026-09-03 measurement | Accepted per format, not closed here; the cost of closing it in SQL is in maven_remote_encrypted_credentials.md |
assertCredentialTableExplainLeftover still calls fx.leftoverCount() and fx.leftoverPage() unconditionally, and npm's test is now its only caller. Nilling npm's two fields without deleting that caller is a nil-func panic |
Step 11, npm's column drop, which has to delete the caller and the helper with it. Named in credential_table.md. Step 8, Container's drop, does not inherit it: Container has no credentialTableFixture and no ExplainLeftoverPlaintext case |
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 |
|---|---|---|---|
20260915130000_drop_maven_remote_plaintext_credentials.sql |
OK (14.59ms / 49.29ms) | OK (15.93ms / 51.64ms) | OK (36.07ms / 37.04ms) |
Read from the three jobs' migration-review.log artifacts on this MR's head
pipeline, 2849947478,
a merge_request_event pipeline over b0f28ca0f. The Re-Up phase is
excluded, per the doc section linked above.
Migration notes:
- The three timings were collected under this file's previous name,
20260912104512_drop_maven_remote_plaintext_credentials.sql, on theb0f28ca0fpipeline named above.6e0e27e07renames the file and changes nothing else in it: the blob is byte-identical, so the figures still describe this migration and no re-collection is owed. - Rollback is slower than apply on PG 16 (49.29ms against 14.59ms) and PG 17
(51.64ms against 15.93ms), and level on PG 18 (37.04ms against 36.07ms).
That is the shape the two halves have rather than an anomaly: the
Upis a catalog update and rewrites no heap, while theDownadds two columns and a validatingCHECKwhose scan covers the parent and each of its 64 partitions. Both figures come from the zero row count theban-drop-columndirective states, which is also what theDown'sstatement_timeout = '30s'is sized on; against a populated table that bound has to be raised before rolling back, and the.sqlfile says so in place. - PG 18's apply, 36.07ms, is 2.26x PG 17's 15.93ms, which crosses the
version-regression threshold this evidence is checked against. It is job
spread rather than anything this migration does. Across all 107 migrations
in the same three logs, PG 18's mean apply is 305.28ms against PG 16's
536.41ms and PG 17's 327.43ms, so PG 18 is the faster job overall; and four
migrations already on
mainshow a higher PG 18 / PG 17 apply ratio than this one in those same logs, up to 15.05x on20260904170100_validate_maven_remote_enc_credentials_check.sql. At 36ms on an empty database the gap sits under the noise floor of a catalog update this small.
Queries
No query-producing method to analyze, so no PostgreSQL container was started
and no plan was collected. Both changed files that dispatch a statement
changed in ways that render no new SQL, measured against this branch's merge
base f9cd4b12f:
internal/datastore/credential_leftover_plaintext.go(+23 / -2): every line fromfunc leftoverPlaintextCredentialsto end of file is byte-identical to the merge base, which covers bothinstrumentQuerydispatch sites and thecountLeftoverPlaintextStmtandselectLeftoverPlaintextPageStmtbuilders. What changed is the header comment, theuuidimport, andLeftoverPlaintextSampleLimit,LeftoverPlaintextRowandLeftoverPlaintextReportarriving frommaven_remote_credentials.go.internal/datastore/maven_remote_credentials.go(+4 / -88): two comment edits, and the deletion of Maven's leftover-plaintext arm together withcountMavenRemoteLeftoverPlaintextStmtandselectMavenRemoteLeftoverPlaintextPageStmt. A deleted statement has no plan to collect.
A statement can also start rendering differently without its own file
changing, so that case was checked rather than assumed. The regenerated
table/maven_remote_repositories.go drops tmp_plaintext_username and
tmp_plaintext_password from both allColumns and mutableColumns, and any
chain taking either list would render a shorter column list at this head. No
chain takes either one: a grep for AllColumns and MutableColumns across
internal/ and cmd/ at b0f28ca0f, excluding _test.go and
internal/datastore/jet/, returns no code hit, only prose in comments. Every
Maven remote statement spells its columns out, so no unchanged statement
renders differently here.
Related to #417 (closed)