Executing pg_createsubscriber with a non-compatible control file

Started by Michael Paquier3 months ago13 messages
#1Michael Paquier
michael@paquier.xyz

Hi all,
(Adding Euler in CC.)

I have mentioned that on this thread:
/messages/by-id/aOM-T6iojyzb7OVB@paquier.xyz

Attempting to execute pg_createsubscriber compiled with a version of
PG_CONTROL_VERSION incompatible with the standby we want to switch to
a logical replica leads to the following error:
$ pg_createsubscriber -D $HOME/data/5433 \
-P "host=/tmp port=5432" -d postgres
pg_createsubscriber: error: control file appears to be corrupt

For example, setup a cluster based on v17 and run the command. This
is confusing, because the control file is not corrupted,
pg_createsubscriber is just missing the fact that the major version it
is compiled with is not able to manipulate the control file of the
target data folder. We enforce such checks for other tools, like
pg_checksums or pg_rewind, and I don't see why pg_createsubscriber
should be an exception?

One simple thing that could be done is to grab the first line of the
standby's PG_VERSION, at least, so as we have a check that does not
depend on the contents of the control file, and it would unlikely be
corrupted. Having a check based on PG_CONTROL_VERSION would be a few
lines less, of course, that we could try in parallel of the CRC check
report if the value is sane-ish, from a past version.

Thoughts?
--
Michael

#2Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Michael Paquier (#1)
Re: Executing pg_createsubscriber with a non-compatible control file

On Sun, Oct 5, 2025 at 9:19 PM Michael Paquier <michael@paquier.xyz> wrote:

Hi all,
(Adding Euler in CC.)

I have mentioned that on this thread:
/messages/by-id/aOM-T6iojyzb7OVB@paquier.xyz

Attempting to execute pg_createsubscriber compiled with a version of
PG_CONTROL_VERSION incompatible with the standby we want to switch to
a logical replica leads to the following error:
$ pg_createsubscriber -D $HOME/data/5433 \
-P "host=/tmp port=5432" -d postgres
pg_createsubscriber: error: control file appears to be corrupt

For example, setup a cluster based on v17 and run the command. This
is confusing, because the control file is not corrupted,
pg_createsubscriber is just missing the fact that the major version it
is compiled with is not able to manipulate the control file of the
target data folder. We enforce such checks for other tools, like
pg_checksums or pg_rewind, and I don't see why pg_createsubscriber
should be an exception?

+1 to add a major version check for better log messages.

Just to be clear, did you mean that pg_checksums and pg_rewind already
do such checks? IIUC pg_checksums does CRC check for the control file,
and if we execute v18-pg_checksums against v17-cluster we end up with
a similar error but with a different log message like "pg_checksums:
error: pg_control CRC value is incorrect". Those log messages are not
helpful either.

Regards,

--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com

#3Michael Paquier
michael@paquier.xyz
In reply to: Masahiko Sawada (#2)
Re: Executing pg_createsubscriber with a non-compatible control file

On Tue, Oct 07, 2025 at 11:51:45AM -0700, Masahiko Sawada wrote:

Just to be clear, did you mean that pg_checksums and pg_rewind already
do such checks? IIUC pg_checksums does CRC check for the control file,
and if we execute v18-pg_checksums against v17-cluster we end up with
a similar error but with a different log message like "pg_checksums:
error: pg_control CRC value is incorrect". Those log messages are not
helpful either.

For the case of pg_checksums, we don't really have a constraint based
on the major version, currently. However, there is a PG_VERSION_NUM
hardcoded when syncing the data folder, so we are pretty much assuming
that it is the case already.

A check based on PG_VERSION has more benefits in the long-term, IMO.
When the CRC check of the control file fails, it would be tempting to
use some of the contents read from the control file but that would
also mean that we could expose some corrupted values to the user, so
that would not be useful.

How about extracting from pg_rewind what it does with the on-disk
PG_VERSION and create an API that returns a major version number with
a data folder given in input? We could then reuse this API for the
other tools, at least pg_createsubscriber and pg_checksums. I am not
sure if there is a point in backpatching any of that, but it would
lead to more user-friendly errors for all of these.
--
Michael

#4Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Michael Paquier (#3)
Re: Executing pg_createsubscriber with a non-compatible control file

On Tue, Oct 7, 2025 at 5:23 PM Michael Paquier <michael@paquier.xyz> wrote:

On Tue, Oct 07, 2025 at 11:51:45AM -0700, Masahiko Sawada wrote:

Just to be clear, did you mean that pg_checksums and pg_rewind already
do such checks? IIUC pg_checksums does CRC check for the control file,
and if we execute v18-pg_checksums against v17-cluster we end up with
a similar error but with a different log message like "pg_checksums:
error: pg_control CRC value is incorrect". Those log messages are not
helpful either.

For the case of pg_checksums, we don't really have a constraint based
on the major version, currently. However, there is a PG_VERSION_NUM
hardcoded when syncing the data folder, so we are pretty much assuming
that it is the case already.

A check based on PG_VERSION has more benefits in the long-term, IMO.
When the CRC check of the control file fails, it would be tempting to
use some of the contents read from the control file but that would
also mean that we could expose some corrupted values to the user, so
that would not be useful.

How about extracting from pg_rewind what it does with the on-disk
PG_VERSION and create an API that returns a major version number with
a data folder given in input? We could then reuse this API for the
other tools, at least pg_createsubscriber and pg_checksums. I am not
sure if there is a point in backpatching any of that, but it would
lead to more user-friendly errors for all of these.

+1, sounds like a good idea to improve user experience. I think we can
use the API in pg_combinebackup.c too since it has a similar function,
read_pg_version_file().

Regards,

--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com

#5Michael Paquier
michael@paquier.xyz
In reply to: Masahiko Sawada (#4)
5 attachment(s)
Re: Executing pg_createsubscriber with a non-compatible control file

On Thu, Oct 09, 2025 at 11:22:47AM -0700, Masahiko Sawada wrote:

+1, sounds like a good idea to improve user experience. I think we can
use the API in pg_combinebackup.c too since it has a similar function,
read_pg_version_file().

Please find attached what I am finishing with. At the end, I have
designed an API that can be reused across the board for the following
tools that do the same things in the tree, removing some duplicated
code:
- pg_resetwal
- pg_upgrade
- pg_combinebackup

The routine that retrieves the contents gets a uint32 number, and it
is optionally possible to get the contents of PG_VERSION (pg_upgrade
wants that for tablespace paths, but that's really for pg_resetwal to
be able to show back buggy data):
extern uint32 get_pg_version(const char *datadir, char **version_str);

This support both the pre-v10 and the post-v10 version formats, for
pg_upgrade.

To ease comparisons with PG_MAJORVERSION_NUM, I have added a small
helper macro (see GET_PG_MAJORVERSION_NUM).

I have also applied the same method to pg_createsubscriber, on top of
that, to take care of my original issue. I have not looked at other
places where the same concept could be applied, at least it's a start.

Thoughts or comments?
--
Michael

Attachments:

v1-0001-Introduce-API-able-to-retrieve-contents-of-PG_VER.patchtext/x-diff; charset=us-asciiDownload
From b856d200e52ccb3d841ba05df149173cb7cd8772 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 10 Oct 2025 15:01:57 +0900
Subject: [PATCH v1 1/5] Introduce API able to retrieve contents of PG_VERSION

This will be used by a set of follow-up patches, to be consumed in
various frontend tools.
---
 src/include/fe_utils/version.h | 23 ++++++++++
 src/fe_utils/Makefile          |  3 +-
 src/fe_utils/meson.build       |  1 +
 src/fe_utils/version.c         | 81 ++++++++++++++++++++++++++++++++++
 4 files changed, 107 insertions(+), 1 deletion(-)
 create mode 100644 src/include/fe_utils/version.h
 create mode 100644 src/fe_utils/version.c

diff --git a/src/include/fe_utils/version.h b/src/include/fe_utils/version.h
new file mode 100644
index 000000000000..3b8c1884de5f
--- /dev/null
+++ b/src/include/fe_utils/version.h
@@ -0,0 +1,23 @@
+/*-------------------------------------------------------------------------
+ *
+ * Routines to retrieve information of PG_VERSION
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/fe_utils/version.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef VERSION_H
+#define VERSION_H
+
+/*
+ * Retrieve the version major number, useful for major version checks
+ * based on PG_MAJORVERSION_NUM.
+ */
+#define GET_PG_MAJORVERSION_NUM(v)	((v) / 10000)
+
+extern uint32 get_pg_version(const char *datadir, char **version_str);
+
+#endif							/* VERSION_H */
diff --git a/src/fe_utils/Makefile b/src/fe_utils/Makefile
index 28196ce0f6ae..b9fd566ff873 100644
--- a/src/fe_utils/Makefile
+++ b/src/fe_utils/Makefile
@@ -37,7 +37,8 @@ OBJS = \
 	query_utils.o \
 	recovery_gen.o \
 	simple_list.o \
-	string_utils.o
+	string_utils.o \
+	version.o
 
 ifeq ($(PORTNAME), win32)
 override CPPFLAGS += -DFD_SETSIZE=1024
diff --git a/src/fe_utils/meson.build b/src/fe_utils/meson.build
index 5a9ddb73463e..ddac3c3a6584 100644
--- a/src/fe_utils/meson.build
+++ b/src/fe_utils/meson.build
@@ -18,6 +18,7 @@ fe_utils_sources = files(
   'recovery_gen.c',
   'simple_list.c',
   'string_utils.c',
+  'version.c',
 )
 
 psqlscan = custom_target('psqlscan',
diff --git a/src/fe_utils/version.c b/src/fe_utils/version.c
new file mode 100644
index 000000000000..8c06bb84b4d6
--- /dev/null
+++ b/src/fe_utils/version.c
@@ -0,0 +1,81 @@
+/*-------------------------------------------------------------------------
+ *
+ * version.c
+ *		Routine to retrieve information of PG_VERSION
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <sys/stat.h>
+
+#include "common/logging.h"
+#include "fe_utils/version.h"
+
+/*
+ * Assumed maximum size of PG_VERSION.  This should be more than enough for
+ * any version numbers that need to be handled.
+ */
+#define PG_VERSION_MAX_SIZE	64
+
+/*
+ * get_major_version
+ *
+ * Retrieve the major version number of the given data folder, from
+ * PG_VERSION.  The result returned is a version number, that can be used
+ * for comparisons based on PG_VERSION_NUM.  For example, if PG_VERSION
+ * contains "18\n", this function returns 180000.
+ *
+ * This supports both the pre-v10 and the post-v10 version numbering.
+ * Optionally. "version_str" can be specified to store the contents
+ * retrieved from PG_VERSION.  It is allocated by this routine; the
+ * caller is responsible for pg_free()-ing it.
+ */
+uint32
+get_pg_version(const char *datadir, char **version_str)
+{
+	FILE	   *version_fd;
+	char		ver_filename[MAXPGPATH];
+	char		buf[PG_VERSION_MAX_SIZE];
+	int			v1 = 0,
+				v2 = 0;
+	struct stat st;
+
+	snprintf(ver_filename, sizeof(ver_filename), "%s/PG_VERSION",
+			 datadir);
+
+	if ((version_fd = fopen(ver_filename, "r")) == NULL)
+		pg_fatal("could not open version file \"%s\": %m", ver_filename);
+
+	if (fstat(fileno(version_fd), &st) != 0)
+		pg_fatal("could not stat file \"%s\": %m", ver_filename);
+	if (st.st_size > PG_VERSION_MAX_SIZE)
+		pg_fatal("file \"%s\" is too large", ver_filename);
+
+	if (fscanf(version_fd, "%63s", buf) == 0 ||
+		sscanf(buf, "%d.%d", &v1, &v2) < 1)
+		pg_fatal("could not parse version file \"%s\"", ver_filename);
+
+	fclose(version_fd);
+
+	if (version_str)
+	{
+		*version_str = pg_malloc(PG_VERSION_MAX_SIZE);
+		memcpy(*version_str, buf, st.st_size);
+	}
+
+	if (v1 < 10)
+	{
+		/* pre-v10 style, e.g. 9.6.1 */
+		return v1 * 10000 + v2 * 100;
+	}
+	else
+	{
+		/* post-v10 style, e.g. 10.1 */
+		return v1 * 10000;
+	}
+}
-- 
2.51.0

v1-0002-pg_upgrade-Use-PG_VERSION-generic-routine.patchtext/x-diff; charset=us-asciiDownload
From 80fe5aa1bcc36a12969a3936e4c18cf374ab5b90 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 10 Oct 2025 15:03:32 +0900
Subject: [PATCH v1 2/5] pg_upgrade: Use PG_VERSION generic routine

---
 src/bin/pg_upgrade/exec.c       |  5 +++--
 src/bin/pg_upgrade/pg_upgrade.h |  3 +--
 src/bin/pg_upgrade/server.c     | 39 ---------------------------------
 3 files changed, 4 insertions(+), 43 deletions(-)

diff --git a/src/bin/pg_upgrade/exec.c b/src/bin/pg_upgrade/exec.c
index 63f2815a7cd1..c045633d0c2a 100644
--- a/src/bin/pg_upgrade/exec.c
+++ b/src/bin/pg_upgrade/exec.c
@@ -12,6 +12,7 @@
 #include <fcntl.h>
 
 #include "common/string.h"
+#include "fe_utils/version.h"
 #include "pg_upgrade.h"
 
 static void check_data_dir(ClusterInfo *cluster);
@@ -343,8 +344,8 @@ check_data_dir(ClusterInfo *cluster)
 	const char *pg_data = cluster->pgdata;
 
 	/* get the cluster version */
-	cluster->major_version = get_major_server_version(cluster);
-
+	cluster->major_version = get_pg_version(cluster->pgdata,
+											&cluster->major_version_str);
 	check_single_dir(pg_data, "");
 	check_single_dir(pg_data, "base");
 	check_single_dir(pg_data, "global");
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 0ef47be0dc19..e86336f4be95 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -298,7 +298,7 @@ typedef struct
 	char	   *sockdir;		/* directory for Unix Domain socket, if any */
 	unsigned short port;		/* port number where postmaster is waiting */
 	uint32		major_version;	/* PG_VERSION of cluster */
-	char		major_version_str[64];	/* string PG_VERSION of cluster */
+	char	   *major_version_str;	/* string PG_VERSION of cluster */
 	uint32		bin_version;	/* version returned from pg_ctl */
 	char	  **tablespaces;	/* tablespace directories */
 	int			num_tablespaces;
@@ -473,7 +473,6 @@ char	   *cluster_conn_opts(ClusterInfo *cluster);
 
 bool		start_postmaster(ClusterInfo *cluster, bool report_and_exit_on_error);
 void		stop_postmaster(bool in_atexit);
-uint32		get_major_server_version(ClusterInfo *cluster);
 void		check_pghost_envvar(void);
 
 
diff --git a/src/bin/pg_upgrade/server.c b/src/bin/pg_upgrade/server.c
index 7eb15bc7d5ac..d51bee885b82 100644
--- a/src/bin/pg_upgrade/server.c
+++ b/src/bin/pg_upgrade/server.c
@@ -148,45 +148,6 @@ executeQueryOrDie(PGconn *conn, const char *fmt,...)
 }
 
 
-/*
- * get_major_server_version()
- *
- * gets the version (in unsigned int form) for the given datadir. Assumes
- * that datadir is an absolute path to a valid pgdata directory. The version
- * is retrieved by reading the PG_VERSION file.
- */
-uint32
-get_major_server_version(ClusterInfo *cluster)
-{
-	FILE	   *version_fd;
-	char		ver_filename[MAXPGPATH];
-	int			v1 = 0,
-				v2 = 0;
-
-	snprintf(ver_filename, sizeof(ver_filename), "%s/PG_VERSION",
-			 cluster->pgdata);
-	if ((version_fd = fopen(ver_filename, "r")) == NULL)
-		pg_fatal("could not open version file \"%s\": %m", ver_filename);
-
-	if (fscanf(version_fd, "%63s", cluster->major_version_str) == 0 ||
-		sscanf(cluster->major_version_str, "%d.%d", &v1, &v2) < 1)
-		pg_fatal("could not parse version file \"%s\"", ver_filename);
-
-	fclose(version_fd);
-
-	if (v1 < 10)
-	{
-		/* old style, e.g. 9.6.1 */
-		return v1 * 10000 + v2 * 100;
-	}
-	else
-	{
-		/* new style, e.g. 10.1 */
-		return v1 * 10000;
-	}
-}
-
-
 static void
 stop_postmaster_atexit(void)
 {
-- 
2.51.0

v1-0003-pg_createsubscriber-Use-PG_VERSION-generic-routin.patchtext/x-diff; charset=us-asciiDownload
From 2cd799e94b8453f7fae4134e37de78328719d411 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 10 Oct 2025 15:04:03 +0900
Subject: [PATCH v1 3/5] pg_createsubscriber: Use PG_VERSION generic routine

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index d29407413d96..c3732adabc86 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -26,6 +26,7 @@
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/simple_list.h"
 #include "fe_utils/string_utils.h"
+#include "fe_utils/version.h"
 #include "getopt_long.h"
 
 #define	DEFAULT_SUB_PORT	"50432"
@@ -407,7 +408,7 @@ static void
 check_data_directory(const char *datadir)
 {
 	struct stat statbuf;
-	char		versionfile[MAXPGPATH];
+	uint32		major_version;
 
 	pg_log_info("checking if directory \"%s\" is a cluster data directory",
 				datadir);
@@ -420,11 +421,18 @@ check_data_directory(const char *datadir)
 			pg_fatal("could not access directory \"%s\": %m", datadir);
 	}
 
-	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
-	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	/*
+	 * Retrieve the contents of this cluster's PG_VERSION.  We require
+	 * compatibility with the same major version as the one this tool is
+	 * compiled with.
+	 */
+	major_version = GET_PG_MAJORVERSION_NUM(get_pg_version(datadir, NULL));
+	if (major_version != PG_MAJORVERSION_NUM)
 	{
-		pg_fatal("directory \"%s\" is not a database cluster directory",
-				 datadir);
+		pg_log_error("data directory is of wrong version");
+		pg_log_error_detail("File \"%s\" contains \"%u\", which is not compatible with this program's version \"%u\".",
+							"PG_VERSION", major_version, PG_MAJORVERSION_NUM);
+		exit(1);
 	}
 }
 
-- 
2.51.0

v1-0004-pg_combinebackup-use-PG_VERSION-generic-routine.patchtext/x-diff; charset=us-asciiDownload
From 241106d3f694403c75ae922446a2eb99e91a63f2 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 10 Oct 2025 15:04:29 +0900
Subject: [PATCH v1 4/5] pg_combinebackup: use PG_VERSION generic routine

---
 src/bin/pg_combinebackup/pg_combinebackup.c | 63 +++------------------
 1 file changed, 7 insertions(+), 56 deletions(-)

diff --git a/src/bin/pg_combinebackup/pg_combinebackup.c b/src/bin/pg_combinebackup/pg_combinebackup.c
index f5cef99f6273..1f0531154b1d 100644
--- a/src/bin/pg_combinebackup/pg_combinebackup.c
+++ b/src/bin/pg_combinebackup/pg_combinebackup.c
@@ -34,6 +34,7 @@
 #include "common/relpath.h"
 #include "copy_file.h"
 #include "fe_utils/option_utils.h"
+#include "fe_utils/version.h"
 #include "getopt_long.h"
 #include "lib/stringinfo.h"
 #include "load_manifest.h"
@@ -117,7 +118,6 @@ static void process_directory_recursively(Oid tsoid,
 										  manifest_data **manifests,
 										  manifest_writer *mwriter,
 										  cb_options *opt);
-static int	read_pg_version_file(char *directory);
 static void remember_to_cleanup_directory(char *target_path, bool rmtopdir);
 static void reset_directory_cleanup_list(void);
 static cb_tablespace *scan_for_existing_tablespaces(char *pathname,
@@ -153,7 +153,7 @@ main(int argc, char *argv[])
 	int			c;
 	int			n_backups;
 	int			n_prior_backups;
-	int			version;
+	uint32		version;
 	uint64		system_identifier;
 	char	  **prior_backup_dirs;
 	cb_options	opt;
@@ -271,7 +271,11 @@ main(int argc, char *argv[])
 	}
 
 	/* Read the server version from the final backup. */
-	version = read_pg_version_file(argv[argc - 1]);
+	version = get_pg_version(argv[argc - 1], NULL);
+	if (GET_PG_MAJORVERSION_NUM(version) < 10)
+		pg_fatal("server version too old");
+	pg_log_debug("read server version %u from file \"%s\"",
+				 GET_PG_MAJORVERSION_NUM(version), "PG_VERSION");
 
 	/* Sanity-check control files. */
 	n_backups = argc - optind;
@@ -1155,59 +1159,6 @@ process_directory_recursively(Oid tsoid,
 	closedir(dir);
 }
 
-/*
- * Read the version number from PG_VERSION and convert it to the usual server
- * version number format. (e.g. If PG_VERSION contains "14\n" this function
- * will return 140000)
- */
-static int
-read_pg_version_file(char *directory)
-{
-	char		filename[MAXPGPATH];
-	StringInfoData buf;
-	int			fd;
-	int			version;
-	char	   *ep;
-
-	/* Construct pathname. */
-	snprintf(filename, MAXPGPATH, "%s/PG_VERSION", directory);
-
-	/* Open file. */
-	if ((fd = open(filename, O_RDONLY, 0)) < 0)
-		pg_fatal("could not open file \"%s\": %m", filename);
-
-	/* Read into memory. Length limit of 128 should be more than generous. */
-	initStringInfo(&buf);
-	slurp_file(fd, filename, &buf, 128);
-
-	/* Close the file. */
-	if (close(fd) != 0)
-		pg_fatal("could not close file \"%s\": %m", filename);
-
-	/* Convert to integer. */
-	errno = 0;
-	version = strtoul(buf.data, &ep, 10);
-	if (errno != 0 || *ep != '\n')
-	{
-		/*
-		 * Incremental backup is not relevant to very old server versions that
-		 * used multi-part version number (e.g. 9.6, or 8.4). So if we see
-		 * what looks like the beginning of such a version number, just bail
-		 * out.
-		 */
-		if (version < 10 && *ep == '.')
-			pg_fatal("%s: server version too old", filename);
-		pg_fatal("%s: could not parse version number", filename);
-	}
-
-	/* Debugging output. */
-	pg_log_debug("read server version %d from file \"%s\"", version, filename);
-
-	/* Release memory and return result. */
-	pfree(buf.data);
-	return version * 10000;
-}
-
 /*
  * Add a directory to the list of output directories to clean up.
  */
-- 
2.51.0

v1-0005-pg_resetwal-use-PG_VERSION-generic-routine.patchtext/x-diff; charset=us-asciiDownload
From d5bd304ee6a0c236ff323cc5d43713f10c1b20e8 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 10 Oct 2025 15:04:47 +0900
Subject: [PATCH v1 5/5] pg_resetwal: use PG_VERSION generic routine

---
 src/bin/pg_resetwal/pg_resetwal.c | 36 +++++++++----------------------
 1 file changed, 10 insertions(+), 26 deletions(-)

diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 7a4e4eb95706..42d15a2612d8 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -55,6 +55,7 @@
 #include "common/restricted_token.h"
 #include "common/string.h"
 #include "fe_utils/option_utils.h"
+#include "fe_utils/version.h"
 #include "getopt_long.h"
 #include "pg_getopt.h"
 #include "storage/large_object.h"
@@ -77,7 +78,7 @@ static int	WalSegSz;
 static int	set_wal_segsize;
 static int	set_char_signedness = -1;
 
-static void CheckDataVersion(void);
+static void CheckDataVersion(const char *datadir);
 static bool read_controlfile(void);
 static void GuessControlValues(void);
 static void PrintControlValues(bool guessed);
@@ -377,7 +378,7 @@ main(int argc, char *argv[])
 				 DataDir);
 
 	/* Check that data directory matches our server version */
-	CheckDataVersion();
+	CheckDataVersion(DataDir);
 
 	/*
 	 * Check for a postmaster lock file --- if there is one, refuse to
@@ -537,37 +538,20 @@ main(int argc, char *argv[])
  * to prevent simple user errors.
  */
 static void
-CheckDataVersion(void)
+CheckDataVersion(const char *datadir)
 {
-	const char *ver_file = "PG_VERSION";
-	FILE	   *ver_fd;
-	char		rawline[64];
+	char	   *version_str;
+	uint32		version = get_pg_version(datadir, &version_str);
 
-	if ((ver_fd = fopen(ver_file, "r")) == NULL)
-		pg_fatal("could not open file \"%s\" for reading: %m",
-				 ver_file);
-
-	/* version number has to be the first line read */
-	if (!fgets(rawline, sizeof(rawline), ver_fd))
-	{
-		if (!ferror(ver_fd))
-			pg_fatal("unexpected empty file \"%s\"", ver_file);
-		else
-			pg_fatal("could not read file \"%s\": %m", ver_file);
-	}
-
-	/* strip trailing newline and carriage return */
-	(void) pg_strip_crlf(rawline);
-
-	if (strcmp(rawline, PG_MAJORVERSION) != 0)
+	if (GET_PG_MAJORVERSION_NUM(version) != PG_MAJORVERSION_NUM)
 	{
 		pg_log_error("data directory is of wrong version");
 		pg_log_error_detail("File \"%s\" contains \"%s\", which is not compatible with this program's version \"%s\".",
-							ver_file, rawline, PG_MAJORVERSION);
+							"PG_VERSION",
+							version_str,
+							PG_MAJORVERSION);
 		exit(1);
 	}
-
-	fclose(ver_fd);
 }
 
 
-- 
2.51.0

#6Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Michael Paquier (#5)
Re: Executing pg_createsubscriber with a non-compatible control file

On Thu, Oct 9, 2025 at 11:08 PM Michael Paquier <michael@paquier.xyz> wrote:

On Thu, Oct 09, 2025 at 11:22:47AM -0700, Masahiko Sawada wrote:

+1, sounds like a good idea to improve user experience. I think we can
use the API in pg_combinebackup.c too since it has a similar function,
read_pg_version_file().

Please find attached what I am finishing with. At the end, I have
designed an API that can be reused across the board for the following
tools that do the same things in the tree, removing some duplicated
code:
- pg_resetwal
- pg_upgrade
- pg_combinebackup

The routine that retrieves the contents gets a uint32 number, and it
is optionally possible to get the contents of PG_VERSION (pg_upgrade
wants that for tablespace paths, but that's really for pg_resetwal to
be able to show back buggy data):
extern uint32 get_pg_version(const char *datadir, char **version_str);

This support both the pre-v10 and the post-v10 version formats, for
pg_upgrade.

To ease comparisons with PG_MAJORVERSION_NUM, I have added a small
helper macro (see GET_PG_MAJORVERSION_NUM).

I have also applied the same method to pg_createsubscriber, on top of
that, to take care of my original issue. I have not looked at other
places where the same concept could be applied, at least it's a start.

Thoughts or comments?

Thank you for creating the patches!

v1-0001-Introduce-API-able-to-retrieve-contents-of-PG_VER.patch:

LGTM

v1-0002-pg_upgrade-Use-PG_VERSION-generic-routine.patch:

LGTM

v1-0003-pg_createsubscriber-Use-PG_VERSION-generic-routin.patch:

+   major_version = GET_PG_MAJORVERSION_NUM(get_pg_version(datadir, NULL));
+   if (major_version != PG_MAJORVERSION_NUM)
    {
-       pg_fatal("directory \"%s\" is not a database cluster directory",
-                datadir);
+       pg_log_error("data directory is of wrong version");
+       pg_log_error_detail("File \"%s\" contains \"%u\", which is not
compatible with this program's version \"%u\".",
+                           "PG_VERSION", major_version, PG_MAJORVERSION_NUM);
+       exit(1);
    }

The new log detail message uses the same message as what pg_resetwal
uses, but pg_createsubscriber shows an integer major version whereas
pg_resetwal shows the raw version string. I guess it's better to unify
the usage for better consistency.

v1-0004-pg_combinebackup-use-PG_VERSION-generic-routine.patch:

+   pg_log_debug("read server version %u from file \"%s\"",
+                GET_PG_MAJORVERSION_NUM(version), "PG_VERSION");

We used to show the full path of PG_VERSION file in the debug message.
While probably we can live with such a small difference, do you think
it's a good idea to move the debug message to get_pg_version()?

v1-0005-pg_resetwal-use-PG_VERSION-generic-routine.patch:

    /* Check that data directory matches our server version */
-   CheckDataVersion();
+   CheckDataVersion(DataDir);

With the patch, pg_resetwal fails to handle a relative path of PGDATA
as follows:

$ bin/pg_resetwal data
pg_resetwal: error: could not open version file "data/PG_VERSION": No
such file or directory

This is because pg_resetwal does chdir() to the given path of the data
directory before checking the version.

Regards,

--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com

#7Michael Paquier
michael@paquier.xyz
In reply to: Masahiko Sawada (#6)
3 attachment(s)
Re: Executing pg_createsubscriber with a non-compatible control file

On Mon, Oct 13, 2025 at 03:35:06PM -0700, Masahiko Sawada wrote:

v1-0001-Introduce-API-able-to-retrieve-contents-of-PG_VER.patch:

v1-0002-pg_upgrade-Use-PG_VERSION-generic-routine.patch:
v1-0003-pg_createsubscriber-Use-PG_VERSION-generic-routin.patch:

Applied both of these.

The new log detail message uses the same message as what pg_resetwal
uses, but pg_createsubscriber shows an integer major version whereas
pg_resetwal shows the raw version string. I guess it's better to unify
the usage for better consistency.

OK, done as suggested to limit the translation work.

v1-0004-pg_combinebackup-use-PG_VERSION-generic-routine.patch:

+   pg_log_debug("read server version %u from file \"%s\"",
+                GET_PG_MAJORVERSION_NUM(version), "PG_VERSION");

We used to show the full path of PG_VERSION file in the debug message.
While probably we can live with such a small difference, do you think
it's a good idea to move the debug message to get_pg_version()?

I cannot get into doing that, leaving that up to the caller with the
error message they want. That's a minor point, I guess, I can see
both sides of the coin.

Switched this one to report the full path, like previously.

v1-0005-pg_resetwal-use-PG_VERSION-generic-routine.patch:

/* Check that data directory matches our server version */
-   CheckDataVersion();
+   CheckDataVersion(DataDir);

With the patch, pg_resetwal fails to handle a relative path of PGDATA
as follows:

$ bin/pg_resetwal data
pg_resetwal: error: could not open version file "data/PG_VERSION": No
such file or directory

This is because pg_resetwal does chdir() to the given path of the data
directory before checking the version.

Right. I've tested with absolute paths and forgot relative paths.
For this one, I would just use a ".", as the chdir is before the
version check. Or we could reverse the chdir() and the version check,
but there is no real benefit in doing so.

Updated patch set attached.
--
Michael

Attachments:

v2-0001-pg_createsubscriber-Use-PG_VERSION-generic-routin.patchtext/x-diff; charset=us-asciiDownload
From 1446b30bea9786432dcd9029ce61958ae334e46b Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 14 Oct 2025 16:41:19 +0900
Subject: [PATCH v2 1/3] pg_createsubscriber: Use PG_VERSION generic routine

---
 src/bin/pg_basebackup/pg_createsubscriber.c | 19 ++++++++++++++-----
 1 file changed, 14 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c
index d29407413d96..9fa5caaf91d6 100644
--- a/src/bin/pg_basebackup/pg_createsubscriber.c
+++ b/src/bin/pg_basebackup/pg_createsubscriber.c
@@ -26,6 +26,7 @@
 #include "fe_utils/recovery_gen.h"
 #include "fe_utils/simple_list.h"
 #include "fe_utils/string_utils.h"
+#include "fe_utils/version.h"
 #include "getopt_long.h"
 
 #define	DEFAULT_SUB_PORT	"50432"
@@ -407,7 +408,8 @@ static void
 check_data_directory(const char *datadir)
 {
 	struct stat statbuf;
-	char		versionfile[MAXPGPATH];
+	uint32		major_version;
+	char	   *version_str;
 
 	pg_log_info("checking if directory \"%s\" is a cluster data directory",
 				datadir);
@@ -420,11 +422,18 @@ check_data_directory(const char *datadir)
 			pg_fatal("could not access directory \"%s\": %m", datadir);
 	}
 
-	snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir);
-	if (stat(versionfile, &statbuf) != 0 && errno == ENOENT)
+	/*
+	 * Retrieve the contents of this cluster's PG_VERSION.  We require
+	 * compatibility with the same major version as the one this tool is
+	 * compiled with.
+	 */
+	major_version = GET_PG_MAJORVERSION_NUM(get_pg_version(datadir, &version_str));
+	if (major_version != PG_MAJORVERSION_NUM)
 	{
-		pg_fatal("directory \"%s\" is not a database cluster directory",
-				 datadir);
+		pg_log_error("data directory is of wrong version");
+		pg_log_error_detail("File \"%s\" contains \"%s\", which is not compatible with this program's version \"%s\".",
+							"PG_VERSION", version_str, PG_MAJORVERSION);
+		exit(1);
 	}
 }
 
-- 
2.51.0

v2-0002-pg_combinebackup-use-PG_VERSION-generic-routine.patchtext/x-diff; charset=us-asciiDownload
From 7185c5d6eb3ce0afb22d7be197e6901f2cab55c4 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 14 Oct 2025 16:47:03 +0900
Subject: [PATCH v2 2/3] pg_combinebackup: use PG_VERSION generic routine

---
 src/bin/pg_combinebackup/pg_combinebackup.c | 65 +++------------------
 1 file changed, 9 insertions(+), 56 deletions(-)

diff --git a/src/bin/pg_combinebackup/pg_combinebackup.c b/src/bin/pg_combinebackup/pg_combinebackup.c
index a330c09d939d..3a3251272092 100644
--- a/src/bin/pg_combinebackup/pg_combinebackup.c
+++ b/src/bin/pg_combinebackup/pg_combinebackup.c
@@ -34,6 +34,7 @@
 #include "common/relpath.h"
 #include "copy_file.h"
 #include "fe_utils/option_utils.h"
+#include "fe_utils/version.h"
 #include "getopt_long.h"
 #include "lib/stringinfo.h"
 #include "load_manifest.h"
@@ -117,7 +118,6 @@ static void process_directory_recursively(Oid tsoid,
 										  manifest_data **manifests,
 										  manifest_writer *mwriter,
 										  cb_options *opt);
-static int	read_pg_version_file(char *directory);
 static void remember_to_cleanup_directory(char *target_path, bool rmtopdir);
 static void reset_directory_cleanup_list(void);
 static cb_tablespace *scan_for_existing_tablespaces(char *pathname,
@@ -153,7 +153,7 @@ main(int argc, char *argv[])
 	int			c;
 	int			n_backups;
 	int			n_prior_backups;
-	int			version;
+	uint32		version;
 	uint64		system_identifier;
 	char	  **prior_backup_dirs;
 	cb_options	opt;
@@ -162,6 +162,7 @@ main(int argc, char *argv[])
 	StringInfo	last_backup_label;
 	manifest_data **manifests;
 	manifest_writer *mwriter;
+	char	   *pgdata;
 
 	pg_logging_init(argv[0]);
 	progname = get_progname(argv[0]);
@@ -271,7 +272,12 @@ main(int argc, char *argv[])
 	}
 
 	/* Read the server version from the final backup. */
-	version = read_pg_version_file(argv[argc - 1]);
+	pgdata = argv[argc - 1];
+	version = get_pg_version(pgdata, NULL);
+	if (GET_PG_MAJORVERSION_NUM(version) < 10)
+		pg_fatal("server version too old");
+	pg_log_debug("read server version %u from file \"%s/%s\"",
+				 GET_PG_MAJORVERSION_NUM(version), pgdata, "PG_VERSION");
 
 	/* Sanity-check control files. */
 	n_backups = argc - optind;
@@ -1155,59 +1161,6 @@ process_directory_recursively(Oid tsoid,
 	closedir(dir);
 }
 
-/*
- * Read the version number from PG_VERSION and convert it to the usual server
- * version number format. (e.g. If PG_VERSION contains "14\n" this function
- * will return 140000)
- */
-static int
-read_pg_version_file(char *directory)
-{
-	char		filename[MAXPGPATH];
-	StringInfoData buf;
-	int			fd;
-	int			version;
-	char	   *ep;
-
-	/* Construct pathname. */
-	snprintf(filename, MAXPGPATH, "%s/PG_VERSION", directory);
-
-	/* Open file. */
-	if ((fd = open(filename, O_RDONLY, 0)) < 0)
-		pg_fatal("could not open file \"%s\": %m", filename);
-
-	/* Read into memory. Length limit of 128 should be more than generous. */
-	initStringInfo(&buf);
-	slurp_file(fd, filename, &buf, 128);
-
-	/* Close the file. */
-	if (close(fd) != 0)
-		pg_fatal("could not close file \"%s\": %m", filename);
-
-	/* Convert to integer. */
-	errno = 0;
-	version = strtoul(buf.data, &ep, 10);
-	if (errno != 0 || *ep != '\n')
-	{
-		/*
-		 * Incremental backup is not relevant to very old server versions that
-		 * used multi-part version number (e.g. 9.6, or 8.4). So if we see
-		 * what looks like the beginning of such a version number, just bail
-		 * out.
-		 */
-		if (version < 10 && *ep == '.')
-			pg_fatal("%s: server version too old", filename);
-		pg_fatal("%s: could not parse version number", filename);
-	}
-
-	/* Debugging output. */
-	pg_log_debug("read server version %d from file \"%s\"", version, filename);
-
-	/* Release memory and return result. */
-	pfree(buf.data);
-	return version * 10000;
-}
-
 /*
  * Add a directory to the list of output directories to clean up.
  */
-- 
2.51.0

v2-0003-pg_resetwal-use-PG_VERSION-generic-routine.patchtext/x-diff; charset=us-asciiDownload
From 5ab7579870f9d2dee1b707c0914308823479f51c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 14 Oct 2025 16:55:04 +0900
Subject: [PATCH v2 3/3] pg_resetwal: use PG_VERSION generic routine

---
 src/bin/pg_resetwal/pg_resetwal.c | 30 +++++++-----------------------
 1 file changed, 7 insertions(+), 23 deletions(-)

diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 7a4e4eb95706..a89d72fc5cfe 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -55,6 +55,7 @@
 #include "common/restricted_token.h"
 #include "common/string.h"
 #include "fe_utils/option_utils.h"
+#include "fe_utils/version.h"
 #include "getopt_long.h"
 #include "pg_getopt.h"
 #include "storage/large_object.h"
@@ -539,35 +540,18 @@ main(int argc, char *argv[])
 static void
 CheckDataVersion(void)
 {
-	const char *ver_file = "PG_VERSION";
-	FILE	   *ver_fd;
-	char		rawline[64];
+	char	   *version_str;
+	uint32		version = get_pg_version(".", &version_str);
 
-	if ((ver_fd = fopen(ver_file, "r")) == NULL)
-		pg_fatal("could not open file \"%s\" for reading: %m",
-				 ver_file);
-
-	/* version number has to be the first line read */
-	if (!fgets(rawline, sizeof(rawline), ver_fd))
-	{
-		if (!ferror(ver_fd))
-			pg_fatal("unexpected empty file \"%s\"", ver_file);
-		else
-			pg_fatal("could not read file \"%s\": %m", ver_file);
-	}
-
-	/* strip trailing newline and carriage return */
-	(void) pg_strip_crlf(rawline);
-
-	if (strcmp(rawline, PG_MAJORVERSION) != 0)
+	if (GET_PG_MAJORVERSION_NUM(version) != PG_MAJORVERSION_NUM)
 	{
 		pg_log_error("data directory is of wrong version");
 		pg_log_error_detail("File \"%s\" contains \"%s\", which is not compatible with this program's version \"%s\".",
-							ver_file, rawline, PG_MAJORVERSION);
+							"PG_VERSION",
+							version_str,
+							PG_MAJORVERSION);
 		exit(1);
 	}
-
-	fclose(ver_fd);
 }
 
 
-- 
2.51.0

#8Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Michael Paquier (#7)
Re: Executing pg_createsubscriber with a non-compatible control file

On Tue, Oct 14, 2025 at 1:04 AM Michael Paquier <michael@paquier.xyz> wrote:

On Mon, Oct 13, 2025 at 03:35:06PM -0700, Masahiko Sawada wrote:

v1-0001-Introduce-API-able-to-retrieve-contents-of-PG_VER.patch:

v1-0002-pg_upgrade-Use-PG_VERSION-generic-routine.patch:
v1-0003-pg_createsubscriber-Use-PG_VERSION-generic-routin.patch:

Applied both of these.

The new log detail message uses the same message as what pg_resetwal
uses, but pg_createsubscriber shows an integer major version whereas
pg_resetwal shows the raw version string. I guess it's better to unify
the usage for better consistency.

OK, done as suggested to limit the translation work.

v1-0004-pg_combinebackup-use-PG_VERSION-generic-routine.patch:

+   pg_log_debug("read server version %u from file \"%s\"",
+                GET_PG_MAJORVERSION_NUM(version), "PG_VERSION");

We used to show the full path of PG_VERSION file in the debug message.
While probably we can live with such a small difference, do you think
it's a good idea to move the debug message to get_pg_version()?

I cannot get into doing that, leaving that up to the caller with the
error message they want. That's a minor point, I guess, I can see
both sides of the coin.

Switched this one to report the full path, like previously.

v1-0005-pg_resetwal-use-PG_VERSION-generic-routine.patch:

/* Check that data directory matches our server version */
-   CheckDataVersion();
+   CheckDataVersion(DataDir);

With the patch, pg_resetwal fails to handle a relative path of PGDATA
as follows:

$ bin/pg_resetwal data
pg_resetwal: error: could not open version file "data/PG_VERSION": No
such file or directory

This is because pg_resetwal does chdir() to the given path of the data
directory before checking the version.

Right. I've tested with absolute paths and forgot relative paths.
For this one, I would just use a ".", as the chdir is before the
version check. Or we could reverse the chdir() and the version check,
but there is no real benefit in doing so.

Updated patch set attached.

Thank you for updating the patch!

All patches look good to me.

Regards,

--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com

#9Michael Paquier
michael@paquier.xyz
In reply to: Masahiko Sawada (#8)
Re: Executing pg_createsubscriber with a non-compatible control file

On Tue, Oct 14, 2025 at 11:54:26AM -0700, Masahiko Sawada wrote:

All patches look good to me.

Thanks. I have applied all that after a second look, not planning to
bother about the back branches for pg_createsubscriber.

We may be able to apply the version rules to more tools. pg_checksums
and pg_rewind have hardcoded requirement based on PG_VERSION_NUM. Not
sure if it's worth bothering about them, though.
--
Michael

#10Michael Banck
mbanck@gmx.net
In reply to: Michael Paquier (#3)
1 attachment(s)
Re: Executing pg_createsubscriber with a non-compatible control file

Hi,

On Wed, Oct 08, 2025 at 09:22:48AM +0900, Michael Paquier wrote:

On Tue, Oct 07, 2025 at 11:51:45AM -0700, Masahiko Sawada wrote:

Just to be clear, did you mean that pg_checksums and pg_rewind already
do such checks? IIUC pg_checksums does CRC check for the control file,
and if we execute v18-pg_checksums against v17-cluster we end up with
a similar error but with a different log message like "pg_checksums:
error: pg_control CRC value is incorrect". Those log messages are not
helpful either.

For the case of pg_checksums, we don't really have a constraint based
on the major version, currently. However, there is a PG_VERSION_NUM
hardcoded when syncing the data folder, so we are pretty much assuming
that it is the case already.

A check based on PG_VERSION has more benefits in the long-term, IMO.
When the CRC check of the control file fails, it would be tempting to
use some of the contents read from the control file but that would
also mean that we could expose some corrupted values to the user, so
that would not be useful.

So it seems the decision was that pg_checksums does not need this major
version check? I agree that it is not entirely obvious is does, but on
the other hand, those "pg_control CRC value is incorrect" are not
helpful either. But even if pg_controldata() manages to read the control
file, pg_checksums then errors out with "cluster is not compatible with
this version of pg_checksums" (without further information) in case the
control file version is different. I tried a few combinations, and
current HEAD works with v18 clusters, while v16 pg_checksums can read
v17 pg_control but then errors out due to that having a different
version. All other combinations I tried so far yield the CRC error.

So I think something like the attached should also be applied.

A while ago I was actually looking at doing the exact same thing you
did, i.e. extracting the PG_VERSION check out of pg_rewind for usage in
pg_checksums but got side-tracked.

While looking at the commit message of fa55be2a5, I noticed "This puts
pg_createsubscriber in line with the documentation" and now I wonder
whether the current major version dependency of pg_checksums shouldn't
be mentioned in the documentation as well?

If (that is a tall order maybe) on the other hand we agree that
pg_checksums should work across major versions, then maybe the proper
fix (on top of making sure everything else works correctly for each
version) would be to teach get_controlfile() how to read older control
files somehow, and pass it a major version as argument? Not sure that
wouldn't be a hazard/ maintenance-nightmare and I don't volunteer to do
that.

Michael

Attachments:

0001-pg_checksums-Use-new-routine-to-retrieve-data-of-PG_.patchtext/x-diff; charset=us-asciiDownload
From 8b9f720854c8c4f9be7fb0e7ff186ddf1cfdcbf1 Mon Sep 17 00:00:00 2001
From: Michael Banck <michael.banck@credativ.de>
Date: Fri, 17 Oct 2025 10:22:20 +0200
Subject: [PATCH] pg_checksums: Use new routine to retrieve data of PG_VERSION

pg_checksums currently refuses to work with clusters of a different version.
Until this limitation is lifted, running it on a cluster with a different major
version errors out either with a "pg_control CRC value is incorrect" message,
or, if that works, with a "cluster is not compatible with this version of
pg_checksums" message in case the control file is different.

The former is confusing as the control file is correct: only the expected
version does not match.  This commit integrates pg_checksums with the facility
added by cd0be131ba6f, where the contents of PG_VERSION are read and compared
with the value of PG_MAJORVERSION_NUM expected by the tool.
---
 src/bin/pg_checksums/pg_checksums.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/src/bin/pg_checksums/pg_checksums.c b/src/bin/pg_checksums/pg_checksums.c
index f20be82862a..46cb2f36efa 100644
--- a/src/bin/pg_checksums/pg_checksums.c
+++ b/src/bin/pg_checksums/pg_checksums.c
@@ -25,6 +25,7 @@
 #include "common/logging.h"
 #include "common/relpath.h"
 #include "fe_utils/option_utils.h"
+#include "fe_utils/version.h"
 #include "getopt_long.h"
 #include "pg_getopt.h"
 #include "storage/bufpage.h"
@@ -448,6 +449,8 @@ main(int argc, char *argv[])
 	int			c;
 	int			option_index;
 	bool		crc_ok;
+	uint32		major_version;
+	char	   *version_str;
 
 	pg_logging_init(argv[0]);
 	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_checksums"));
@@ -543,6 +546,20 @@ main(int argc, char *argv[])
 		exit(1);
 	}
 
+	/*
+	 * Retrieve the contents of this cluster's PG_VERSION.  We require
+	 * compatibility with the same major version as the one this tool is
+	 * compiled with.
+	 */
+	major_version = GET_PG_MAJORVERSION_NUM(get_pg_version(DataDir, &version_str));
+	if (major_version != PG_MAJORVERSION_NUM)
+	{
+		pg_log_error("data directory is of wrong version");
+		pg_log_error_detail("File \"%s\" contains \"%s\", which is not compatible with this program's version \"%s\".",
+							"PG_VERSION", version_str, PG_MAJORVERSION);
+		exit(1);
+	}
+
 	/* Read the control file and check compatibility */
 	ControlFile = get_controlfile(DataDir, &crc_ok);
 	if (!crc_ok)
-- 
2.39.5

#11Michael Paquier
michael@paquier.xyz
In reply to: Michael Banck (#10)
Re: Executing pg_createsubscriber with a non-compatible control file

On Fri, Oct 17, 2025 at 10:32:32AM +0200, Michael Banck wrote:

So it seems the decision was that pg_checksums does not need this major
version check? I agree that it is not entirely obvious is does, but on
the other hand, those "pg_control CRC value is incorrect" are not
helpful either. But even if pg_controldata() manages to read the control
file, pg_checksums then errors out with "cluster is not compatible with
this version of pg_checksums" (without further information) in case the
control file version is different. I tried a few combinations, and
current HEAD works with v18 clusters, while v16 pg_checksums can read
v17 pg_control but then errors out due to that having a different
version. All other combinations I tried so far yield the CRC error.

I just did not feel strongly about adding the check, but now that we
have the version API we could also do it, based on the case of PGDATA
with a cluster that has a control version different than HEAD:
$ pg_checksums $PGDATA
pg_checksums: error: pg_control CRC value is incorrect

So I think something like the attached should also be applied.

I see that you have used the same error message. That works for me.
Any comments and/or objections from others?

While looking at the commit message of fa55be2a5, I noticed "This puts
pg_createsubscriber in line with the documentation" and now I wonder
whether the current major version dependency of pg_checksums shouldn't
be mentioned in the documentation as well?

With the addition of the version check in the binary, there is no
downside in documenting this requirement.

If (that is a tall order maybe) on the other hand we agree that
pg_checksums should work across major versions, then maybe the proper
fix (on top of making sure everything else works correctly for each
version) would be to teach get_controlfile() how to read older control
files somehow, and pass it a major version as argument? Not sure that
wouldn't be a hazard/ maintenance-nightmare and I don't volunteer to do
that.

I cannot get excited about the complications and the extra
maintenance. Attempting backward-compatibility for the sake of this
tool would bring its own set of bugs because we would need to maintain
the same control data structures across multiple branches, labelled
based on the control file versions. The current state of the tree is
simpler now.
--
Michael

#12Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#11)
Re: Executing pg_createsubscriber with a non-compatible control file

On Sat, Oct 18, 2025 at 08:55:35AM +0900, Michael Paquier wrote:

With the addition of the version check in the binary, there is no
downside in documenting this requirement.

Okay, I have added a note about that in the docs, then applied your
patch on HEAD. Thanks!

If others would like to apply the same kind of version checks in some
of the other in-core tools, please feel free to mention them here or
on a new thread.
--
Michael

#13Michael Banck
mbanck@gmx.net
In reply to: Michael Paquier (#12)
Re: Executing pg_createsubscriber with a non-compatible control file

On Mon, Oct 20, 2025 at 09:54:52AM +0900, Michael Paquier wrote:

On Sat, Oct 18, 2025 at 08:55:35AM +0900, Michael Paquier wrote:

With the addition of the version check in the binary, there is no
downside in documenting this requirement.

Okay, I have added a note about that in the docs, then applied your
patch on HEAD. Thanks!

Thank you!

Michael