PoC: true in-place major version upgrades
Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.
Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:
git clone --branch t254296_1 https://github.com/hackorum-dev/postgres.gitIn a checkout you already have, add the fork once:
git remote add hackorum https://github.com/hackorum-dev/postgres.gitthen, for this patchset and every later one:
git fetch hackorum t254296_1 && git checkout t254296_1Patchset v1 (message #1) is on t254296_1
pg_upgrade has worked for many years, but it still has well-known
limitations, in particular:
- Does not scale with big schemas, leading to hours of downtime
- No PITR across major versions
I think it's time for some innovation. The attached series is a
"tracer bullet", _rough_, proof of concept of in-place upgrade. With
the exception of 0001/2, the patches are not quite ready for code
review, but it's far enough along to demonstrate and get feedback on
architecture and orchestration. An earlier prototype showed this
should be much faster for big-schema databases, but it's not fair to
measure yet since it's not fully functional.
I have fleshed out a start-to-finish demo in the attached v1-demo.txt.
To give you a preview, the orchestration basically goes like this
(prepare, swap, golive):
$NEW/pg_inplace_upgrade -D $DEMO/primary -b $OLD -B $NEW \
--standby 'host=/tmp port=6001 dbname=postgres' prepare
$NEW/pg_inplace_upgrade -D $DEMO/primary -b $OLD -B $NEW -l
$DEMO/primary.log swap
$NEW/pg_inplace_upgrade -D $DEMO/primary -b $OLD -B $NEW golive
$NEW/pg_inplace_upgrade -D $DEMO/standby -b $OLD -B $NEW -l
$DEMO/standby.log swap
Here's what it does in a nutshell:
The 'prepare' stage runs some checks, freezes the catalogs, and shuts
down. The shutdown checkpoint is flagged as an upgrade boundary. The
standby will replay up to the primary's shutdown checkpoint and pause.
The 'swap' stages start the new binary. On the standby, it first shuts
down the old binary. On the primary, the startup process 'takes over'
the server by converting pg_control to the new format and writing a
start checkpoint. In a new 'upgrade' mode, a postmaster child migrates
the catalogs. (In the PoC, the catalog layout and data don't actually
change, so this simply rewrites the old catalogs' rows unchanged into
new files. It builds new indexes, and arranges for the relmapper and
pg_class to find the new files.) The catalog writes are WAL-logged,
but WAL archiving, sending, and recycling are suppressed for the
duration.
Up until 'golive', rollback is possible. This entails unlinking newly
created files (including catalog relations and pg_xact segments) and
restoring e.g. the control file and relmapper.
The 'golive' stage writes a "go-live" checkpoint, and resumes normal
WAL streaming and archiving. Rollback is now impossible.
The 'swap' stage on the standby allows it to replay the new-version
WAL generated during the migration, so the standby's catalog migration
is just applying normal FPIs. After replay passes the go-live
checkpoint, the standby can accept connections again.
*** Catalogs (TODO)
While the PoC sidesteps this entirely for now, this is actually how I
started. About a year and half ago, I experimented with catalog header
annotations so that the usual couple hundred lines of Perl would
generate the code that transforms the catalog tuples. Earlier this
year, I turned that into a throwaway "offline upgrade" (i.e. no WAL)
prototype. That exercise indicated that this method could feasibly
handle all catalog schema changes since PG12. Example from pg_class:
```
/* # of tuples (not always up-to-date; -1 means "unknown") */
float4 reltuples BKI_DEFAULT(-1) CHANGEDVALUE(14,
FUNC(reltuples_zero_to_unknown));
[...]
/* # of all-frozen blocks (not always up-to-date) */
int32 relallfrozen BKI_DEFAULT(0) FIRSTVERSION(18, CONST(0));
```
This says:
- "when upgrading from before PG14, call this function", which changes
'0' to '-1', otherwise takes reltuples as-is.
- "when generating tuple descriptors, leave out relallfrozen for
versions before PG18, and fill in a '0' for the new tuples"
There were some more difficult cases, but not many. The most difficult
aspect is node trees. I don't think it's possible to transform those
on-the-fly. My offline prototype dumped the deparsed node trees into
SQL in the usual way, which were then "recompiled" to node trees into
the new version's catalogs.
It should be possible for the catalog migration to bring the planner
statistics over as a matter of course.
*** Next steps
In order for in-place upgrade to actually work as a feature,
1) the old version needs to have 0001/2, so in practice that means the
upgraded-from version is PG20 at the earliest, with the rest of the
effort for PG21+.
2) At some point, catalog OIDs (e.g. between 10k and 12k) that now
vary between versions should be made immutable. Technically, it's not
necessary, but it makes it a lot nicer.
To justify committing those first two pieces during the PG20 cycle,
I'll need to progress the entire series to the point where there are
no serious architectural questions, and can be plausibly finished in
PG21+ with "just work". That will be a challenge, but I think it's
possible.
*** AI Disclaimer
I have used Claude extensively in the course of developing this. I was
skeptical about use of agents in software development until fairly
recently, but when given the right adult supervision they can be a
*huge* aid for complex software projects. I've re-written parts by
hand, but there's a lot more to clean up and harden. There are
operational gotchas and flaws, some of them left there on-purpose --
making this prematurely robust would be distracting and not a good use
of time before the architecture settles. The more boilerplate-y parts
have not had much scrutiny at all, but the important functionality
has.
--
John Naylor
Amazon Web Services
Attachments:
t254296_1v1-demo.txttext/plain; charset=US-ASCII; name=v1-demo.txtDownload
v1-0016-pg_inplace_upgrade-Add-the-golive-command.patchapplication/x-patch; name=v1-0016-pg_inplace_upgrade-Add-the-golive-command.patchDownload+965-26
v1-0015-pg_inplace_upgrade-Add-the-rollback-command.patchapplication/x-patch; name=v1-0015-pg_inplace_upgrade-Add-the-rollback-command.patchDownload+707-6
v1-0013-Take-btree-support-procedures-from-the-index-rela.patchapplication/x-patch; name=v1-0013-Take-btree-support-procedures-from-the-index-rela.patchDownload+47-26
v1-0017-Cross-a-standby-to-the-new-version-of-an-in-place.patchapplication/x-patch; name=v1-0017-Cross-a-standby-to-the-new-version-of-an-in-place.patchDownload+1165-274
v1-0014-Migrate-the-catalogs-for-an-in-place-upgrade.patchapplication/x-patch; name=v1-0014-Migrate-the-catalogs-for-an-in-place-upgrade.patchDownload+1278-8
v1-0009-Infrastructure-for-migrating-catalogs.patchapplication/x-patch; name=v1-0009-Infrastructure-for-migrating-catalogs.patchDownload+1369-110
v1-0012-Export-the-btree-spool-interface-of-nbtsort.c.patchapplication/x-patch; name=v1-0012-Export-the-btree-spool-interface-of-nbtsort.c.patchDownload+37-10
v1-0008-Suppress-checkpoints-and-archiving-during-an-in-p.patchapplication/x-patch; name=v1-0008-Suppress-checkpoints-and-archiving-during-an-in-p.patchDownload+150-23
v1-0011-Generate-a-description-of-every-catalog-s-relatio.patchapplication/x-patch; name=v1-0011-Generate-a-description-of-every-catalog-s-relatio.patchDownload+278-10
v1-0010-pg_inplace_upgrade-Add-the-swap-command.patchapplication/x-patch; name=v1-0010-pg_inplace_upgrade-Add-the-swap-command.patchDownload+264-11
v1-0005-Add-pg_inplace_upgrade-the-program-that-drives-an.patchapplication/x-patch; name=v1-0005-Add-pg_inplace_upgrade-the-program-that-drives-an.patchDownload+1963-1
v1-0003-Declare-pg_inplace_upgrade_golive-in-the-old-vers.patchapplication/x-patch; name=v1-0003-Declare-pg_inplace_upgrade_golive-in-the-old-vers.patchDownload+22-1
v1-0004-Stamp-HEAD-as-21devel.patchapplication/x-patch; name=v1-0004-Stamp-HEAD-as-21devel.patchDownload+13-14
v1-0006-Mock-bumping-magic-numbers-for-mechanically-testi.patchapplication/x-patch; name=v1-0006-Mock-bumping-magic-numbers-for-mechanically-testi.patchDownload+172-5
v1-0007-Take-over-the-primary-at-startup-for-an-in-place-.patchapplication/x-patch; name=v1-0007-Take-over-the-primary-at-startup-for-an-in-place-.patchDownload+1012-47
v1-0002-Pause-archive-recovery-at-an-upgrade-boundary.patchapplication/x-patch; name=v1-0002-Pause-archive-recovery-at-an-upgrade-boundary.patchDownload+446-6
v1-0001-Allow-marking-a-shutdown-checkpoint-as-an-upgrade.patchapplication/x-patch; name=v1-0001-Allow-marking-a-shutdown-checkpoint-as-an-upgrade.patchDownload+411-4
On Wed, Oct 21, 2026 at 07:00:10AM +0700, John Naylor wrote:
*** Catalogs (TODO)
While the PoC sidesteps this entirely for now, this is actually how I
started. About a year and half ago, I experimented with catalog header
annotations so that the usual couple hundred lines of Perl would
generate the code that transforms the catalog tuples. Earlier this
year, I turned that into a throwaway "offline upgrade" (i.e. no WAL)
prototype. That exercise indicated that this method could feasibly
handle all catalog schema changes since PG12. Example from pg_class:```
/* # of tuples (not always up-to-date; -1 means "unknown") */
float4 reltuples BKI_DEFAULT(-1) CHANGEDVALUE(14,
FUNC(reltuples_zero_to_unknown));[...]
/* # of all-frozen blocks (not always up-to-date) */
int32 relallfrozen BKI_DEFAULT(0) FIRSTVERSION(18, CONST(0));
```
As I understand it, instead of recreating the catalogs from pg_dump
--schema-only, you are modifying the actual catalogs. Yes, this will
certainly be faster than what we do now. I never tried this since it
would require changes/testing for each major release, and I didn't think
that would be reasonable. It might now be possible given our resources.
--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com
Do not let urgent matters crowd out time for investment in the future.
John Naylor <johncnaylorls@gmail.com> writes:
*** Catalogs (TODO)
While the PoC sidesteps this entirely for now, this is actually how I
started. About a year and half ago, I experimented with catalog header
annotations so that the usual couple hundred lines of Perl would
generate the code that transforms the catalog tuples. Earlier this
year, I turned that into a throwaway "offline upgrade" (i.e. no WAL)
prototype. That exercise indicated that this method could feasibly
handle all catalog schema changes since PG12. Example from pg_class:
I'm just really, really skeptical that this is practical for
anything except the most trivial changes (such as your two examples).
You already mentioned that stored expressions (node trees) won't work,
but what about cases where we move information across catalogs, or
create things that just weren't there before? A handy example is the
recent changes to make pg_constraint entries for column NOT NULL
constraints. Adding pg_depend entries for linkages where we didn't
formerly require one seems like another plausible-and-hard-to-implement
case. So I'm afraid where this is going to lead us is either a
straitjacket on what we can change in the catalog representation,
or an additional requirement for even more complex, hard-to-test code
that has to be written in order to put forward such changes. We
built pg_upgrade the way we did precisely to avoid such problems,
and I don't want to give that up.
2) At some point, catalog OIDs (e.g. between 10k and 12k) that now
vary between versions should be made immutable. Technically, it's not
necessary, but it makes it a lot nicer.
The core reason why that's not so today is that the set of
pg_collation entries made by initdb depends on the platform. I don't
think you're going to be able to make that not be the case, at least
not without significant downsides.
regards, tom lane