fix: default personal_access_token.version and .type - #2166
Merged
Conversation
V1_72 added both columns as NOT NULL without a default, so every INSERT into the table now has to name them. The application always does - PersonalAccessToken has no @DynamicInsert, so Hibernate lists every column on every insert, and a code path that forgets setType still fails loudly on the constraint - but hand-written SQL has to know about two columns it previously didn't. The bootstrap procedure documented for registries without a login provider (deploy/kubernetes/README.md, and the wiki) fails outright on a current schema. Default them to the values V1_72's own backfill would have left behind, so an INSERT written against the older schema still produces a working token. version 0 is deliberately the *oldest* format rather than the current one: the hash pepper lives in the application configuration and never reaches the database, so a value inserted by hand cannot be anything but plaintext, and useAccessToken hashes such a row in place on first use. import-db-dump.sh keyed its "column the dump can't supply" detection on "NOT NULL and no default", which these two columns now stop matching - and with it the backfill that classifies pre-V1_72 one-time tokens as OTT by description, silently importing them as long-lived LLT tokens instead. Split the two things it was conflating: every target column the dump lacks may carry a backfill, and only the ones that cannot be imported at all are required to have one and get their NOT NULL dropped around the copy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
V1_72 extended personal_access_token.value to 128 characters - the digest of whichever algorithm ovsx.access-token.token-hash-algorithm names need not be SHA-256's 64 hex characters - but the entity kept declaring 64. Nothing reads it today under ddl-auto: none; it is simply wrong about the schema it maps. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
There are a couple of concrete correctness/robustness issues in the new migration/script comments/parsing that should be adjusted before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR addresses schema compatibility and operational tooling around personal_access_token after V1_72 introduced version and type as NOT NULL without defaults, which breaks older/manual INSERT workflows and the documented bootstrap procedure.
Changes:
- Adds Flyway migration V1_74 to default
personal_access_token.versionto0andpersonal_access_token.typetoLLTto restore compatibility for INSERTs that omit these newer columns. - Updates
scripts/import-db-dump.shgap detection to distinguish (1) columns that are truly un-importable without a backfill (NOT NULL + no default) from (2) columns that still need a backfill to preserve semantics even if a default exists.
File summaries
| File | Description |
|---|---|
server/src/main/resources/db/migration/V1_74__PersonalAccessToken_Column_Defaults.sql |
Sets DB-level defaults for personal_access_token.version and .type to keep manual/older INSERTs working post-V1_72. |
scripts/import-db-dump.sh |
Refines dump import gap detection to run backfills for semantic correctness while only dropping NOT NULL when strictly required. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Both metadata queries ended in `| grep -v '^$' || true`, stripping the blank line psql's tuples-only output leaves behind. grep exits 1 when it matches nothing, which under pipefail is indistinguishable from psql itself having failed, so the `|| true` that tolerated an empty result swallowed a psql that couldn't reach the target along with it - and the checks concluded that nothing had been dropped and nothing needed backfilling. sed exits 0 either way, so an unreachable target now aborts the run before anything is touched. Also -X on the target psql: the first branch runs the caller's own psql when one is installed, and a ~/.psqlrc that turns on \timing, or echoes anything, would be parsed as schema metadata. And in V1_74, don't claim the defaults reproduce V1_72's backfill exactly: that backfill also derived type = 'OTT' from the description of the one-time publish tokens the server used to create. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Problem
V1_72__Trusted_Publisher.sqladdedpersonal_access_token.versionand.typeasNOT NULLwith no default, so every INSERT into the table now has to name them. The bootstrap procedure documented for registries deployed without a login provider —deploy/kubernetes/README.md:197, and the wiki's Deleting Extensions page — fails outright on a current schema:The dev seed in
server/src/dev/resources/db/migration/V1_0_1__Super_user.sqlonly still works because Flyway runs it before V1_72, which then backfills it.Change
V1_74defaults the two columns to exactly what V1_72's own backfill would have left behind, so an INSERT written against the pre-V1_72 schema still produces a working token.versiondefaults to0— deliberately the oldest format, not the current one. The hash pepper lives in the application configuration and never reaches the database, so a value inserted by hand cannot be anything but plaintext;AccessTokenService#useAccessTokenfalls back to a plaintext lookup and hashes the row in place on first use, and the startup upgrade job does the same for tokens that are never used. The migration comment spells out that this must stay pinned at0rather than trackingTOKEN_CURRENT_VERSION, and that version 0 has to keep being understood byupgradeTokenfor as long as the default stands — raising it would make such a row read as an already-hashed value and silently never authenticate.typedefaults toLLT, the only type an operator would create by hand;OTTandTPTare issued by the server and carry state an INSERT omitting the type wouldn't be supplying either.This can't mask a bug in the application's own inserts.
PersonalAccessTokenhas@DynamicUpdatebut not@DynamicInsert, so Hibernate lists every column on every insert — a code path that forgetssetTypestill sends an explicit NULL and still fails on the constraint. The defaults are only ever reachable from hand-written SQL that omits the columns.import-db-dump.shThe script's gap detection keyed on
is_nullable='NO' and column_default is null, which these two columns now stop matching — and with themKNOWN_BACKFILLS["personal_access_token.type"], the backfill that classifies pre-V1_72 one-time tokens asOTTby description. Without it they'd import asLLT, quietly turning a used-once credential into a permanent one.Split the two things that check was conflating:
\copy;NOT NULL, no default) are required to have one, and only those get theirNOT NULLdropped around the copy.Testing
Exercised the rewritten detection against a scratch Postgres in three shapes:
REQUIRED_GAPSBACKFILL_COLUMNSUNKNOWN_GAPSversion,typeversion,typeversion,typeNOT NULLcolumnbrand_newversion,typebrand_new(refused)Newly-gained nullable columns (
scope_extension_id,claims) are correctly ignored, and an older target still behaves exactly as before. Also ran the simulated import — dump-era column list, then the registered backfill — and confirmed aOne time use publish tokenrow still lands asOTTwhile the rest default toLLTatversion = 0, and appliedV1_74itself against a post-V1_72 table.Stale entity mapping
V1_72 also widened
personal_access_token.valueto 128 characters — the digest of whichever algorithmovsx.access-token.token-hash-algorithmnames need not be SHA-256's 64 hex characters — whilePersonalAccessTokenkept declaring@Column(length = 64). Nothing reads it underddl-auto: none; it was just wrong about the schema it maps. Corrected, with a note on where the number comes from.Wiki
The Deleting Extensions page documents the required columns in detail and needs the matching edit; that's prepared separately and not part of this PR.
🤖 Generated with Claude Code