From 57382d99270442cf042bf0d481cc9233264842c5 Mon Sep 17 00:00:00 2001
From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Mon, 2 Feb 2026 15:23:45 -0500
Subject: [PATCH v3 2/2] Add new alignment info to pg_control, and check it
 during pg_upgrade.

Having decoupled ALIGNOF_INT64_T and ALIGNOF_DOUBLE from
MAXIMUM_ALIGNOF, we had better record all three values in pg_control,
and check them to verify database compatibility.

In pg_upgrade, if the source cluster's pg_controldata doesn't report
the new fields, assume they have values equal to its MAXALIGN.
This allows safe upgrading from pre-v19 databases.

This will require a catversion bump.

Discussion: https://postgr.es/m/1127261.1769649624@sss.pgh.pa.us
---
 doc/src/sgml/func/func-info.sgml        | 10 ++++
 src/backend/access/transam/xlog.c       | 22 ++++++++
 src/backend/utils/misc/pg_controldata.c | 49 +++++++++---------
 src/bin/pg_controldata/pg_controldata.c |  4 ++
 src/bin/pg_resetwal/pg_resetwal.c       |  6 +++
 src/bin/pg_upgrade/controldata.c        | 67 ++++++++++++++++++++++---
 src/bin/pg_upgrade/pg_upgrade.h         |  9 +++-
 src/include/catalog/pg_control.h        | 14 +++---
 src/include/catalog/pg_proc.dat         |  6 +--
 9 files changed, 144 insertions(+), 43 deletions(-)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 294f45e82a3..9c235f47611 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3545,6 +3545,16 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
      <tbody>
 
+      <row>
+       <entry><structfield>int8_data_alignment</structfield></entry>
+       <entry><type>integer</type></entry>
+      </row>
+
+      <row>
+       <entry><structfield>float8_data_alignment</structfield></entry>
+       <entry><type>integer</type></entry>
+      </row>
+
       <row>
        <entry><structfield>max_data_alignment</structfield></entry>
        <entry><type>integer</type></entry>
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 13ec6225b85..8cb5d033911 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4284,6 +4284,8 @@ WriteControlFile(void)
 	ControlFile->pg_control_version = PG_CONTROL_VERSION;
 	ControlFile->catalog_version_no = CATALOG_VERSION_NO;
 
+	ControlFile->int64Align = ALIGNOF_INT64_T;
+	ControlFile->doubleAlign = ALIGNOF_DOUBLE;
 	ControlFile->maxAlign = MAXIMUM_ALIGNOF;
 	ControlFile->floatFormat = FLOATFORMAT_VALUE;
 
@@ -4473,6 +4475,26 @@ ReadControlFile(void)
 						   "CATALOG_VERSION_NO", ControlFile->catalog_version_no,
 						   "CATALOG_VERSION_NO", CATALOG_VERSION_NO),
 				 errhint("It looks like you need to initdb.")));
+	if (ControlFile->int64Align != ALIGNOF_INT64_T)
+		ereport(FATAL,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("database files are incompatible with server"),
+		/* translator: %s is a variable name and %d is its value */
+				 errdetail("The database cluster was initialized with %s %d,"
+						   " but the server was compiled with %s %d.",
+						   "ALIGNOF_INT64_T", ControlFile->int64Align,
+						   "ALIGNOF_INT64_T", ALIGNOF_INT64_T),
+				 errhint("It looks like you need to initdb.")));
+	if (ControlFile->doubleAlign != ALIGNOF_DOUBLE)
+		ereport(FATAL,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("database files are incompatible with server"),
+		/* translator: %s is a variable name and %d is its value */
+				 errdetail("The database cluster was initialized with %s %d,"
+						   " but the server was compiled with %s %d.",
+						   "ALIGNOF_DOUBLE", ControlFile->doubleAlign,
+						   "ALIGNOF_DOUBLE", ALIGNOF_DOUBLE),
+				 errhint("It looks like you need to initdb.")));
 	if (ControlFile->maxAlign != MAXIMUM_ALIGNOF)
 		ereport(FATAL,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
diff --git a/src/backend/utils/misc/pg_controldata.c b/src/backend/utils/misc/pg_controldata.c
index c6d9cbb1577..1f901dacc01 100644
--- a/src/backend/utils/misc/pg_controldata.c
+++ b/src/backend/utils/misc/pg_controldata.c
@@ -203,8 +203,9 @@ pg_control_recovery(PG_FUNCTION_ARGS)
 Datum
 pg_control_init(PG_FUNCTION_ARGS)
 {
-	Datum		values[12];
-	bool		nulls[12];
+	Datum		values[14];
+	bool		nulls[14];
+	int			j = 0;
 	TupleDesc	tupdesc;
 	HeapTuple	htup;
 	ControlFileData *ControlFile;
@@ -221,41 +222,37 @@ pg_control_init(PG_FUNCTION_ARGS)
 		ereport(ERROR,
 				(errmsg("calculated CRC checksum does not match value stored in file")));
 
-	values[0] = Int32GetDatum(ControlFile->maxAlign);
-	nulls[0] = false;
+	memset(nulls, 0, sizeof(nulls));
 
-	values[1] = Int32GetDatum(ControlFile->blcksz);
-	nulls[1] = false;
+	values[j++] = Int32GetDatum(ControlFile->int64Align);
 
-	values[2] = Int32GetDatum(ControlFile->relseg_size);
-	nulls[2] = false;
+	values[j++] = Int32GetDatum(ControlFile->doubleAlign);
 
-	values[3] = Int32GetDatum(ControlFile->xlog_blcksz);
-	nulls[3] = false;
+	values[j++] = Int32GetDatum(ControlFile->maxAlign);
 
-	values[4] = Int32GetDatum(ControlFile->xlog_seg_size);
-	nulls[4] = false;
+	values[j++] = Int32GetDatum(ControlFile->blcksz);
 
-	values[5] = Int32GetDatum(ControlFile->nameDataLen);
-	nulls[5] = false;
+	values[j++] = Int32GetDatum(ControlFile->relseg_size);
 
-	values[6] = Int32GetDatum(ControlFile->indexMaxKeys);
-	nulls[6] = false;
+	values[j++] = Int32GetDatum(ControlFile->xlog_blcksz);
 
-	values[7] = Int32GetDatum(ControlFile->toast_max_chunk_size);
-	nulls[7] = false;
+	values[j++] = Int32GetDatum(ControlFile->xlog_seg_size);
 
-	values[8] = Int32GetDatum(ControlFile->loblksize);
-	nulls[8] = false;
+	values[j++] = Int32GetDatum(ControlFile->nameDataLen);
 
-	values[9] = BoolGetDatum(ControlFile->float8ByVal);
-	nulls[9] = false;
+	values[j++] = Int32GetDatum(ControlFile->indexMaxKeys);
 
-	values[10] = Int32GetDatum(ControlFile->data_checksum_version);
-	nulls[10] = false;
+	values[j++] = Int32GetDatum(ControlFile->toast_max_chunk_size);
 
-	values[11] = BoolGetDatum(ControlFile->default_char_signedness);
-	nulls[11] = false;
+	values[j++] = Int32GetDatum(ControlFile->loblksize);
+
+	values[j++] = BoolGetDatum(ControlFile->float8ByVal);
+
+	values[j++] = Int32GetDatum(ControlFile->data_checksum_version);
+
+	values[j++] = BoolGetDatum(ControlFile->default_char_signedness);
+
+	Assert(j == lengthof(values));
 
 	htup = heap_form_tuple(tupdesc, values, nulls);
 
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index a4060309ae0..b93f6521326 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -317,6 +317,10 @@ main(int argc, char *argv[])
 		   ControlFile->max_locks_per_xact);
 	printf(_("track_commit_timestamp setting:       %s\n"),
 		   ControlFile->track_commit_timestamp ? _("on") : _("off"));
+	printf(_("int8 data alignment:                  %u\n"),
+		   ControlFile->int64Align);
+	printf(_("float8 data alignment:                %u\n"),
+		   ControlFile->doubleAlign);
 	printf(_("Maximum data alignment:               %u\n"),
 		   ControlFile->maxAlign);
 	/* we don't print floatFormat since can't say much useful about it */
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 431b83a67da..59edf644d06 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -724,6 +724,8 @@ GuessControlValues(void)
 	ControlFile.max_prepared_xacts = 0;
 	ControlFile.max_locks_per_xact = 64;
 
+	ControlFile.int64Align = ALIGNOF_INT64_T;
+	ControlFile.doubleAlign = ALIGNOF_DOUBLE;
 	ControlFile.maxAlign = MAXIMUM_ALIGNOF;
 	ControlFile.floatFormat = FLOATFORMAT_VALUE;
 	ControlFile.blcksz = BLCKSZ;
@@ -791,6 +793,10 @@ PrintControlValues(bool guessed)
 		   ControlFile.checkPointCopy.oldestCommitTsXid);
 	printf(_("Latest checkpoint's newestCommitTsXid:%u\n"),
 		   ControlFile.checkPointCopy.newestCommitTsXid);
+	printf(_("int8 data alignment:                  %u\n"),
+		   ControlFile.int64Align);
+	printf(_("float8 data alignment:                %u\n"),
+		   ControlFile.doubleAlign);
 	printf(_("Maximum data alignment:               %u\n"),
 		   ControlFile.maxAlign);
 	/* we don't print floatFormat since can't say much useful about it */
diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c
index aa6e8b4de5d..d096affa550 100644
--- a/src/bin/pg_upgrade/controldata.c
+++ b/src/bin/pg_upgrade/controldata.c
@@ -52,7 +52,9 @@ get_control_data(ClusterInfo *cluster)
 	bool		got_mxoff = false;
 	bool		got_nextxlogfile = false;
 	bool		got_float8_pass_by_value = false;
-	bool		got_align = false;
+	bool		got_int64align = false;
+	bool		got_doublealign = false;
+	bool		got_maxalign = false;
 	bool		got_blocksz = false;
 	bool		got_largesz = false;
 	bool		got_walsz = false;
@@ -383,6 +385,28 @@ get_control_data(ClusterInfo *cluster)
 			cluster->controldata.float8_pass_by_value = strstr(p, "by value") != NULL;
 			got_float8_pass_by_value = true;
 		}
+		else if ((p = strstr(bufin, "int8 data alignment:")) != NULL)
+		{
+			p = strchr(p, ':');
+
+			if (p == NULL || strlen(p) <= 1)
+				pg_fatal("%d: controldata retrieval problem", __LINE__);
+
+			p++;				/* remove ':' char */
+			cluster->controldata.int64align = str2uint(p);
+			got_int64align = true;
+		}
+		else if ((p = strstr(bufin, "float8 data alignment:")) != NULL)
+		{
+			p = strchr(p, ':');
+
+			if (p == NULL || strlen(p) <= 1)
+				pg_fatal("%d: controldata retrieval problem", __LINE__);
+
+			p++;				/* remove ':' char */
+			cluster->controldata.doublealign = str2uint(p);
+			got_doublealign = true;
+		}
 		else if ((p = strstr(bufin, "Maximum data alignment:")) != NULL)
 		{
 			p = strchr(p, ':');
@@ -391,8 +415,8 @@ get_control_data(ClusterInfo *cluster)
 				pg_fatal("%d: controldata retrieval problem", __LINE__);
 
 			p++;				/* remove ':' char */
-			cluster->controldata.align = str2uint(p);
-			got_align = true;
+			cluster->controldata.maxalign = str2uint(p);
+			got_maxalign = true;
 		}
 		else if ((p = strstr(bufin, "Database block size:")) != NULL)
 		{
@@ -598,13 +622,28 @@ get_control_data(ClusterInfo *cluster)
 #endif
 	}
 
+	/*
+	 * Pre-v19 database clusters will not report int64align or doublealign.
+	 * Their values must be equal to maxalign in that case.
+	 */
+	if (cluster->controldata.ctrl_ver < INT64_ALIGN_PG_CONTROL_VER)
+	{
+		Assert(!got_int64align);
+		cluster->controldata.int64align = cluster->controldata.maxalign;
+		got_int64align = true;
+		Assert(!got_doublealign);
+		cluster->controldata.doublealign = cluster->controldata.maxalign;
+		got_doublealign = true;
+	}
+
 	/* verify that we got all the mandatory pg_control data */
 	if (!got_xid || !got_oid ||
 		!got_multi || !got_oldestxid ||
 		(!got_oldestmulti &&
 		 cluster->controldata.cat_ver >= MULTIXACT_FORMATCHANGE_CAT_VER) ||
 		!got_mxoff || (!live_check && !got_nextxlogfile) ||
-		!got_float8_pass_by_value || !got_align || !got_blocksz ||
+		!got_float8_pass_by_value || !got_blocksz ||
+		!got_int64align || !got_doublealign || !got_maxalign ||
 		!got_largesz || !got_walsz || !got_walseg || !got_ident ||
 		!got_index || !got_toast ||
 		(!got_large_object &&
@@ -645,7 +684,13 @@ get_control_data(ClusterInfo *cluster)
 		if (!got_float8_pass_by_value)
 			pg_log(PG_REPORT, "  float8 argument passing method");
 
-		if (!got_align)
+		if (!got_int64align)
+			pg_log(PG_REPORT, "  int8 alignment");
+
+		if (!got_doublealign)
+			pg_log(PG_REPORT, "  float8 alignment");
+
+		if (!got_maxalign)
 			pg_log(PG_REPORT, "  maximum alignment");
 
 		if (!got_blocksz)
@@ -698,8 +743,16 @@ void
 check_control_data(ControlData *oldctrl,
 				   ControlData *newctrl)
 {
-	if (oldctrl->align == 0 || oldctrl->align != newctrl->align)
-		pg_fatal("old and new pg_controldata alignments are invalid or do not match.\n"
+	if (oldctrl->int64align == 0 || oldctrl->int64align != newctrl->int64align)
+		pg_fatal("old and new pg_controldata int8 alignments are invalid or do not match.\n"
+				 "Likely one cluster is a 32-bit install, the other 64-bit");
+
+	if (oldctrl->doublealign == 0 || oldctrl->doublealign != newctrl->doublealign)
+		pg_fatal("old and new pg_controldata float8 alignments are invalid or do not match.\n"
+				 "Likely one cluster is a 32-bit install, the other 64-bit");
+
+	if (oldctrl->maxalign == 0 || oldctrl->maxalign != newctrl->maxalign)
+		pg_fatal("old and new pg_controldata maximum alignments are invalid or do not match.\n"
 				 "Likely one cluster is a 32-bit install, the other 64-bit");
 
 	if (oldctrl->blocksz == 0 || oldctrl->blocksz != newctrl->blocksz)
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index ec018e4f292..6caa00a7bed 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -138,6 +138,11 @@ extern char *output_files[];
  */
 #define DEFAULT_CHAR_SIGNEDNESS_CAT_VER 202502212
 
+/*
+ * Separate int64Align and doubleAlign fields were added to pg_control here
+ */
+#define INT64_ALIGN_PG_CONTROL_VER 1902
+
 /*
  * Each relation is represented by a relinfo structure.
  */
@@ -245,7 +250,9 @@ typedef struct
 	uint64		chkpnt_nxtmxoff;
 	uint32		chkpnt_oldstMulti;
 	uint32		chkpnt_oldstxid;
-	uint32		align;
+	uint32		int64align;
+	uint32		doublealign;
+	uint32		maxalign;
 	uint32		blocksz;
 	uint32		largesz;
 	uint32		walsz;
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 7503db1af51..f4f579651ca 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -22,7 +22,7 @@
 
 
 /* Version identifier for this pg_control format */
-#define PG_CONTROL_VERSION	1901
+#define PG_CONTROL_VERSION	1902
 
 /* Nonce key length, see below */
 #define MOCK_AUTH_NONCE_LEN		32
@@ -190,15 +190,17 @@ typedef struct ControlFileData
 	 * This data is used to check for hardware-architecture compatibility of
 	 * the database and the backend executable.  We need not check endianness
 	 * explicitly, since the pg_control version will surely look wrong to a
-	 * machine of different endianness, but we do need to worry about MAXALIGN
-	 * and floating-point format.  (Note: storage layout nominally also
-	 * depends on SHORTALIGN and INTALIGN, but in practice these are the same
-	 * on all architectures of interest.)
+	 * machine of different endianness, but we do need to worry about data
+	 * alignment and floating-point format.  (Note: storage layout nominally
+	 * also depends on SHORTALIGN and INTALIGN, but in practice these are the
+	 * same on all architectures of interest.)
 	 *
 	 * Testing just one double value is not a very bulletproof test for
 	 * floating-point compatibility, but it will catch most cases.
 	 */
-	uint32		maxAlign;		/* alignment requirement for tuples */
+	uint8		int64Align;		/* alignment requirement for 8-byte integers */
+	uint8		doubleAlign;	/* alignment requirement for float8 */
+	uint8		maxAlign;		/* alignment requirement for tuples */
 	double		floatFormat;	/* constant 1234567.0 */
 #define FLOATFORMAT_VALUE	1234567.0
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5e5e33f64fc..238bb96f24e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12384,9 +12384,9 @@
   descr => 'pg_controldata init state information as a function',
   proname => 'pg_control_init', provolatile => 'v', prorettype => 'record',
   proargtypes => '',
-  proallargtypes => '{int4,int4,int4,int4,int4,int4,int4,int4,int4,bool,int4,bool}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{max_data_alignment,database_block_size,blocks_per_segment,wal_block_size,bytes_per_wal_segment,max_identifier_length,max_index_columns,max_toast_chunk_size,large_object_chunk_size,float8_pass_by_value,data_page_checksum_version,default_char_signedness}',
+  proallargtypes => '{int4,int4,int4,int4,int4,int4,int4,int4,int4,int4,int4,bool,int4,bool}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{int8_data_alignment,float8_data_alignment,max_data_alignment,database_block_size,blocks_per_segment,wal_block_size,bytes_per_wal_segment,max_identifier_length,max_index_columns,max_toast_chunk_size,large_object_chunk_size,float8_pass_by_value,data_page_checksum_version,default_char_signedness}',
   prosrc => 'pg_control_init' },
 
 # subscripting support for built-in types
-- 
2.43.7

